Spaces:
Sleeping
Sleeping
| const { useState, useEffect, useRef } = React; | |
| const EXAMPLE_PROMPTS = [ | |
| { text: "Analyze System Performance", color: "green", icon: "chart" }, | |
| { text: "Debug Error Logs", color: "yellow", icon: "warning" }, | |
| { text: "Compare Configurations", color: "blue", icon: "document" } | |
| ]; | |
| const ICONS = { | |
| chat: "M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z", | |
| download: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4", | |
| settings: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", | |
| menu: "M4 6h16M4 12h16M4 18h16", | |
| attach: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13", | |
| send: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z", | |
| chart: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z", | |
| warning: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z", | |
| document: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" | |
| }; | |
| const Icon = ({ path, className }) => ( | |
| <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| {path.includes('M2.01') ? ( | |
| <path fill="currentColor" d={path} /> | |
| ) : ( | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={path} /> | |
| )} | |
| </svg> | |
| ); | |
| const App = () => { | |
| const [sidebarOpen, setSidebarOpen] = useState(true); | |
| const [input, setInput] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [currentSessionId, setCurrentSessionId] = useState(null); | |
| const [sessions, setSessions] = useState([]); | |
| const [uploadedFiles, setUploadedFiles] = useState([]); | |
| const messagesEndRef = useRef(null); | |
| const fileInputRef = useRef(null); | |
| useEffect(() => { | |
| const initialSession = { | |
| id: crypto.randomUUID(), | |
| title: 'New Chat', | |
| messages: [], | |
| updatedAt: Date.now() | |
| }; | |
| setSessions([initialSession]); | |
| setCurrentSessionId(initialSession.id); | |
| }, []); | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [sessions, currentSessionId]); | |
| const createNewSession = () => ({ | |
| id: crypto.randomUUID(), | |
| title: 'New Chat', | |
| messages: [], | |
| updatedAt: Date.now() | |
| }); | |
| const handleNewChat = () => { | |
| const newSession = createNewSession(); | |
| setSessions(prev => [newSession, ...prev]); | |
| setCurrentSessionId(newSession.id); | |
| if (window.innerWidth < 1024) setSidebarOpen(false); | |
| }; | |
| const handleDeleteSession = (e, sessionId) => { | |
| e.stopPropagation(); | |
| setSessions(prev => { | |
| const filtered = prev.filter(s => s.id !== sessionId); | |
| if (filtered.length === 0) { | |
| const newSession = createNewSession(); | |
| setCurrentSessionId(newSession.id); | |
| return [newSession]; | |
| } | |
| if (sessionId === currentSessionId) { | |
| setCurrentSessionId(filtered[0].id); | |
| } | |
| return filtered; | |
| }); | |
| }; | |
| const updateSessionMessages = (sessionId, newMessage) => { | |
| setSessions(prev => prev.map(s => | |
| s.id === sessionId | |
| ? { ...s, messages: [...s.messages, newMessage] } | |
| : s | |
| )); | |
| }; | |
| const handleSendMessage = async () => { | |
| if ((!input.trim() && uploadedFiles.length === 0) || !currentSessionId) return; | |
| const userMessage = input; | |
| const filesToSend = [...uploadedFiles]; | |
| const userMsg = { role: 'user', content: userMessage, files: filesToSend.map(f => f.name) }; | |
| setInput(''); | |
| setUploadedFiles([]); | |
| setIsLoading(true); | |
| updateSessionMessages(currentSessionId, userMsg); | |
| try { | |
| setSessions(prevSessions => { | |
| const currentSession = prevSessions.find(s => s.id === currentSessionId); | |
| const formData = new FormData(); | |
| formData.append('message', userMessage); | |
| formData.append('history', JSON.stringify(currentSession?.messages || [])); | |
| filesToSend.forEach(file => { | |
| formData.append('files', file); | |
| }); | |
| fetch('/api/chat', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.reply) { | |
| updateSessionMessages(currentSessionId, { | |
| role: 'model', | |
| content: data.reply | |
| }); | |
| } | |
| setIsLoading(false); | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| updateSessionMessages(currentSessionId, { | |
| role: 'model', | |
| content: "Error: Could not connect to agent." | |
| }); | |
| setIsLoading(false); | |
| }); | |
| return prevSessions; | |
| }); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| updateSessionMessages(currentSessionId, { | |
| role: 'model', | |
| content: "Error: Could not connect to agent." | |
| }); | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleKeyDown = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| handleSendMessage(); | |
| } | |
| }; | |
| const selectSession = (sessionId) => { | |
| setCurrentSessionId(sessionId); | |
| if (window.innerWidth < 1024) setSidebarOpen(false); | |
| }; | |
| const handleFileSelect = (e) => { | |
| const files = Array.from(e.target.files); | |
| setUploadedFiles(prev => [...prev, ...files]); | |
| }; | |
| const removeFile = (index) => { | |
| setUploadedFiles(prev => prev.filter((_, i) => i !== index)); | |
| }; | |
| const handleExportChat = () => { | |
| if (!currentSession || currentSession.messages.length === 0) return; | |
| const chatData = { | |
| title: currentSession.title, | |
| exportedAt: new Date().toISOString(), | |
| messages: currentSession.messages | |
| }; | |
| const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `chat-${currentSession.title.replace(/\s+/g, '-').toLowerCase()}-${Date.now()}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }; | |
| const currentSession = sessions.find(s => s.id === currentSessionId); | |
| return ( | |
| <div className="flex h-screen bg-gray-950 text-gray-100 font-sans overflow-hidden"> | |
| <aside className={`fixed lg:static inset-y-0 left-0 z-30 w-[280px] bg-[#1a1d29] border-r border-gray-800 flex flex-col transition-transform duration-300 transform ${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}> | |
| <div className="p-4 flex items-center gap-2 border-b border-gray-800"> | |
| <div className="w-6 h-6 rounded bg-gradient-to-br from-green-500 to-emerald-700 flex items-center justify-center text-white text-sm font-bold"> | |
| G | |
| </div> | |
| <span className="text-gray-100 font-semibold text-base">GAIA Agent</span> | |
| </div> | |
| <div className="px-3 py-4"> | |
| <button | |
| onClick={handleNewChat} | |
| className="w-full flex items-center gap-2 px-3 py-2.5 bg-transparent hover:bg-gray-800/50 text-gray-300 rounded-lg transition-colors border border-gray-700 hover:border-gray-600" | |
| > | |
| <span className="text-lg">+</span> | |
| <span className="text-sm font-medium">New Chat</span> | |
| </button> | |
| </div> | |
| <div className="flex-1 overflow-y-auto px-3 space-y-1"> | |
| <div className="px-2 pb-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">History</div> | |
| {sessions.length === 0 ? ( | |
| <div className="px-4 py-8 text-center text-gray-600 text-xs"> | |
| No conversation history. | |
| </div> | |
| ) : ( | |
| sessions.map((session) => ( | |
| <div | |
| key={session.id} | |
| onClick={() => selectSession(session.id)} | |
| className={`group flex items-center gap-2 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${ | |
| session.id === currentSessionId | |
| ? 'bg-gray-800/70 text-white' | |
| : 'text-gray-400 hover:bg-gray-800/40 hover:text-gray-200' | |
| }`} | |
| > | |
| <Icon path={ICONS.chat} className="w-4 h-4 flex-shrink-0" /> | |
| <span className="flex-1 text-xs truncate">{session.title}</span> | |
| <button | |
| onClick={(e) => handleDeleteSession(e, session.id)} | |
| className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-500/20 rounded transition-all" | |
| title="Delete chat" | |
| > | |
| <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| <div className="p-3 border-t border-gray-800 space-y-1"> | |
| <button | |
| onClick={handleExportChat} | |
| disabled={!currentSession || currentSession.messages.length === 0} | |
| className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800/50 transition-colors text-xs disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| <Icon path={ICONS.download} className="w-4 h-4" /> | |
| <span>Export Chat</span> | |
| </button> | |
| <button className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800/50 transition-colors text-xs"> | |
| <Icon path={ICONS.settings} className="w-4 h-4" /> | |
| <span>Settings</span> | |
| </button> | |
| </div> | |
| </aside> | |
| <main className="flex-1 flex flex-col relative w-full h-full bg-[#0f1118]"> | |
| <div className="lg:hidden p-4 border-b border-gray-800 flex items-center gap-4 bg-[#1a1d29]"> | |
| <button onClick={() => setSidebarOpen(true)} className="text-gray-400 hover:text-white"> | |
| <Icon path={ICONS.menu} className="w-6 h-6" /> | |
| </button> | |
| <span className="font-semibold">GAIA Agent</span> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-6 space-y-6"> | |
| {!currentSession || currentSession.messages.length === 0 ? ( | |
| <div className="h-full flex flex-col items-center justify-center max-w-4xl mx-auto"> | |
| <div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-green-500 to-emerald-700 flex items-center justify-center text-white text-3xl font-bold mb-6 shadow-lg"> | |
| G | |
| </div> | |
| <h1 className="text-3xl font-bold text-white mb-3">GAIA Agent</h1> | |
| <p className="text-gray-400 text-center text-sm mb-12 max-w-md"> | |
| Your advanced AI assistant for system analysis, debugging, and configuration management. | |
| </p> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4 w-full max-w-3xl"> | |
| {EXAMPLE_PROMPTS.map(({ text, color, icon }) => ( | |
| <button | |
| key={text} | |
| onClick={() => setInput(text)} | |
| className="group p-6 bg-[#1a1d29] hover:bg-[#22253a] border border-gray-800 hover:border-gray-700 rounded-xl transition-all text-left" | |
| > | |
| <div className={`w-12 h-12 bg-${color}-500/10 rounded-lg flex items-center justify-center mb-4 group-hover:bg-${color}-500/20 transition-colors`}> | |
| <Icon path={ICONS[icon]} className={`w-6 h-6 text-${color}-500`} /> | |
| </div> | |
| <p className="text-white font-medium text-sm">{text}</p> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ) : ( | |
| currentSession.messages.map((msg, idx) => ( | |
| <div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}> | |
| <div className={`max-w-[85%] rounded-2xl px-5 py-3 ${ | |
| msg.role === 'user' ? 'bg-[#1a1d29] text-white' : 'bg-transparent text-gray-200' | |
| }`}> | |
| {msg.files && msg.files.length > 0 && ( | |
| <div className="mb-2 flex flex-wrap gap-1"> | |
| {msg.files.map((file, i) => ( | |
| <span key={i} className="inline-flex items-center gap-1 px-2 py-1 bg-gray-800 rounded text-xs"> | |
| <Icon path={ICONS.attach} className="w-3 h-3" /> | |
| {file} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| <p className="whitespace-pre-wrap text-sm" dangerouslySetInnerHTML={{ __html: msg.content.replace(/\n/g, '<br />') }}></p> | |
| </div> | |
| </div> | |
| )) | |
| )} | |
| {isLoading && ( | |
| <div className="flex justify-start"> | |
| <div className="px-5 py-3 text-gray-400 text-sm italic animate-pulse"> | |
| Thinking... | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <div className="p-4 bg-[#0f1118] border-t border-gray-800"> | |
| <div className="max-w-4xl mx-auto relative bg-[#1a1d29] rounded-xl border border-gray-800 focus-within:border-gray-700 transition-colors"> | |
| {uploadedFiles.length > 0 && ( | |
| <div className="px-4 pt-3 flex flex-wrap gap-2"> | |
| {uploadedFiles.map((file, idx) => ( | |
| <div key={idx} className="inline-flex items-center gap-2 px-3 py-1.5 bg-gray-800 rounded-lg text-xs"> | |
| <Icon path={ICONS.attach} className="w-3 h-3" /> | |
| <span>{file.name}</span> | |
| <button onClick={() => removeFile(idx)} className="text-gray-400 hover:text-white"> | |
| × | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div className="flex items-center gap-2 px-4"> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| multiple | |
| onChange={handleFileSelect} | |
| className="hidden" | |
| /> | |
| <button | |
| onClick={() => fileInputRef.current?.click()} | |
| className="p-2 text-gray-500 hover:text-gray-300 transition-colors" | |
| title="Attach" | |
| > | |
| <Icon path={ICONS.attach} className="w-5 h-5" /> | |
| </button> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder="Message GAIA Agent..." | |
| className="flex-1 bg-transparent text-white py-3 outline-none text-sm placeholder-gray-500" | |
| /> | |
| <button | |
| onClick={handleSendMessage} | |
| disabled={isLoading || (!input.trim() && uploadedFiles.length === 0)} | |
| className={`p-2 rounded-lg transition-all ${ | |
| (input.trim() || uploadedFiles.length > 0) ? 'text-white hover:bg-gray-800' : 'text-gray-600 cursor-not-allowed' | |
| }`} | |
| > | |
| <Icon path={ICONS.send} className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="text-center mt-3 text-xs text-gray-500"> | |
| GAIA Agent can make mistakes. Consider checking important information. | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| ReactDOM.createRoot(document.getElementById('root')).render(<App />); | |