Spaces:
Build error
Build error
| import React, { useState, useRef, useEffect } from 'react'; | |
| export default function ChatInterface() { | |
| const [messages, setMessages] = useState([]); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [image, setImage] = useState(null); | |
| const messagesEndRef = useRef(null); | |
| const fileInputRef = useRef(null); | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| const handleImageUpload = (e) => { | |
| const file = e.target.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| setImage(reader.result); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }; | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!input.trim() && !image) return; | |
| const userMessage = { | |
| role: 'user', | |
| content: input, | |
| image: image, | |
| }; | |
| setMessages(prev => [...prev, userMessage]); | |
| setInput(''); | |
| setIsLoading(true); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| messages: [...messages, userMessage].map(msg => ({ | |
| role: msg.role, | |
| content: msg.content, | |
| })), | |
| image: image, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: data.response, | |
| }]); | |
| } else { | |
| throw new Error(data.error || 'Failed to get response'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: 'Sorry, I encountered an error. Please try again.', | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| setImage(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| } | |
| }; | |
| return ( | |
| <div className="flex-1 flex flex-col overflow-hidden"> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4"> | |
| {messages.length === 0 && ( | |
| <div className="text-center text-gray-500 mt-8"> | |
| <p className="text-lg">Welcome to GLM-4.6V-Flash</p> | |
| <p className="text-sm mt-2">Upload an image and ask me about it!</p> | |
| </div> | |
| )} | |
| {messages.map((message, index) => ( | |
| <div | |
| key={index} | |
| className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`} | |
| > | |
| <div className={`message-bubble ${message.role === 'user' ? 'user-bubble' : 'assistant-bubble'}`}> | |
| {message.image && ( | |
| <img | |
| src={message.image} | |
| alt="Uploaded" | |
| className="mb-2 rounded-lg max-w-full h-auto" | |
| style={{ maxHeight: '200px' }} | |
| /> | |
| )} | |
| <p className="whitespace-pre-wrap">{message.content}</p> | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="flex justify-start"> | |
| <div className="message-bubble assistant-bubble"> | |
| <div className="flex space-x-1"> | |
| <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots"></div> | |
| <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.2s' }}></div> | |
| <div className="w-2 h-2 bg-gray-600 rounded-full animate-pulse-dots" style={{ animationDelay: '0.4s' }}></div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <div className="border-t bg-white p-4"> | |
| {image && ( | |
| <div className="mb-3 relative inline-block"> | |
| <img | |
| src={image} | |
| alt="Preview" | |
| className="rounded-lg max-w-full h-auto" | |
| style={{ maxHeight: '100px' }} | |
| /> | |
| <button | |
| onClick={() => { | |
| setImage(null); | |
| if (fileInputRef.current) { | |
| fileInputRef.current.value = ''; | |
| } | |
| className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-600 transition-colors" | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| )} | |
| <form onSubmit={handleSubmit} className="flex space-x-2"> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| onChange={handleImageUpload} | |
| accept="image/*" | |
| className="hidden" | |
| id="image-upload" | |
| /> | |
| <label | |
| htmlFor="image-upload" | |
| className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors cursor-pointer flex items-center justify-center" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
| </svg> | |
| </label> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| placeholder="Type your message..." | |
| className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| disabled={isLoading} | |
| /> | |
| <button | |
| type="submit" | |
| disabled={isLoading || (!input.trim() && !image)} | |
| className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors" | |
| > | |
| {isLoading ? ( | |
| <div className="flex space-x-1"> | |
| <div className="w-1 h-4 bg-white rounded-full animate-pulse"></div> | |
| <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div> | |
| <div className="w-1 h-4 bg-white rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div> | |
| </div> | |
| ) : ( | |
| 'Send' | |
| )} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| ); | |
| } |