qulab-infinite / frontend /src /ChatInterface.tsx
workofarttattoo's picture
πŸš€ QuLab MCP Server: Complete Experiment Taxonomy Deployment
91994bf
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;