'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { AlertCircle, Check, Download, FileText, Loader2, Puzzle, RefreshCw, Rocket, Send, ShieldCheck, Trash2, Upload, X, XCircle, } from 'lucide-react'; import { approveSkillDraft, deleteSkill, disablePublishedSkill, downloadSkill, listSkillCandidates, listSkillDrafts, listSkills, migrateSkills, publishSkillDraft, 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 type { Skill, SkillDraft, 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 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 hiddenCandidateStatuses = new Set(['rejected', 'superseded', 'published']); const visibleCandidates = candidates.filter( (candidate) => !ignoredCandidates.has(candidate.candidate_id) && !hiddenCandidateStatuses.has(candidate.status) ); 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)} /> )} {t('已发布', 'Published')} {t('候选', 'Candidates')} {t('草稿评审', 'Draft review')} 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={(name) => { const target = window.prompt(t('回滚到版本,例如 v0001', 'Rollback target version, for example v0001')); if (target) { void runAction(`rollback:${name}`, () => rollbackPublishedSkill(name, target, t('人工回滚', 'Manual rollback')) ); } }} /> {t('学习候选', 'Learning candidates')} {visibleCandidates.length === 0 ? ( } text={t('暂无学习候选', 'No learning candidates yet')} /> ) : (
{visibleCandidates.map((candidate) => (
{candidate.kind} {candidate.status} {candidate.risk_level || 'medium'} {candidate.candidate_id}

{candidate.reason}

{candidate.evidence_summary && (

{candidate.evidence_summary}

)}

{t('来源 runs', 'Source runs')}: {candidate.source_run_ids.join(', ') || '-'}

{candidate.related_skill_names.length > 0 && (

{t('关联技能', 'Related skills')}: {candidate.related_skill_names.join(', ')}

)} {candidate.last_error && (

{candidate.last_error}

)}
{candidate.draft_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) ) } onPublish={(confirmHighRisk) => runAction(`publish:${draft.draft_id}`, () => publishSkillDraft(draft.skill_name, draft.draft_id, '', confirmHighRisk) ) } /> ))}
)}
); } function PublishedSkillsTable({ skills, onDownload, onDelete, onDisable, onRollback, }: { skills: Skill[]; 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) => ( {skill.name} {skill.description} {skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')} {skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')}
{skill.source === 'workspace' && ( <> )}
))}
)}
); } function DraftCard({ draft, actionId, onSubmit, onApprove, onReject, onPublish, }: { draft: SkillDraft; actionId: string | null; onSubmit: () => Promise; onApprove: () => Promise; onReject: () => 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 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 handlePublish = () => { if (isHighRisk) { const confirmed = window.confirm( t('该草稿被标记为高风险。确认发布?', 'This draft is marked high risk. Publish anyway?') ); if (!confirmed) return; } void onPublish(isHighRisk); }; return (
{draft.proposal_kind} {draft.status} {safety && ( {safety.risk_level} )} {evalReport && ( {evalReport.status === 'skipped_provider_unavailable' ? t('未评估', 'Eval skipped') : evalReport.passed ? t('评估通过', 'Eval passed') : t('评估失败', 'Eval failed')} )} {draft.skill_name}/{draft.draft_id}

{draft.reason || t('无说明', 'No notes')}

{t('base', 'base')}: {draft.base_version || '-'}

{isHighRisk && (
{t('高风险理由来自 safety report', 'High-risk reason from safety report')}
{highRiskReason || t('未提供具体理由', 'No concrete reason provided')}
)}
{t('当前版本', 'Current version')}
            {draft.base_version ? `${t('基线版本', 'Base version')}: ${draft.base_version}` : t('无基线版本,视为新增技能', 'No base version, treated as a new skill')}
          
{t('草稿变更', 'Draft changes')}
            {JSON.stringify(draft.proposed_frontmatter, null, 2)}
            {'\n\n---\n\n'}
            {draft.proposed_content}
          
); } function ReportBlock({ title, empty, payload }: { title: string; empty: string; payload: unknown }) { return (
{title}
{payload ? (
{JSON.stringify(payload, null, 2)}
) : (

{empty}

)}
); } 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.')}

); }