diff --git a/app.json b/app.json index c9a90b6..9d108dd 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Out Live", "slug": "digital-pilates", - "version": "1.0.5", + "version": "1.0.11", "orientation": "portrait", "scheme": "digitalpilates", "userInterfaceStyle": "light", diff --git a/app/voice-record.tsx b/app/voice-record.tsx index bc4bf51..9821d50 100644 --- a/app/voice-record.tsx +++ b/app/voice-record.tsx @@ -1,6 +1,9 @@ import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; +import { useAppDispatch } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { analyzeFoodFromText } from '@/services/foodRecognition'; +import { saveRecognitionResult, setError, setLoading } from '@/store/foodRecognitionSlice'; import { triggerHapticFeedback } from '@/utils/haptics'; import { Ionicons } from '@expo/vector-icons'; import Voice from '@react-native-voice/voice'; @@ -19,22 +22,26 @@ import { const { width } = Dimensions.get('window'); -type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result'; +type VoiceRecordState = 'idle' | 'listening' | 'processing' | 'result' | 'analyzing'; export default function VoiceRecordScreen() { const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; const { mealType = 'dinner' } = useLocalSearchParams<{ mealType?: string }>(); + const dispatch = useAppDispatch(); // 状态管理 const [recordState, setRecordState] = useState('idle'); const [recognizedText, setRecognizedText] = useState(''); const [isListening, setIsListening] = useState(false); + const [analysisProgress, setAnalysisProgress] = useState(0); // 动画相关 const scaleAnimation = useRef(new Animated.Value(1)).current; const pulseAnimation = useRef(new Animated.Value(1)).current; const waveAnimation = useRef(new Animated.Value(0)).current; + const glowAnimation = useRef(new Animated.Value(0)).current; + const progressAnimation = useRef(new Animated.Value(0)).current; useEffect(() => { // 初始化语音识别 @@ -80,13 +87,43 @@ export default function VoiceRecordScreen() { ).start(); }; + // 启动科幻分析动画 + const startAnalysisAnimation = () => { + // 光环动画 + Animated.loop( + Animated.sequence([ + Animated.timing(glowAnimation, { + toValue: 1, + duration: 2000, + useNativeDriver: false, + }), + Animated.timing(glowAnimation, { + toValue: 0, + duration: 2000, + useNativeDriver: false, + }), + ]) + ).start(); + + // 进度条动画 + Animated.timing(progressAnimation, { + toValue: 1, + duration: 8000, // 8秒完成 + useNativeDriver: false, + }).start(); + }; + // 停止所有动画 const stopAnimations = () => { pulseAnimation.stopAnimation(); waveAnimation.stopAnimation(); + glowAnimation.stopAnimation(); + progressAnimation.stopAnimation(); scaleAnimation.setValue(1); pulseAnimation.setValue(1); waveAnimation.setValue(0); + glowAnimation.setValue(0); + progressAnimation.setValue(0); }; // 语音识别回调 @@ -165,11 +202,68 @@ export default function VoiceRecordScreen() { startRecording(); }; - // 确认并返回结果 - const confirmResult = () => { - triggerHapticFeedback('impactMedium'); - // TODO: 处理识别结果,可以传递给食物分析页面 - router.back(); + // 确认并分析食物文本 + const confirmResult = async () => { + if (!recognizedText.trim()) { + Alert.alert('提示', '请先进行语音识别'); + return; + } + + try { + triggerHapticFeedback('impactMedium'); + setRecordState('analyzing'); + setAnalysisProgress(0); + + // 启动科幻分析动画 + startAnalysisAnimation(); + + // 模拟进度更新 + const progressInterval = setInterval(() => { + setAnalysisProgress(prev => { + if (prev >= 90) { + clearInterval(progressInterval); + return prev; + } + return prev + Math.random() * 15; + }); + }, 500); + + // 调用文本分析API + dispatch(setLoading(true)); + const result = await analyzeFoodFromText({ text: recognizedText }); + + // 清理进度定时器 + clearInterval(progressInterval); + setAnalysisProgress(100); + + // 生成识别结果ID并保存到Redux + const recognitionId = `text_${Date.now()}`; + dispatch(saveRecognitionResult({ id: recognitionId, result })); + + // 停止动画并导航到结果页面 + stopAnimations(); + + // 延迟一点让用户看到100%完成 + setTimeout(() => { + router.replace({ + pathname: '/food/analysis-result', + params: { + recognitionId, + mealType: mealType, + hideRecordBar: 'false' + } + }); + }, 800); + + } catch (error) { + console.error('食物分析失败:', error); + stopAnimations(); + setRecordState('result'); + + const errorMessage = error instanceof Error ? error.message : '分析失败,请重试'; + dispatch(setError(errorMessage)); + Alert.alert('分析失败', errorMessage); + } }; const handleBack = () => { @@ -188,6 +282,8 @@ export default function VoiceRecordScreen() { return '正在聆听...'; case 'processing': return 'AI处理中...'; + case 'analyzing': + return 'AI大模型分析中...'; case 'result': return '识别完成'; default: @@ -219,6 +315,13 @@ export default function VoiceRecordScreen() { icon: 'hourglass', size: 80, }; + case 'analyzing': + return { + onPress: () => { }, + color: '#00D4AA', + icon: 'analytics', + size: 80, + }; case 'result': return { onPress: confirmResult, @@ -271,6 +374,52 @@ export default function VoiceRecordScreen() { )} + {/* 科幻分析特效 */} + {recordState === 'analyzing' && ( + <> + {/* 外光环 */} + + {/* 内光环 */} + + + )} + {/* 主录音按钮 */} )} + + {recordState === 'analyzing' && ( + + + 分析进度: {Math.round(analysisProgress)}% + + + + + + AI正在深度分析您的食物描述... + + + )} {/* 识别结果 */} @@ -468,4 +641,60 @@ const styles = StyleSheet.create({ fontWeight: '500', color: 'white', }, + // 科幻分析特效样式 + glowRing: { + position: 'absolute', + width: 200, + height: 200, + borderRadius: 100, + backgroundColor: 'rgba(0, 212, 170, 0.1)', + borderWidth: 2, + borderColor: '#00D4AA', + shadowColor: '#00D4AA', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.8, + shadowRadius: 20, + }, + innerGlowRing: { + position: 'absolute', + width: 180, + height: 180, + borderRadius: 90, + backgroundColor: 'rgba(0, 212, 170, 0.05)', + borderWidth: 1, + borderColor: 'rgba(0, 212, 170, 0.3)', + }, + analysisProgressContainer: { + alignItems: 'center', + marginTop: 20, + paddingHorizontal: 20, + }, + progressText: { + fontSize: 16, + fontWeight: '600', + marginBottom: 12, + }, + progressBarContainer: { + width: '100%', + height: 6, + backgroundColor: 'rgba(0, 212, 170, 0.2)', + borderRadius: 3, + overflow: 'hidden', + marginBottom: 8, + }, + progressBar: { + height: '100%', + backgroundColor: '#00D4AA', + borderRadius: 3, + shadowColor: '#00D4AA', + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.8, + shadowRadius: 4, + }, + analysisHint: { + fontSize: 14, + textAlign: 'center', + lineHeight: 20, + fontStyle: 'italic', + }, }); \ No newline at end of file diff --git a/assets/icon.icon/Assets/icon-1756312748268.jpg b/assets/icon.icon/Assets/icon-1756312748268.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/assets/icon.icon/Assets/icon-1756312748268.jpg differ diff --git a/assets/icon.icon/icon.json b/assets/icon.icon/icon.json index 652df4f..1125024 100644 --- a/assets/icon.icon/icon.json +++ b/assets/icon.icon/icon.json @@ -7,7 +7,7 @@ "blend-mode": "normal", "glass": true, "hidden": false, - "image-name": "icon-1756312748268.png", + "image-name": "icon-1756312748268.jpg", "name": "icon-1756312748268", "opacity": 1 } diff --git a/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/Contents.json index 43d35a2..744ad79 100644 --- a/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "icon-1756312748268.png", + "filename" : "icon-1756312748268.jpg", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/icon-1756312748268.jpg b/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/icon-1756312748268.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/AppIcon.appiconset/icon-1756312748268.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/Contents.json b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/Contents.json index 1dad293..495cb1c 100644 --- a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/Contents.json +++ b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "icon-1756312748268.png", + "filename" : "icon-1756312748268.jpg", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "icon-1756312748268 1.png", + "filename" : "icon-1756312748268 1.jpg", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "icon-1756312748268 2.png", + "filename" : "icon-1756312748268 2.jpg", "idiom" : "universal", "scale" : "3x" } diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 1.jpg b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 1.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 1.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 1.png b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 1.png deleted file mode 100644 index f7799d6..0000000 Binary files a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 1.png and /dev/null differ diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 2.jpg b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 2.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 2.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 2.png b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 2.png deleted file mode 100644 index f7799d6..0000000 Binary files a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268 2.png and /dev/null differ diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268.jpg b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268.png b/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268.png deleted file mode 100644 index f7799d6..0000000 Binary files a/ios/digitalpilates/Images.xcassets/SplashScreenLogo.imageset/icon-1756312748268.png and /dev/null differ diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/Contents.json b/ios/digitalpilates/Images.xcassets/logo.imageset/Contents.json index 1dad293..495cb1c 100644 --- a/ios/digitalpilates/Images.xcassets/logo.imageset/Contents.json +++ b/ios/digitalpilates/Images.xcassets/logo.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "icon-1756312748268.png", + "filename" : "icon-1756312748268.jpg", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "icon-1756312748268 1.png", + "filename" : "icon-1756312748268 1.jpg", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "icon-1756312748268 2.png", + "filename" : "icon-1756312748268 2.jpg", "idiom" : "universal", "scale" : "3x" } diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 1.jpg b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 1.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 1.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 1.png b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 1.png deleted file mode 100644 index f7799d6..0000000 Binary files a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 1.png and /dev/null differ diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 2.jpg b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 2.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 2.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 2.png b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 2.png deleted file mode 100644 index f7799d6..0000000 Binary files a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268 2.png and /dev/null differ diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268.jpg b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268.jpg new file mode 100644 index 0000000..a29eefe Binary files /dev/null and b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268.jpg differ diff --git a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268.png b/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268.png deleted file mode 100644 index f7799d6..0000000 Binary files a/ios/digitalpilates/Images.xcassets/logo.imageset/icon-1756312748268.png and /dev/null differ diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index 14d1062..d07ab55 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -24,7 +24,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.10 + 1.0.11 CFBundleSignature ???? CFBundleURLTypes diff --git a/services/foodRecognition.ts b/services/foodRecognition.ts index 268e353..2121e15 100644 --- a/services/foodRecognition.ts +++ b/services/foodRecognition.ts @@ -21,6 +21,10 @@ export type FoodRecognitionRequest = { imageUrls: string[]; }; +export type TextFoodAnalysisRequest = { + text: string; +}; + export type FoodRecognitionResponse = { items: FoodConfirmationOption[]; analysisText: string; @@ -31,4 +35,8 @@ export type FoodRecognitionResponse = { export async function recognizeFood(request: FoodRecognitionRequest): Promise { return api.post('/ai-coach/food-recognition', request); +} + +export async function analyzeFoodFromText(request: TextFoodAnalysisRequest): Promise { + return api.post('/ai-coach/text-food-analysis', request); } \ No newline at end of file