'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { AlertCircle, Check, Download, FileText, Loader2, Puzzle, RefreshCw, Rocket, Send, ShieldCheck, Trash2, Upload, Wand2, X, XCircle, } from 'lucide-react'; import { approveSkillDraft, deleteSkill, disablePublishedSkill, downloadSkill, listSkillCandidates, listSkillDrafts, listSkills, publishSkillDraft, regenerateSkillDraft, rejectSkillDraft, rollbackPublishedSkill, runSkillLearningOnce, 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'; 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 [deleting, setDeleting] = useState(null); 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 confirmDelete = async (name: string) => { await runAction(`delete:${name}`, async () => { await deleteSkill(name); setDeleting(null); }); }; if (loading) { return (
); } return (

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

{error && (
{error}
)} {showUpload && ( { setShowUpload(false); void load(); }} onCancel={() => setShowUpload(false)} onError={(msg) => setError(msg)} /> )} {deleting && (

{t('确定删除技能', 'Delete skill')} {deleting}?

)} {t('已发布', 'Published')} {t('候选', 'Candidates')} {t('草稿/评审', 'Drafts')} downloadSkill(name).catch((err) => setError(err.message))} onDelete={(name) => setDeleting(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')} {candidates.length === 0 ? ( } text={t('暂无学习候选', 'No learning candidates yet')} /> ) : (
{candidates.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')} {drafts.length === 0 ? ( } text={t('暂无草稿', 'No drafts yet')} /> ) : (
{drafts.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={() => runAction(`publish:${draft.draft_id}`, async () => { const confirmHighRisk = draft.safety_report?.risk_level === 'high'; if (confirmHighRisk && !window.confirm(t('这是高风险草稿,确认发布?', 'This is a high-risk draft. Publish anyway?'))) { return; } await 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: () => 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.passed || safety.risk_level === 'critical' || (evalReport?.status !== 'skipped_provider_unavailable' && evalReport?.passed === false); 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 || '-'}

          {JSON.stringify(draft.proposed_frontmatter, null, 2)}
        
	          {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')}
); }