@geminilight/mindos 0.5.64 → 0.5.66
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 +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +6 -11
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +1 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +35 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +70 -5
- package/app/components/agents/AgentsMcpSection.tsx +474 -159
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +739 -121
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +292 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +1 -2
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +12 -104
- package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
- package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
- package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +3 -3
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +2 -2
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +4 -2
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/prompt.ts +17 -29
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +9 -0
- package/app/lib/i18n-en.ts +259 -33
- package/app/lib/i18n-zh.ts +258 -32
- package/app/lib/mcp-agents.ts +231 -2
- package/app/lib/types.ts +2 -0
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
BookOpen,
|
|
6
|
+
Code2,
|
|
7
|
+
Copy,
|
|
8
|
+
Check,
|
|
9
|
+
FileText,
|
|
10
|
+
Loader2,
|
|
11
|
+
Zap,
|
|
12
|
+
Search,
|
|
13
|
+
Server,
|
|
14
|
+
ToggleLeft,
|
|
15
|
+
Trash2,
|
|
16
|
+
X,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { apiFetch } from '@/lib/api';
|
|
19
|
+
import { copyToClipboard } from '@/lib/clipboard';
|
|
20
|
+
import type { SkillInfo } from '@/components/settings/types';
|
|
21
|
+
import { Toggle } from '@/components/settings/Primitives';
|
|
22
|
+
import { AgentAvatar, ConfirmDialog } from './AgentsPrimitives';
|
|
23
|
+
import { capabilityFromText, type SkillCapability } from './agents-content-model';
|
|
24
|
+
|
|
25
|
+
/* ────────── Types ────────── */
|
|
26
|
+
|
|
27
|
+
export interface SkillDetailPopoverCopy {
|
|
28
|
+
close: string;
|
|
29
|
+
source: string;
|
|
30
|
+
sourceBuiltin: string;
|
|
31
|
+
sourceUser: string;
|
|
32
|
+
sourceNative: string;
|
|
33
|
+
capability: string;
|
|
34
|
+
path: string;
|
|
35
|
+
enabled: string;
|
|
36
|
+
disabled: string;
|
|
37
|
+
agents: string;
|
|
38
|
+
noAgents: string;
|
|
39
|
+
content: string;
|
|
40
|
+
loading: string;
|
|
41
|
+
loadFailed: string;
|
|
42
|
+
retry: string;
|
|
43
|
+
copyContent: string;
|
|
44
|
+
copied: string;
|
|
45
|
+
noDescription: string;
|
|
46
|
+
deleteSkill: string;
|
|
47
|
+
confirmDeleteTitle: string;
|
|
48
|
+
confirmDeleteMessage: (name: string) => string;
|
|
49
|
+
confirmDeleteAction: string;
|
|
50
|
+
cancelAction: string;
|
|
51
|
+
deleted: string;
|
|
52
|
+
deleteFailed: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SkillDetailPopoverProps {
|
|
56
|
+
open: boolean;
|
|
57
|
+
skillName: string | null;
|
|
58
|
+
skill?: SkillInfo | null;
|
|
59
|
+
agentNames?: string[];
|
|
60
|
+
isNative?: boolean;
|
|
61
|
+
nativeSourcePath?: string;
|
|
62
|
+
copy: SkillDetailPopoverCopy;
|
|
63
|
+
onClose: () => void;
|
|
64
|
+
onToggle?: (name: string, enabled: boolean) => Promise<boolean>;
|
|
65
|
+
onDelete?: (name: string) => Promise<void>;
|
|
66
|
+
onRefresh?: () => Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ────────── Capability Icon ────────── */
|
|
70
|
+
|
|
71
|
+
const CAPABILITY_ICONS: Record<SkillCapability, React.ComponentType<{ size?: number; className?: string }>> = {
|
|
72
|
+
research: Search,
|
|
73
|
+
coding: Code2,
|
|
74
|
+
docs: FileText,
|
|
75
|
+
ops: Server,
|
|
76
|
+
memory: BookOpen,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/* ────────── Component ────────── */
|
|
80
|
+
|
|
81
|
+
export default function SkillDetailPopover({
|
|
82
|
+
open,
|
|
83
|
+
skillName,
|
|
84
|
+
skill,
|
|
85
|
+
agentNames = [],
|
|
86
|
+
isNative = false,
|
|
87
|
+
nativeSourcePath,
|
|
88
|
+
copy,
|
|
89
|
+
onClose,
|
|
90
|
+
onToggle,
|
|
91
|
+
onDelete,
|
|
92
|
+
onRefresh,
|
|
93
|
+
}: SkillDetailPopoverProps) {
|
|
94
|
+
const [content, setContent] = useState<string | null>(null);
|
|
95
|
+
const [nativeDesc, setNativeDesc] = useState<string>('');
|
|
96
|
+
const [loading, setLoading] = useState(false);
|
|
97
|
+
const [loadError, setLoadError] = useState(false);
|
|
98
|
+
const [copied, setCopied] = useState(false);
|
|
99
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
100
|
+
const [deleting, setDeleting] = useState(false);
|
|
101
|
+
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
|
102
|
+
const [toggleBusy, setToggleBusy] = useState(false);
|
|
103
|
+
|
|
104
|
+
const fetchContent = useCallback(async () => {
|
|
105
|
+
if (!skillName) return;
|
|
106
|
+
setLoading(true);
|
|
107
|
+
setLoadError(false);
|
|
108
|
+
try {
|
|
109
|
+
if (isNative && nativeSourcePath) {
|
|
110
|
+
const res = await apiFetch<{ content: string; description?: string }>('/api/skills', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ action: 'read-native', name: skillName, sourcePath: nativeSourcePath }),
|
|
114
|
+
});
|
|
115
|
+
setContent(res.content);
|
|
116
|
+
setNativeDesc(res.description || '');
|
|
117
|
+
} else if (!isNative) {
|
|
118
|
+
const res = await apiFetch<{ content: string }>('/api/skills', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({ action: 'read', name: skillName }),
|
|
122
|
+
});
|
|
123
|
+
setContent(res.content);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
setLoadError(true);
|
|
127
|
+
} finally {
|
|
128
|
+
setLoading(false);
|
|
129
|
+
}
|
|
130
|
+
}, [skillName, isNative, nativeSourcePath]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (open && skillName) {
|
|
134
|
+
setContent(null);
|
|
135
|
+
setNativeDesc('');
|
|
136
|
+
setLoadError(false);
|
|
137
|
+
setCopied(false);
|
|
138
|
+
setDeleteMsg(null);
|
|
139
|
+
fetchContent();
|
|
140
|
+
}
|
|
141
|
+
}, [open, skillName, fetchContent]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!open) return;
|
|
145
|
+
function onKey(e: KeyboardEvent) {
|
|
146
|
+
if (e.key === 'Escape') onClose();
|
|
147
|
+
}
|
|
148
|
+
document.addEventListener('keydown', onKey);
|
|
149
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
150
|
+
}, [open, onClose]);
|
|
151
|
+
|
|
152
|
+
const handleCopy = useCallback(async () => {
|
|
153
|
+
if (!content) return;
|
|
154
|
+
const ok = await copyToClipboard(content);
|
|
155
|
+
if (ok) {
|
|
156
|
+
setCopied(true);
|
|
157
|
+
setTimeout(() => setCopied(false), 1500);
|
|
158
|
+
}
|
|
159
|
+
}, [content]);
|
|
160
|
+
|
|
161
|
+
const handleToggle = useCallback(async (enabled: boolean) => {
|
|
162
|
+
if (!skillName || !onToggle) return;
|
|
163
|
+
setToggleBusy(true);
|
|
164
|
+
try {
|
|
165
|
+
await onToggle(skillName, enabled);
|
|
166
|
+
} finally {
|
|
167
|
+
setToggleBusy(false);
|
|
168
|
+
}
|
|
169
|
+
}, [skillName, onToggle]);
|
|
170
|
+
|
|
171
|
+
const handleDelete = useCallback(async () => {
|
|
172
|
+
if (!skillName || !onDelete) return;
|
|
173
|
+
setConfirmDelete(false);
|
|
174
|
+
setDeleting(true);
|
|
175
|
+
try {
|
|
176
|
+
await onDelete(skillName);
|
|
177
|
+
setDeleteMsg(copy.deleted);
|
|
178
|
+
await onRefresh?.();
|
|
179
|
+
setTimeout(() => onClose(), 800);
|
|
180
|
+
} catch {
|
|
181
|
+
setDeleteMsg(copy.deleteFailed);
|
|
182
|
+
setDeleting(false);
|
|
183
|
+
}
|
|
184
|
+
}, [skillName, onDelete, onRefresh, onClose, copy.deleted, copy.deleteFailed]);
|
|
185
|
+
|
|
186
|
+
if (!open || !skillName) return null;
|
|
187
|
+
|
|
188
|
+
const capability = skill
|
|
189
|
+
? capabilityFromText(`${skill.name} ${skill.description}`)
|
|
190
|
+
: capabilityFromText(skillName);
|
|
191
|
+
const CapIcon = CAPABILITY_ICONS[capability] ?? Zap;
|
|
192
|
+
const isUserSkill = skill?.source === 'user';
|
|
193
|
+
const sourceLabel = isNative
|
|
194
|
+
? copy.sourceNative
|
|
195
|
+
: skill?.source === 'builtin'
|
|
196
|
+
? copy.sourceBuiltin
|
|
197
|
+
: copy.sourceUser;
|
|
198
|
+
const description = skill?.description || nativeDesc || '';
|
|
199
|
+
const skillPath = skill?.path || (isNative && nativeSourcePath ? `${nativeSourcePath}/${skillName}/SKILL.md` : '');
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
{/* Backdrop */}
|
|
204
|
+
<div
|
|
205
|
+
className="fixed inset-0 z-40 bg-black/30 backdrop-blur-[2px]"
|
|
206
|
+
onClick={onClose}
|
|
207
|
+
aria-hidden="true"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
{/* Slide-over panel */}
|
|
211
|
+
<div
|
|
212
|
+
role="dialog"
|
|
213
|
+
aria-modal="true"
|
|
214
|
+
aria-label={skillName}
|
|
215
|
+
className="fixed right-0 top-0 z-50 h-full w-full max-w-md border-l border-border bg-card shadow-2xl flex flex-col animate-in slide-in-from-right duration-200"
|
|
216
|
+
>
|
|
217
|
+
{/* ─── Header ─── */}
|
|
218
|
+
<div className="flex items-center gap-3 px-5 py-4 border-b border-border shrink-0">
|
|
219
|
+
<div className="w-9 h-9 rounded-lg bg-muted/60 flex items-center justify-center text-muted-foreground shrink-0">
|
|
220
|
+
<CapIcon size={18} />
|
|
221
|
+
</div>
|
|
222
|
+
<div className="flex-1 min-w-0">
|
|
223
|
+
<h2 className="text-sm font-semibold text-foreground truncate">{skillName}</h2>
|
|
224
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
225
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded font-medium select-none ${
|
|
226
|
+
isNative
|
|
227
|
+
? 'bg-muted text-muted-foreground'
|
|
228
|
+
: skill?.source === 'builtin'
|
|
229
|
+
? 'bg-muted text-muted-foreground'
|
|
230
|
+
: 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
231
|
+
}`}>
|
|
232
|
+
{sourceLabel}
|
|
233
|
+
</span>
|
|
234
|
+
<span className="text-2xs text-muted-foreground capitalize">{capability}</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={onClose}
|
|
240
|
+
className="shrink-0 p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
241
|
+
aria-label={copy.close}
|
|
242
|
+
>
|
|
243
|
+
<X size={16} />
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* ─── Body (scrollable) ─── */}
|
|
248
|
+
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
|
|
249
|
+
{/* Description */}
|
|
250
|
+
{description ? (
|
|
251
|
+
<p className="text-sm text-foreground leading-relaxed">{description}</p>
|
|
252
|
+
) : !isNative ? (
|
|
253
|
+
<p className="text-sm text-muted-foreground italic">{copy.noDescription}</p>
|
|
254
|
+
) : null}
|
|
255
|
+
|
|
256
|
+
{/* Quick meta */}
|
|
257
|
+
<div className="grid grid-cols-2 gap-3">
|
|
258
|
+
{!isNative && skill && (
|
|
259
|
+
<MetaCard label={copy.enabled} value={skill.enabled ? '✓' : '—'} tone={skill.enabled ? 'ok' : 'muted'} />
|
|
260
|
+
)}
|
|
261
|
+
<MetaCard label={copy.capability} value={capability} />
|
|
262
|
+
<MetaCard label={copy.source} value={sourceLabel} />
|
|
263
|
+
<MetaCard label={copy.agents} value={String(agentNames.length)} />
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Path */}
|
|
267
|
+
{skillPath && (
|
|
268
|
+
<div className="rounded-lg border border-border bg-background p-3">
|
|
269
|
+
<span className="text-2xs text-muted-foreground block mb-1">{copy.path}</span>
|
|
270
|
+
<code className="text-xs text-foreground font-mono break-all leading-relaxed">{skillPath}</code>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{/* Agents */}
|
|
275
|
+
{agentNames.length > 0 && (
|
|
276
|
+
<div>
|
|
277
|
+
<h3 className="text-xs font-medium text-muted-foreground mb-2">{copy.agents}</h3>
|
|
278
|
+
<div className="flex flex-wrap gap-2">
|
|
279
|
+
{agentNames.map((name) => (
|
|
280
|
+
<AgentAvatar key={name} name={name} size="sm" />
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
{agentNames.length === 0 && (
|
|
286
|
+
<div className="rounded-lg border border-dashed border-border p-3 text-center">
|
|
287
|
+
<p className="text-xs text-muted-foreground">{copy.noAgents}</p>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Content */}
|
|
292
|
+
{(content !== null || loading || loadError) && (
|
|
293
|
+
<div>
|
|
294
|
+
<div className="flex items-center justify-between mb-2">
|
|
295
|
+
<h3 className="text-xs font-medium text-muted-foreground">{copy.content}</h3>
|
|
296
|
+
{content && (
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={handleCopy}
|
|
300
|
+
className="inline-flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1.5 py-0.5"
|
|
301
|
+
aria-label={copy.copyContent}
|
|
302
|
+
>
|
|
303
|
+
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
304
|
+
{copied ? copy.copied : copy.copyContent}
|
|
305
|
+
</button>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{loading && (
|
|
310
|
+
<div className="rounded-lg border border-border bg-background p-6 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
311
|
+
<Loader2 size={14} className="animate-spin" />
|
|
312
|
+
{copy.loading}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{loadError && (
|
|
317
|
+
<div className="rounded-lg border border-destructive/20 bg-destructive/[0.03] p-4 text-center space-y-2">
|
|
318
|
+
<p className="text-sm text-destructive">{copy.loadFailed}</p>
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={fetchContent}
|
|
322
|
+
className="text-xs text-foreground underline hover:no-underline cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
|
|
323
|
+
>
|
|
324
|
+
{copy.retry}
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{content && !loading && !loadError && (
|
|
330
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground font-mono overflow-x-auto max-h-80 leading-relaxed whitespace-pre-wrap break-words">
|
|
331
|
+
{content}
|
|
332
|
+
</pre>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* Delete message */}
|
|
338
|
+
{deleteMsg && (
|
|
339
|
+
<div role="status" aria-live="polite" className="rounded-md border border-border bg-muted/50 px-3 py-2 text-xs text-muted-foreground animate-in fade-in duration-200">
|
|
340
|
+
{deleteMsg}
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* ─── Footer actions ─── */}
|
|
346
|
+
{!isNative && skill && (
|
|
347
|
+
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center gap-2">
|
|
348
|
+
{/* Toggle */}
|
|
349
|
+
{onToggle && (
|
|
350
|
+
<div className="flex items-center gap-2 mr-auto">
|
|
351
|
+
<ToggleLeft size={14} className="text-muted-foreground" aria-hidden="true" />
|
|
352
|
+
<span className="text-xs text-muted-foreground">
|
|
353
|
+
{skill.enabled ? copy.enabled : copy.disabled}
|
|
354
|
+
</span>
|
|
355
|
+
<Toggle
|
|
356
|
+
size="sm"
|
|
357
|
+
checked={skill.enabled}
|
|
358
|
+
onChange={(v) => void handleToggle(v)}
|
|
359
|
+
disabled={toggleBusy}
|
|
360
|
+
/>
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
|
|
364
|
+
{/* Delete */}
|
|
365
|
+
{isUserSkill && onDelete && (
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onClick={() => setConfirmDelete(true)}
|
|
369
|
+
disabled={deleting}
|
|
370
|
+
className="inline-flex items-center gap-1.5 px-2.5 min-h-[32px] text-xs rounded-md border border-destructive/30 text-destructive hover:bg-destructive/10 cursor-pointer disabled:opacity-50 transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
371
|
+
>
|
|
372
|
+
{deleting ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
373
|
+
{copy.deleteSkill}
|
|
374
|
+
</button>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
{/* Delete confirmation */}
|
|
381
|
+
<ConfirmDialog
|
|
382
|
+
open={confirmDelete}
|
|
383
|
+
title={copy.confirmDeleteTitle}
|
|
384
|
+
message={skillName ? copy.confirmDeleteMessage(skillName) : ''}
|
|
385
|
+
confirmLabel={copy.confirmDeleteAction}
|
|
386
|
+
cancelLabel={copy.cancelAction}
|
|
387
|
+
onConfirm={() => void handleDelete()}
|
|
388
|
+
onCancel={() => setConfirmDelete(false)}
|
|
389
|
+
variant="destructive"
|
|
390
|
+
/>
|
|
391
|
+
</>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ────────── Meta Card ────────── */
|
|
396
|
+
|
|
397
|
+
function MetaCard({
|
|
398
|
+
label,
|
|
399
|
+
value,
|
|
400
|
+
tone = 'default',
|
|
401
|
+
}: {
|
|
402
|
+
label: string;
|
|
403
|
+
value: string;
|
|
404
|
+
tone?: 'ok' | 'muted' | 'default';
|
|
405
|
+
}) {
|
|
406
|
+
const valueColor =
|
|
407
|
+
tone === 'ok' ? 'text-emerald-600 dark:text-emerald-400'
|
|
408
|
+
: tone === 'muted' ? 'text-muted-foreground'
|
|
409
|
+
: 'text-foreground';
|
|
410
|
+
return (
|
|
411
|
+
<div className="rounded-lg border border-border bg-background px-3 py-2.5">
|
|
412
|
+
<span className="text-2xs text-muted-foreground block mb-0.5">{label}</span>
|
|
413
|
+
<span className={`text-sm font-medium capitalize ${valueColor}`}>{value}</span>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
}
|