anycoder-728b0a8c / components /ChatInterface.jsx
akhaliq's picture
akhaliq HF Staff
Upload components/ChatInterface.jsx with huggingface_hub
76f7d49 verified
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>
);
}