groove-dev 0.27.141 → 0.27.142

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.
Files changed (71) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +18 -7
  4. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  5. package/node_modules/@groove-dev/daemon/src/journalist.js +3 -2
  6. package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
  7. package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
  8. package/node_modules/@groove-dev/daemon/src/process.js +5 -16
  9. package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
  10. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  11. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +984 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +1 -0
  13. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +6 -7
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +5 -6
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
  20. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +2 -53
  21. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
  22. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
  23. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
  24. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  25. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +0 -1
  26. package/node_modules/@groove-dev/gui/src/stores/groove.js +3 -3
  27. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
  28. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
  29. package/node_modules/@groove-dev/gui/src/views/models.jsx +3 -3
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/package.json +1 -1
  33. package/packages/daemon/src/api.js +18 -7
  34. package/packages/daemon/src/introducer.js +1 -1
  35. package/packages/daemon/src/journalist.js +3 -2
  36. package/packages/daemon/src/keeper.js +2 -2
  37. package/packages/daemon/src/memory.js +8 -5
  38. package/packages/daemon/src/process.js +5 -16
  39. package/packages/daemon/src/rotator.js +25 -8
  40. package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  41. package/packages/gui/dist/assets/index-Bjd91ufV.js +984 -0
  42. package/packages/gui/dist/assets/index-BqdwIFn4.css +1 -0
  43. package/packages/gui/dist/index.html +3 -3
  44. package/packages/gui/package.json +1 -1
  45. package/packages/gui/src/app.jsx +0 -2
  46. package/packages/gui/src/components/agents/agent-chat.jsx +6 -7
  47. package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
  48. package/packages/gui/src/components/agents/agent-file-tree.jsx +5 -6
  49. package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
  50. package/packages/gui/src/components/agents/workspace-mode.jsx +2 -53
  51. package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
  52. package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
  53. package/packages/gui/src/components/editor/code-editor.jsx +2 -68
  54. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  55. package/packages/gui/src/components/layout/terminal-panel.jsx +0 -1
  56. package/packages/gui/src/stores/groove.js +3 -3
  57. package/packages/gui/src/views/dashboard.jsx +2 -0
  58. package/packages/gui/src/views/marketplace.jsx +3 -71
  59. package/packages/gui/src/views/models.jsx +3 -3
  60. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +0 -1
  61. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +0 -8696
  62. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
  63. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
  64. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
  65. package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
  66. package/packages/gui/dist/assets/index-A4e1gIDh.css +0 -1
  67. package/packages/gui/dist/assets/index-P1hsM27-.js +0 -8696
  68. package/packages/gui/src/components/toys/toy-card.jsx +0 -78
  69. package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
  70. package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
  71. package/packages/gui/src/views/toys.jsx +0 -162
