'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { AlertCircle, BarChart3, Check, CheckCircle2, ChevronDown, ClipboardList, Download, FileCode2, FileText, GitCompare, Info, ListChecks, Loader2, Puzzle, RefreshCw, Rocket, Send, ShieldCheck, ShieldAlert, Trash2, Upload, X, XCircle, } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { approveSkillDraft, deleteSkill, disablePublishedSkill, downloadSkill, getSkillDetail, getSkillFile, getSkillVersion, listSkillCandidates, listSkillDrafts, listSkills, publishSkillDraft, recheckSkillDraftSafety, regenerateSkillDraft, rejectSkillDraft, rollbackPublishedSkill, submitSkillDraft, synthesizeSkillDraft, uploadSkill, } from '@/lib/api'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { SkillDetailView } from '@/components/skills/SkillDetailView'; import type { Skill, SkillDetailResponse, SkillDraft, SkillDraftEvalReport, SkillDraftSafetyReport, SkillFileContent, SkillLearningCandidate, } from '@/types'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; const TERMINAL_DRAFT_STATUSES = new Set(['rejected', 'published', 'disabled', 'archived']); const REJECTABLE_DRAFT_STATUSES = new Set(['draft', 'in_review', 'approved']); export default function SkillsPage() { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const [skills, setSkills] = useState([]); const [candidates, setCandidates] = useState([]); const [drafts, setDrafts] = useState([]); const [loading, setLoading] = useState(true); const [actionId, setActionId] = useState(null); const [error, setError] = useState(null); const [showUpload, setShowUpload] = useState(false); const [ignoredCandidates, setIgnoredCandidates] = useState>(new Set()); const [selectedSkillName, setSelectedSkillName] = useState(null); const [skillDetail, setSkillDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [versionLoading, setVersionLoading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [fileLoading, setFileLoading] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const [skillData, candidateData, draftData] = await Promise.all([ listSkills(), listSkillCandidates().catch(() => []), listSkillDrafts().catch(() => []), ]); setSkills(Array.isArray(skillData) ? skillData : []); setCandidates(Array.isArray(candidateData) ? candidateData : []); setDrafts(Array.isArray(draftData) ? draftData : []); } catch (err: any) { setError(err.message || pickAppText(locale, '加载技能失败', 'Failed to load skills')); } finally { setLoading(false); } }, [locale]); useEffect(() => { void load(); }, [load]); const runAction = async (id: string, action: () => Promise) => { setActionId(id); setError(null); try { await action(); await load(); } catch (err: any) { setError(err.message || t('操作失败', 'Action failed')); } finally { setActionId(null); } }; const openSkillDetail = async (name: string) => { setSelectedSkillName(name); setSkillDetail(null); setSelectedFile(null); setDetailLoading(true); setError(null); try { setSkillDetail(await getSkillDetail(name)); } catch (err: any) { setError(err.message || t('加载技能详情失败', 'Failed to load skill details')); setSelectedSkillName(null); } finally { setDetailLoading(false); } }; const openSkillVersion = async (version: string) => { if (!selectedSkillName || skillDetail?.currentVersion === version) return; setVersionLoading(true); setSelectedFile(null); setError(null); try { setSkillDetail(await getSkillVersion(selectedSkillName, version)); } catch (err: any) { setError(err.message || t('加载技能版本失败', 'Failed to load skill version')); } finally { setVersionLoading(false); } }; const openSkillFile = async (filePath: string) => { if (!selectedSkillName || !skillDetail) return; setFileLoading(true); setError(null); try { setSelectedFile(await getSkillFile(selectedSkillName, skillDetail.currentVersion, filePath)); } catch (err: any) { setError(err.message || t('加载文件失败', 'Failed to load file')); } finally { setFileLoading(false); } }; const hiddenCandidateStatuses = new Set(['draft_ready', 'in_review', 'approved', 'rejected', 'superseded', 'published']); const visibleCandidates = candidates.filter( (candidate) => !ignoredCandidates.has(candidate.candidate_id) && !hiddenCandidateStatuses.has(candidate.status) && !candidate.draft_id ); const visibleDrafts = drafts.filter((draft) => !TERMINAL_DRAFT_STATUSES.has(draft.status)); if (loading) { return (
); } return (

{t('技能', 'Skills')}

{error && (
{error}
)} {showUpload && ( { setShowUpload(false); void load(); }} onCancel={() => setShowUpload(false)} onError={(msg) => setError(msg)} /> )} {selectedSkillName && (
{detailLoading || !skillDetail ? ( ) : ( void openSkillVersion(version)} onOpenFile={(filePath) => void openSkillFile(filePath)} badges={ <> {skillDetail.skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')} {skillDetail.skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')} } actions={
{skillDetail.skill.source === 'workspace' && ( <> )}
} labels={{ overview: t('说明', 'Overview'), files: t('文件', 'Files'), versions: t('版本', 'Versions'), noReadme: t('暂无说明', 'No overview available'), noFiles: t('暂无文件', 'No files'), selectFile: t('选择一个文件查看详情', 'Select a file to view details'), binaryFile: t('二进制文件暂不预览', 'Binary file preview is not available'), current: t('当前', 'Current'), size: t('大小', 'Size'), }} /> )}
)} {!selectedSkillName && ( {t('已发布', 'Published')} {t('候选', 'Candidates')} {t('草稿评审', 'Draft review')} void openSkillDetail(name)} onDownload={(name) => downloadSkill(name).catch((err) => setError(err.message))} onDelete={(name) => void runAction(`delete:${name}`, () => deleteSkill(name))} onDisable={(name) => runAction(`disable:${name}`, () => disablePublishedSkill(name, t('人工禁用', 'Manual disable'))) } onRollback={onRollbackSkill} /> {t('学习候选', 'Learning candidates')} {visibleCandidates.length === 0 ? ( } text={t('暂无学习候选', 'No learning candidates yet')} /> ) : (
{visibleCandidates.map((candidate) => ( setIgnoredCandidates((prev) => new Set(prev).add(candidate.candidate_id))} onSynthesize={() => runAction(`draft:${candidate.candidate_id}`, () => synthesizeSkillDraft(candidate.candidate_id) ) } onRegenerate={() => runAction(`regen:${candidate.candidate_id}`, () => regenerateSkillDraft(candidate.candidate_id) ) } /> ))}
)}
{t('草稿、评审与发布', 'Drafts, review, and publish')} {visibleDrafts.length === 0 ? ( } text={t('暂无草稿', 'No drafts yet')} /> ) : (
{visibleDrafts.map((draft) => ( runAction(`submit:${draft.draft_id}`, () => submitSkillDraft(draft.skill_name, draft.draft_id) ) } onApprove={() => runAction(`approve:${draft.draft_id}`, () => approveSkillDraft(draft.skill_name, draft.draft_id) ) } onReject={() => runAction(`reject:${draft.draft_id}`, () => rejectSkillDraft(draft.skill_name, draft.draft_id) ) } onRecheckSafety={() => runAction(`safety:${draft.draft_id}`, () => recheckSkillDraftSafety(draft.skill_name, draft.draft_id) ) } onPublish={(confirmHighRisk) => runAction(`publish:${draft.draft_id}`, () => publishSkillDraft(draft.skill_name, draft.draft_id, '', confirmHighRisk) ) } /> ))}
)}
)}
); function onRollbackSkill(name: string) { const target = window.prompt(t('回滚到版本,例如 v0001', 'Rollback target version, for example v0001')); if (target) { void runAction(`rollback:${name}`, () => rollbackPublishedSkill(name, target, t('人工回滚', 'Manual rollback')) ).then(() => { if (selectedSkillName === name) { void openSkillDetail(name); } }); } } } function PublishedSkillsTable({ skills, onOpen, onDownload, onDelete, onDisable, onRollback, }: { skills: Skill[]; onOpen: (name: string) => void; onDownload: (name: string) => void; onDelete: (name: string) => void; onDisable: (name: string) => void; onRollback: (name: string) => void; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); return ( {skills.length === 0 ? ( } text={t('暂无技能', 'No skills yet')} /> ) : ( {t('名称', 'Name')} {t('描述', 'Description')} {t('来源', 'Source')} {t('状态', 'Status')} {t('操作', 'Actions')} {skills.map((skill) => ( onOpen(skill.name)} > {skill.name} {skill.description} {skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')} {skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')}
{skill.source === 'workspace' && ( <> )}
))}
)}
); } function CandidateCard({ candidate, actionId, onIgnore, onSynthesize, onRegenerate, }: { candidate: SkillLearningCandidate; actionId: string | null; onIgnore: () => void; onSynthesize: () => Promise; onRegenerate: () => Promise; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const evidence = candidate.evidence || {}; const title = candidateTitle(candidate, t); const affectedSkills = candidate.draft_skill_name ? [candidate.draft_skill_name] : candidate.related_skill_names.length > 0 ? candidate.related_skill_names : []; const sourceRuns = candidate.source_run_ids || []; const sourceSessions = candidate.source_session_ids || []; const risk = candidate.risk_level || 'medium'; const confidence = typeof candidate.confidence === 'number' && candidate.confidence > 0 ? `${Math.round(candidate.confidence * 100)}%` : null; return (
{candidateKindLabel(candidate.kind, t)} {candidateStatusLabel(candidate.status, t)} {t('风险', 'Risk')}: {riskLabel(risk, t)} {confidence && {t('置信度', 'Confidence')}: {confidence}} {typeof candidate.priority === 'number' && candidate.priority > 0 && ( {t('优先级', 'Priority')}: {candidate.priority} )}

{title}

{candidate.reason || t('没有提供候选理由。', 'No candidate reason was provided.')}

} label={t('候选任务', 'Candidate task')} value={candidateTaskSummary(candidate, t)} /> } label={t('影响范围', 'Impact')} value={affectedSkills.length > 0 ? affectedSkills.join(', ') : t('新增技能', 'New skill')} /> } label={t('证据数量', 'Evidence')} value={t( `${sourceRuns.length} 个运行,${sourceSessions.length} 个会话`, `${sourceRuns.length} run(s), ${sourceSessions.length} session(s)` )} />
{(candidate.evidence_summary || candidate.trigger_reason || candidate.last_error) && (
{candidate.evidence_summary && (

{t('证据摘要', 'Evidence summary')}:{' '} {candidate.evidence_summary}

)} {candidate.trigger_reason && (

{t('触发原因', 'Trigger')}:{' '} {triggerReasonLabel(candidate.trigger_reason, t)}

)} {candidate.last_error && (

{t('最近错误', 'Last error')}: {candidate.last_error}

)}
)}
{candidate.candidate_id} {String(evidence.task_id || '') && {t('任务', 'Task')}: {String(evidence.task_id)}} {String(evidence.skill_version || '') && {t('基线版本', 'Base version')}: {String(evidence.skill_version)}} {candidate.created_at && {t('创建于', 'Created')}: {formatDateTime(candidate.created_at)}}
{candidate.draft_id && ( )}
); } function DraftCard({ draft, actionId, onSubmit, onApprove, onReject, onRecheckSafety, onPublish, }: { draft: SkillDraft; actionId: string | null; onSubmit: () => Promise; onApprove: () => Promise; onReject: () => Promise; onRecheckSafety: () => Promise; onPublish: (confirmHighRisk: boolean) => Promise; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const busy = Boolean(actionId); const safety = draft.safety_report; const evalReport = draft.eval_report; const frontmatter = draft.proposed_frontmatter || {}; const description = String(frontmatter.description || '').trim(); const toolHints = normalizeStringList(frontmatter.tools); const publishBlocked = draft.status !== 'approved' || !safety || safety.risk_level === 'critical' || (evalReport?.status !== 'skipped_provider_unavailable' && evalReport?.passed === false); const isHighRisk = safety?.risk_level === 'high'; const highRiskReason = [ ...(safety?.blocked_reasons ?? []), ...(safety?.issues ?? []), safety?.suggested_fix, ].filter(Boolean).join('\n'); const safetyBlocksReview = Boolean(safety && (!safety.passed || safety.risk_level === 'critical')); const submitBlocked = draft.status !== 'draft' || safetyBlocksReview; const approveBlocked = draft.status !== 'in_review' || safetyBlocksReview; const rejectBlocked = !REJECTABLE_DRAFT_STATUSES.has(draft.status); const canPublishLabel = publishBlocked ? publishBlockReason(draft, t) : isHighRisk ? t('高风险草稿,发布前需要再次确认。', 'High-risk draft; publishing requires confirmation.') : t('已满足发布门禁。', 'Publish gates are satisfied.'); const handlePublish = () => { if (isHighRisk) { const confirmed = window.confirm( t('该草稿被标记为高风险。确认发布?', 'This draft is marked high risk. Publish anyway?') ); if (!confirmed) return; } void onPublish(isHighRisk); }; return (
{candidateKindLabel(draft.proposal_kind, t)} {draftStatusLabel(draft.status, t)} {safety && ( {t('安全', 'Safety')}: {safety.passed ? t('通过', 'Pass') : t('阻断', 'Blocked')} · {riskLabel(safety.risk_level, t)} )} {evalReport && ( {evalReport.status === 'skipped_provider_unavailable' ? t('评估跳过', 'Eval skipped') : evalReport.passed ? t('评估通过', 'Eval passed') : t('评估失败', 'Eval failed')} )}
{t('技能名', 'Skill name')}

{draft.skill_name}

{draft.reason || description || t('没有提供草稿说明。', 'No draft notes were provided.')}

} label={t('草稿内容', 'Draft content')} value={description || t('未填写技能描述', 'No skill description')} /> } label={t('基线版本', 'Base version')} value={draft.base_version || t('新增技能,无基线', 'New skill, no base')} /> } label={t('来源', 'Source')} value={draft.trigger_run_id || draft.trigger_session_id || draft.created_by || '-'} />
{isHighRisk && (
{t('高风险理由来自 safety report', 'High-risk reason from safety report')}
{highRiskReason || t('未提供具体理由', 'No concrete reason provided')}
)}
{draft.skill_name}/{draft.draft_id} {t('创建者', 'Author')}: {draft.created_by} {t('创建于', 'Created')}: {formatDateTime(draft.created_at)}
{t('拟发布的技能正文', 'Proposed skill body')}
{toolHints.length > 0 && (
{toolHints.map((tool) => ( {tool} ))}
)}
{draft.proposed_content.trim() ? ( ) : (

{t('草稿没有正文内容。', 'This draft has no body content.')}

)}
); } function SafetyReportPanel({ report }: { report?: SkillDraftSafetyReport | null }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); if (!report) { return ( } title={t('安全报告', 'Safety report')} empty={t('还没有安全报告,不能送审或发布。', 'No safety report yet; review and publishing are blocked.')} /> ); } const problems = [...(report.blocked_reasons || []), ...(report.issues || [])]; return (
{report.passed ? : } {t('安全报告', 'Safety report')}
{report.passed ? t('通过', 'Pass') : t('阻断', 'Blocked')} · {riskLabel(report.risk_level, t)}
{problems.length > 0 ? (

{t('发现的问题', 'Findings')}

    {problems.map((item, index) => (
  • {item}
  • ))}
) : (

{t('没有发现阻断项或需要人工注意的问题。', 'No blockers or manual-review issues were found.')}

)} {report.suggested_fix && (

{t('建议处理', 'Suggested fix')}: {report.suggested_fix}

)}
{formatDateTime(report.created_at)}
); } function EvalReportPanel({ report }: { report?: SkillDraftEvalReport | null }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); if (!report) { return ( } title={t('评估报告', 'Eval report')} empty={t('还没有评估报告。若 provider 不可用,系统会标记为跳过。', 'No eval report yet. If the provider is unavailable, it will be marked as skipped.')} /> ); } if (report.status === 'skipped_provider_unavailable') { return (
{t('评估报告', 'Eval report')}

{t('评估被跳过:当前没有可用 provider。这个状态不会阻断发布,但代表没有回放验证。', 'Eval was skipped because no provider was available. This does not block publishing, but no replay validation was run.')}

); } return (
{t('评估报告', 'Eval report')}
{report.passed ? t('通过', 'Pass') : t('失败', 'Failed')}
= 0 ? '+' : ''}${formatScore(report.score_delta)}`} tone={report.score_delta < 0 ? 'bad' : report.score_delta > 0 ? 'good' : 'neutral'} />
} label={t('改进', 'Improved')} value={String(report.improved_count)} /> } label={t('回退', 'Regressed')} value={String(report.regression_count)} /> } label={t('不变', 'Unchanged')} value={String(report.unchanged_count)} />
{report.cases.length > 0 && (
{t('回放案例', 'Replay cases')}
{report.cases.map((item, index) => ( ))}
{t('运行', 'Run')} {t('基线', 'Baseline')} {t('候选', 'Candidate')} {t('变化', 'Delta')}
{String(item.run_id || '-')} {formatScore(toNumber(item.baseline_score))} {formatScore(toNumber(item.candidate_score))} {formatSignedScore(toNumber(item.delta))}
)}
{formatDateTime(report.created_at)}
); } function GateSummary({ title, summary, items, }: { title: string; summary: string; items: Array<{ label: string; ok: boolean }>; }) { return (
{title}

{summary}

{items.map((item) => (
{item.ok ? : } {item.label}
))}
); } function ReadablePanel({ icon, title, empty, }: { icon: React.ReactNode; title: string; empty: string; }) { return (
{icon} {title}

{empty}

); } function ReadableFact({ icon, label, value, }: { icon: React.ReactNode; label: string; value: string; }) { return (
{icon} {label}
{value || '-'}
); } function MetricTile({ label, value, tone = 'neutral', }: { label: string; value: string; tone?: 'neutral' | 'good' | 'bad'; }) { const toneClass = tone === 'bad' ? 'text-destructive' : 'text-foreground'; return (
{label}
{value}
); } function RawDetails({ title, payload }: { title: string; payload: unknown }) { return (
{title}
        {JSON.stringify(payload, null, 2)}
      
); } function MarkdownPreview({ content }: { content: string }) { return (
{content}
); } function candidateTitle(candidate: SkillLearningCandidate, t: (zh: string, en: string) => string): string { const evidence = candidate.evidence || {}; const theme = String(evidence.theme || '').trim(); const taskId = String(evidence.task_id || '').trim(); const related = candidate.related_skill_names.filter(Boolean).join(', '); if (candidate.draft_skill_name) { return t(`为 ${candidate.draft_skill_name} 生成草稿`, `Draft for ${candidate.draft_skill_name}`); } if (candidate.kind === 'new_skill') { return theme ? t(`把“${theme}”沉淀成新技能`, `Extract "${theme}" into a new skill`) : taskId ? t(`从任务 ${taskId} 提炼新技能`, `Extract a new skill from task ${taskId}`) : t('提炼一个新技能', 'Extract a new skill'); } if (candidate.kind === 'revise_skill') { return related ? t(`修订技能 ${related}`, `Revise skill ${related}`) : t('修订已有技能', 'Revise an existing skill'); } if (candidate.kind === 'merge_skills') { return related ? t(`合并技能 ${related}`, `Merge skills ${related}`) : t('合并相似技能', 'Merge similar skills'); } if (candidate.kind === 'retire_skill') { return related ? t(`考虑下线技能 ${related}`, `Consider retiring ${related}`) : t('考虑下线技能', 'Consider retiring a skill'); } return candidate.reason || candidate.candidate_id; } function candidateTaskSummary(candidate: SkillLearningCandidate, t: (zh: string, en: string) => string): string { const evidence = candidate.evidence || {}; const taskId = String(evidence.task_id || '').trim(); const theme = String(evidence.theme || '').trim(); const triggerRun = String(evidence.trigger_run_id || '').trim(); if (taskId) return t(`任务 ${taskId}`, `Task ${taskId}`); if (theme) return t(`主题:${theme}`, `Theme: ${theme}`); if (triggerRun) return t(`由运行 ${triggerRun} 触发`, `Triggered by run ${triggerRun}`); const firstRun = candidate.source_run_ids?.[0]; if (firstRun) return t(`来自 ${candidate.source_run_ids.length} 个运行`, `From ${candidate.source_run_ids.length} run(s)`); return t('系统学习候选', 'System learning candidate'); } function candidateKindLabel(kind: string, t: (zh: string, en: string) => string): string { const labels: Record = { new_skill: t('新增技能', 'New skill'), revise_skill: t('修订技能', 'Revise skill'), merge_skills: t('合并技能', 'Merge skills'), retire_skill: t('下线技能', 'Retire skill'), }; return labels[kind] || kind; } function candidateStatusLabel(status: string, t: (zh: string, en: string) => string): string { const labels: Record = { open: t('待处理', 'Open'), queued: t('排队中', 'Queued'), synthesizing: t('生成中', 'Synthesizing'), draft_ready: t('草稿已就绪', 'Draft ready'), safety_failed: t('安全未通过', 'Safety failed'), eval_failed: t('评估未通过', 'Eval failed'), review_pending: t('等待评审', 'Review pending'), approved: t('已批准', 'Approved'), rejected: t('已拒绝', 'Rejected'), published: t('已发布', 'Published'), failed: t('失败', 'Failed'), superseded: t('已被替代', 'Superseded'), }; return labels[status] || status; } function draftStatusLabel(status: string, t: (zh: string, en: string) => string): string { const labels: Record = { draft: t('草稿', 'Draft'), in_review: t('评审中', 'In review'), approved: t('已批准', 'Approved'), rejected: t('已拒绝', 'Rejected'), published: t('已发布', 'Published'), disabled: t('已禁用', 'Disabled'), archived: t('已归档', 'Archived'), }; return labels[status] || candidateStatusLabel(status, t); } function riskLabel(risk: string, t: (zh: string, en: string) => string): string { const labels: Record = { low: t('低', 'Low'), medium: t('中', 'Medium'), high: t('高', 'High'), critical: t('严重', 'Critical'), }; return labels[risk] || risk; } function triggerReasonLabel(reason: string, t: (zh: string, en: string) => string): string { const labels: Record = { validation_accepted_and_user_satisfied: t('任务验证通过且用户满意', 'Validation accepted and user satisfied'), }; return labels[reason] || reason; } function publishBlockReason(draft: SkillDraft, t: (zh: string, en: string) => string): string { if (draft.status !== 'approved') return t('草稿还没有批准,不能发布。', 'The draft is not approved yet.'); if (!draft.safety_report) return t('缺少安全报告,不能发布。', 'A safety report is required before publishing.'); if (draft.safety_report.risk_level === 'critical' || !draft.safety_report.passed) { return t('安全报告存在阻断项,不能发布。', 'The safety report has blockers.'); } if (draft.eval_report?.status !== 'skipped_provider_unavailable' && draft.eval_report?.passed === false) { return t('评估报告显示回退或未达标,不能发布。', 'The eval report shows a regression or failed score.'); } return t('当前状态不能发布。', 'The current state cannot be published.'); } function normalizeStringList(value: unknown): string[] { if (Array.isArray(value)) { return value.map((item) => String(item).trim()).filter(Boolean); } if (typeof value === 'string') { return value.split(',').map((item) => item.trim()).filter(Boolean); } return []; } function formatDateTime(value?: string | null): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); } function formatScore(value: number): string { if (!Number.isFinite(value)) return '-'; return value.toFixed(2); } function formatSignedScore(value: number): string { if (!Number.isFinite(value)) return '-'; return `${value >= 0 ? '+' : ''}${value.toFixed(2)}`; } function toNumber(value: unknown): number { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; } function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) { return (
{icon}

{text}

); } function UploadSkillForm({ onDone, onCancel, onError, }: { onDone: () => void; onCancel: () => void; onError: (msg: string) => void; }) { const { locale } = useAppI18n(); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const file = fileRef.current?.files?.[0]; if (!file) return; setUploading(true); try { await uploadSkill(file); onDone(); } catch (err: any) { onError(err.message || pickAppText(locale, '上传失败', 'Upload failed')); } finally { setUploading(false); } }; return (
{pickAppText(locale, '上传技能', 'Upload skill')}

{pickAppText(locale, '上传后进入草稿评审,并自动运行 safety 和 eval。', 'After upload, the skill enters draft review and runs safety and eval automatically.')}

); }