LeVinh
fix unable to answer agent
cfdcca2
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 />);