@@ -1,78 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Card } from '../ui/card';
3
- import { Badge } from '../ui/badge';
4
- import * as Icons from 'lucide-react';
5
-
6
- const DIFFICULTY_VARIANT = {
7
- beginner: 'success',
8
- intermediate: 'warning',
9
- advanced: 'danger',
10
- };
11
-
12
- function resolveIcon(name) {
13
- if (!name) return Icons.Box;
14
- const pascal = name.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase());
15
- return Icons[pascal] || Icons[name] || Icons.Box;
16
- }
17
-
18
- export function ToyCard({ toy, onClick }) {
19
- const Icon = resolveIcon(toy.icon);
20
-
21
- return (
22
- <Card
23
- hover
24
- className="group flex flex-col p-4 gap-3 transition-all duration-150 hover:scale-[1.02] hover:shadow-lg hover:shadow-black/20"
25
- onClick={() => onClick(toy)}
26
- >
27
- {/* Icon + name + category */}
28
- <div className="flex items-start gap-3">
29
- <div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-accent/10 border border-accent/20 text-accent">
30
- <Icon size={20} />
31
- </div>
32
- <div className="flex-1 min-w-0 pt-0.5">
33
- <h3 className="text-sm font-semibold text-text-0 font-sans truncate pr-1">{toy.name}</h3>
34
- </div>
35
- <Badge variant="default" className="flex-shrink-0 mt-0.5">
36
- {toy.category}
37
- </Badge>
38
- </div>
39
-
40
- {/* Description — 2 lines max */}
41
- <p className="text-xs text-text-2 font-sans leading-relaxed line-clamp-2">{toy.description}</p>
42
-
43
- {/* Bottom badges */}
44
- <div className="flex items-center gap-1.5 mt-auto flex-wrap">
45
- {toy.custom && (
46
- <Badge variant="accent">Custom</Badge>
47
- )}
48
- <Badge variant={DIFFICULTY_VARIANT[toy.difficulty] || 'default'}>
49
- {toy.difficulty || 'beginner'}
50
- </Badge>
51
- <Badge variant={toy.authType === 'none' ? 'success' : 'warning'}>
52
- {toy.authType === 'none' ? 'No Key Required' : 'API Key Required'}
53
- </Badge>
54
- </div>
55
- </Card>
56
- );
57
- }
58
-
59
- export function ToyCardSkeleton() {
60
- return (
61
- <Card className="p-4 space-y-3">
62
- <div className="flex items-start gap-3">
63
- <div className="w-10 h-10 rounded-lg bg-surface-4 animate-pulse" />
64
- <div className="flex-1 space-y-2 pt-1">
65
- <div className="h-3.5 w-24 bg-surface-4 rounded animate-pulse" />
66
- </div>
67
- </div>
68
- <div className="space-y-1.5">
69
- <div className="h-3 w-full bg-surface-4 rounded animate-pulse" />
70
- <div className="h-3 w-3/4 bg-surface-4 rounded animate-pulse" />
71
- </div>
72
- <div className="flex gap-1.5">
73
- <div className="h-5 w-16 bg-surface-4 rounded animate-pulse" />
74
- <div className="h-5 w-24 bg-surface-4 rounded animate-pulse" />
75
- </div>
76
- </Card>
77
- );
78
- }
@@ -1,144 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState } from 'react';
3
- import { Sheet, SheetContent } from '../ui/sheet';
4
- import { Button } from '../ui/button';
5
- import { ScrollArea } from '../ui/scroll-area';
6
- import { api } from '../../lib/api';
7
- import { useToast } from '../../lib/hooks/use-toast';
8
- import { Globe, Loader2, Sparkles, Link2 } from 'lucide-react';
9
-
10
- const STEPS = {
11
- input: 'input',
12
- researching: 'researching',
13
- };
14
-
15
- export function ToyCreator({ open, onClose, onCreated }) {
16
- const [url, setUrl] = useState('');
17
- const [name, setName] = useState('');
18
- const [step, setStep] = useState(STEPS.input);
19
- const toast = useToast();
20
-
21
- function reset() {
22
- setUrl('');
23
- setName('');
24
- setStep(STEPS.input);
25
- }
26
-
27
- function handleClose() {
28
- if (step === STEPS.researching) return;
29
- reset();
30
- onClose();
31
- }
32
-
33
- async function handleCreate() {
34
- const trimmed = url.trim();
35
- if (!trimmed) {
36
- toast.warning('URL required', 'Paste an API documentation or reference URL.');
37
- return;
38
- }
39
-
40
- setStep(STEPS.researching);
41
- try {
42
- const toy = await api.post('/toys', {
43
- docsUrl: trimmed,
44
- name: name.trim() || undefined,
45
- });
46
- toast.success('Toy created', `${toy.name || 'New toy'} is ready to launch.`);
47
- onCreated?.(toy);
48
- reset();
49
- onClose();
50
- } catch (err) {
51
- toast.error('Research failed', err.message);
52
- setStep(STEPS.input);
53
- }
54
- }
55
-
56
- return (
57
- <Sheet open={open} onOpenChange={(v) => !v && handleClose()}>
58
- <SheetContent title="New Toy" width={440}>
59
- <ScrollArea className="h-[calc(100%-65px)]">
60
- <div className="px-5 py-4 space-y-5">
61
- {step === STEPS.input && (
62
- <>
63
- <div className="flex items-start gap-3 p-3 rounded-lg bg-accent/5 border border-accent/10">
64
- <Sparkles size={16} className="text-accent mt-0.5 flex-shrink-0" />
65
- <p className="text-xs text-text-2 font-sans leading-relaxed">
66
- Paste a docs URL and Groove's AI will research the API — endpoints, auth, data structures, rate limits — and build a reusable toy card you can launch anytime.
67
- </p>
68
- </div>
69
-
70
- <div>
71
- <h4 className="text-xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">
72
- API Docs URL
73
- </h4>
74
- <div className="relative">
75
- <Link2 size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-4" />
76
- <input
77
- type="url"
78
- value={url}
79
- onChange={(e) => setUrl(e.target.value)}
80
- placeholder="https://api.example.com/docs"
81
- className="w-full bg-surface-3 border border-border-subtle rounded pl-8 pr-3 py-2 text-xs text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:border-accent/50"
82
- onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
83
- autoFocus
84
- />
85
- </div>
86
- </div>
87
-
88
- <div>
89
- <h4 className="text-xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">
90
- Name <span className="text-text-4 normal-case tracking-normal">(optional)</span>
91
- </h4>
92
- <input
93
- type="text"
94
- value={name}
95
- onChange={(e) => setName(e.target.value)}
96
- placeholder="Auto-detected from docs"
97
- className="w-full bg-surface-3 border border-border-subtle rounded px-3 py-2 text-xs text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:border-accent/50"
98
- onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
99
- />
100
- </div>
101
- </>
102
- )}
103
-
104
- {step === STEPS.researching && (
105
- <div className="flex flex-col items-center justify-center py-12 gap-4">
106
- <div className="w-14 h-14 rounded-xl bg-accent/10 border border-accent/20 flex items-center justify-center">
107
- <Globe size={24} className="text-accent animate-pulse" />
108
- </div>
109
- <div className="text-center space-y-1.5">
110
- <p className="text-sm font-semibold text-text-0 font-sans">Researching API...</p>
111
- <p className="text-xs text-text-3 font-sans max-w-[260px]">
112
- Reading documentation, mapping endpoints, and building your toy card. This takes 15–30 seconds.
113
- </p>
114
- </div>
115
- <Loader2 size={16} className="text-accent animate-spin" />
116
- </div>
117
- )}
118
- </div>
119
- </ScrollArea>
120
-
121
- <div className="px-5 py-3 border-t border-border-subtle bg-surface-1">
122
- {step === STEPS.input && (
123
- <Button
124
- variant="primary"
125
- size="lg"
126
- className="w-full gap-2"
127
- onClick={handleCreate}
128
- disabled={!url.trim()}
129
- >
130
- <Sparkles size={14} />
131
- Research & Create
132
- </Button>
133
- )}
134
- {step === STEPS.researching && (
135
- <Button variant="ghost" size="lg" className="w-full" disabled>
136
- <Loader2 size={14} className="animate-spin mr-2" />
137
- AI is analyzing...
138
- </Button>
139
- )}
140
- </div>
141
- </SheetContent>
142
- </Sheet>
143
- );
144
- }
@@ -1,187 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState } from 'react';
3
- import { Sheet, SheetContent } from '../ui/sheet';
4
- import { Button } from '../ui/button';
5
- import { Badge } from '../ui/badge';
6
- import { ScrollArea } from '../ui/scroll-area';
7
- import { api } from '../../lib/api';
8
- import { useToast } from '../../lib/hooks/use-toast';
9
- import { useGrooveStore } from '../../stores/groove';
10
- import { ExternalLink, Eye, EyeOff, Rocket, Loader2 } from 'lucide-react';
11
- import * as Icons from 'lucide-react';
12
-
13
- function resolveIcon(name) {
14
- if (!name) return Icons.Box;
15
- const pascal = name.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase());
16
- return Icons[pascal] || Icons[name] || Icons.Box;
17
- }
18
-
19
- export function ToyLauncher({ toy, open, onClose }) {
20
- const [apiKey, setApiKey] = useState('');
21
- const [showKey, setShowKey] = useState(false);
22
- const [selectedPrompt, setSelectedPrompt] = useState(null);
23
- const [launching, setLaunching] = useState(false);
24
- const toast = useToast();
25
- const setActiveView = useGrooveStore((s) => s.setActiveView);
26
-
27
- if (!toy) return null;
28
- const Icon = resolveIcon(toy.icon);
29
- const needsKey = toy.authType !== 'none';
30
-
31
- async function handleLaunch() {
32
- if (needsKey && !apiKey.trim()) {
33
- toast.warning('API key required', `${toy.name} needs an API key to work.`);
34
- return;
35
- }
36
- setLaunching(true);
37
- try {
38
- await api.post(`/toys/${toy.id}/launch`, {
39
- apiKey: needsKey ? apiKey.trim() : undefined,
40
- starterPrompt: selectedPrompt || undefined,
41
- });
42
- toast.success(`${toy.name} launched`, 'Team is spinning up — switching to agents view.');
43
- setActiveView('agents');
44
- onClose();
45
- } catch (err) {
46
- toast.error('Launch failed', err.message);
47
- }
48
- setLaunching(false);
49
- }
50
-
51
- return (
52
- <Sheet open={open} onOpenChange={(v) => !v && onClose()}>
53
- <SheetContent title={toy.name} width={440}>
54
- <ScrollArea className="h-[calc(100%-65px)]">
55
- <div className="px-5 py-4 space-y-5">
56
- {/* Header */}
57
- <div className="flex items-start gap-3">
58
- <div className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0 bg-accent/10 border border-accent/20 text-accent">
59
- <Icon size={24} />
60
- </div>
61
- <div className="flex-1 min-w-0">
62
- <div className="flex items-center gap-2">
63
- <Badge variant={toy.authType === 'none' ? 'success' : 'warning'}>
64
- {toy.authType === 'none' ? 'Free' : 'Key Required'}
65
- </Badge>
66
- <Badge variant="default">{toy.category}</Badge>
67
- </div>
68
- <p className="text-xs text-text-2 font-sans leading-relaxed mt-2">{toy.description}</p>
69
- </div>
70
- </div>
71
-
72
- {/* Base URL */}
73
- {toy.baseUrl && (
74
- <div>
75
- <h4 className="text-xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">Base URL</h4>
76
- <code className="text-xs text-accent font-mono bg-surface-3 px-2.5 py-1.5 rounded block truncate">
77
- {toy.baseUrl}
78
- </code>
79
- </div>
80
- )}
81
-
82
- {/* Sample Endpoints */}
83
- {toy.sampleEndpoints?.length > 0 && (
84
- <div>
85
- <h4 className="text-xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">Endpoints</h4>
86
- <div className="space-y-1">
87
- {toy.sampleEndpoints.map((ep, i) => {
88
- const str = typeof ep === 'string' ? ep : `${ep.method || 'GET'} ${ep.path || ''}`;
89
- const spaceIdx = str.indexOf(' ');
90
- const method = spaceIdx > 0 ? str.slice(0, spaceIdx) : 'GET';
91
- const path = spaceIdx > 0 ? str.slice(spaceIdx + 1) : str;
92
- return (
93
- <div key={i} className="flex items-center gap-2 text-xs font-mono text-text-2 bg-surface-3 px-2.5 py-1.5 rounded">
94
- <Badge variant="accent" className="text-[9px] px-1 py-0">{method}</Badge>
95
- <span className="truncate">{path}</span>
96
- </div>
97
- );
98
- })}
99
- </div>
100
- </div>
101
- )}
102
-
103
- {/* Starter Ideas */}
104
- {toy.starterPrompts?.length > 0 && (
105
- <div>
106
- <h4 className="text-xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">Starter Ideas</h4>
107
- <div className="flex flex-wrap gap-1.5">
108
- {toy.starterPrompts.map((prompt, i) => (
109
- <button
110
- key={i}
111
- onClick={() => setSelectedPrompt(selectedPrompt === prompt ? null : prompt)}
112
- className={`px-3 py-1.5 text-xs font-sans rounded-full cursor-pointer select-none transition-colors border ${
113
- selectedPrompt === prompt
114
- ? 'bg-accent/15 text-accent border-accent/25'
115
- : 'text-text-2 bg-surface-3 border-border-subtle hover:text-text-0 hover:border-border'
116
- }`}
117
- >
118
- {prompt}
119
- </button>
120
- ))}
121
- </div>
122
- </div>
123
- )}
124
-
125
- {/* API Key Input */}
126
- {needsKey && (
127
- <div>
128
- <h4 className="text-xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-1.5">API Key</h4>
129
- <div className="relative">
130
- <input
131
- type={showKey ? 'text' : 'password'}
132
- value={apiKey}
133
- onChange={(e) => setApiKey(e.target.value)}
134
- placeholder={`Enter your ${toy.name} API key`}
135
- className="w-full bg-surface-3 border border-border-subtle rounded px-3 py-2 text-xs text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:border-accent/50 pr-9"
136
- />
137
- <button
138
- onClick={() => setShowKey(!showKey)}
139
- className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-1 cursor-pointer bg-transparent border-0 p-0.5"
140
- >
141
- {showKey ? <EyeOff size={14} /> : <Eye size={14} />}
142
- </button>
143
- </div>
144
- </div>
145
- )}
146
-
147
- {/* Docs link */}
148
- {toy.docsUrl && (
149
- <a
150
- href={toy.docsUrl}
151
- target="_blank"
152
- rel="noopener noreferrer"
153
- className="inline-flex items-center gap-1.5 text-xs text-accent font-sans hover:underline"
154
- >
155
- <ExternalLink size={12} />
156
- View Documentation
157
- </a>
158
- )}
159
- </div>
160
- </ScrollArea>
161
-
162
- {/* Footer — sticky launch button */}
163
- <div className="px-5 py-3 border-t border-border-subtle bg-surface-1">
164
- <Button
165
- variant="primary"
166
- size="lg"
167
- className="w-full gap-2"
168
- onClick={handleLaunch}
169
- disabled={launching || (needsKey && !apiKey.trim())}
170
- >
171
- {launching ? (
172
- <>
173
- <Loader2 size={14} className="animate-spin" />
174
- Launching...
175
- </>
176
- ) : (
177
- <>
178
- <Rocket size={14} />
179
- Launch Team
180
- </>
181
- )}
182
- </Button>
183
- </div>
184
- </SheetContent>
185
- </Sheet>
186
- );
187
- }
@@ -1,162 +0,0 @@
1
- // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useEffect, useCallback } from 'react';
3
- import { ScrollArea } from '../components/ui/scroll-area';
4
- import { Badge } from '../components/ui/badge';
5
- import { useGrooveStore } from '../stores/groove';
6
- import { api } from '../lib/api';
7
- import { ToyCard, ToyCardSkeleton } from '../components/toys/toy-card';
8
- import { ToyLauncher } from '../components/toys/toy-launcher';
9
- import { ToyCreator } from '../components/toys/toy-creator';
10
- import { Gamepad2, Search, Rocket, Plus } from 'lucide-react';
11
-
12
- const CATEGORIES = ['All', 'Space', 'Weather', 'Finance', 'Fun', 'Maps', 'Data'];
13
-
14
- export default function ToysView() {
15
- const [toys, setToys] = useState([]);
16
- const [loading, setLoading] = useState(true);
17
- const [search, setSearch] = useState('');
18
- const [category, setCategory] = useState('All');
19
- const [selectedToy, setSelectedToy] = useState(null);
20
- const [creatorOpen, setCreatorOpen] = useState(false);
21
- const teams = useGrooveStore((s) => s.teams);
22
-
23
- const fetchToys = useCallback(() => {
24
- setLoading(true);
25
- api.get('/toys')
26
- .then((d) => setToys(d.toys || d.items || (Array.isArray(d) ? d : [])))
27
- .catch(() => setToys([]))
28
- .finally(() => setLoading(false));
29
- }, []);
30
-
31
- useEffect(() => { fetchToys(); }, [fetchToys]);
32
-
33
- const searchLower = search.toLowerCase();
34
- const filtered = toys.filter((t) => {
35
- if (category !== 'All' && t.category?.toLowerCase() !== category.toLowerCase()) return false;
36
- if (searchLower && !t.name?.toLowerCase().includes(searchLower) && !t.description?.toLowerCase().includes(searchLower)) return false;
37
- return true;
38
- });
39
-
40
- const recentToyTeams = (teams || []).filter((t) => (t.name || '').startsWith('Toy:'));
41
-
42
- return (
43
- <div className="flex flex-col h-full">
44
- {/* Header */}
45
- <div className="flex-shrink-0 px-5 pt-5 pb-4">
46
- <div className="flex items-center gap-2.5 mb-1">
47
- <Gamepad2 size={18} className="text-accent" />
48
- <h1 className="text-lg font-bold text-text-0 font-sans">Toys</h1>
49
- <div className="flex-1" />
50
- <button
51
- onClick={() => setCreatorOpen(true)}
52
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold font-sans rounded-full cursor-pointer select-none transition-colors bg-accent/15 text-accent border border-accent/25 hover:bg-accent/25"
53
- >
54
- <Plus size={13} />
55
- New
56
- </button>
57
- </div>
58
- <p className="text-xs text-text-3 font-sans">Plug in an API and start building</p>
59
- </div>
60
-
61
- {/* Search + category bar */}
62
- <div className="flex-shrink-0 px-5 pb-4 space-y-3">
63
- {/* Search */}
64
- <div className="relative w-full max-w-sm">
65
- <Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-4" />
66
- <input
67
- type="text"
68
- value={search}
69
- onChange={(e) => setSearch(e.target.value)}
70
- placeholder="Search toys..."
71
- className="w-full bg-surface-1 border border-border-subtle rounded pl-8 pr-3 py-1.5 text-xs text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:border-accent/50"
72
- />
73
- </div>
74
-
75
- {/* Categories */}
76
- <div className="flex items-center gap-1.5 flex-wrap">
77
- {CATEGORIES.map((cat) => (
78
- <button
79
- key={cat}
80
- onClick={() => setCategory(cat)}
81
- className={`px-3 py-1.5 text-xs font-semibold font-sans rounded-full cursor-pointer select-none transition-colors ${
82
- category === cat
83
- ? 'bg-accent/15 text-accent border border-accent/25'
84
- : 'text-text-3 hover:text-text-1 border border-transparent hover:border-border-subtle'
85
- }`}
86
- >
87
- {cat}
88
- </button>
89
- ))}
90
- <div className="flex-1" />
91
- <span className="text-2xs text-text-4 font-mono">{filtered.length}</span>
92
- </div>
93
- </div>
94
-
95
- {/* Content */}
96
- <ScrollArea className="flex-1 min-h-0">
97
- <div className="px-5 pt-1 pb-5">
98
- {/* Grid */}
99
- <div className="grid gap-3 grid-cols-[repeat(auto-fill,minmax(260px,1fr))]">
100
- {loading
101
- ? Array.from({ length: 6 }).map((_, i) => <ToyCardSkeleton key={i} />)
102
- : <>
103
- {filtered.map((t) => <ToyCard key={t.id} toy={t} onClick={setSelectedToy} />)}
104
- <button
105
- onClick={() => setCreatorOpen(true)}
106
- className="flex flex-col items-center justify-center gap-2 p-4 rounded-xl border-2 border-dashed border-border-subtle text-text-4 hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-colors cursor-pointer min-h-[140px]"
107
- >
108
- <Plus size={20} />
109
- <span className="text-xs font-semibold font-sans">Add API</span>
110
- </button>
111
- </>
112
- }
113
- </div>
114
-
115
- {!loading && filtered.length === 0 && (
116
- <div className="text-center py-16 text-text-4 font-sans text-sm">
117
- No toys found. Try a different search or category.
118
- </div>
119
- )}
120
-
121
- {/* Recent Toys */}
122
- {recentToyTeams.length > 0 && (
123
- <div className="mt-8">
124
- <h3 className="text-xs font-semibold text-text-2 font-sans uppercase tracking-wider mb-3 flex items-center gap-1.5">
125
- <Rocket size={12} />
126
- Recent Toys ({recentToyTeams.length})
127
- </h3>
128
- <div className="space-y-1.5">
129
- {recentToyTeams.map((team) => (
130
- <div key={team.id} className="flex items-center gap-3 px-3 py-2.5 rounded-md bg-surface-1 border border-border-subtle">
131
- <div className="w-8 h-8 rounded-md bg-accent/10 flex items-center justify-center flex-shrink-0">
132
- <Gamepad2 size={14} className="text-accent" />
133
- </div>
134
- <div className="flex-1 min-w-0">
135
- <div className="text-xs font-semibold text-text-0 font-sans truncate">{team.name}</div>
136
- <div className="text-2xs text-text-4 font-sans">{team.agents?.length || 0} agents</div>
137
- </div>
138
- <Badge variant="accent">Active</Badge>
139
- </div>
140
- ))}
141
- </div>
142
- </div>
143
- )}
144
- </div>
145
- </ScrollArea>
146
-
147
- {/* Launcher sheet */}
148
- <ToyLauncher
149
- toy={selectedToy}
150
- open={!!selectedToy}
151
- onClose={() => setSelectedToy(null)}
152
- />
153
-
154
- {/* Creator sheet */}
155
- <ToyCreator
156
- open={creatorOpen}
157
- onClose={() => setCreatorOpen(false)}
158
- onCreated={() => fetchToys()}
159
- />
160
- </div>
161
- );
162
- }