Spaces:
No application file
No application file
| import React, { useState, useEffect, useRef } from 'react'; | |
| import axios from 'axios'; | |
| interface ChatInterfaceProps { | |
| onCommand: (cmd: { task: string; type: '3d' | 'pcb' }) => void; | |
| } | |
| const ChatInterface: React.FC<ChatInterfaceProps> = ({ onCommand: _onCommand }) => { | |
| const [messages, setMessages] = useState([ | |
| { role: 'assistant', content: 'Welcome to QuLab Infinite. I am ECH0, your R&D Navigator. What are we building today?' } | |
| ]); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const endRef = useRef<HTMLDivElement>(null); | |
| const scrollToBottom = () => { | |
| endRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| const handleSend = async () => { | |
| if (!input.trim() || isLoading) return; | |
| const newMsg = { role: 'user', content: input }; | |
| setMessages(prev => [...prev, newMsg]); | |
| setInput(''); | |
| setIsLoading(true); | |
| try { | |
| const res = await axios.post('http://localhost:8000/chat', { message: newMsg.content }); | |
| const data = res.data; | |
| const assistantMsg = { role: 'assistant', content: data.response }; | |
| setMessages(prev => [...prev, assistantMsg]); | |
| if (data.action_result) { | |
| // If ECH0 performed a simulation, we can log it or show it. | |
| // For now, we print it as a system message. | |
| const sysMsg = { | |
| role: 'system', | |
| content: `[SIMULATION OUTPUT]: ${JSON.stringify(data.action_result.products)} produced. ${data.action_result.observations[0]}` | |
| }; | |
| setMessages(prev => [...prev, sysMsg]); | |
| // If it was a PCB/3D task (legacy check), we could still trigger onCommand | |
| // But generally ECH0Service handles universal lab now. | |
| // We'll leave the prop just in case we want to force UI switches, | |
| // but for now relying on the chat response is cleaner. | |
| } | |
| } catch (error) { | |
| console.error("Chat Error", error); | |
| setMessages(prev => [...prev, { role: 'assistant', content: "Communication Application Error. Is the backend or Ollama online?" }]); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Enter') handleSend(); | |
| }; | |
| return ( | |
| <div className="glass-panel" style={{ | |
| width: '90%', | |
| maxWidth: '800px', | |
| height: '80vh', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| overflow: 'hidden' | |
| }}> | |
| <div style={{ | |
| padding: '1.5rem', | |
| background: 'rgba(0, 240, 255, 0.05)', | |
| borderBottom: '1px solid var(--glass-border)', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '1rem' | |
| }}> | |
| <div style={{ | |
| width: '40px', height: '40px', borderRadius: '50%', | |
| background: 'var(--accent-cyan)', | |
| boxShadow: '0 0 15px var(--accent-cyan)', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', | |
| fontWeight: 'bold', fontSize: '1.2rem', color: '#000' | |
| }}> | |
| AI | |
| </div> | |
| <div> | |
| <h2 style={{ fontSize: '1.2rem' }}>ECH0 COMMAND</h2> | |
| <span style={{ fontSize: '0.8rem', color: isLoading ? 'var(--accent-purple)' : 'var(--accent-green)' }}> | |
| β {isLoading ? 'THINKING...' : 'ONLINE'} | |
| </span> | |
| </div> | |
| </div> | |
| <div style={{ flexGrow: 1, padding: '1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| {messages.map((msg, i) => ( | |
| <div key={i} style={{ | |
| display: 'flex', | |
| justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start' | |
| }}> | |
| <div style={{ | |
| maxWidth: '70%', | |
| padding: '1rem', | |
| borderRadius: '12px', | |
| background: msg.role === 'user' ? 'linear-gradient(135deg, var(--accent-purple), #8800ff)' : (msg.role === 'system' ? 'rgba(255,100,0,0.2)' : 'var(--bg-tertiary)'), | |
| border: msg.role === 'assistant' ? '1px solid var(--glass-border)' : (msg.role === 'system' ? '1px dashed var(--accent-cyan)' : 'none'), | |
| color: msg.role === 'system' ? 'var(--accent-cyan)' : 'white', | |
| boxShadow: '0 4px 10px rgba(0,0,0,0.2)', | |
| fontFamily: msg.role === 'system' ? 'var(--font-mono)' : 'inherit' | |
| }}> | |
| {msg.content} | |
| </div> | |
| </div> | |
| ))} | |
| <div ref={endRef} /> | |
| </div> | |
| <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.2)' }}> | |
| <div style={{ | |
| display: 'flex', | |
| background: 'var(--bg-secondary)', | |
| border: '1px solid var(--glass-border)', | |
| borderRadius: '8px', | |
| padding: '0.5rem' | |
| }}> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder="Type a command (e.g., 'Combine Sodium and Water')..." | |
| disabled={isLoading} | |
| style={{ | |
| flexGrow: 1, | |
| background: 'transparent', | |
| border: 'none', | |
| color: 'white', | |
| padding: '0.5rem', | |
| outline: 'none', | |
| fontFamily: 'var(--font-main)', | |
| fontSize: '1rem', | |
| opacity: isLoading ? 0.5 : 1 | |
| }} | |
| /> | |
| <button onClick={handleSend} style={{ width: 'auto', padding: '0 1.5rem' }} disabled={isLoading}>SEND</button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default ChatInterface; | |