Spaces:
Build error
Build error
| import { useState, useRef, useEffect } from 'react' | |
| import MessageBubble from './MessageBubble' | |
| import ChatInput from './ChatInput' | |
| import TypingIndicator from './TypingIndicator' | |
| export default function ChatInterface() { | |
| const [messages, setMessages] = useState([ | |
| { | |
| role: 'assistant', | |
| content: 'Hello! I can analyze images and answer questions about them. Upload an image and ask me anything!', | |
| }, | |
| ]) | |
| const [input, setInput] = useState('') | |
| const [uploadedImage, setUploadedImage] = useState(null) | |
| const [isLoading, setIsLoading] = useState(false) | |
| const [error, setError] = useState(null) | |
| const messagesEndRef = useRef(null) | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) | |
| } | |
| useEffect(() => { | |
| scrollToBottom() | |
| }, [messages, isLoading]) | |
| const handleImageUpload = (file) => { | |
| if (!file.type.startsWith('image/')) { | |
| setError('Please upload a valid image file') | |
| return | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| setError('Image size must be less than 10MB') | |
| return | |
| } | |
| const reader = new FileReader() | |
| reader.onload = (e) => { | |
| setUploadedImage({ | |
| file, | |
| preview: e.target.result, | |
| base64: e.target.result.split(',')[1], | |
| }) | |
| setError(null) | |
| } | |
| reader.readAsDataURL(file) | |
| } | |
| const handleSubmit = async () => { | |
| if (!input.trim() && !uploadedImage) { | |
| setError('Please enter a message or upload an image') | |
| return | |
| } | |
| setError(null) | |
| setIsLoading(true) | |
| const userMessage = { | |
| role: 'user', | |
| content: [], | |
| } | |
| if (input.trim()) { | |
| userMessage.content.push({ | |
| type: 'text', | |
| text: input, | |
| }) | |
| } | |
| if (uploadedImage) { | |
| userMessage.content.push({ | |
| type: 'image_url', | |
| image_url: { | |
| url: `data:image/jpeg;base64,${uploadedImage.base64}`, | |
| }, | |
| }) | |
| } | |
| setMessages(prev => [...prev, userMessage]) | |
| setInput('') | |
| setUploadedImage(null) | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| messages: [...messages, userMessage], | |
| }), | |
| }) | |
| if (!response.ok) { | |
| throw new Error('Failed to get response from the AI') | |
| } | |
| const reader = response.body.getReader() | |
| const decoder = new TextDecoder() | |
| let accumulatedContent = '' | |
| setMessages(prev => [...prev, { role: 'assistant', content: '' }]) | |
| while (true) { | |
| const { done, value } = await reader.read() | |
| if (done) break | |
| const chunk = decoder.decode(value, { stream: true }) | |
| accumulatedContent += chunk | |
| setMessages(prev => { | |
| const newMessages = [...prev] | |
| const lastMessage = newMessages[newMessages.length - 1] | |
| if (lastMessage.role === 'assistant') { | |
| lastMessage.content = accumulatedContent | |
| } | |
| return newMessages | |
| }) | |
| } | |
| } catch (err) { | |
| console.error('Chat error:', err) | |
| setError(err.message || 'An error occurred. Please try again.') | |
| } finally { | |
| setIsLoading(false) | |
| } | |
| } | |
| const handleKeyPress = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault() | |
| handleSubmit() | |
| } | |
| } | |
| return ( | |
| <div className="flex flex-col flex-1 overflow-hidden"> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4"> | |
| {messages.map((message, index) => ( | |
| <MessageBubble | |
| key={index} | |
| message={message} | |
| isLast={index === messages.length - 1} | |
| /> | |
| ))} | |
| {isLoading && <TypingIndicator />} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {error && ( | |
| <div className="mx-4 mb-2 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg"> | |
| {error} | |
| </div> | |
| )} | |
| <ChatInput | |
| input={input} | |
| setInput={setInput} | |
| uploadedImage={uploadedImage} | |
| setUploadedImage={setUploadedImage} | |
| onImageUpload={handleImageUpload} | |
| onSubmit={handleSubmit} | |
| onKeyPress={handleKeyPress} | |
| isLoading={isLoading} | |
| /> | |
| </div> | |
| ) | |
| } |