diff --git a/dashboard/src/lib/ctxClient.ts b/dashboard/src/lib/ctxClient.ts index c973b56..ba6581d 100644 --- a/dashboard/src/lib/ctxClient.ts +++ b/dashboard/src/lib/ctxClient.ts @@ -50,6 +50,8 @@ import type { SkillToolsRequest, SkillToolsResponse, StatusIdResponse, + UpdateRequest, + UpdateResponse, UpstreamRequest, UpstreamResponse, } from "./types"; @@ -145,6 +147,7 @@ async function getText(path: string): Promise { export const ctx = { add: (req: AddRequest) => post("/add", req), + update: (req: UpdateRequest) => post("/update", req), retrieve: (req: RetrieveRequest) => post("/retrieve", req), expand: (req: ExpandRequest) => post("/expand", req), forget: (req: ForgetRequest) => post("/forget", req), diff --git a/dashboard/src/lib/i18n.tsx b/dashboard/src/lib/i18n.tsx index d36d384..e9263e1 100644 --- a/dashboard/src/lib/i18n.tsx +++ b/dashboard/src/lib/i18n.tsx @@ -300,6 +300,22 @@ const zh: Dict = { "skills.detail.confidence": "置信度", "skills.detail.source": "来源", "skills.detail.createdAt": "创建时间", + "skills.detail.name": "名称", + "skills.detail.tags": "标签", + "skills.detail.edit": "编辑", + "skills.detail.save": "保存", + "skills.detail.saving": "保存中…", + "skills.detail.saved": "已保存", + "skills.detail.confirm": "确认", + "skills.detail.confirming": "确认中…", + "skills.detail.confirmed": "已确认", + "skills.detail.saveError": "保存失败", + "skills.detail.confirmError": "确认失败", + "skills.detail.state.autoGenerated": "自动生成", + "skills.detail.state.edited": "已编辑", + "skills.detail.state.confirmed": "已确认", + "skills.detail.editHint": "编辑后请点击「保存」使更改生效;确认后置信度将设为 100%。", + "skills.detail.tagsPlaceholder": "逗号分隔,如: python, rag", // env vault "envVault.vault.title": "环境变量保险箱", @@ -666,6 +682,22 @@ const en: Dict = { "skills.detail.confidence": "Confidence", "skills.detail.source": "Source", "skills.detail.createdAt": "Created at", + "skills.detail.name": "Name", + "skills.detail.tags": "Tags", + "skills.detail.edit": "Edit", + "skills.detail.save": "Save", + "skills.detail.saving": "Saving…", + "skills.detail.saved": "Saved", + "skills.detail.confirm": "Confirm", + "skills.detail.confirming": "Confirming…", + "skills.detail.confirmed": "Confirmed", + "skills.detail.saveError": "Save failed", + "skills.detail.confirmError": "Confirmation failed", + "skills.detail.state.autoGenerated": "Auto-generated", + "skills.detail.state.edited": "Edited", + "skills.detail.state.confirmed": "Confirmed", + "skills.detail.editHint": "Click Save after editing to persist changes. Confirming sets confidence to 100%.", + "skills.detail.tagsPlaceholder": "Comma-separated, e.g. python, rag", "envVault.vault.title": "Env Vault", "envVault.vault.desc": "Reusable KEY=value records, stored encrypted on disk (fixed app-level key, no passphrase).", @@ -1030,6 +1062,22 @@ const ja: Dict = { "skills.detail.confidence": "信頼度", "skills.detail.source": "ソース", "skills.detail.createdAt": "作成日時", + "skills.detail.name": "名前", + "skills.detail.tags": "タグ", + "skills.detail.edit": "編集", + "skills.detail.save": "保存", + "skills.detail.saving": "保存中…", + "skills.detail.saved": "保存済み", + "skills.detail.confirm": "確認", + "skills.detail.confirming": "確認中…", + "skills.detail.confirmed": "確認済み", + "skills.detail.saveError": "保存に失敗", + "skills.detail.confirmError": "確認に失敗", + "skills.detail.state.autoGenerated": "自動生成", + "skills.detail.state.edited": "編集済み", + "skills.detail.state.confirmed": "確認済み", + "skills.detail.editHint": "編集後に「保存」をクリックして変更を反映します。確認すると信頼度が100%になります。", + "skills.detail.tagsPlaceholder": "カンマ区切り, 例: python, rag", "envVault.vault.title": "環境変数ボールト", "envVault.vault.desc": "再利用可能な KEY=value レコードを、暗号化してローカル保存します(アプリ固定キー、パスフレーズ不要)。", diff --git a/dashboard/src/lib/types.ts b/dashboard/src/lib/types.ts index b5376a9..631fc8d 100644 --- a/dashboard/src/lib/types.ts +++ b/dashboard/src/lib/types.ts @@ -514,6 +514,24 @@ export interface SkillMdItem { content: string; } +export interface UpdateRequest { + scope: string; + item_id: string; + name?: string; + description?: string; + body?: string; + parameters?: Record; + tags?: string[]; + confirm?: boolean; +} + +export interface UpdateResponse { + status: string; + id: string; + confidence: number; + verified: boolean; +} + export interface SkillMdRequest { scope: string; } diff --git a/dashboard/src/panels/SkillsPanel.tsx b/dashboard/src/panels/SkillsPanel.tsx index d3dd1d5..af1bab3 100644 --- a/dashboard/src/panels/SkillsPanel.tsx +++ b/dashboard/src/panels/SkillsPanel.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { ReactNode } from "react"; import { Badge } from "@/components/ui/badge"; @@ -10,6 +10,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { useScope } from "@/context/ScopeContext"; import { ctx } from "@/lib/ctxClient"; import { useI18n } from "@/lib/i18n"; @@ -87,17 +89,66 @@ function ContentBlock({ ); } +type SkillState = "auto-generated" | "edited" | "confirmed"; + +function getSkillState(item: ContextItem): SkillState { + const confidence = item.provenance.confidence; + if (confidence >= 1.0 || item.provenance.verified) return "confirmed"; + if (item.tags?.includes("edited")) return "edited"; + return "auto-generated"; +} + +function SkillStateBadge({ state }: { state: SkillState }) { + const { t } = useI18n(); + const colorMap: Record = { + "auto-generated": "bg-blue-100 text-blue-700 border-blue-200", + edited: "bg-yellow-100 text-yellow-700 border-yellow-200", + confirmed: "bg-green-100 text-green-700 border-green-200", + }; + const labelMap: Record = { + "auto-generated": t("skills.detail.state.autoGenerated"), + edited: t("skills.detail.state.edited"), + confirmed: t("skills.detail.state.confirmed"), + }; + return ( + + {labelMap[state]} + + ); +} + function SkillDetailDialog({ item, onClose, + onUpdated, }: { item: ContextItem | null; onClose: () => void; + onUpdated: () => void; }) { const { t } = useI18n(); + const { scope } = useScope(); + + const [isEditing, setIsEditing] = useState(false); + const [saving, setSaving] = useState(false); + const [confirming, setConfirming] = useState(false); + const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + // Reset state when item changes + useEffect(() => { + setIsEditing(false); + setError(null); + setSuccessMsg(null); + setSaving(false); + setConfirming(false); + }, [item?.id]); + if (!item) return null; - // Handle both string-content and dict-content skills + // Parse content const isStringContent = typeof item.content === "string"; const dictContent = item.content && typeof item.content === "object" @@ -105,64 +156,233 @@ function SkillDetailDialog({ : null; const name = (dictContent?.name as string) || item.summary || item.id; - const description = dictContent?.description as string | undefined; - // For string-content skills, the body IS the raw content string + const description = (dictContent?.description as string) || ""; const body = isStringContent ? (item.content as string) - : (dictContent?.body as string | undefined); + : ((dictContent?.body as string) || ""); const parameters = dictContent?.parameters as Record | undefined; const inputSchema = dictContent?.inputSchema as Record | undefined; const version = dictContent?.version as string | undefined; const contentTags = dictContent?.tags as string[] | undefined; - // Show item.tags but exclude generic stage/type tags already visible as Badge const displayTags = [ ...(contentTags ?? []), ...(item.tags ?? []).filter( - (t) => !["prompt_skill", "tool_skill", "mcp_skill", "prompt", "tool", "mcp"].includes(t), + (t) => !["prompt_skill", "tool_skill", "mcp_skill", "prompt", "tool", "mcp", "edited"].includes(t), ), ].filter((v, i, a) => a.indexOf(v) === i); const confidence = Math.round(item.provenance.confidence * 100); const confidenceColor = confidence >= 80 ? "text-green-500" : confidence >= 60 ? "text-yellow-500" : "text-red-500"; + const skillState = getSkillState(item); + + // Form state + const [formName, setFormName] = useState(name); + const [formDesc, setFormDesc] = useState(description); + const [formBody, setFormBody] = useState(body); + const [formParamsText, setFormParamsText] = useState( + parameters ? JSON.stringify(parameters, null, 2) : "" + ); + const [formTagsText, setFormTagsText] = useState(displayTags.join(", ")); + const [paramsError, setParamsError] = useState(null); + + function enterEditMode() { + setFormName(name); + setFormDesc(description); + setFormBody(body); + setFormParamsText(parameters ? JSON.stringify(parameters, null, 2) : ""); + setFormTagsText(displayTags.join(", ")); + setParamsError(null); + setError(null); + setSuccessMsg(null); + setIsEditing(true); + } + + function cancelEdit() { + setIsEditing(false); + setError(null); + setParamsError(null); + } + + async function handleSave() { + if (!item) return; + // Validate JSON parameters + let parsedParams: Record | undefined; + if (formParamsText.trim()) { + try { + parsedParams = JSON.parse(formParamsText); + } catch { + setParamsError("Parameters must be valid JSON"); + return; + } + } + setParamsError(null); + + // Parse tags + const parsedTags = formTagsText + .split(/[,,]/) + .map((t) => t.trim()) + .filter(Boolean); + + setSaving(true); + setError(null); + setSuccessMsg(null); + try { + await ctx.update({ + scope, + item_id: item.id, + name: formName || undefined, + description: formDesc || undefined, + body: formBody, + parameters: parsedParams, + tags: [...parsedTags, "edited"], + }); + setSuccessMsg(t("skills.detail.saved")); + setIsEditing(false); + onUpdated(); + } catch (e) { + setError(t("skills.detail.saveError") + ": " + (e instanceof Error ? e.message : String(e))); + } finally { + setSaving(false); + } + } + + async function handleConfirm() { + if (!item) return; + setConfirming(true); + setError(null); + setSuccessMsg(null); + try { + await ctx.update({ + scope, + item_id: item.id, + confirm: true, + }); + setSuccessMsg(t("skills.detail.confirmed")); + onUpdated(); + } catch (e) { + setError(t("skills.detail.confirmError") + ": " + (e instanceof Error ? e.message : String(e))); + } finally { + setConfirming(false); + } + } + + const isEditedUnconfirmed = skillState === "edited"; return ( { if (!open) onClose(); }}> - + {/* Header */}
- - {name} - +
+ + {isEditing ? ( + setFormName(e.target.value)} + className="text-lg font-semibold h-auto py-1" + placeholder={t("skills.detail.name")} + /> + ) : ( + name + )} + + +
- {description && !isStringContent && ( + {!isEditing && description && !isStringContent && (

{description}

)} - {version && ( + {isEditing && ( +
+ setFormDesc(e.target.value)} + className="text-sm" + placeholder={t("skills.detail.description")} + /> +
+ )} + {version && !isEditing && (

v{version}

)}
{/* Scrollable body */}
- {body && ( - + {/* Error / Success messages */} + {error && ( +
+ {error} +
+ )} + {successMsg && ( +
+ {successMsg} +
)} - {parameters && ( - + {/* Edit hint */} + {isEditing && ( +

{t("skills.detail.editHint")}

)} - {inputSchema && ( + {/* Body */} + {isEditing ? ( +
+ + {t("skills.detail.body")} + +