@praveencs/agent 0.9.29 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -9
- package/ROADMAP.md +42 -50
- package/agent-skills/README.md +192 -0
- package/agent-skills/plugins/aws/plugin.json +22 -0
- package/agent-skills/plugins/aws/skills/aws-manager.md +17 -0
- package/agent-skills/plugins/aws/tools/aws.js +152 -0
- package/agent-skills/plugins/discord/plugin.json +22 -0
- package/agent-skills/plugins/discord/skills/discord-manager.md +15 -0
- package/agent-skills/plugins/discord/tools/discord.js +150 -0
- package/agent-skills/plugins/docker/plugin.json +21 -0
- package/agent-skills/plugins/docker/skills/docker-manager.md +15 -0
- package/agent-skills/plugins/docker/tools/docker.js +135 -0
- package/agent-skills/plugins/firebase/plugin.json +22 -0
- package/agent-skills/plugins/firebase/skills/firebase-manager.md +14 -0
- package/agent-skills/plugins/firebase/tools/firebase.js +157 -0
- package/agent-skills/plugins/github/plugin.json +23 -0
- package/agent-skills/plugins/github/skills/github-manager.md +15 -0
- package/agent-skills/plugins/github/tools/github.js +133 -0
- package/agent-skills/plugins/huggingface/plugin.json +22 -0
- package/agent-skills/plugins/huggingface/skills/huggingface-manager.md +16 -0
- package/agent-skills/plugins/huggingface/tools/huggingface.js +149 -0
- package/agent-skills/plugins/linear/plugin.json +22 -0
- package/agent-skills/plugins/linear/skills/linear-manager.md +16 -0
- package/agent-skills/plugins/linear/tools/linear.js +199 -0
- package/agent-skills/plugins/mongodb/plugin.json +21 -0
- package/agent-skills/plugins/mongodb/skills/mongodb-manager.md +14 -0
- package/agent-skills/plugins/mongodb/tools/mongodb.js +123 -0
- package/agent-skills/plugins/notion/plugin.json +22 -0
- package/agent-skills/plugins/notion/skills/notion-manager.md +16 -0
- package/agent-skills/plugins/notion/tools/notion.js +158 -0
- package/agent-skills/plugins/openai/plugin.json +23 -0
- package/agent-skills/plugins/openai/skills/openai-manager.md +15 -0
- package/agent-skills/plugins/openai/tools/openai.js +137 -0
- package/agent-skills/plugins/resend/plugin.json +21 -0
- package/agent-skills/plugins/resend/skills/resend-manager.md +15 -0
- package/agent-skills/plugins/resend/tools/resend.js +102 -0
- package/agent-skills/plugins/slack/plugin.json +22 -0
- package/agent-skills/plugins/slack/skills/slack-manager.md +15 -0
- package/agent-skills/plugins/slack/tools/slack.js +168 -0
- package/agent-skills/plugins/stripe/plugin.json +22 -0
- package/agent-skills/plugins/stripe/skills/stripe-manager.md +16 -0
- package/agent-skills/plugins/stripe/tools/stripe.js +174 -0
- package/agent-skills/plugins/supabase/plugin.json +22 -0
- package/agent-skills/plugins/supabase/skills/supabase-manager.md +16 -0
- package/agent-skills/plugins/supabase/tools/supabase.js +153 -0
- package/agent-skills/plugins/telegram/plugin.json +21 -0
- package/agent-skills/plugins/telegram/skills/telegram-manager.md +14 -0
- package/agent-skills/plugins/telegram/tools/telegram.js +131 -0
- package/agent-skills/plugins/vercel/plugin.json +22 -0
- package/agent-skills/plugins/vercel/skills/vercel-manager.md +15 -0
- package/agent-skills/plugins/vercel/tools/vercel.js +145 -0
- package/agent-skills/registry.json +675 -0
- package/agent-skills/skills/api-tester/prompt.md +27 -0
- package/agent-skills/skills/api-tester/skill.json +16 -0
- package/agent-skills/skills/backup/prompt.md +12 -0
- package/agent-skills/skills/backup/skill.json +1 -0
- package/agent-skills/skills/code-review/prompt.md +29 -0
- package/agent-skills/skills/code-review/skill.json +18 -0
- package/agent-skills/skills/create-note/prompt.md +21 -0
- package/agent-skills/skills/create-note/skill.json +15 -0
- package/agent-skills/skills/cron-scheduler/prompt.md +25 -0
- package/agent-skills/skills/cron-scheduler/skill.json +1 -0
- package/agent-skills/skills/db-query/prompt.md +12 -0
- package/agent-skills/skills/db-query/skill.json +1 -0
- package/agent-skills/skills/docker-deploy/prompt.md +21 -0
- package/agent-skills/skills/docker-deploy/skill.json +16 -0
- package/agent-skills/skills/file-organizer/prompt.md +32 -0
- package/agent-skills/skills/file-organizer/skill.json +19 -0
- package/agent-skills/skills/git-commit/prompt.md +21 -0
- package/agent-skills/skills/git-commit/skill.json +17 -0
- package/agent-skills/skills/log-analyzer/prompt.md +16 -0
- package/agent-skills/skills/log-analyzer/skill.json +1 -0
- package/agent-skills/skills/npm-publish/prompt.md +24 -0
- package/agent-skills/skills/npm-publish/skill.json +20 -0
- package/agent-skills/skills/open-vscode/prompt.md +8 -0
- package/agent-skills/skills/open-vscode/skill.json +15 -0
- package/agent-skills/skills/project-scaffold/prompt.md +43 -0
- package/agent-skills/skills/project-scaffold/skill.json +17 -0
- package/agent-skills/skills/send-email/prompt.md +19 -0
- package/agent-skills/skills/send-email/send.js +60 -0
- package/agent-skills/skills/send-email/skill.json +16 -0
- package/agent-skills/skills/system-monitor/prompt.md +27 -0
- package/agent-skills/skills/system-monitor/skill.json +15 -0
- package/agent-skills/skills/web-search/prompt.md +26 -0
- package/agent-skills/skills/web-search/skill.json +16 -0
- package/dist/src/cli/commands/desktop.d.ts +3 -0
- package/dist/src/cli/commands/desktop.d.ts.map +1 -0
- package/dist/src/cli/commands/desktop.js +80 -0
- package/dist/src/cli/commands/desktop.js.map +1 -0
- package/dist/src/cli/commands/multimodal.d.ts +3 -0
- package/dist/src/cli/commands/multimodal.d.ts.map +1 -0
- package/dist/src/cli/commands/multimodal.js +78 -0
- package/dist/src/cli/commands/multimodal.js.map +1 -0
- package/dist/src/cli/commands/plugins.d.ts.map +1 -1
- package/dist/src/cli/commands/plugins.js +63 -12
- package/dist/src/cli/commands/plugins.js.map +1 -1
- package/dist/src/cli/commands/sandbox.d.ts +3 -0
- package/dist/src/cli/commands/sandbox.d.ts.map +1 -0
- package/dist/src/cli/commands/sandbox.js +89 -0
- package/dist/src/cli/commands/sandbox.js.map +1 -0
- package/dist/src/cli/commands/skills.d.ts.map +1 -1
- package/dist/src/cli/commands/skills.js +52 -121
- package/dist/src/cli/commands/skills.js.map +1 -1
- package/dist/src/cli/commands/swarm.d.ts +3 -0
- package/dist/src/cli/commands/swarm.d.ts.map +1 -0
- package/dist/src/cli/commands/swarm.js +120 -0
- package/dist/src/cli/commands/swarm.js.map +1 -0
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +42 -0
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/config/loader.d.ts +4 -0
- package/dist/src/config/loader.d.ts.map +1 -1
- package/dist/src/config/loader.js +27 -6
- package/dist/src/config/loader.js.map +1 -1
- package/dist/src/config/schema.d.ts +261 -0
- package/dist/src/config/schema.d.ts.map +1 -1
- package/dist/src/config/schema.js +58 -0
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/desktop/engine.d.ts +42 -0
- package/dist/src/desktop/engine.d.ts.map +1 -0
- package/dist/src/desktop/engine.js +77 -0
- package/dist/src/desktop/engine.js.map +1 -0
- package/dist/src/desktop/index.d.ts +6 -0
- package/dist/src/desktop/index.d.ts.map +1 -0
- package/dist/src/desktop/index.js +6 -0
- package/dist/src/desktop/index.js.map +1 -0
- package/dist/src/desktop/input.d.ts +20 -0
- package/dist/src/desktop/input.d.ts.map +1 -0
- package/dist/src/desktop/input.js +160 -0
- package/dist/src/desktop/input.js.map +1 -0
- package/dist/src/desktop/screen.d.ts +17 -0
- package/dist/src/desktop/screen.d.ts.map +1 -0
- package/dist/src/desktop/screen.js +120 -0
- package/dist/src/desktop/screen.js.map +1 -0
- package/dist/src/desktop/types.d.ts +50 -0
- package/dist/src/desktop/types.d.ts.map +1 -0
- package/dist/src/desktop/types.js +10 -0
- package/dist/src/desktop/types.js.map +1 -0
- package/dist/src/hub/lockfile.d.ts +29 -0
- package/dist/src/hub/lockfile.d.ts.map +1 -0
- package/dist/src/hub/lockfile.js +72 -0
- package/dist/src/hub/lockfile.js.map +1 -0
- package/dist/src/hub/publisher.d.ts +13 -0
- package/dist/src/hub/publisher.d.ts.map +1 -0
- package/dist/src/hub/publisher.js +159 -0
- package/dist/src/hub/publisher.js.map +1 -0
- package/dist/src/hub/registry.d.ts +40 -0
- package/dist/src/hub/registry.d.ts.map +1 -0
- package/dist/src/hub/registry.js +123 -0
- package/dist/src/hub/registry.js.map +1 -0
- package/dist/src/multimodal/engine.d.ts +33 -0
- package/dist/src/multimodal/engine.d.ts.map +1 -0
- package/dist/src/multimodal/engine.js +63 -0
- package/dist/src/multimodal/engine.js.map +1 -0
- package/dist/src/multimodal/index.d.ts +7 -0
- package/dist/src/multimodal/index.d.ts.map +1 -0
- package/dist/src/multimodal/index.js +7 -0
- package/dist/src/multimodal/index.js.map +1 -0
- package/dist/src/multimodal/tts.d.ts +11 -0
- package/dist/src/multimodal/tts.d.ts.map +1 -0
- package/dist/src/multimodal/tts.js +48 -0
- package/dist/src/multimodal/tts.js.map +1 -0
- package/dist/src/multimodal/types.d.ts +55 -0
- package/dist/src/multimodal/types.d.ts.map +1 -0
- package/dist/src/multimodal/types.js +20 -0
- package/dist/src/multimodal/types.js.map +1 -0
- package/dist/src/multimodal/vision.d.ts +15 -0
- package/dist/src/multimodal/vision.d.ts.map +1 -0
- package/dist/src/multimodal/vision.js +98 -0
- package/dist/src/multimodal/vision.js.map +1 -0
- package/dist/src/multimodal/voice.d.ts +11 -0
- package/dist/src/multimodal/voice.d.ts.map +1 -0
- package/dist/src/multimodal/voice.js +43 -0
- package/dist/src/multimodal/voice.js.map +1 -0
- package/dist/src/plans/types.d.ts +18 -18
- package/dist/src/plugins/loader.d.ts.map +1 -1
- package/dist/src/plugins/loader.js +10 -0
- package/dist/src/plugins/loader.js.map +1 -1
- package/dist/src/sandbox/docker.d.ts +42 -0
- package/dist/src/sandbox/docker.d.ts.map +1 -0
- package/dist/src/sandbox/docker.js +131 -0
- package/dist/src/sandbox/docker.js.map +1 -0
- package/dist/src/sandbox/engine.d.ts +50 -0
- package/dist/src/sandbox/engine.d.ts.map +1 -0
- package/dist/src/sandbox/engine.js +133 -0
- package/dist/src/sandbox/engine.js.map +1 -0
- package/dist/src/sandbox/index.d.ts +5 -0
- package/dist/src/sandbox/index.d.ts.map +1 -0
- package/dist/src/sandbox/index.js +5 -0
- package/dist/src/sandbox/index.js.map +1 -0
- package/dist/src/sandbox/types.d.ts +41 -0
- package/dist/src/sandbox/types.d.ts.map +1 -0
- package/dist/src/sandbox/types.js +12 -0
- package/dist/src/sandbox/types.js.map +1 -0
- package/dist/src/scripts/types.d.ts +2 -2
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +133 -0
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/skills/loader.d.ts.map +1 -1
- package/dist/src/skills/loader.js +10 -0
- package/dist/src/skills/loader.js.map +1 -1
- package/dist/src/swarm/bus.d.ts +27 -0
- package/dist/src/swarm/bus.d.ts.map +1 -0
- package/dist/src/swarm/bus.js +64 -0
- package/dist/src/swarm/bus.js.map +1 -0
- package/dist/src/swarm/index.d.ts +6 -0
- package/dist/src/swarm/index.d.ts.map +1 -0
- package/dist/src/swarm/index.js +6 -0
- package/dist/src/swarm/index.js.map +1 -0
- package/dist/src/swarm/orchestrator.d.ts +79 -0
- package/dist/src/swarm/orchestrator.d.ts.map +1 -0
- package/dist/src/swarm/orchestrator.js +271 -0
- package/dist/src/swarm/orchestrator.js.map +1 -0
- package/dist/src/swarm/roles.d.ts +18 -0
- package/dist/src/swarm/roles.d.ts.map +1 -0
- package/dist/src/swarm/roles.js +83 -0
- package/dist/src/swarm/roles.js.map +1 -0
- package/dist/src/swarm/types.d.ts +58 -0
- package/dist/src/swarm/types.d.ts.map +1 -0
- package/dist/src/swarm/types.js +10 -0
- package/dist/src/swarm/types.js.map +1 -0
- package/dist/src/tools/core/cmd.d.ts.map +1 -1
- package/dist/src/tools/core/cmd.js +28 -0
- package/dist/src/tools/core/cmd.js.map +1 -1
- package/dist/src/utils/paths.d.ts +4 -4
- package/dist/src/utils/paths.d.ts.map +1 -1
- package/dist/src/utils/paths.js +8 -8
- package/dist/src/utils/paths.js.map +1 -1
- package/docs/DOCUMENTATION.md +137 -0
- package/package.json +4 -2
- package/studio/README.md +73 -0
- package/studio/eslint.config.js +23 -0
- package/studio/index.html +13 -0
- package/studio/package-lock.json +4350 -0
- package/studio/package.json +44 -0
- package/studio/postcss.config.js +6 -0
- package/studio/public/vite.svg +1 -0
- package/studio/src/App.tsx +243 -0
- package/studio/src/assets/react.svg +1 -0
- package/studio/src/components/Capabilities.tsx +80 -0
- package/studio/src/components/CommandsManager.tsx +94 -0
- package/studio/src/components/CostDashboard.tsx +182 -0
- package/studio/src/components/CredentialCapture.tsx +196 -0
- package/studio/src/components/CredentialsManager.tsx +257 -0
- package/studio/src/components/DaemonPanel.tsx +91 -0
- package/studio/src/components/DesktopPanel.tsx +118 -0
- package/studio/src/components/GoalTemplates.tsx +190 -0
- package/studio/src/components/GoalsPanel.tsx +235 -0
- package/studio/src/components/MemoryExplorer.tsx +152 -0
- package/studio/src/components/MultimodalPanel.tsx +150 -0
- package/studio/src/components/NotificationsPanel.tsx +175 -0
- package/studio/src/components/PluginsManager.tsx +60 -0
- package/studio/src/components/SandboxPanel.tsx +118 -0
- package/studio/src/components/ScriptsManager.tsx +269 -0
- package/studio/src/components/SkillsManager.tsx +123 -0
- package/studio/src/components/SwarmPanel.tsx +149 -0
- package/studio/src/components/TaskStreaming.tsx +189 -0
- package/studio/src/components/Terminal.tsx +200 -0
- package/studio/src/index.css +51 -0
- package/studio/src/main.tsx +13 -0
- package/studio/tailwind.config.js +47 -0
- package/studio/tsconfig.app.json +28 -0
- package/studio/tsconfig.json +7 -0
- package/studio/tsconfig.node.json +26 -0
- package/studio/vite.config.ts +7 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { Brain, Search, Plus, Trash2 } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
const API = 'http://localhost:3333/api';
|
|
6
|
+
|
|
7
|
+
const categoryColors: Record<string, string> = {
|
|
8
|
+
project: 'text-blue-400 bg-blue-500/10',
|
|
9
|
+
preference: 'text-purple-400 bg-purple-500/10',
|
|
10
|
+
fact: 'text-emerald-400 bg-emerald-500/10',
|
|
11
|
+
learned: 'text-amber-400 bg-amber-500/10',
|
|
12
|
+
general: 'text-neutral-400 bg-neutral-500/10',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function MemoryExplorer() {
|
|
16
|
+
const { id } = useParams<{ id: string }>();
|
|
17
|
+
const [data, setData] = useState<any>(null);
|
|
18
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
19
|
+
const [searchResults, setSearchResults] = useState<any[] | null>(null);
|
|
20
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
21
|
+
const [form, setForm] = useState({ content: '', category: 'general', tags: '' });
|
|
22
|
+
|
|
23
|
+
const emptyData = { stats: { total: 0, byCategory: {}, bySource: {} }, memories: [] };
|
|
24
|
+
|
|
25
|
+
const load = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${API}/instances/${id}/memory`);
|
|
28
|
+
if (!res.ok) { setData(emptyData); return; }
|
|
29
|
+
const d = await res.json();
|
|
30
|
+
setData(d ?? emptyData);
|
|
31
|
+
} catch { setData(emptyData); }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
useEffect(() => { load(); }, [id]);
|
|
35
|
+
|
|
36
|
+
const search = async () => {
|
|
37
|
+
if (!searchQuery.trim()) { setSearchResults(null); return; }
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${API}/instances/${id}/memory/search?q=${encodeURIComponent(searchQuery)}`);
|
|
40
|
+
if (!res.ok) { setSearchResults([]); return; }
|
|
41
|
+
const d = await res.json();
|
|
42
|
+
setSearchResults(d.results || []);
|
|
43
|
+
} catch { setSearchResults([]); }
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const addMemory = async () => {
|
|
47
|
+
if (!form.content.trim()) return;
|
|
48
|
+
await fetch(`${API}/instances/${id}/memory`, {
|
|
49
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
content: form.content, category: form.category,
|
|
52
|
+
tags: form.tags ? form.tags.split(',').map(s => s.trim()) : [],
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
setForm({ content: '', category: 'general', tags: '' });
|
|
56
|
+
setShowAdd(false);
|
|
57
|
+
load();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const deleteMemory = async (memoryId: number) => {
|
|
61
|
+
await fetch(`${API}/instances/${id}/memory/${memoryId}`, { method: 'DELETE' });
|
|
62
|
+
load();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!data) return <div className="p-8 text-neutral-500 animate-pulse">Loading memories...</div>;
|
|
66
|
+
|
|
67
|
+
const stats = data.stats ?? { total: 0, byCategory: {}, bySource: {} };
|
|
68
|
+
const memories = searchResults ?? data.memories ?? [];
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col h-full bg-neutral-950 overflow-y-auto p-8">
|
|
72
|
+
<div className="flex items-center justify-between mb-6">
|
|
73
|
+
<div>
|
|
74
|
+
<h2 className="text-xl font-semibold flex items-center gap-2">
|
|
75
|
+
<Brain className="text-violet-400" size={22} /> Memory Explorer
|
|
76
|
+
</h2>
|
|
77
|
+
<p className="text-neutral-500 text-sm mt-1">{stats.total} memories stored</p>
|
|
78
|
+
</div>
|
|
79
|
+
<button onClick={() => setShowAdd(!showAdd)}
|
|
80
|
+
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-violet-500/20 border border-violet-500/30 text-violet-300 text-sm font-medium hover:bg-violet-500/30 transition-colors">
|
|
81
|
+
<Plus size={16} /> Add Memory
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Stats */}
|
|
86
|
+
<div className="grid grid-cols-5 gap-3 mb-6">
|
|
87
|
+
{Object.entries(stats.byCategory || {}).map(([cat, count]) => (
|
|
88
|
+
<div key={cat} className="border border-white/10 rounded-lg p-3 bg-white/[0.02] text-center">
|
|
89
|
+
<div className={`text-xs font-medium uppercase tracking-wider ${categoryColors[cat]?.split(' ')[0] ?? 'text-neutral-400'}`}>{cat}</div>
|
|
90
|
+
<div className="text-lg font-semibold text-neutral-200 mt-1">{count as number}</div>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Search */}
|
|
96
|
+
<div className="flex gap-3 mb-6">
|
|
97
|
+
<div className="flex-1 relative">
|
|
98
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-600" size={16} />
|
|
99
|
+
<input value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
|
|
100
|
+
onKeyDown={e => e.key === 'Enter' && search()}
|
|
101
|
+
placeholder="Search memories..."
|
|
102
|
+
className="w-full bg-neutral-900 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 text-sm placeholder-neutral-600 focus:outline-none focus:border-violet-500/50" />
|
|
103
|
+
</div>
|
|
104
|
+
<button onClick={search} className="px-4 py-2.5 rounded-lg bg-violet-500/20 border border-violet-500/30 text-violet-300 text-sm font-medium hover:bg-violet-500/30 transition-colors">Search</button>
|
|
105
|
+
{searchResults && (
|
|
106
|
+
<button onClick={() => { setSearchResults(null); setSearchQuery(''); }} className="px-3 text-sm text-neutral-500 hover:text-neutral-300">Clear</button>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Add Memory Form */}
|
|
111
|
+
{showAdd && (
|
|
112
|
+
<div className="border border-white/10 rounded-xl p-5 bg-white/[0.02] mb-6 space-y-4">
|
|
113
|
+
<textarea value={form.content} onChange={e => setForm({ ...form, content: e.target.value })} placeholder="Memory content..."
|
|
114
|
+
rows={3} className="w-full bg-neutral-900 border border-white/10 rounded-lg px-4 py-2.5 text-sm placeholder-neutral-600 focus:outline-none focus:border-violet-500/50 resize-none" />
|
|
115
|
+
<div className="flex gap-4">
|
|
116
|
+
<select value={form.category} onChange={e => setForm({ ...form, category: e.target.value })}
|
|
117
|
+
className="bg-neutral-900 border border-white/10 rounded-lg px-3 py-2 text-sm">
|
|
118
|
+
<option value="general">General</option>
|
|
119
|
+
<option value="fact">Fact</option>
|
|
120
|
+
<option value="project">Project</option>
|
|
121
|
+
<option value="preference">Preference</option>
|
|
122
|
+
<option value="learned">Learned</option>
|
|
123
|
+
</select>
|
|
124
|
+
<input value={form.tags} onChange={e => setForm({ ...form, tags: e.target.value })} placeholder="Tags (comma-separated)..."
|
|
125
|
+
className="flex-1 bg-neutral-900 border border-white/10 rounded-lg px-4 py-2 text-sm placeholder-neutral-600 focus:outline-none focus:border-violet-500/50" />
|
|
126
|
+
<button onClick={addMemory} className="px-4 py-2 rounded-lg bg-violet-500 text-white text-sm font-medium hover:bg-violet-600 transition-colors">Save</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Memory List */}
|
|
132
|
+
<div className="space-y-2">
|
|
133
|
+
{memories.map((m: any) => (
|
|
134
|
+
<div key={m.id} className="border border-white/10 rounded-lg bg-white/[0.02] p-4 flex items-start gap-3 hover:border-white/20 transition-all">
|
|
135
|
+
<span className={`text-[10px] font-mono px-2 py-0.5 rounded mt-0.5 ${categoryColors[m.category] ?? categoryColors.general}`}>{m.category}</span>
|
|
136
|
+
<div className="flex-1 min-w-0">
|
|
137
|
+
<div className="text-sm text-neutral-300 leading-relaxed">{m.content}</div>
|
|
138
|
+
<div className="flex items-center gap-3 mt-2">
|
|
139
|
+
<span className="text-[10px] text-neutral-600">{m.source}</span>
|
|
140
|
+
<span className="text-[10px] text-neutral-600">{new Date(m.created_at).toLocaleString()}</span>
|
|
141
|
+
{m.tags?.length > 0 && m.tags.map((t: string) => (
|
|
142
|
+
<span key={t} className="text-[10px] text-neutral-500 bg-neutral-800 px-1.5 py-0.5 rounded">{t}</span>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<button onClick={() => deleteMemory(m.id)} className="p-1.5 rounded hover:bg-white/10 text-red-400 shrink-0"><Trash2 size={14} /></button>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function MultimodalPanel() {
|
|
4
|
+
const [activeTab, setActiveTab] = useState<'voice' | 'vision' | 'tts'>('voice');
|
|
5
|
+
const [result, setResult] = useState<string>('');
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const [imagePrompt, setImagePrompt] = useState('Describe this image in detail.');
|
|
8
|
+
|
|
9
|
+
const tabs = [
|
|
10
|
+
{ id: 'voice' as const, label: '🎤 Voice', desc: 'Speech-to-Text (Whisper)' },
|
|
11
|
+
{ id: 'vision' as const, label: '👁️ Vision', desc: 'Image Analysis (GPT-4o)' },
|
|
12
|
+
{ id: 'tts' as const, label: '🔊 TTS', desc: 'Text-to-Speech' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const handleVoiceUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
|
+
const file = e.target.files?.[0];
|
|
17
|
+
if (!file) return;
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setResult('');
|
|
20
|
+
try {
|
|
21
|
+
const formData = new FormData();
|
|
22
|
+
formData.append('audio', file);
|
|
23
|
+
const res = await fetch('/api/multimodal/transcribe', { method: 'POST', body: formData });
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
setResult(data.text || JSON.stringify(data));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
setResult(`Error: ${(err as Error).message}`);
|
|
28
|
+
} finally { setLoading(false); }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
|
+
const file = e.target.files?.[0];
|
|
33
|
+
if (!file) return;
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setResult('');
|
|
36
|
+
try {
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
formData.append('image', file);
|
|
39
|
+
formData.append('prompt', imagePrompt);
|
|
40
|
+
const res = await fetch('/api/multimodal/analyze', { method: 'POST', body: formData });
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
setResult(data.description || JSON.stringify(data));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setResult(`Error: ${(err as Error).message}`);
|
|
45
|
+
} finally { setLoading(false); }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const [ttsText, setTtsText] = useState('');
|
|
49
|
+
const [ttsVoice, setTtsVoice] = useState('alloy');
|
|
50
|
+
|
|
51
|
+
const handleSpeak = async () => {
|
|
52
|
+
if (!ttsText.trim()) return;
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setResult('');
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch('/api/multimodal/speak', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({ text: ttsText, voice: ttsVoice }),
|
|
60
|
+
});
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
setResult(`Audio saved: ${data.audioPath} (${data.format})`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
setResult(`Error: ${(err as Error).message}`);
|
|
65
|
+
} finally { setLoading(false); }
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="p-6 space-y-6 overflow-y-auto h-full">
|
|
70
|
+
<div>
|
|
71
|
+
<h2 className="text-xl font-semibold mb-1">🌈 Multimodal Interfaces</h2>
|
|
72
|
+
<p className="text-neutral-500 text-sm">Voice input, image analysis, and text-to-speech powered by OpenAI.</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Tab Switcher */}
|
|
76
|
+
<div className="flex gap-2">
|
|
77
|
+
{tabs.map(t => (
|
|
78
|
+
<button
|
|
79
|
+
key={t.id}
|
|
80
|
+
onClick={() => { setActiveTab(t.id); setResult(''); }}
|
|
81
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium border transition-colors ${activeTab === t.id
|
|
82
|
+
? 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20'
|
|
83
|
+
: 'bg-neutral-900/40 text-neutral-400 border-white/10 hover:bg-white/5'
|
|
84
|
+
}`}
|
|
85
|
+
>
|
|
86
|
+
{t.label}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Active Panel */}
|
|
92
|
+
<div className="border border-white/10 rounded-xl bg-neutral-900/40 p-6">
|
|
93
|
+
{activeTab === 'voice' && (
|
|
94
|
+
<div className="space-y-4">
|
|
95
|
+
<h3 className="text-sm font-medium">Upload Audio File</h3>
|
|
96
|
+
<p className="text-xs text-neutral-500">Supports WAV, MP3, WebM. Uses OpenAI Whisper for transcription.</p>
|
|
97
|
+
<input type="file" accept="audio/*" onChange={handleVoiceUpload}
|
|
98
|
+
className="text-sm text-neutral-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border file:border-white/10 file:bg-neutral-800 file:text-neutral-300 file:text-sm hover:file:bg-neutral-700 file:cursor-pointer" />
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{activeTab === 'vision' && (
|
|
103
|
+
<div className="space-y-4">
|
|
104
|
+
<h3 className="text-sm font-medium">Upload Image for Analysis</h3>
|
|
105
|
+
<p className="text-xs text-neutral-500">Uses GPT-4o vision to understand image content.</p>
|
|
106
|
+
<input value={imagePrompt} onChange={e => setImagePrompt(e.target.value)}
|
|
107
|
+
className="w-full bg-neutral-800 border border-white/10 rounded-lg px-3 py-2 text-sm" placeholder="Analysis prompt..." />
|
|
108
|
+
<input type="file" accept="image/*" onChange={handleImageUpload}
|
|
109
|
+
className="text-sm text-neutral-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border file:border-white/10 file:bg-neutral-800 file:text-neutral-300 file:text-sm hover:file:bg-neutral-700 file:cursor-pointer" />
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{activeTab === 'tts' && (
|
|
114
|
+
<div className="space-y-4">
|
|
115
|
+
<h3 className="text-sm font-medium">Text to Speech</h3>
|
|
116
|
+
<p className="text-xs text-neutral-500">Convert text to natural speech using OpenAI TTS.</p>
|
|
117
|
+
<textarea value={ttsText} onChange={e => setTtsText(e.target.value)} rows={3}
|
|
118
|
+
className="w-full bg-neutral-800 border border-white/10 rounded-lg px-3 py-2 text-sm resize-none" placeholder="Enter text to speak..." />
|
|
119
|
+
<div className="flex items-center gap-3">
|
|
120
|
+
<select value={ttsVoice} onChange={e => setTtsVoice(e.target.value)}
|
|
121
|
+
className="bg-neutral-800 border border-white/10 rounded-lg px-3 py-2 text-sm">
|
|
122
|
+
{['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'].map(v =>
|
|
123
|
+
<option key={v} value={v}>{v}</option>
|
|
124
|
+
)}
|
|
125
|
+
</select>
|
|
126
|
+
<button onClick={handleSpeak}
|
|
127
|
+
className="px-4 py-2 bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 rounded-lg text-sm hover:bg-indigo-500/20 transition-colors">
|
|
128
|
+
Generate Speech
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Result Area */}
|
|
136
|
+
{(loading || result) && (
|
|
137
|
+
<div className="border border-white/10 rounded-xl bg-neutral-900/40 p-5">
|
|
138
|
+
<h3 className="text-sm font-medium mb-3">Result</h3>
|
|
139
|
+
{loading ? (
|
|
140
|
+
<div className="text-neutral-500 text-sm">Processing...</div>
|
|
141
|
+
) : (
|
|
142
|
+
<div className="bg-black/30 rounded-lg p-4 text-sm text-neutral-300 whitespace-pre-wrap">
|
|
143
|
+
{result}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { Bell, CheckCircle2, AlertTriangle, XCircle, Info, RefreshCw, Clock } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
const API = '';
|
|
6
|
+
|
|
7
|
+
interface Notification {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
level: string;
|
|
10
|
+
title: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const levelConfig: Record<string, { icon: typeof Bell; color: string; bg: string; border: string; label: string }> = {
|
|
15
|
+
success: { icon: CheckCircle2, color: '#22c55e', bg: 'rgba(34,197,94,0.08)', border: 'rgba(34,197,94,0.2)', label: 'SUCCESS' },
|
|
16
|
+
error: { icon: XCircle, color: '#ef4444', bg: 'rgba(239,68,68,0.08)', border: 'rgba(239,68,68,0.2)', label: 'ERROR' },
|
|
17
|
+
warning: { icon: AlertTriangle, color: '#f59e0b', bg: 'rgba(245,158,11,0.08)', border: 'rgba(245,158,11,0.2)', label: 'WARNING' },
|
|
18
|
+
info: { icon: Info, color: '#6366f1', bg: 'rgba(99,102,241,0.08)', border: 'rgba(99,102,241,0.2)', label: 'INFO' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function timeAgo(timestamp: string): string {
|
|
22
|
+
const diff = Date.now() - new Date(timestamp).getTime();
|
|
23
|
+
if (diff < 60000) return 'just now';
|
|
24
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
25
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
26
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function NotificationsPanel() {
|
|
30
|
+
const { id } = useParams();
|
|
31
|
+
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [filter, setFilter] = useState<string>('all');
|
|
34
|
+
|
|
35
|
+
const load = useCallback(() => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
fetch(`${API}/api/instances/${id}/notifications`)
|
|
38
|
+
.then(r => r.json())
|
|
39
|
+
.then(d => setNotifications(d.notifications || []))
|
|
40
|
+
.catch(console.error)
|
|
41
|
+
.finally(() => setLoading(false));
|
|
42
|
+
}, [id]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
load();
|
|
46
|
+
const interval = setInterval(load, 10000);
|
|
47
|
+
return () => clearInterval(interval);
|
|
48
|
+
}, [load]);
|
|
49
|
+
|
|
50
|
+
const filtered = filter === 'all' ? notifications : notifications.filter(n => n.level === filter);
|
|
51
|
+
|
|
52
|
+
const counts = {
|
|
53
|
+
all: notifications.length,
|
|
54
|
+
success: notifications.filter(n => n.level === 'success').length,
|
|
55
|
+
error: notifications.filter(n => n.level === 'error').length,
|
|
56
|
+
warning: notifications.filter(n => n.level === 'warning').length,
|
|
57
|
+
info: notifications.filter(n => n.level === 'info').length,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div style={{ padding: 32, maxWidth: 900, overflowY: 'auto', height: '100%' }}>
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 }}>
|
|
64
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
65
|
+
<Bell size={28} color="#f59e0b" />
|
|
66
|
+
<h2 style={{ margin: 0, color: '#fff', fontSize: 22 }}>Notifications</h2>
|
|
67
|
+
<span style={{ color: '#555', fontSize: 13 }}>({notifications.length})</span>
|
|
68
|
+
</div>
|
|
69
|
+
<button
|
|
70
|
+
onClick={load}
|
|
71
|
+
style={{
|
|
72
|
+
display: 'flex', alignItems: 'center', gap: 6,
|
|
73
|
+
padding: '6px 14px', background: '#1a1a2e',
|
|
74
|
+
border: '1px solid #2a2a40', borderRadius: 8,
|
|
75
|
+
color: '#aaa', fontSize: 12, cursor: 'pointer',
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<RefreshCw size={13} className={loading ? 'animate-spin' : ''} /> Refresh
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Filter Tabs */}
|
|
83
|
+
<div style={{ display: 'flex', gap: 6, marginBottom: 20, flexWrap: 'wrap' }}>
|
|
84
|
+
{(['all', 'success', 'error', 'warning', 'info'] as const).map(f => {
|
|
85
|
+
const isActive = filter === f;
|
|
86
|
+
const cfg = f === 'all' ? null : levelConfig[f];
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
key={f}
|
|
90
|
+
onClick={() => setFilter(f)}
|
|
91
|
+
style={{
|
|
92
|
+
padding: '5px 14px', borderRadius: 20,
|
|
93
|
+
border: `1px solid ${isActive ? (cfg?.color || '#6366f1') : '#2a2a40'}`,
|
|
94
|
+
background: isActive ? (cfg?.bg || 'rgba(99,102,241,0.08)') : 'transparent',
|
|
95
|
+
color: isActive ? (cfg?.color || '#6366f1') : '#666',
|
|
96
|
+
fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
|
97
|
+
display: 'flex', alignItems: 'center', gap: 6,
|
|
98
|
+
transition: 'all 0.15s',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
102
|
+
{(counts as any)[f] > 0 && (
|
|
103
|
+
<span style={{
|
|
104
|
+
background: isActive ? (cfg?.color || '#6366f1') : '#333',
|
|
105
|
+
color: isActive ? '#fff' : '#888',
|
|
106
|
+
padding: '1px 6px', borderRadius: 10, fontSize: 10, fontWeight: 700,
|
|
107
|
+
}}>
|
|
108
|
+
{(counts as any)[f]}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Notification List */}
|
|
117
|
+
{filtered.length === 0 ? (
|
|
118
|
+
<div style={{
|
|
119
|
+
color: '#555', fontSize: 13, padding: 40,
|
|
120
|
+
background: '#1a1a2e', borderRadius: 12, textAlign: 'center',
|
|
121
|
+
}}>
|
|
122
|
+
<Bell size={36} style={{ opacity: 0.2, marginBottom: 12 }} />
|
|
123
|
+
<p style={{ margin: 0 }}>No notifications yet</p>
|
|
124
|
+
<p style={{ margin: '4px 0 0', fontSize: 11, color: '#444' }}>
|
|
125
|
+
Notifications appear when goals complete, fail, or tasks finish.
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
130
|
+
{filtered.map((n, i) => {
|
|
131
|
+
const cfg = levelConfig[n.level] || levelConfig.info;
|
|
132
|
+
const Icon = cfg.icon;
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
key={i}
|
|
136
|
+
style={{
|
|
137
|
+
display: 'flex', alignItems: 'flex-start', gap: 12,
|
|
138
|
+
padding: '12px 16px', background: cfg.bg,
|
|
139
|
+
border: `1px solid ${cfg.border}`,
|
|
140
|
+
borderRadius: 10, transition: 'all 0.15s',
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<Icon size={18} color={cfg.color} style={{ marginTop: 2, flexShrink: 0 }} />
|
|
144
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
145
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
|
146
|
+
<span style={{
|
|
147
|
+
fontSize: 10, fontWeight: 700, letterSpacing: 0.5,
|
|
148
|
+
color: cfg.color, background: `${cfg.color}22`,
|
|
149
|
+
padding: '1px 6px', borderRadius: 4,
|
|
150
|
+
}}>
|
|
151
|
+
{cfg.label}
|
|
152
|
+
</span>
|
|
153
|
+
<span style={{ fontWeight: 600, color: '#ddd', fontSize: 13 }}>
|
|
154
|
+
{n.title}
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
<div style={{ color: '#999', fontSize: 12, lineHeight: 1.5, wordBreak: 'break-word' }}>
|
|
158
|
+
{n.message}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div style={{
|
|
162
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
163
|
+
color: '#555', fontSize: 11, whiteSpace: 'nowrap', flexShrink: 0,
|
|
164
|
+
}}>
|
|
165
|
+
<Clock size={11} />
|
|
166
|
+
{timeAgo(n.timestamp)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { Package, Trash2 } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
const API = 'http://localhost:3333/api';
|
|
6
|
+
|
|
7
|
+
export function PluginsManager() {
|
|
8
|
+
const { id } = useParams<{ id: string }>();
|
|
9
|
+
const [plugins, setPlugins] = useState<any[]>([]);
|
|
10
|
+
|
|
11
|
+
const load = () => {
|
|
12
|
+
fetch(`${API}/instances/${id}/plugins`).then(r => r.json()).then(d => setPlugins(d.plugins || [])).catch(console.error);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
useEffect(() => { load(); }, [id]);
|
|
16
|
+
|
|
17
|
+
const deletePlugin = async (name: string) => {
|
|
18
|
+
if (!confirm(`Remove plugin "${name}"?`)) return;
|
|
19
|
+
await fetch(`${API}/instances/${id}/plugins/${name}`, { method: 'DELETE' });
|
|
20
|
+
load();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col h-full bg-neutral-950 overflow-y-auto p-8">
|
|
25
|
+
<div className="flex items-center justify-between mb-8">
|
|
26
|
+
<div>
|
|
27
|
+
<h2 className="text-xl font-semibold flex items-center gap-2">
|
|
28
|
+
<Package className="text-indigo-400" size={22} /> Plugins
|
|
29
|
+
</h2>
|
|
30
|
+
<p className="text-neutral-500 text-sm mt-1">{plugins.length} plugin{plugins.length !== 1 ? 's' : ''} installed</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{plugins.length === 0 ? (
|
|
35
|
+
<div className="border border-white/10 border-dashed rounded-xl p-16 text-center">
|
|
36
|
+
<Package className="mx-auto mb-4 text-neutral-600" size={32} />
|
|
37
|
+
<p className="text-neutral-500">No plugins installed.</p>
|
|
38
|
+
<p className="text-neutral-600 text-sm mt-2">Install plugins via CLI: <code className="bg-neutral-800 px-1.5 py-0.5 rounded text-neutral-400">agent plugins install ./my-plugin</code></p>
|
|
39
|
+
</div>
|
|
40
|
+
) : (
|
|
41
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
42
|
+
{plugins.map((p) => (
|
|
43
|
+
<div key={p.name} className="border border-white/10 rounded-xl bg-white/[0.02] p-5 flex flex-col hover:border-white/20 transition-all">
|
|
44
|
+
<div className="flex items-start justify-between mb-2">
|
|
45
|
+
<div className="font-medium text-neutral-200">{p.name}</div>
|
|
46
|
+
<button onClick={() => deletePlugin(p.name)} className="p-1.5 rounded hover:bg-white/10 text-red-400"><Trash2 size={14} /></button>
|
|
47
|
+
</div>
|
|
48
|
+
<p className="text-sm text-neutral-400 mt-1 flex-1">{p.description}</p>
|
|
49
|
+
<div className="flex items-center gap-2 mt-3">
|
|
50
|
+
<span className="text-[10px] font-mono text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded">v{p.version || '1.0.0'}</span>
|
|
51
|
+
{p.skills?.length > 0 && <span className="text-[10px] text-neutral-500">{p.skills.length} skill(s)</span>}
|
|
52
|
+
{p.commands?.length > 0 && <span className="text-[10px] text-neutral-500">{p.commands.length} cmd(s)</span>}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface SandboxStatus {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
running: boolean;
|
|
6
|
+
image: string;
|
|
7
|
+
containerId?: string;
|
|
8
|
+
uptime?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function SandboxPanel() {
|
|
12
|
+
const [status, setStatus] = useState<SandboxStatus | null>(null);
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
fetchStatus();
|
|
17
|
+
const interval = setInterval(fetchStatus, 5000);
|
|
18
|
+
return () => clearInterval(interval);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const fetchStatus = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch('/api/sandbox/status');
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
setStatus(data);
|
|
26
|
+
} catch {
|
|
27
|
+
setStatus(null);
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleStart = async () => {
|
|
34
|
+
await fetch('/api/sandbox/start', { method: 'POST' });
|
|
35
|
+
fetchStatus();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleStop = async () => {
|
|
39
|
+
await fetch('/api/sandbox/stop', { method: 'POST' });
|
|
40
|
+
fetchStatus();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const formatUptime = (ms: number) => {
|
|
44
|
+
const s = Math.floor(ms / 1000);
|
|
45
|
+
const m = Math.floor(s / 60);
|
|
46
|
+
const h = Math.floor(m / 60);
|
|
47
|
+
return h > 0 ? `${h}h ${m % 60}m` : `${m}m ${s % 60}s`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (loading) {
|
|
51
|
+
return <div className="p-6 text-neutral-500">Loading sandbox status...</div>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="p-6 space-y-6 overflow-y-auto h-full">
|
|
56
|
+
<div>
|
|
57
|
+
<h2 className="text-xl font-semibold mb-1 flex items-center gap-2">
|
|
58
|
+
🐳 Sandboxed Execution
|
|
59
|
+
</h2>
|
|
60
|
+
<p className="text-neutral-500 text-sm">
|
|
61
|
+
Run commands inside isolated Docker containers for safe execution.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Status Card */}
|
|
66
|
+
<div className="border border-white/10 rounded-xl bg-neutral-900/40 p-6">
|
|
67
|
+
<div className="grid grid-cols-2 gap-4 mb-6">
|
|
68
|
+
<div>
|
|
69
|
+
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Status</div>
|
|
70
|
+
<div className={`text-sm font-medium ${status?.running ? 'text-emerald-400' : 'text-neutral-400'}`}>
|
|
71
|
+
{status?.running ? '● Running' : '○ Stopped'}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Image</div>
|
|
76
|
+
<div className="text-sm font-mono text-cyan-400">{status?.image || 'node:20-slim'}</div>
|
|
77
|
+
</div>
|
|
78
|
+
{status?.containerId && (
|
|
79
|
+
<div>
|
|
80
|
+
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Container</div>
|
|
81
|
+
<div className="text-sm font-mono text-neutral-300">{status.containerId.slice(0, 12)}</div>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
{status?.uptime && (
|
|
85
|
+
<div>
|
|
86
|
+
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Uptime</div>
|
|
87
|
+
<div className="text-sm text-neutral-300">{formatUptime(status.uptime)}</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="flex gap-3">
|
|
93
|
+
{!status?.running ? (
|
|
94
|
+
<button
|
|
95
|
+
onClick={handleStart}
|
|
96
|
+
className="px-4 py-2 bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 rounded-lg text-sm font-medium hover:bg-emerald-500/20 transition-colors"
|
|
97
|
+
>
|
|
98
|
+
Start Sandbox
|
|
99
|
+
</button>
|
|
100
|
+
) : (
|
|
101
|
+
<button
|
|
102
|
+
onClick={handleStop}
|
|
103
|
+
className="px-4 py-2 bg-red-500/10 text-red-400 border border-red-500/20 rounded-lg text-sm font-medium hover:bg-red-500/20 transition-colors"
|
|
104
|
+
>
|
|
105
|
+
Stop Sandbox
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Info */}
|
|
112
|
+
<div className="border border-white/5 rounded-xl bg-neutral-900/20 p-5 text-sm text-neutral-400 space-y-2">
|
|
113
|
+
<p>When the sandbox is <strong className="text-neutral-200">active</strong>, all <code className="bg-neutral-800 px-1 rounded text-xs">cmd.run</code> commands execute inside the Docker container instead of your host machine.</p>
|
|
114
|
+
<p>Configure in <code className="bg-neutral-800 px-1 rounded text-xs">agent.yaml</code>: image, network mode, volume mounts, and timeouts.</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|