groove-dev 0.14.2 → 0.15.1
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/node_modules/@groove-dev/daemon/src/api.js +37 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +26 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
- package/node_modules/@groove-dev/daemon/src/skills.js +20 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-8Kqi_LVo.js +74 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +129 -0
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +84 -0
- package/node_modules/@groove-dev/gui/src/views/SkillsMarketplace.jsx +113 -2
- package/package.json +1 -1
- package/packages/daemon/src/api.js +37 -0
- package/packages/daemon/src/introducer.js +26 -0
- package/packages/daemon/src/registry.js +2 -1
- package/packages/daemon/src/skills.js +20 -0
- package/packages/daemon/src/validate.js +10 -0
- package/packages/gui/dist/assets/index-8Kqi_LVo.js +74 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/src/components/AgentActions.jsx +129 -0
- package/packages/gui/src/components/SpawnPanel.jsx +84 -0
- package/packages/gui/src/views/SkillsMarketplace.jsx +113 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-TcP3URUY.js +0 -74
- package/packages/gui/dist/assets/index-TcP3URUY.js +0 -74
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>GROOVE</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-8Kqi_LVo.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-Gfb8Zxy9.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -18,11 +18,14 @@ export default function AgentActions({ agent }) {
|
|
|
18
18
|
const [editingPrompt, setEditingPrompt] = useState(false);
|
|
19
19
|
const [selectedModel, setSelectedModel] = useState(agent.model || '');
|
|
20
20
|
const [providerList, setProviderList] = useState([]);
|
|
21
|
+
const [installedSkills, setInstalledSkills] = useState([]);
|
|
22
|
+
const [showSkillPicker, setShowSkillPicker] = useState(false);
|
|
21
23
|
|
|
22
24
|
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
23
25
|
|
|
24
26
|
useEffect(() => {
|
|
25
27
|
fetch('/api/providers').then(r => r.json()).then(setProviderList).catch(() => {});
|
|
28
|
+
fetch('/api/skills/installed').then(r => r.json()).then(setInstalledSkills).catch(() => {});
|
|
26
29
|
}, []);
|
|
27
30
|
|
|
28
31
|
const currentProvider = providerList.find((p) => p.id === agent.provider);
|
|
@@ -67,6 +70,24 @@ export default function AgentActions({ agent }) {
|
|
|
67
70
|
setConfirmDelete(false);
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
async function handleAttachSkill(skillId) {
|
|
74
|
+
try {
|
|
75
|
+
await fetch(`/api/agents/${agent.id}/skills/${skillId}`, { method: 'POST' });
|
|
76
|
+
showStatus(`skill attached to ${agent.name}`);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
showStatus(`attach failed: ${err.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleDetachSkill(skillId) {
|
|
83
|
+
try {
|
|
84
|
+
await fetch(`/api/agents/${agent.id}/skills/${skillId}`, { method: 'DELETE' });
|
|
85
|
+
showStatus(`skill detached from ${agent.name}`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
showStatus(`detach failed: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
async function handleClone() {
|
|
71
92
|
try {
|
|
72
93
|
const newAgent = await spawnAgent({
|
|
@@ -75,6 +96,7 @@ export default function AgentActions({ agent }) {
|
|
|
75
96
|
prompt: agent.prompt,
|
|
76
97
|
provider: agent.provider,
|
|
77
98
|
model: agent.model,
|
|
99
|
+
skills: agent.skills,
|
|
78
100
|
});
|
|
79
101
|
showStatus(`cloned as ${newAgent.name}`);
|
|
80
102
|
} catch (err) {
|
|
@@ -119,6 +141,7 @@ export default function AgentActions({ agent }) {
|
|
|
119
141
|
prompt: agent.prompt,
|
|
120
142
|
provider: agent.provider,
|
|
121
143
|
model: agent.model,
|
|
144
|
+
skills: agent.skills,
|
|
122
145
|
});
|
|
123
146
|
showStatus(`restarted as ${newAgent.name}`);
|
|
124
147
|
} catch (err) {
|
|
@@ -232,6 +255,75 @@ export default function AgentActions({ agent }) {
|
|
|
232
255
|
</div>
|
|
233
256
|
)}
|
|
234
257
|
|
|
258
|
+
{/* Skills */}
|
|
259
|
+
<div style={{ ...styles.sectionLabel, marginTop: 20 }}>
|
|
260
|
+
SKILLS ({(agent.skills || []).length})
|
|
261
|
+
</div>
|
|
262
|
+
{(agent.skills || []).length > 0 ? (
|
|
263
|
+
<div style={styles.skillsList}>
|
|
264
|
+
{(agent.skills || []).map((skillId) => {
|
|
265
|
+
const skill = installedSkills.find((s) => s.id === skillId);
|
|
266
|
+
return (
|
|
267
|
+
<div key={skillId} style={styles.skillRow}>
|
|
268
|
+
<span style={styles.skillRowIcon}>
|
|
269
|
+
{skill?.icon || skillId.charAt(0).toUpperCase()}
|
|
270
|
+
</span>
|
|
271
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
272
|
+
<div style={{ fontSize: 11, color: 'var(--text-primary)', fontWeight: 500 }}>
|
|
273
|
+
{skill?.name || skillId}
|
|
274
|
+
</div>
|
|
275
|
+
<div style={{ fontSize: 9, color: 'var(--text-muted)' }}>
|
|
276
|
+
{skill?.author || 'unknown'}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<button
|
|
280
|
+
onClick={() => handleDetachSkill(skillId)}
|
|
281
|
+
style={styles.detachBtn}
|
|
282
|
+
title="Detach skill"
|
|
283
|
+
>
|
|
284
|
+
{'\u2715'}
|
|
285
|
+
</button>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</div>
|
|
290
|
+
) : (
|
|
291
|
+
<div style={styles.noPrompt}>No skills attached</div>
|
|
292
|
+
)}
|
|
293
|
+
{!showSkillPicker && installedSkills.length > 0 && (
|
|
294
|
+
<button
|
|
295
|
+
onClick={() => setShowSkillPicker(true)}
|
|
296
|
+
style={{ ...styles.editBtn, marginTop: 6 }}
|
|
297
|
+
>
|
|
298
|
+
+ Attach Skill
|
|
299
|
+
</button>
|
|
300
|
+
)}
|
|
301
|
+
{showSkillPicker && (
|
|
302
|
+
<div style={styles.skillPicker}>
|
|
303
|
+
{installedSkills
|
|
304
|
+
.filter((s) => !(agent.skills || []).includes(s.id))
|
|
305
|
+
.map((skill) => (
|
|
306
|
+
<button
|
|
307
|
+
key={skill.id}
|
|
308
|
+
onClick={() => { handleAttachSkill(skill.id); setShowSkillPicker(false); }}
|
|
309
|
+
style={styles.skillPickerItem}
|
|
310
|
+
>
|
|
311
|
+
<span style={styles.skillRowIcon}>
|
|
312
|
+
{skill.icon || skill.name.charAt(0)}
|
|
313
|
+
</span>
|
|
314
|
+
<div style={{ flex: 1 }}>
|
|
315
|
+
<div style={{ fontSize: 11, color: 'var(--text-primary)' }}>{skill.name}</div>
|
|
316
|
+
<div style={{ fontSize: 9, color: 'var(--text-muted)' }}>{skill.author}</div>
|
|
317
|
+
</div>
|
|
318
|
+
</button>
|
|
319
|
+
))}
|
|
320
|
+
{installedSkills.filter((s) => !(agent.skills || []).includes(s.id)).length === 0 && (
|
|
321
|
+
<div style={{ fontSize: 10, color: 'var(--text-dim)', padding: 8 }}>All installed skills are already attached</div>
|
|
322
|
+
)}
|
|
323
|
+
<button onClick={() => setShowSkillPicker(false)} style={styles.cancelBtn}>cancel</button>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
|
|
235
327
|
{/* Current config */}
|
|
236
328
|
<div style={{ ...styles.sectionLabel, marginTop: 20 }}>CONFIGURATION</div>
|
|
237
329
|
<ConfigRow label="ID" value={agent.id} />
|
|
@@ -346,4 +438,41 @@ const styles = {
|
|
|
346
438
|
display: 'flex', gap: 8, padding: '3px 0',
|
|
347
439
|
borderBottom: '1px solid var(--bg-surface)',
|
|
348
440
|
},
|
|
441
|
+
skillsList: {
|
|
442
|
+
display: 'flex', flexDirection: 'column', gap: 3,
|
|
443
|
+
},
|
|
444
|
+
skillRow: {
|
|
445
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
446
|
+
padding: '5px 8px',
|
|
447
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
448
|
+
borderRadius: 2,
|
|
449
|
+
},
|
|
450
|
+
skillRowIcon: {
|
|
451
|
+
width: 20, height: 20, borderRadius: 4,
|
|
452
|
+
background: 'var(--accent)',
|
|
453
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
454
|
+
fontSize: 9, fontWeight: 700, color: 'var(--bg-base)',
|
|
455
|
+
flexShrink: 0,
|
|
456
|
+
},
|
|
457
|
+
detachBtn: {
|
|
458
|
+
background: 'none', border: 'none',
|
|
459
|
+
color: 'var(--text-muted)', fontSize: 11,
|
|
460
|
+
cursor: 'pointer', fontFamily: 'var(--font)',
|
|
461
|
+
padding: '2px 4px', flexShrink: 0,
|
|
462
|
+
},
|
|
463
|
+
skillPicker: {
|
|
464
|
+
marginTop: 6, padding: 6,
|
|
465
|
+
background: 'var(--bg-base)', border: '1px solid var(--border)',
|
|
466
|
+
borderRadius: 2,
|
|
467
|
+
display: 'flex', flexDirection: 'column', gap: 2,
|
|
468
|
+
maxHeight: 180, overflowY: 'auto',
|
|
469
|
+
},
|
|
470
|
+
skillPickerItem: {
|
|
471
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
472
|
+
padding: '5px 8px', width: '100%',
|
|
473
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
474
|
+
borderRadius: 2, cursor: 'pointer', textAlign: 'left',
|
|
475
|
+
fontFamily: 'var(--font)',
|
|
476
|
+
transition: 'border-color 0.1s',
|
|
477
|
+
},
|
|
349
478
|
};
|
|
@@ -41,10 +41,13 @@ export default function SpawnPanel() {
|
|
|
41
41
|
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
42
42
|
const [keySaving, setKeySaving] = useState(false);
|
|
43
43
|
const [showDirPicker, setShowDirPicker] = useState(false);
|
|
44
|
+
const [installedSkills, setInstalledSkills] = useState([]);
|
|
45
|
+
const [selectedSkills, setSelectedSkills] = useState([]);
|
|
44
46
|
|
|
45
47
|
useEffect(() => {
|
|
46
48
|
fetchProviders();
|
|
47
49
|
fetchWorkspaces();
|
|
50
|
+
fetchInstalledSkills();
|
|
48
51
|
}, []);
|
|
49
52
|
|
|
50
53
|
async function fetchProviders() {
|
|
@@ -62,6 +65,19 @@ export default function SpawnPanel() {
|
|
|
62
65
|
} catch { /* ignore */ }
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
async function fetchInstalledSkills() {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch('/api/skills/installed');
|
|
71
|
+
setInstalledSkills(await res.json());
|
|
72
|
+
} catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toggleSkill(skillId) {
|
|
76
|
+
setSelectedSkills((prev) =>
|
|
77
|
+
prev.includes(skillId) ? prev.filter((s) => s !== skillId) : [...prev, skillId]
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
|
|
66
82
|
const effectiveScope = role === 'custom'
|
|
67
83
|
? scope
|
|
@@ -127,6 +143,7 @@ export default function SpawnPanel() {
|
|
|
127
143
|
provider,
|
|
128
144
|
permission,
|
|
129
145
|
...(workingDir.trim() ? { workingDir: workingDir.trim() } : {}),
|
|
146
|
+
...(selectedSkills.length > 0 ? { skills: selectedSkills } : {}),
|
|
130
147
|
});
|
|
131
148
|
closeDetail();
|
|
132
149
|
} catch (err) {
|
|
@@ -281,6 +298,57 @@ export default function SpawnPanel() {
|
|
|
281
298
|
))}
|
|
282
299
|
</div>
|
|
283
300
|
|
|
301
|
+
{/* Skills picker */}
|
|
302
|
+
{installedSkills.length > 0 && (
|
|
303
|
+
<>
|
|
304
|
+
<div style={styles.label}>SKILLS</div>
|
|
305
|
+
<div style={styles.skillsGrid}>
|
|
306
|
+
{installedSkills.map((skill) => {
|
|
307
|
+
const active = selectedSkills.includes(skill.id);
|
|
308
|
+
return (
|
|
309
|
+
<button
|
|
310
|
+
key={skill.id}
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={() => toggleSkill(skill.id)}
|
|
313
|
+
style={{
|
|
314
|
+
...styles.skillBtn,
|
|
315
|
+
borderColor: active ? 'var(--accent)' : 'var(--border)',
|
|
316
|
+
background: active ? 'rgba(51, 175, 188, 0.08)' : 'var(--bg-surface)',
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<span style={{
|
|
320
|
+
...styles.skillIcon,
|
|
321
|
+
background: active ? 'var(--accent)' : 'var(--bg-active)',
|
|
322
|
+
color: active ? 'var(--bg-base)' : 'var(--text-dim)',
|
|
323
|
+
}}>
|
|
324
|
+
{skill.icon || skill.name.charAt(0)}
|
|
325
|
+
</span>
|
|
326
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
327
|
+
<div style={{
|
|
328
|
+
fontSize: 11, fontWeight: 600,
|
|
329
|
+
color: active ? 'var(--text-bright)' : 'var(--text-primary)',
|
|
330
|
+
}}>
|
|
331
|
+
{skill.name}
|
|
332
|
+
</div>
|
|
333
|
+
<div style={{ fontSize: 9, color: 'var(--text-muted)' }}>
|
|
334
|
+
{skill.author || 'local'}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
{active && (
|
|
338
|
+
<span style={{ fontSize: 10, color: 'var(--accent)', flexShrink: 0 }}>{'\u2713'}</span>
|
|
339
|
+
)}
|
|
340
|
+
</button>
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
343
|
+
</div>
|
|
344
|
+
{selectedSkills.length > 0 && (
|
|
345
|
+
<div style={styles.hint}>
|
|
346
|
+
{selectedSkills.length} skill{selectedSkills.length !== 1 ? 's' : ''} will be injected into this agent's context
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</>
|
|
350
|
+
)}
|
|
351
|
+
|
|
284
352
|
{/* Advanced toggle */}
|
|
285
353
|
<button
|
|
286
354
|
type="button"
|
|
@@ -505,6 +573,22 @@ const styles = {
|
|
|
505
573
|
permDesc: {
|
|
506
574
|
fontSize: 10, color: 'var(--text-dim)',
|
|
507
575
|
},
|
|
576
|
+
skillsGrid: {
|
|
577
|
+
display: 'flex', flexDirection: 'column', gap: 3,
|
|
578
|
+
},
|
|
579
|
+
skillBtn: {
|
|
580
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
581
|
+
padding: '6px 8px', width: '100%',
|
|
582
|
+
border: '1px solid var(--border)',
|
|
583
|
+
borderRadius: 2, cursor: 'pointer', textAlign: 'left',
|
|
584
|
+
fontFamily: 'var(--font)',
|
|
585
|
+
transition: 'border-color 0.1s, background 0.1s',
|
|
586
|
+
},
|
|
587
|
+
skillIcon: {
|
|
588
|
+
width: 22, height: 22, borderRadius: 4,
|
|
589
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
590
|
+
fontSize: 10, fontWeight: 700, flexShrink: 0,
|
|
591
|
+
},
|
|
508
592
|
advancedToggle: {
|
|
509
593
|
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
510
594
|
fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
|
|
@@ -89,10 +89,55 @@ function sortSkills(skills, sortBy) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// ── Interactive Star Rating ──────────────────────────────────────────
|
|
93
|
+
function StarRating({ current, onRate, disabled }) {
|
|
94
|
+
const [hover, setHover] = useState(0);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div style={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
|
98
|
+
{[1, 2, 3, 4, 5].map((star) => {
|
|
99
|
+
const filled = hover > 0 ? star <= hover : star <= (current || 0);
|
|
100
|
+
return (
|
|
101
|
+
<span
|
|
102
|
+
key={star}
|
|
103
|
+
onClick={() => !disabled && onRate(star)}
|
|
104
|
+
onMouseEnter={() => !disabled && setHover(star)}
|
|
105
|
+
onMouseLeave={() => setHover(0)}
|
|
106
|
+
style={{
|
|
107
|
+
fontSize: 18,
|
|
108
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
109
|
+
color: filled ? 'var(--amber)' : 'var(--bg-active)',
|
|
110
|
+
transition: 'color 0.1s, transform 0.1s',
|
|
111
|
+
transform: hover === star ? 'scale(1.2)' : 'none',
|
|
112
|
+
userSelect: 'none',
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{'\u2605'}
|
|
116
|
+
</span>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
// ── Skill Detail Modal ──────────────────────────────────────────────
|
|
93
|
-
function SkillDetailModal({ skill, content, installing, onInstall, onUninstall, onClose }) {
|
|
124
|
+
function SkillDetailModal({ skill, content, installing, onInstall, onUninstall, onRate, onClose }) {
|
|
125
|
+
const [userRating, setUserRating] = useState(0);
|
|
126
|
+
const [ratingSubmitted, setRatingSubmitted] = useState(false);
|
|
127
|
+
const [submittingRating, setSubmittingRating] = useState(false);
|
|
128
|
+
|
|
94
129
|
if (!skill) return null;
|
|
95
130
|
|
|
131
|
+
async function handleRate(stars) {
|
|
132
|
+
setSubmittingRating(true);
|
|
133
|
+
try {
|
|
134
|
+
await onRate(skill.id, stars);
|
|
135
|
+
setUserRating(stars);
|
|
136
|
+
setRatingSubmitted(true);
|
|
137
|
+
} catch { /* ignore */ }
|
|
138
|
+
setSubmittingRating(false);
|
|
139
|
+
}
|
|
140
|
+
|
|
96
141
|
return (
|
|
97
142
|
<div style={modal.overlay} onClick={onClose}>
|
|
98
143
|
<div style={modal.container} onClick={(e) => e.stopPropagation()}>
|
|
@@ -162,6 +207,31 @@ function SkillDetailModal({ skill, content, installing, onInstall, onUninstall,
|
|
|
162
207
|
{skill.price === 0 && !skill.installed && <span style={modal.freeLabel}>Free</span>}
|
|
163
208
|
</div>
|
|
164
209
|
|
|
210
|
+
{/* Rating */}
|
|
211
|
+
<div style={modal.section}>
|
|
212
|
+
<div style={modal.sectionTitle}>Rating</div>
|
|
213
|
+
<div style={modal.ratingRow}>
|
|
214
|
+
<div style={modal.ratingLeft}>
|
|
215
|
+
<div style={modal.ratingBig}>{skill.rating || '-'}</div>
|
|
216
|
+
<div style={{ color: 'var(--amber)', fontSize: 13 }}>{renderStars(skill.rating)}</div>
|
|
217
|
+
<div style={modal.ratingCount}>{skill.ratingCount || 0} ratings</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div style={modal.ratingRight}>
|
|
220
|
+
<div style={modal.rateLabel}>
|
|
221
|
+
{ratingSubmitted ? 'Thanks for rating!' : 'Rate this skill'}
|
|
222
|
+
</div>
|
|
223
|
+
<StarRating
|
|
224
|
+
current={userRating}
|
|
225
|
+
onRate={handleRate}
|
|
226
|
+
disabled={submittingRating || ratingSubmitted}
|
|
227
|
+
/>
|
|
228
|
+
{submittingRating && (
|
|
229
|
+
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>Submitting...</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
165
235
|
{/* Description */}
|
|
166
236
|
<div style={modal.section}>
|
|
167
237
|
<div style={modal.sectionTitle}>About</div>
|
|
@@ -413,6 +483,23 @@ export default function SkillsMarketplace() {
|
|
|
413
483
|
setInstalling(null);
|
|
414
484
|
}
|
|
415
485
|
|
|
486
|
+
async function handleRate(id, rating) {
|
|
487
|
+
const res = await fetch(`/api/skills/${id}/rate`, {
|
|
488
|
+
method: 'POST',
|
|
489
|
+
headers: { 'Content-Type': 'application/json' },
|
|
490
|
+
body: JSON.stringify({ rating }),
|
|
491
|
+
});
|
|
492
|
+
if (!res.ok) throw new Error('Rating failed');
|
|
493
|
+
const data = await res.json();
|
|
494
|
+
// Update the skill in local state with new rating
|
|
495
|
+
setSkills((prev) => prev.map((s) =>
|
|
496
|
+
s.id === id ? { ...s, rating: data.rating, ratingCount: data.rating_count ?? data.ratingCount } : s
|
|
497
|
+
));
|
|
498
|
+
if (selectedSkill?.id === id) {
|
|
499
|
+
setSelectedSkill((prev) => prev ? { ...prev, rating: data.rating, ratingCount: data.rating_count ?? data.ratingCount } : null);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
416
503
|
async function handleSelect(skill) {
|
|
417
504
|
setSelectedSkill(skill);
|
|
418
505
|
setSkillContent(null);
|
|
@@ -561,6 +648,7 @@ export default function SkillsMarketplace() {
|
|
|
561
648
|
installing={installing}
|
|
562
649
|
onInstall={handleInstall}
|
|
563
650
|
onUninstall={handleUninstall}
|
|
651
|
+
onRate={handleRate}
|
|
564
652
|
onClose={() => { setSelectedSkill(null); setSkillContent(null); }}
|
|
565
653
|
/>
|
|
566
654
|
</div>
|
|
@@ -653,7 +741,7 @@ const styles = {
|
|
|
653
741
|
|
|
654
742
|
// Scroll area
|
|
655
743
|
scrollArea: {
|
|
656
|
-
flex: 1, overflowY: 'auto', padding: '
|
|
744
|
+
flex: 1, overflowY: 'auto', padding: '4px 20px 20px',
|
|
657
745
|
},
|
|
658
746
|
|
|
659
747
|
// Featured
|
|
@@ -876,6 +964,29 @@ const modal = {
|
|
|
876
964
|
section: {
|
|
877
965
|
marginBottom: 16,
|
|
878
966
|
},
|
|
967
|
+
ratingRow: {
|
|
968
|
+
display: 'flex', gap: 20, alignItems: 'center',
|
|
969
|
+
padding: '12px 14px',
|
|
970
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
971
|
+
borderRadius: 6,
|
|
972
|
+
},
|
|
973
|
+
ratingLeft: {
|
|
974
|
+
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
|
|
975
|
+
minWidth: 70,
|
|
976
|
+
},
|
|
977
|
+
ratingBig: {
|
|
978
|
+
fontSize: 24, fontWeight: 700, color: 'var(--text-bright)',
|
|
979
|
+
lineHeight: 1,
|
|
980
|
+
},
|
|
981
|
+
ratingCount: {
|
|
982
|
+
fontSize: 9, color: 'var(--text-muted)', marginTop: 2,
|
|
983
|
+
},
|
|
984
|
+
ratingRight: {
|
|
985
|
+
flex: 1,
|
|
986
|
+
},
|
|
987
|
+
rateLabel: {
|
|
988
|
+
fontSize: 11, color: 'var(--text-dim)', marginBottom: 6,
|
|
989
|
+
},
|
|
879
990
|
sectionTitle: {
|
|
880
991
|
fontSize: 10, fontWeight: 600, color: 'var(--text-muted)',
|
|
881
992
|
textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 8,
|