Spaces:
Build error
Build error
| import { useState, useRef, useEffect } from 'react' | |
| import { Send, Loader2, MessageCircle } from 'lucide-react' | |
| import ReactMarkdown from 'react-markdown' | |
| import rehypeHighlight from 'rehype-highlight' | |
| export default function ChatInterface() { | |
| const [messages, setMessages] = useState([ | |
| { | |
| id: 1, | |
| role: 'assistant', | |
| content: "Hello! I'm your AI assistant. I can help you with questions, have conversations, provide explanations, and assist with various topics. What would you like to talk about?", | |
| timestamp: new Date().toISOString() | |
| } | |
| ]) | |
| const [inputMessage, setInputMessage] = useState('') | |
| const [isLoading, setIsLoading] = useState(false) | |
| const messagesEndRef = useRef(null) | |
| const inputRef = useRef(null) | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) | |
| } | |
| useEffect(() => { | |
| scrollToBottom() | |
| }, [messages]) | |
| const handleSubmit = async (e) => { | |
| e.preventDefault() | |
| if (!inputMessage.trim() || isLoading) return | |
| const userMessage = { | |
| id: Date.now(), | |
| role: 'user', | |
| content: inputMessage.trim(), | |
| timestamp: new Date().toISOString() | |
| } | |
| setMessages(prev => [...prev, userMessage]) | |
| setInputMessage('') | |
| setIsLoading(true) | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| message: userMessage.content, | |
| messages: messages.filter(m => m.role !== 'system') | |
| }), | |
| }) | |
| if (!response.ok) { | |
| throw new Error('Failed to get response') | |
| } | |
| const data = await response.json() | |
| const assistantMessage = { | |
| id: Date.now() + 1, | |
| role: 'assistant', | |
| content: data.message, | |
| timestamp: data.timestamp || new Date().toISOString() | |
| } | |
| setMessages(prev => [...prev, assistantMessage]) | |
| } catch (error) { | |
| console.error('Chat error:', error) | |
| const errorMessage = { | |
| id: Date.now() + 1, | |
| role: 'assistant', | |
| content: "I apologize, but I'm having trouble connecting right now. Please try again in a moment.", | |
| timestamp: new Date().toISOString() | |
| } | |
| setMessages(prev => [...prev, errorMessage]) | |
| } finally { | |
| setIsLoading(false) | |
| inputRef.current?.focus() | |
| } | |
| } | |
| const handleKeyPress = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault() | |
| handleSubmit(e) | |
| } | |
| } | |
| const formatTime = (timestamp) => { | |
| return new Date(timestamp).toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }) | |
| } | |
| return ( | |
| <div className="flex flex-col h-[600px]"> | |
| {/* Messages Area */} | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide"> | |
| {messages.length === 0 && ( | |
| <div className="flex items-center justify-center h-full text-gray-500"> | |
| <div className="text-center"> | |
| <MessageCircle className="w-12 h-12 mx-auto mb-3 text-gray-400" /> | |
| <p>Start a conversation with the AI assistant</p> | |
| </div> | |
| </div> | |
| )} | |
| {messages.map((message) => ( | |
| <div | |
| key={message.id} | |
| className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`} | |
| > | |
| <div | |
| className={`message-bubble ${ | |
| message.role === 'user' | |
| ? 'message-user' | |
| : 'message-assistant' | |
| }`} | |
| > | |
| <div className="prose prose-sm max-w-none"> | |
| <ReactMarkdown | |
| rehypePlugins={[rehypeHighlight]} | |
| components={{ | |
| p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>, | |
| code: ({ children, className }) => { | |
| const isInline = !className | |
| return isInline ? ( | |
| <code className="bg-gray-100 px-1 py-0.5 rounded text-sm"> | |
| {children} | |
| </code> | |
| ) : ( | |
| <code className="block bg-gray-100 p-2 rounded text-sm overflow-x-auto"> | |
| {children} | |
| </code> | |
| ) | |
| } | |
| > | |
| {message.content} | |
| </ReactMarkdown> | |
| </div> | |
| <div className={`text-xs mt-2 ${ | |
| message.role === 'user' ? 'text-blue-100' : 'text-gray-500' | |
| }`}> | |
| {formatTime(message.timestamp)} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="flex justify-start"> | |
| <div className="message-bubble message-assistant"> | |
| <div className="flex items-center space-x-2"> | |
| <Loader2 className="w-4 h-4 animate-spin" /> | |
| <span className="text-gray-600">Thinking...</span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Input Area */} | |
| <div className="border-t border-gray-200 p-4"> | |
| <form onSubmit={handleSubmit} className="flex space-x-3"> | |
| <div className="flex-1"> | |
| <textarea | |
| ref={inputRef} | |
| value={inputMessage} | |
| onChange={(e) => setInputMessage(e.target.value)} | |
| onKeyPress={handleKeyPress} | |
| placeholder="Type your message... (Press Enter to send, Shift+Enter for new line)" | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none" | |
| rows={1} | |
| style={{ | |
| minHeight: '48px', | |
| maxHeight: '120px', | |
| height: 'auto' | |
| onInput={(e) => { | |
| e.target.style.height = 'auto' | |
| e.target.style.height = e.target.scrollHeight + 'px' | |
| disabled={isLoading} | |
| /> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={!inputMessage.trim() || isLoading} | |
| className="px-6 py-3 bg-primary-500 text-white rounded-lg hover:bg-primary-600 focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center space-x-2" | |
| > | |
| {isLoading ? ( | |
| <Loader2 className="w-5 h-5 animate-spin" /> | |
| ) : ( | |
| <Send className="w-5 h-5" /> | |
| )} | |
| <span className="hidden sm:inline">Send</span> | |
| </button> | |
| </form> | |
| <div className="mt-2 text-xs text-gray-500 text-center"> | |
| Press Enter to send, Shift+Enter for new line | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| This chatbot application includes: | |
| **Key Features:** | |
| - **Modern UI**: Clean, responsive design with Tailwind CSS | |
| - **Real-time Chat**: Smooth messaging interface with loading states | |
| - **Markdown Support**: Rich text formatting with code highlighting | |
| - **Keyboard Shortcuts**: Enter to send, Shift+Enter for new lines | |
| - **Error Handling**: Graceful error handling and retry mechanisms | |
| - **Accessibility**: Proper ARIA labels and keyboard navigation | |
| - **Mobile Responsive**: Works perfectly on all screen sizes | |
| **Technical Implementation:** | |
| - **Next.js 14** with app router architecture | |
| - **API Route** for chat backend logic | |
| - **React Hooks** for state management | |
| - **Lucide React** icons for modern UI elements | |
| - **React Markdown** for rich text rendering | |
| - **Tailwind CSS** for styling | |
| - **Docker** configuration for HuggingFace Spaces deployment | |
| **The "Built with anycoder" link** is prominently displayed in the header as requested, linking to the specified HuggingFace Spaces page. | |
| The application is ready for deployment and provides a complete, professional chatbot experience! |