Spaces:
Sleeping
Sleeping
| 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 /> | |
| 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; | |