ghostdrive1's picture
Upload folder using huggingface_hub
6118c36 verified
Raw
History Blame Contribute Delete
30.4 kB
import { useState, useEffect } from 'react';
import { Bot, PlusSquare, Settings, Database, Plug, FileText, Plus, Monitor, ArrowUp, X, Play, SkipForward, Save, Trash2, Eye, EyeOff, ChevronDown, Search } from 'lucide-react';
// ─── Storage ──────────────────────────────────────────────────────────────────
const STORAGE_KEY = 'manus_config_v2';
const defaultConfig = {
providers: { OpenAI: [], Anthropic: [], Groq: [], HuggingFace: [], OpenRouter: [], Nvidia: [], Mistral: [], Cohere: [] },
activeProvider: 'Groq',
activeKey: '',
selectedModel: 'llama-3.1-8b-instant',
ragConfig: { vectorDbUrl: '', embeddingModel: 'text-embedding-3-small (OpenAI)', systemPrompt: '' },
mcpServers: [''],
};
function loadConfig() {
try { const s = localStorage.getItem(STORAGE_KEY); return s ? { ...defaultConfig, ...JSON.parse(s) } : defaultConfig; }
catch { return defaultConfig; }
}
function saveConfig(cfg) { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); }
// ─── Full Model Catalog ───────────────────────────────────────────────────────
const MODELS = {
OpenAI: [
'gpt-4o', 'gpt-4o-2024-11-20', 'gpt-4o-2024-08-06', 'gpt-4o-2024-05-13',
'gpt-4o-mini', 'gpt-4o-mini-2024-07-18', 'o1', 'o1-2024-12-17',
'o1-mini', 'o1-mini-2024-09-12', 'o3-mini', 'o3-mini-2025-01-31',
'gpt-4-turbo', 'gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview',
'gpt-4-0125-preview', 'gpt-4-1106-preview', 'gpt-4', 'gpt-4-0613',
'gpt-4-0314', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-1106'
],
Anthropic: [
'claude-3-7-sonnet-20250219', 'claude-3-5-sonnet-20241022',
'claude-3-5-sonnet-20240620', 'claude-3-5-haiku-20241022',
'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'
],
Groq: [
'llama-3.1-8b-instant', 'llama-3.3-70b-versatile',
'llama3-8b-8192', 'llama3-70b-8192', 'mixtral-8x7b-32768',
'qwen/qwen3-32b', 'qwen/qwen3.6-27b', 'groq/compound', 'groq/compound-mini',
'deepseek-r1-distill-llama-70b', 'meta-llama/llama-4-scout-17b-16e-instruct'
],
HuggingFace: [
'meta-llama/Llama-3.3-70B-Instruct', 'meta-llama/Llama-3.1-8B-Instruct',
'meta-llama/Llama-3.1-70B-Instruct', 'meta-llama/Meta-Llama-3-8B-Instruct',
'mistralai/Mixtral-8x7B-Instruct-v0.1', 'mistralai/Mistral-7B-Instruct-v0.3',
'google/gemma-2-9b-it', 'google/gemma-2-27b-it',
'Qwen/Qwen2.5-72B-Instruct', 'Qwen/Qwen2.5-Coder-32B-Instruct',
'microsoft/Phi-3.5-mini-instruct', 'microsoft/Phi-3-medium-128k-instruct'
],
OpenRouter: [
'openai/gpt-4o', 'openai/gpt-4o-mini', 'openai/o1', 'openai/o1-mini', 'openai/o3-mini',
'anthropic/claude-3.7-sonnet', 'anthropic/claude-3.5-sonnet',
'anthropic/claude-3.5-haiku', 'anthropic/claude-3-opus',
'google/gemini-2.0-pro-exp-02-05', 'google/gemini-2.0-flash-exp',
'google/gemini-1.5-pro', 'google/gemini-1.5-flash',
'deepseek/deepseek-chat', 'deepseek/deepseek-coder', 'deepseek/deepseek-r1',
'meta-llama/llama-3.3-70b-instruct', 'meta-llama/llama-3.1-405b-instruct',
'x-ai/grok-2-1212', 'x-ai/grok-2-vision-1212',
'mistralai/mistral-large', 'mistralai/mixtral-8x22b-instruct',
'qwen/qwq-32b'
],
Nvidia: [
'minimaxai/minimax-m3', 'moonshotai/kimi-k2.6',
'deepseek-ai/deepseek-v4-pro', 'z-ai/glm-5.1',
'minimaxai/minimax-m1-40k', 'moonshotai/kimi-k2',
'deepseek-ai/deepseek-r1-0528', 'z-ai/glm-4.5',
'nvidia/llama-3.1-nemotron-70b-instruct', 'nvidia/nemotron-4-340b-instruct',
'meta/llama-3.1-405b-instruct', 'meta/llama-3.1-70b-instruct',
'mistralai/mistral-large-2-instruct', 'mistralai/mixtral-8x22b-instruct',
'google/gemma-2-27b-it', 'microsoft/phi-3-medium-128k-instruct'
],
Mistral: [
'mistral-large-latest', 'mistral-large-2411', 'mistral-large-2407',
'pixtral-large-latest', 'pixtral-large-2411',
'ministral-3b-latest', 'ministral-8b-latest',
'mistral-small-latest', 'mistral-small-2409',
'codestral-latest', 'codestral-2501',
'open-mistral-nemo', 'open-mixtral-8x22b'
],
Cohere: [
'command-r-plus-08-2024', 'command-r-08-2024',
'command-r-plus', 'command-r', 'command', 'command-light'
],
};
const EMBEDDING_MODELS = [
'text-embedding-3-small (OpenAI)', 'text-embedding-3-large (OpenAI)',
'nomic-embed-text (Nomic)', 'bge-m3 (BAAI)', 'e5-mistral-7b-instruct',
];
// ─── Key Row ─────────────────────────────────────────────────────────────────
function KeyRow({ value, isActive, onToggleActive, onChange, onDelete, index }) {
const [show, setShow] = useState(!value); // auto-show when empty/new
const masked = value.length > 12 ? value.slice(0, 6) + '••••••••••••' + value.slice(-4) : '••••';
return (
<div className={`flex items-center gap-2 p-2.5 rounded-xl border transition-all ${isActive ? 'border-blue-500 bg-blue-950/40' : 'border-white/10 bg-white/5 hover:border-white/20'}`}>
{/* Active dot */}
<button title={isActive ? 'Active key' : 'Click to use this key'} onClick={onToggleActive}
className={`w-2.5 h-2.5 rounded-full flex-shrink-0 border-2 transition-all ${isActive ? 'bg-blue-400 border-blue-400 shadow-[0_0_6px_rgba(96,165,250,0.8)]' : 'bg-transparent border-gray-600 hover:border-blue-400'}`}
/>
{/* Index */}
<span className="text-[10px] text-gray-600 flex-shrink-0 w-4">#{index + 1}</span>
{/* Input always present, toggle visibility */}
<div className="flex-grow relative">
<input
type={show ? 'text' : 'password'}
value={value}
onChange={e => onChange(e.target.value)}
placeholder="Paste your API key here..."
className="w-full bg-transparent text-white text-xs font-mono outline-none placeholder-gray-600 pr-2"
autoFocus={!value}
/>
{!show && value && (
<span className="absolute inset-0 flex items-center text-xs font-mono text-gray-400 pointer-events-none">{masked}</span>
)}
</div>
{/* Eye toggle */}
<button onClick={() => setShow(s => !s)} className="text-gray-600 hover:text-gray-300 transition-colors flex-shrink-0">
{show ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
{/* Delete */}
<button onClick={onDelete} className="text-gray-700 hover:text-red-400 transition-colors flex-shrink-0">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
);
}
// ─── Model Picker ────────────────────────────────────────────────────────────
function ModelPicker({ provider, value, onChange }) {
const [query, setQuery] = useState('');
const allModels = MODELS[provider] || [];
const filtered = query ? allModels.filter(m => m.toLowerCase().includes(query.toLowerCase())) : allModels;
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 bg-white/5 border border-white/10 rounded-xl px-3 py-2">
<Search className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder={`Search ${allModels.length} models...`}
className="bg-transparent text-white text-xs outline-none flex-grow placeholder-gray-600"
/>
{query && <button onClick={() => setQuery('')} className="text-gray-600 hover:text-gray-300"><X className="w-3 h-3" /></button>}
</div>
<div className="max-h-48 overflow-y-auto rounded-xl border border-white/10 bg-white/5 divide-y divide-white/5">
{filtered.length === 0 && <p className="text-xs text-gray-600 text-center py-4">No models match "{query}"</p>}
{filtered.map(m => (
<button key={m} onClick={() => onChange(m)}
className={`w-full text-left px-3 py-2 text-xs font-mono transition-colors hover:bg-white/10 ${value === m ? 'text-blue-400 bg-blue-950/40' : 'text-gray-300'}`}>
{value === m && <span className="mr-2"></span>}{m}
</button>
))}
</div>
{value && <div className="text-[10px] text-gray-500 px-1">Selected: <span className="text-blue-400 font-mono">{value}</span></div>}
</div>
);
}
// ─── Main App ────────────────────────────────────────────────────────────────
const App = () => {
const [config, setConfig] = useState(loadConfig);
const [showComputer, setShowComputer] = useState(true);
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const [loading, setLoading] = useState(false);
const [activeModal, setActiveModal] = useState(null);
const [draft, setDraft] = useState(null);
const [providerTab, setProviderTab] = useState('Groq');
const openModal = (name) => {
setDraft(JSON.parse(JSON.stringify(config)));
setProviderTab(config.activeProvider || 'Groq');
setActiveModal(name);
};
const commitSave = () => { setConfig(draft); saveConfig(draft); setActiveModal(null); };
// Switch tab and update selected model if current one isn't in new provider
const switchTab = (tab) => {
setProviderTab(tab);
setDraft(d => {
const tabModels = MODELS[tab] || [];
const modelValid = tabModels.includes(d.selectedModel);
return modelValid ? d : { ...d, selectedModel: tabModels[0] || d.selectedModel };
});
};
const handleSend = async () => {
if (!inputText.trim() || loading) return;
setMessages(prev => [...prev, { role: 'user', text: inputText }]);
const prompt = inputText;
setInputText('');
setLoading(true);
try {
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, model: config.selectedModel, api_key: config.activeKey, provider: config.activeProvider }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.detail || `HTTP ${res.status}`);
}
setMessages(prev => [...prev, { role: 'agent', text: data.response, doc: true }]);
} catch (e) {
setMessages(prev => [...prev, { role: 'agent', text: `Error: ${e.message}` }]);
} finally { setLoading(false); }
};
// ── Providers Modal ───────────────────────────────────────────────────────────
const renderProviders = () => {
if (!draft || activeModal !== 'providers') return null;
const keys = draft.providers[providerTab] || [];
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
<div className="bg-[#0f0f0f] border border-white/10 rounded-2xl w-full max-w-2xl shadow-2xl flex flex-col" style={{ maxHeight: '90vh' }}>
{/* Header */}
<div className="flex justify-between items-center px-6 py-4 border-b border-white/10 flex-shrink-0">
<h2 className="text-base font-bold text-white">LLM Providers</h2>
<button onClick={() => setActiveModal(null)} className="text-gray-500 hover:text-white transition-colors"><X className="w-5 h-5" /></button>
</div>
{/* Provider tabs */}
<div className="flex gap-1.5 px-6 pt-4 flex-wrap flex-shrink-0">
{Object.keys(MODELS).map(p => {
const count = (draft.providers[p] || []).filter(k => k.trim()).length;
return (
<button key={p} onClick={() => switchTab(p)}
className={`px-3 py-1.5 text-xs rounded-full border transition-all font-medium ${providerTab === p ? 'bg-blue-600 border-blue-500 text-white' : 'bg-white/5 border-white/10 text-gray-400 hover:text-white hover:border-white/25'}`}>
{p}
{count > 0 && <span className="ml-1.5 bg-green-500 text-black text-[9px] px-1.5 py-0.5 rounded-full font-bold">{count}</span>}
</button>
);
})}
</div>
{/* Scrollable body */}
<div className="flex-grow overflow-y-auto px-6 py-4 space-y-6">
{/* API Keys section */}
<div>
<div className="flex justify-between items-center mb-3">
<div>
<label className="text-xs font-semibold text-gray-300 uppercase tracking-widest">API Keys for {providerTab}</label>
<p className="text-[10px] text-gray-600 mt-0.5">Add multiple keys — click ● to set active</p>
</div>
<button onClick={() => setDraft(d => ({ ...d, providers: { ...d.providers, [providerTab]: [...(d.providers[providerTab] || []), ''] } }))}
className="flex items-center gap-1.5 text-xs text-blue-400 hover:text-blue-300 border border-blue-800 bg-blue-900/20 hover:bg-blue-900/40 px-3 py-1.5 rounded-lg transition-all">
<Plus className="w-3.5 h-3.5" /> Add Key
</button>
</div>
<div className="space-y-2">
{keys.length === 0
? <div className="border border-dashed border-white/10 rounded-xl py-6 text-center text-xs text-gray-600">No keys yet — click <span className="text-blue-400">Add Key</span> above</div>
: keys.map((k, i) => (
<KeyRow key={i} index={i} value={k}
isActive={draft.activeProvider === providerTab && draft.activeKey === k && k !== ''}
onToggleActive={() => { if (k) setDraft(d => ({ ...d, activeProvider: providerTab, activeKey: k })); }}
onChange={val => setDraft(d => { const nk = [...(d.providers[providerTab] || [])]; nk[i] = val; return { ...d, providers: { ...d.providers, [providerTab]: nk } }; })}
onDelete={() => setDraft(d => { const nk = (d.providers[providerTab] || []).filter((_, idx) => idx !== i); return { ...d, providers: { ...d.providers, [providerTab]: nk }, activeKey: d.activeKey === k ? '' : d.activeKey }; })}
/>
))
}
</div>
</div>
{/* Model picker */}
<div>
<label className="text-xs font-semibold text-gray-300 uppercase tracking-widest block mb-3">
Select Model <span className="normal-case font-normal text-gray-600 ml-1">({MODELS[providerTab]?.length} available for {providerTab})</span>
</label>
<ModelPicker
provider={providerTab}
value={draft.selectedModel}
onChange={m => setDraft(d => {
// Auto-switch activeProvider to the tab where model was selected
const hasKeyForTab = (d.providers[providerTab] || []).some(k => k.trim());
return {
...d,
selectedModel: m,
activeProvider: hasKeyForTab ? providerTab : d.activeProvider,
// If switching provider, clear active key so user picks one
activeKey: hasKeyForTab ? (d.activeProvider === providerTab ? d.activeKey : (d.providers[providerTab] || [])[0] || '') : d.activeKey,
};
})}
/>
</div>
{/* Active config summary */}
{draft.activeKey ? (
<div className="bg-green-950/40 border border-green-800/50 rounded-xl p-3 text-xs flex items-center gap-3">
<div className="w-2 h-2 bg-green-400 rounded-full shadow-[0_0_6px_rgba(74,222,128,0.8)] flex-shrink-0" />
<div>
<span className="text-green-300 font-semibold">{draft.activeProvider}</span>
<span className="text-gray-500 mx-2">·</span>
<span className="text-gray-300 font-mono">{draft.selectedModel}</span>
<span className="text-gray-500 mx-2">·</span>
<span className="text-gray-400 font-mono">key ends in …{draft.activeKey.slice(-4)}</span>
</div>
</div>
) : (
<div className="bg-yellow-950/30 border border-yellow-800/40 rounded-xl p-3 text-xs text-yellow-600">
No active key — add a key and click the ● dot to activate it
</div>
)}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-white/10 flex justify-end gap-3 flex-shrink-0">
<button onClick={() => setActiveModal(null)} className="px-4 py-2 rounded-xl text-sm text-gray-400 hover:bg-white/5 transition-colors">Cancel</button>
<button onClick={commitSave} className="px-5 py-2 rounded-xl text-sm bg-blue-600 hover:bg-blue-500 text-white flex items-center gap-2 font-medium transition-colors">
<Save className="w-4 h-4" /> Save & Apply
</button>
</div>
</div>
</div>
);
};
// ── RAG Modal ────────────────────────────────────────────────────────────────
const renderRag = () => {
if (!draft || activeModal !== 'rag') return null;
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
<div className="bg-[#0f0f0f] border border-white/10 rounded-2xl w-full max-w-lg shadow-2xl flex flex-col" style={{ maxHeight: '85vh' }}>
<div className="flex justify-between items-center px-6 py-4 border-b border-white/10">
<h2 className="text-base font-bold text-white">RAG Pipeline</h2>
<button onClick={() => setActiveModal(null)} className="text-gray-500 hover:text-white"><X className="w-5 h-5" /></button>
</div>
<div className="p-6 space-y-4 overflow-y-auto flex-grow">
{[
{ label: 'Vector Database URL', key: 'vectorDbUrl', placeholder: 'https://your-pinecone-or-weaviate-url...' },
].map(({ label, key, placeholder }) => (
<div key={key}>
<label className="text-xs text-gray-400 uppercase tracking-widest font-semibold block mb-1.5">{label}</label>
<input type="text" placeholder={placeholder} value={draft.ragConfig?.[key] || ''}
onChange={e => setDraft(d => ({ ...d, ragConfig: { ...d.ragConfig, [key]: e.target.value } }))}
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-white text-sm outline-none focus:border-blue-500 transition-colors" />
</div>
))}
<div>
<label className="text-xs text-gray-400 uppercase tracking-widest font-semibold block mb-1.5">Embedding Model</label>
<div className="space-y-1">
{EMBEDDING_MODELS.map(m => (
<button key={m} onClick={() => setDraft(d => ({ ...d, ragConfig: { ...d.ragConfig, embeddingModel: m } }))}
className={`w-full text-left px-3 py-2 text-xs rounded-lg border transition-all ${draft.ragConfig?.embeddingModel === m ? 'border-blue-500 bg-blue-950/40 text-blue-300' : 'border-white/10 bg-white/5 text-gray-400 hover:text-white hover:border-white/20'}`}>
{draft.ragConfig?.embeddingModel === m && '● '}{m}
</button>
))}
</div>
</div>
<div>
<label className="text-xs text-gray-400 uppercase tracking-widest font-semibold block mb-1.5">System Prompt Override</label>
<textarea rows="5" placeholder="You are an autonomous coding agent..." value={draft.ragConfig?.systemPrompt || ''}
onChange={e => setDraft(d => ({ ...d, ragConfig: { ...d.ragConfig, systemPrompt: e.target.value } }))}
className="w-full bg-white/5 border border-white/10 rounded-xl p-3 text-white text-sm outline-none focus:border-blue-500 transition-colors resize-none" />
</div>
</div>
<div className="px-6 py-4 border-t border-white/10 flex justify-end gap-3">
<button onClick={() => setActiveModal(null)} className="px-4 py-2 rounded-xl text-sm text-gray-400 hover:bg-white/5">Cancel</button>
<button onClick={commitSave} className="px-5 py-2 rounded-xl text-sm bg-blue-600 hover:bg-blue-500 text-white flex items-center gap-2"><Save className="w-4 h-4" /> Save</button>
</div>
</div>
</div>
);
};
// ── MCP Modal ────────────────────────────────────────────────────────────────
const renderMcp = () => {
if (!draft || activeModal !== 'mcp') return null;
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-md z-50 flex items-center justify-center p-4">
<div className="bg-[#0f0f0f] border border-white/10 rounded-2xl w-full max-w-lg shadow-2xl flex flex-col" style={{ maxHeight: '85vh' }}>
<div className="flex justify-between items-center px-6 py-4 border-b border-white/10">
<h2 className="text-base font-bold text-white">MCP Servers</h2>
<button onClick={() => setActiveModal(null)} className="text-gray-500 hover:text-white"><X className="w-5 h-5" /></button>
</div>
<div className="p-6 space-y-3 overflow-y-auto flex-grow">
<p className="text-xs text-gray-500">Each server exposes tools the agent can call via Model Context Protocol.</p>
{(draft.mcpServers || ['']).map((url, i) => (
<div key={i} className="flex gap-2">
<input type="text" value={url} placeholder={`https://mcp-server-${i + 1}.example.com`}
onChange={e => setDraft(d => { const s = [...(d.mcpServers || [])]; s[i] = e.target.value; return { ...d, mcpServers: s }; })}
className="flex-grow bg-white/5 border border-white/10 rounded-xl p-3 text-white text-sm outline-none focus:border-blue-500 transition-colors font-mono" />
<button onClick={() => setDraft(d => ({ ...d, mcpServers: (d.mcpServers || []).filter((_, idx) => idx !== i) }))}
className="text-gray-600 hover:text-red-400 transition-colors px-2"><Trash2 className="w-4 h-4" /></button>
</div>
))}
<button onClick={() => setDraft(d => ({ ...d, mcpServers: [...(d.mcpServers || []), ''] }))}
className="w-full py-2 text-xs text-blue-400 border border-dashed border-blue-900 bg-blue-900/10 hover:bg-blue-900/20 rounded-xl transition-colors flex items-center justify-center gap-1.5">
<Plus className="w-3.5 h-3.5" /> Add Server
</button>
</div>
<div className="px-6 py-4 border-t border-white/10 flex justify-end gap-3">
<button onClick={() => setActiveModal(null)} className="px-4 py-2 rounded-xl text-sm text-gray-400 hover:bg-white/5">Cancel</button>
<button onClick={commitSave} className="px-5 py-2 rounded-xl text-sm bg-blue-600 hover:bg-blue-500 text-white flex items-center gap-2"><Save className="w-4 h-4" /> Save</button>
</div>
</div>
</div>
);
};
return (
<div className="flex h-screen overflow-hidden bg-[#0a0a0a] text-[#e8e8e8] font-sans">
{/* Sidebar */}
<div className="w-[220px] h-screen border-r border-white/5 bg-[#0d0d0d] flex flex-col p-3 shrink-0">
<div className="flex items-center gap-2 px-2 mb-6 font-bold text-sm">
<Bot className="w-4 h-4 text-blue-400" /> Manus Clone
</div>
{config.activeKey && (
<div className="mb-3 mx-2 px-2 py-2 bg-green-950/30 border border-green-900/50 rounded-lg">
<div className="text-[9px] font-bold text-green-400 uppercase tracking-widest">● Active</div>
<div className="text-[10px] text-gray-400 mt-0.5">{config.activeProvider}</div>
<div className="text-[9px] text-gray-600 font-mono truncate">{config.selectedModel}</div>
</div>
)}
<nav className="space-y-0.5">
{[
{ icon: PlusSquare, label: 'New task', onClick: () => setMessages([]) },
{ icon: Settings, label: 'Providers', onClick: () => openModal('providers') },
{ icon: Database, label: 'RAG Settings', onClick: () => openModal('rag') },
{ icon: Plug, label: 'MCP Servers', onClick: () => openModal('mcp') },
].map(({ icon: Icon, label, onClick }) => (
<button key={label} onClick={onClick}
className="w-full flex items-center gap-3 px-2 py-2.5 rounded-lg text-gray-400 hover:text-white hover:bg-white/5 transition-colors text-sm text-left">
<Icon className="w-4 h-4 flex-shrink-0" /> {label}
</button>
))}
</nav>
</div>
{/* Chat */}
<div className="flex-grow flex flex-col overflow-hidden relative">
<div className="flex-grow overflow-y-auto">
<div className="max-w-2xl mx-auto p-8 space-y-5 pb-32">
{messages.length === 0 && (
<div className="text-center mt-20">
<Bot className="w-10 h-10 mx-auto mb-4 text-gray-700" />
<p className="text-gray-600 text-lg font-light">How can I help?</p>
{!config.activeKey && (
<button onClick={() => openModal('providers')}
className="mt-4 text-xs text-yellow-600 border border-yellow-900/50 bg-yellow-950/20 px-4 py-2 rounded-lg hover:bg-yellow-950/40 transition-colors">
⚠ No API key — click to configure
</button>
)}
</div>
)}
{messages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.doc
? <div className="bg-[#111] border border-white/10 rounded-2xl p-5 w-full">
<div className="flex items-center gap-2 text-blue-400 mb-3 text-[10px] font-bold uppercase tracking-widest">
<FileText className="w-3 h-3" /> Agent Output
</div>
<p className="text-sm text-gray-300 whitespace-pre-wrap leading-relaxed">{msg.text}</p>
</div>
: <div className="bg-[#1a1a2e] border border-white/10 px-4 py-3 rounded-2xl rounded-br-sm max-w-[80%] text-sm">{msg.text}</div>
}
</div>
))}
{loading && (
<div className="flex gap-1.5 px-4 py-3">
{[0, 150, 300].map(d => <div key={d} className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: `${d}ms` }} />)}
</div>
)}
</div>
</div>
{/* Input bar */}
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/90 to-transparent">
<div className="bg-[#141414] border border-white/10 rounded-2xl px-4 py-3 max-w-2xl mx-auto flex items-center gap-3">
<button onClick={() => setShowComputer(s => !s)} className="text-gray-600 hover:text-gray-300 transition-colors">
<Monitor className="w-4 h-4" />
</button>
<input type="text" value={inputText} onChange={e => setInputText(e.target.value)}
onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()}
placeholder={config.activeKey ? `Message (${config.selectedModel})…` : 'Set an API key first…'}
className="flex-grow bg-transparent text-sm text-white outline-none placeholder-gray-600" />
<button onClick={handleSend} disabled={loading || !inputText.trim()}
className="bg-blue-600 hover:bg-blue-500 disabled:bg-white/10 disabled:cursor-not-allowed p-2 rounded-xl transition-colors">
<ArrowUp className="w-4 h-4 text-white" />
</button>
</div>
</div>
</div>
{/* Computer panel */}
{showComputer && (
<div className="w-[38%] shrink-0 border-l border-white/5 bg-black h-screen flex flex-col">
<div className="px-4 py-3 border-b border-white/5 text-[10px] text-gray-600 font-mono flex justify-between items-center">
<span>Manus's computer › Editor</span>
<button onClick={() => setShowComputer(false)}><X className="w-3 h-3 hover:text-white" /></button>
</div>
<div className="flex-grow p-4 font-mono text-xs text-green-400 overflow-y-auto">
<span className="text-gray-700"># Logic Engine Connected</span><br /><br />
<span className="text-blue-400">def</span> <span className="text-yellow-300">run</span>():<br />
&nbsp;&nbsp;&nbsp;&nbsp;print(<span className="text-orange-300">"Agent ready…"</span>)<br />
<span className="animate-pulse text-gray-700"></span>
</div>
<div className="px-4 py-3 border-t border-white/5 flex items-center gap-3 text-[10px] text-gray-700">
<Play className="w-3 h-3 hover:text-white cursor-pointer" />
<SkipForward className="w-3 h-3 hover:text-white cursor-pointer" />
<div className="flex-grow h-0.5 bg-white/5 rounded"><div className="h-full bg-blue-600 rounded w-full" /></div>
<span className="text-blue-500">Live</span>
</div>
</div>
)}
{renderProviders()}
{renderRag()}
{renderMcp()}
</div>
);
};
export default App;