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 (
{/* Active dot */}
{/* Index */}
#{index + 1}
{/* Input always present, toggle visibility */}
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 && (
{masked}
)}
{/* Eye toggle */}
{/* Delete */}
);
}
// ─── 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 (
setQuery(e.target.value)}
placeholder={`Search ${allModels.length} models...`}
className="bg-transparent text-white text-xs outline-none flex-grow placeholder-gray-600"
/>
{query && }
{filtered.length === 0 &&
No models match "{query}"
}
{filtered.map(m => (
))}
{value &&
Selected: {value}
}
);
}
// ─── 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 (
{/* Header */}
LLM Providers
{/* Provider tabs */}
{Object.keys(MODELS).map(p => {
const count = (draft.providers[p] || []).filter(k => k.trim()).length;
return (
);
})}
{/* Scrollable body */}
{/* API Keys section */}
Add multiple keys — click ● to set active
{keys.length === 0
?
No keys yet — click Add Key above
: keys.map((k, i) => (
{ 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 }; })}
/>
))
}
{/* Model picker */}
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,
};
})}
/>
{/* Active config summary */}
{draft.activeKey ? (
{draft.activeProvider}
·
{draft.selectedModel}
·
key ends in …{draft.activeKey.slice(-4)}
) : (
No active key — add a key and click the ● dot to activate it
)}
{/* Footer */}
);
};
// ── RAG Modal ────────────────────────────────────────────────────────────────
const renderRag = () => {
if (!draft || activeModal !== 'rag') return null;
return (
RAG Pipeline
{[
{ label: 'Vector Database URL', key: 'vectorDbUrl', placeholder: 'https://your-pinecone-or-weaviate-url...' },
].map(({ label, key, placeholder }) => (
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" />
))}
{EMBEDDING_MODELS.map(m => (
))}
);
};
// ── MCP Modal ────────────────────────────────────────────────────────────────
const renderMcp = () => {
if (!draft || activeModal !== 'mcp') return null;
return (
MCP Servers
Each server exposes tools the agent can call via Model Context Protocol.
{(draft.mcpServers || ['']).map((url, i) => (
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" />
))}
);
};
return (
{/* Sidebar */}
Manus Clone
{config.activeKey && (
● Active
{config.activeProvider}
{config.selectedModel}
)}
{/* Chat */}
{messages.length === 0 && (
How can I help?
{!config.activeKey && (
)}
)}
{messages.map((msg, i) => (
{msg.doc
?
:
{msg.text}
}
))}
{loading && (
{[0, 150, 300].map(d =>
)}
)}
{/* Input bar */}
{/* Computer panel */}
{showComputer && (
Manus's computer › Editor
# Logic Engine Connected
def run():
print("Agent ready…")
█
)}
{renderProviders()}
{renderRag()}
{renderMcp()}
);
};
export default App;