Backend (Spring Boot 3.2 / Java 21 / PostgreSQL): - JWT auth with BCrypt password hashing - User profile + Mifflin-St Jeor BMR calculator - Food search + barcode via OpenFoodFacts API with local cache - Meal CRUD with user data isolation and ownership checks - AI photo analysis (OpenAI Vision) with confidence intervals - AI correction feedback loop for personalisation - Flyway DB migrations + RFC-7807 error responses Mobile (React Native / TypeScript): - Full navigation stack (Auth → Tabs → Home stack) - Design tokens (WCAG 2.2 AA colours, 8px grid, 48px touch targets) - 10 screens: Login, Register, Home, Search, Camera, AI Result, Edit Meal, Daily Details, History, Profile - Confidence-aware calorie display (kcal ± range) - Repeat last meal shortcut + macro tracking Docs: - docs/PLAN-AND-REQUIREMENTS.md - docs/traceability.csv (35 requirements, all Implemented)
93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
// Generated by GitHub Copilot
|
|
import React, { useState } from 'react';
|
|
import { View, Text, ScrollView, StyleSheet, Alert } from 'react-native';
|
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
import AISuggestionCard from '../components/AISuggestionCard';
|
|
import Button from '../components/Button';
|
|
import { AiSuggestion } from '../services/api';
|
|
import { Colors } from '../theme/colors';
|
|
import { Spacing } from '../theme/spacing';
|
|
|
|
/**
|
|
* AI result screen — shows detected items with confidence scores.
|
|
* NEVER auto-saves. User must confirm or edit first. (REQ-AI-002)
|
|
* REQ-MOB-004, REQ-INT-001
|
|
*/
|
|
export default function AIResultScreen() {
|
|
const navigation = useNavigation<any>();
|
|
const route = useRoute<any>();
|
|
const { analysisId, suggestions: initialSuggestions } = route.params as {
|
|
analysisId: string;
|
|
suggestions: AiSuggestion[];
|
|
};
|
|
|
|
const [suggestions, setSuggestions] = useState<AiSuggestion[]>(initialSuggestions);
|
|
|
|
const handleGramsChange = (index: number, grams: number) => {
|
|
setSuggestions(prev => prev.map((s, i) =>
|
|
i === index
|
|
? {
|
|
...s,
|
|
grams,
|
|
estimatedCalories: grams * 2,
|
|
confidenceLow: Math.max(0, grams * 2 * (1 - (1 - s.confidence) * 0.4)),
|
|
confidenceHigh: grams * 2 * (1 + (1 - s.confidence) * 0.4),
|
|
}
|
|
: s
|
|
));
|
|
};
|
|
|
|
const confirmAndNavigate = () => {
|
|
// Pass adjusted suggestions to EditMeal for final save
|
|
navigation.navigate('EditMeal', { items: suggestions, analysisId });
|
|
};
|
|
|
|
if (suggestions.length === 0) {
|
|
return (
|
|
<View style={styles.empty}>
|
|
<Text style={styles.emptyText}>No food items detected. Try Search instead.</Text>
|
|
<Button label="Search Food" onPress={() => navigation.navigate('Search')} />
|
|
<Button label="Retake Photo" variant="secondary" onPress={() => navigation.goBack()} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
<AISuggestionCard suggestions={suggestions} onGramsChange={handleGramsChange} />
|
|
|
|
<View style={styles.actions}>
|
|
<Button
|
|
label="✅ Confirm Meal"
|
|
onPress={confirmAndNavigate}
|
|
accessibilityHint="Proceeds to the edit and save screen"
|
|
/>
|
|
<Button
|
|
label="Edit Items"
|
|
variant="secondary"
|
|
onPress={confirmAndNavigate}
|
|
accessibilityHint="Edit portion sizes before saving"
|
|
/>
|
|
<Button
|
|
label="← Retake Photo"
|
|
variant="ghost"
|
|
onPress={() => navigation.goBack()}
|
|
/>
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: Colors.background },
|
|
content: { padding: Spacing.md },
|
|
actions: { gap: Spacing.sm },
|
|
empty: {
|
|
flex: 1, padding: Spacing.lg, justifyContent: 'center', alignItems: 'center',
|
|
backgroundColor: Colors.background,
|
|
},
|
|
emptyText: {
|
|
fontSize: 16, color: Colors.gray700, textAlign: 'center', marginBottom: Spacing.lg,
|
|
},
|
|
});
|