From 8994a3e045d463fcc0655048e8599b435c70a711 Mon Sep 17 00:00:00 2001 From: Ada Date: Wed, 4 Feb 2026 17:19:51 -0800 Subject: [PATCH] feat(flow): image attachment and 422 error message display - Attach image then send with optional text or image-only (default prompt) - Attached image preview above input with remove - AI image API error detail no longer shows [object Object] --- src/screens/FlowScreen.tsx | 215 +++++++++++++++++++------------------ src/services/ai.service.ts | 7 +- 2 files changed, 118 insertions(+), 104 deletions(-) diff --git a/src/screens/FlowScreen.tsx b/src/screens/FlowScreen.tsx index 5a890db..82ced72 100644 --- a/src/screens/FlowScreen.tsx +++ b/src/screens/FlowScreen.tsx @@ -74,7 +74,8 @@ export default function FlowScreen() { const [newContent, setNewContent] = useState(''); const [isSending, setIsSending] = useState(false); const [isRecording, setIsRecording] = useState(false); - const [selectedImage, setSelectedImage] = useState(null); + /** Attached image for next send (uri + base64); user can add optional text then send together */ + const [attachedImage, setAttachedImage] = useState<{ uri: string; base64: string } | null>(null); // AI Role state - start with null to detect first load const [selectedRole, setSelectedRole] = useState(aiRoles[0] || null); @@ -253,10 +254,12 @@ export default function FlowScreen() { // ============================================================================= /** - * Handle sending a message to AI + * Handle sending a message to AI (text-only via LangGraph, or image + optional text via vision API) */ const handleSendMessage = async () => { - if (!newContent.trim() || isSending || !selectedRole) return; + const hasText = !!newContent.trim(); + const hasImage = !!attachedImage; + if ((!hasText && !hasImage) || isSending || !selectedRole) return; // Check authentication if (!token) { @@ -268,11 +271,64 @@ export default function FlowScreen() { return; } - const userMessage = newContent.trim(); setIsSending(true); + + // --- Path: send with image (optional text) --- + if (hasImage && attachedImage) { + const imageUri = attachedImage.uri; + const imageBase64 = attachedImage.base64; + const userText = newContent.trim() || '请描述或分析这张图片'; + setAttachedImage(null); + setNewContent(''); + + const userMsg: ChatMessage = { + id: Date.now().toString(), + role: 'user', + content: userText, + imageUri, + createdAt: new Date(), + }; + setMessages(prev => [...prev, userMsg]); + + try { + const aiResponse = await aiService.sendMessageWithImage(userText, imageBase64, token); + const aiMsg: ChatMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: aiResponse, + createdAt: new Date(), + }; + setMessages(prev => [...prev, aiMsg]); + } catch (error) { + console.error('AI image request failed:', error); + const errorMessage = error instanceof Error ? error.message : String(error); + const isAuthError = + errorMessage.includes('401') || + errorMessage.includes('Unauthorized') || + errorMessage.includes('credentials') || + errorMessage.includes('validate'); + if (isAuthError) { + signOut(); + Alert.alert('Session Expired', 'Your login session has expired. Please login again.', [{ text: 'OK' }]); + return; + } + const errorMsg: ChatMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: `⚠️ Error: ${errorMessage}`, + createdAt: new Date(), + }; + setMessages(prev => [...prev, errorMsg]); + } finally { + setIsSending(false); + } + return; + } + + // --- Path: text-only via LangGraph (unchanged) --- + const userMessage = newContent.trim(); setNewContent(''); - // Add user message immediately const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', @@ -282,25 +338,15 @@ export default function FlowScreen() { setMessages(prev => [...prev, userMsg]); try { - // 1. Convert current messages history to LangChain format const history: (HumanMessage | LangChainAIMessage | SystemMessage)[] = messages.map(msg => { if (msg.role === 'user') return new HumanMessage(msg.content); return new LangChainAIMessage(msg.content); }); - - // 2. Add system prompt const systemPrompt = new SystemMessage(selectedRole?.systemPrompt || ''); - - // 3. Add current new message const currentMsg = new HumanMessage(userMessage); - - // 4. Combine all messages for LangGraph processing const fullMessages = [systemPrompt, ...history, currentMsg]; - - // 5. Execute via LangGraph service (handles token limits and context) const aiResponse = await langGraphService.execute(fullMessages, token); - // Add AI response const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', @@ -308,20 +354,15 @@ export default function FlowScreen() { createdAt: new Date(), }; setMessages(prev => [...prev, aiMsg]); - } catch (error) { console.error('AI request failed:', error); - const errorMessage = error instanceof Error ? error.message : String(error); - - // Handle authentication errors (401, credentials, unauthorized) const isAuthError = errorMessage.includes('401') || errorMessage.includes('credentials') || errorMessage.includes('Unauthorized') || errorMessage.includes('Not authenticated') || errorMessage.includes('validate'); - if (isAuthError) { signOut(); Alert.alert( @@ -331,8 +372,6 @@ export default function FlowScreen() { ); return; } - - // Show error as AI message const errorMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', @@ -354,17 +393,15 @@ export default function FlowScreen() { }; /** - * Handle image attachment - pick image and analyze with AI + * Handle image attachment - pick image and attach to next message (user can add text then send) */ const handleAddImage = async () => { - // Request permission const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission Required', 'Please grant permission to access photos'); return; } - // Pick image const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, @@ -373,78 +410,11 @@ export default function FlowScreen() { }); if (!result.canceled && result.assets[0]) { - const imageAsset = result.assets[0]; - setSelectedImage(imageAsset.uri); - - // Check authentication - if (!token) { - Alert.alert( - 'Login Required', - 'Please login to analyze images', - [{ text: 'OK', onPress: () => signOut() }] - ); - return; - } - - setIsSending(true); - - // Add user message with image - const userMsg: ChatMessage = { - id: Date.now().toString(), - role: 'user', - content: 'Analyze this image', - imageUri: imageAsset.uri, - createdAt: new Date(), - }; - setMessages(prev => [...prev, userMsg]); - - try { - // Call AI with image (using base64) - const aiResponse = await aiService.sendMessageWithImage( - 'Please describe and analyze this image in detail.', - imageAsset.base64 || '', - token - ); - - const aiMsg: ChatMessage = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: aiResponse, - createdAt: new Date(), - }; - setMessages(prev => [...prev, aiMsg]); - } catch (error) { - console.error('AI image analysis failed:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - - // Handle authentication errors - const isAuthError = - errorMessage.includes('401') || - errorMessage.includes('Unauthorized') || - errorMessage.includes('credentials') || - errorMessage.includes('validate'); - - if (isAuthError) { - signOut(); - Alert.alert( - 'Session Expired', - 'Your login session has expired. Please login again.', - [{ text: 'OK' }] - ); - return; - } - - const errorMsg: ChatMessage = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: `⚠️ Error analyzing image: ${errorMessage}`, - createdAt: new Date(), - }; - setMessages(prev => [...prev, errorMsg]); - } finally { - setIsSending(false); - setSelectedImage(null); - } + const asset = result.assets[0]; + setAttachedImage({ + uri: asset.uri, + base64: asset.base64 || '', + }); } }; @@ -817,21 +787,35 @@ export default function FlowScreen() { {/* Bottom Input Bar */} + {/* Attached image preview (optional text then send) */} + {attachedImage && ( + + + 可输入文字后发送 + setAttachedImage(null)} + hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} + > + + + + )} {/* Image attachment button */} - + {/* Text Input */} - {/* Send or Voice button */} - {newContent.trim() || isSending ? ( + {/* Send or Voice button: show send when has text or attached image */} + {newContent.trim() || attachedImage || isSending ? ( e.msg).join('; '); + else if (d && typeof d === 'object') errorDetail = JSON.stringify(d); } catch { errorDetail = errorText || errorDetail; }