feat: initial implementation — all 35 requirements across phases 1-3
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)
This commit is contained in:
92
mobile/src/screens/AIResultScreen.tsx
Normal file
92
mobile/src/screens/AIResultScreen.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user