feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
AlertCircle,
|
||||
BarChart3,
|
||||
@ -77,13 +78,25 @@ import { containedJsonTextClass, containedLongTextClass } from '@/lib/text-wrapp
|
||||
|
||||
const TERMINAL_DRAFT_STATUSES = new Set(['rejected', 'published', 'disabled', 'archived']);
|
||||
const REJECTABLE_DRAFT_STATUSES = new Set(['draft', 'in_review', 'approved']);
|
||||
type SkillsTab = 'published' | 'candidates' | 'drafts';
|
||||
|
||||
function normalizeSkillsTab(value: string | null | undefined): SkillsTab {
|
||||
if (value === 'candidates' || value === 'drafts') {
|
||||
return value;
|
||||
}
|
||||
return 'published';
|
||||
}
|
||||
|
||||
export default function SkillsPage() {
|
||||
const { locale } = useAppI18n();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = (zh: string, en: string) => pickAppText(locale, zh, en);
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [candidates, setCandidates] = useState<SkillLearningCandidate[]>([]);
|
||||
const [drafts, setDrafts] = useState<SkillDraft[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<SkillsTab>(() => normalizeSkillsTab(searchParams?.get('tab')));
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [actionId, setActionId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -119,6 +132,23 @@ export default function SkillsPage() {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(normalizeSkillsTab(searchParams?.get('tab')));
|
||||
}, [searchParams]);
|
||||
|
||||
const changeTab = (value: string) => {
|
||||
const nextTab = normalizeSkillsTab(value);
|
||||
setActiveTab(nextTab);
|
||||
const nextParams = new URLSearchParams(searchParams?.toString());
|
||||
if (nextTab === 'published') {
|
||||
nextParams.delete('tab');
|
||||
} else {
|
||||
nextParams.set('tab', nextTab);
|
||||
}
|
||||
const query = nextParams.toString();
|
||||
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
||||
};
|
||||
|
||||
const runAction = async (id: string, action: () => Promise<unknown>) => {
|
||||
setActionId(id);
|
||||
setError(null);
|
||||
@ -193,18 +223,18 @@ export default function SkillsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl space-y-6 bg-white p-6 text-black [--background:0_0%_100%] [--card:0_0%_100%] [--card-foreground:0_0%_0%] [--foreground:0_0%_0%] [--muted-foreground:0_0%_0%] [--popover:0_0%_100%] [--popover-foreground:0_0%_0%] [--secondary-foreground:0_0%_0%]">
|
||||
<div className="mx-auto w-full max-w-6xl space-y-6 overflow-x-hidden bg-white px-4 py-6 text-black [--background:0_0%_100%] [--card:0_0%_100%] [--card-foreground:0_0%_0%] [--foreground:0_0%_0%] [--muted-foreground:0_0%_0%] [--popover:0_0%_100%] [--popover-foreground:0_0%_0%] [--secondary-foreground:0_0%_0%] sm:px-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
||||
<Puzzle className="w-6 h-6" />
|
||||
{t('技能', 'Skills')}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => void load()} variant="outline" size="sm">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button onClick={() => void load()} variant="outline" size="sm" className="h-11">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('刷新', 'Refresh')}
|
||||
</Button>
|
||||
<Button onClick={() => setShowUpload(true)} size="sm">
|
||||
<Button onClick={() => setShowUpload(true)} size="sm" className="h-11">
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{t('上传技能', 'Upload skill')}
|
||||
</Button>
|
||||
@ -238,6 +268,7 @@ export default function SkillsPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-11"
|
||||
onClick={() => {
|
||||
setSelectedSkillName(null);
|
||||
setSkillDetail(null);
|
||||
@ -277,19 +308,20 @@ export default function SkillsPage() {
|
||||
}
|
||||
actions={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => downloadSkill(skillDetail.skill.name).catch((err) => setError(err.message))}>
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => downloadSkill(skillDetail.skill.name).catch((err) => setError(err.message))}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{t('下载', 'Download')}
|
||||
</Button>
|
||||
{skillDetail.skill.source === 'workspace' && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => onRollbackSkill(skillDetail.skill.name)}>
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => onRollbackSkill(skillDetail.skill.name)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('回滚', 'Rollback')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-11"
|
||||
disabled={Boolean(actionId)}
|
||||
onClick={() => void runAction(`disable:${skillDetail.skill.name}`, () => disablePublishedSkill(skillDetail.skill.name, t('人工禁用', 'Manual disable')))}
|
||||
>
|
||||
@ -299,7 +331,7 @@ export default function SkillsPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="h-11 text-destructive hover:text-destructive"
|
||||
disabled={Boolean(actionId)}
|
||||
onClick={() => void runAction(`delete:${skillDetail.skill.name}`, () => deleteSkill(skillDetail.skill.name)).then(() => {
|
||||
setSelectedSkillName(null);
|
||||
@ -330,14 +362,14 @@ export default function SkillsPage() {
|
||||
)}
|
||||
|
||||
{!selectedSkillName && (
|
||||
<Tabs defaultValue="published" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="published">{t('已发布', 'Published')}</TabsTrigger>
|
||||
<TabsTrigger value="candidates">{t('候选', 'Candidates')}</TabsTrigger>
|
||||
<TabsTrigger value="drafts">{t('草稿评审', 'Draft review')}</TabsTrigger>
|
||||
<Tabs value={activeTab} onValueChange={changeTab} className="min-w-0 space-y-4">
|
||||
<TabsList className="h-auto min-h-11 w-full max-w-full justify-start overflow-x-auto sm:w-auto">
|
||||
<TabsTrigger value="published" className="h-10">{t('已发布', 'Published')}</TabsTrigger>
|
||||
<TabsTrigger value="candidates" className="h-10">{t('候选', 'Candidates')}</TabsTrigger>
|
||||
<TabsTrigger value="drafts" className="h-10">{t('草稿评审', 'Draft review')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="published">
|
||||
<TabsContent value="published" className="min-w-0">
|
||||
<PublishedSkillsTable
|
||||
skills={skills}
|
||||
onOpen={(name) => void openSkillDetail(name)}
|
||||
@ -350,7 +382,7 @@ export default function SkillsPage() {
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="candidates">
|
||||
<TabsContent value="candidates" className="min-w-0">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('学习候选', 'Learning candidates')}</CardTitle>
|
||||
@ -384,7 +416,7 @@ export default function SkillsPage() {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="drafts">
|
||||
<TabsContent value="drafts" className="min-w-0">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t('草稿、评审与发布', 'Drafts, review, and publish')}</CardTitle>
|
||||
@ -473,6 +505,54 @@ function PublishedSkillsTable({
|
||||
{skills.length === 0 ? (
|
||||
<EmptyState icon={<Puzzle className="h-8 w-8" />} text={t('暂无技能', 'No skills yet')} />
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3 p-3 md:hidden">
|
||||
{skills.map((skill) => (
|
||||
<div key={`${skill.source}:${skill.name}:card`} className="min-w-0 rounded-lg border border-border bg-white p-4">
|
||||
<button
|
||||
type="button"
|
||||
className="block min-h-11 w-full text-left"
|
||||
onClick={() => onOpen(skill.name)}
|
||||
>
|
||||
<div className={`text-sm font-semibold ${containedLongTextClass}`}>{skill.name}</div>
|
||||
<div className={`mt-1 text-sm leading-5 text-muted-foreground ${containedLongTextClass}`}>
|
||||
{skill.description || '-'}
|
||||
</div>
|
||||
</button>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Badge variant={skill.source === 'builtin' ? 'secondary' : 'default'} className="text-xs">
|
||||
{skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')}
|
||||
</Badge>
|
||||
<Badge variant={skill.available ? 'default' : 'outline'} className="text-xs">
|
||||
{skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => onDownload(skill.name)}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{t('下载', 'Download')}
|
||||
</Button>
|
||||
{skill.source === 'workspace' && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => onRollback(skill.name)}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{t('回滚', 'Rollback')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-11" onClick={() => onDisable(skill.name)}>
|
||||
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||
{t('禁用', 'Disable')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-11 text-destructive hover:text-destructive" onClick={() => onDelete(skill.name)}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t('删除', 'Delete')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@ -490,7 +570,7 @@ function PublishedSkillsTable({
|
||||
className="cursor-pointer"
|
||||
onClick={() => onOpen(skill.name)}
|
||||
>
|
||||
<TableCell className="font-medium">{skill.name}</TableCell>
|
||||
<TableCell className={`font-medium ${containedLongTextClass}`}>{skill.name}</TableCell>
|
||||
<TableCell>
|
||||
<span className="block max-w-[360px] truncate text-sm text-muted-foreground">
|
||||
{skill.description}
|
||||
@ -508,7 +588,14 @@ function PublishedSkillsTable({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={(event) => { event.stopPropagation(); onDownload(skill.name); }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11"
|
||||
aria-label={t('下载', 'Download')}
|
||||
title={t('下载', 'Download')}
|
||||
onClick={(event) => { event.stopPropagation(); onDownload(skill.name); }}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{skill.source === 'workspace' && (
|
||||
@ -516,7 +603,9 @@ function PublishedSkillsTable({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
className="h-11 w-11"
|
||||
aria-label={t('回滚', 'Rollback')}
|
||||
title={t('回滚', 'Rollback')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onRollback(skill.name);
|
||||
@ -527,7 +616,9 @@ function PublishedSkillsTable({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
className="h-11 w-11"
|
||||
aria-label={t('禁用', 'Disable')}
|
||||
title={t('禁用', 'Disable')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDisable(skill.name);
|
||||
@ -538,7 +629,9 @@ function PublishedSkillsTable({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive"
|
||||
className="h-11 w-11 text-destructive hover:text-destructive"
|
||||
aria-label={t('删除', 'Delete')}
|
||||
title={t('删除', 'Delete')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onDelete(skill.name);
|
||||
@ -554,6 +647,8 @@ function PublishedSkillsTable({
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -590,7 +685,7 @@ function CandidateCard({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-white p-4">
|
||||
<div className="min-w-0 max-w-full rounded-lg border border-border bg-white p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@ -607,7 +702,7 @@ function CandidateCard({
|
||||
|
||||
<div>
|
||||
<h3 className="break-words text-base font-semibold tracking-normal">{title}</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||
<p className={`mt-1 text-sm leading-6 text-muted-foreground ${containedLongTextClass}`}>
|
||||
{candidate.reason || t('没有提供候选理由。', 'No candidate reason was provided.')}
|
||||
</p>
|
||||
</div>
|
||||
@ -656,7 +751,7 @@ function CandidateCard({
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<span className="font-mono">{candidate.candidate_id}</span>
|
||||
<span className={`font-mono ${containedLongTextClass}`}>{candidate.candidate_id}</span>
|
||||
{String(evidence.task_id || '') && <span>{t('任务', 'Task')}: {String(evidence.task_id)}</span>}
|
||||
{String(evidence.skill_version || '') && <span>{t('基线版本', 'Base version')}: {String(evidence.skill_version)}</span>}
|
||||
{candidate.created_at && <span>{t('创建于', 'Created')}: {formatDateTime(candidate.created_at)}</span>}
|
||||
@ -666,11 +761,12 @@ function CandidateCard({
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" disabled={Boolean(actionId)} onClick={onIgnore}>
|
||||
<Button size="sm" variant="outline" className="h-11" disabled={Boolean(actionId)} onClick={onIgnore}>
|
||||
{t('忽略', 'Ignore')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-11"
|
||||
disabled={Boolean(actionId)}
|
||||
onClick={() => void onSynthesize()}
|
||||
>
|
||||
@ -685,6 +781,7 @@ function CandidateCard({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-11"
|
||||
disabled={Boolean(actionId)}
|
||||
onClick={() => void onRegenerate()}
|
||||
>
|
||||
@ -753,7 +850,7 @@ function DraftCard({
|
||||
void onPublish(isHighRisk);
|
||||
};
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-white p-4">
|
||||
<div className="min-w-0 max-w-full rounded-lg border border-border bg-white p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@ -776,9 +873,9 @@ function DraftCard({
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="text-xs font-medium text-muted-foreground">{t('技能名', 'Skill name')}</div>
|
||||
<h3 className="break-words text-lg font-semibold tracking-normal">{draft.skill_name}</h3>
|
||||
<h3 className={`text-lg font-semibold tracking-normal ${containedLongTextClass}`}>{draft.skill_name}</h3>
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||
<p className={`mt-1 text-sm leading-6 text-muted-foreground ${containedLongTextClass}`}>
|
||||
{draft.reason || description || t('没有提供草稿说明。', 'No draft notes were provided.')}
|
||||
</p>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
||||
@ -805,37 +902,37 @@ function DraftCard({
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<span className="font-mono">{draft.skill_name}/{draft.draft_id}</span>
|
||||
<span className={`font-mono ${containedLongTextClass}`}>{draft.skill_name}/{draft.draft_id}</span>
|
||||
<span>{t('创建者', 'Author')}: {draft.created_by}</span>
|
||||
<span>{t('创建于', 'Created')}: {formatDateTime(draft.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" disabled={busy || submitBlocked} onClick={() => void onSubmit()}>
|
||||
<Button variant="outline" size="sm" className="h-11" disabled={busy || submitBlocked} onClick={() => void onSubmit()}>
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{t('送审', 'Submit')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={busy || approveBlocked} onClick={() => void onApprove()}>
|
||||
<Button variant="outline" size="sm" className="h-11" disabled={busy || approveBlocked} onClick={() => void onApprove()}>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
{t('批准', 'Approve')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={busy || rejectBlocked} onClick={() => void onReject()}>
|
||||
<Button variant="outline" size="sm" className="h-11" disabled={busy || rejectBlocked} onClick={() => void onReject()}>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
{t('拒绝', 'Reject')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" disabled={busy || TERMINAL_DRAFT_STATUSES.has(draft.status)} onClick={() => void onRecheckSafety()}>
|
||||
<Button variant="outline" size="sm" className="h-11" disabled={busy || TERMINAL_DRAFT_STATUSES.has(draft.status)} onClick={() => void onRecheckSafety()}>
|
||||
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||
{t('复检', 'Recheck')}
|
||||
</Button>
|
||||
<Button size="sm" disabled={busy || publishBlocked} onClick={handlePublish}>
|
||||
<Button size="sm" className="h-11" disabled={busy || publishBlocked} onClick={handlePublish}>
|
||||
<Rocket className="mr-2 h-4 w-4" />
|
||||
{t('发布', 'Publish')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 lg:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="mt-4 grid min-w-0 gap-3 lg:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div className="min-w-0 max-w-full rounded-md border border-border bg-muted/20 p-3 sm:p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
@ -858,7 +955,7 @@ function DraftCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="min-w-0 space-y-3">
|
||||
<GateSummary
|
||||
title={t('发布门禁', 'Publish gates')}
|
||||
summary={canPublishLabel}
|
||||
@ -883,7 +980,7 @@ function DraftCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
<div className="mt-3 grid min-w-0 gap-3 md:grid-cols-2">
|
||||
<SafetyReportPanel report={safety} />
|
||||
<EvalReportPanel report={evalReport} />
|
||||
</div>
|
||||
@ -905,7 +1002,7 @@ function SafetyReportPanel({ report }: { report?: SkillDraftSafetyReport | null
|
||||
}
|
||||
const problems = [...(report.blocked_reasons || []), ...(report.issues || [])];
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="min-w-0 rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
{report.passed ? <ShieldCheck className="h-4 w-4 text-muted-foreground" /> : <ShieldAlert className="h-4 w-4 text-destructive" />}
|
||||
@ -922,7 +1019,7 @@ function SafetyReportPanel({ report }: { report?: SkillDraftSafetyReport | null
|
||||
{problems.map((item, index) => (
|
||||
<li key={`${item}:${index}`} className="flex gap-2">
|
||||
<AlertCircle className="mt-1 h-3.5 w-3.5 shrink-0 text-destructive" />
|
||||
<span>{item}</span>
|
||||
<span className={containedLongTextClass}>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -933,7 +1030,7 @@ function SafetyReportPanel({ report }: { report?: SkillDraftSafetyReport | null
|
||||
</p>
|
||||
)}
|
||||
{report.suggested_fix && (
|
||||
<p className="mt-3 rounded-md border border-border bg-white p-3 text-sm">
|
||||
<p className={`mt-3 rounded-md border border-border bg-white p-3 text-sm ${containedLongTextClass}`}>
|
||||
<span className="font-medium">{t('建议处理', 'Suggested fix')}:</span> {report.suggested_fix}
|
||||
</p>
|
||||
)}
|
||||
@ -957,7 +1054,7 @@ function EvalReportPanel({ report }: { report?: SkillDraftEvalReport | null }) {
|
||||
}
|
||||
if (report.status === 'skipped_provider_unavailable') {
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="min-w-0 rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
||||
{t('评估报告', 'Eval report')}
|
||||
@ -970,7 +1067,7 @@ function EvalReportPanel({ report }: { report?: SkillDraftEvalReport | null }) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="min-w-0 rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
||||
@ -1002,7 +1099,19 @@ function EvalReportPanel({ report }: { report?: SkillDraftEvalReport | null }) {
|
||||
<div className="border-b border-border px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||
{t('回放案例', 'Replay cases')}
|
||||
</div>
|
||||
<div className="max-h-48 overflow-auto">
|
||||
<div className="space-y-2 p-3 md:hidden">
|
||||
{report.cases.map((item, index) => (
|
||||
<div key={`${String(item.run_id || index)}:${index}:card`} className="rounded-md border border-border bg-muted/20 p-3 text-xs">
|
||||
<div className={`font-mono ${containedLongTextClass}`}>{String(item.run_id || '-')}</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-2">
|
||||
<MetricTile label={t('基线', 'Baseline')} value={formatScore(toNumber(item.baseline_score))} />
|
||||
<MetricTile label={t('候选', 'Candidate')} value={formatScore(toNumber(item.candidate_score))} />
|
||||
<MetricTile label={t('变化', 'Delta')} value={formatSignedScore(toNumber(item.delta))} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden max-h-48 overflow-auto md:block">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="bg-muted/40 text-muted-foreground">
|
||||
<tr>
|
||||
@ -1042,7 +1151,7 @@ function GateSummary({
|
||||
items: Array<{ label: string; ok: boolean }>;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="min-w-0 rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||
<ListChecks className="h-4 w-4 text-muted-foreground" />
|
||||
{title}
|
||||
@ -1070,7 +1179,7 @@ function ReadablePanel({
|
||||
empty: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="min-w-0 rounded-md border border-border bg-muted/20 p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-medium">
|
||||
{icon}
|
||||
{title}
|
||||
@ -1090,7 +1199,7 @@ function ReadableFact({
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-white p-3">
|
||||
<div className="min-w-0 rounded-md border border-border bg-white p-3">
|
||||
<div className="mb-1 flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
{icon}
|
||||
{label}
|
||||
@ -1111,7 +1220,7 @@ function MetricTile({
|
||||
}) {
|
||||
const toneClass = tone === 'bad' ? 'text-destructive' : 'text-foreground';
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-white p-3">
|
||||
<div className="min-w-0 rounded-md border border-border bg-white p-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">{label}</div>
|
||||
<div className={`mt-1 text-lg font-semibold ${toneClass}`}>{value}</div>
|
||||
</div>
|
||||
@ -1121,7 +1230,7 @@ function MetricTile({
|
||||
function RawDetails({ title, payload }: { title: string; payload: unknown }) {
|
||||
return (
|
||||
<details className="mt-3 min-w-0 max-w-full overflow-hidden rounded-md border border-border bg-white">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||
<summary className="flex h-11 cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||
{title}
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</summary>
|
||||
@ -1134,7 +1243,7 @@ function RawDetails({ title, payload }: { title: string; payload: unknown }) {
|
||||
|
||||
function MarkdownPreview({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="prose prose-sm max-w-none text-black prose-a:text-black prose-code:rounded prose-code:bg-white prose-code:px-1 prose-code:py-0.5 prose-code:text-black prose-headings:text-black prose-headings:tracking-normal prose-li:text-black prose-p:text-black prose-pre:border prose-pre:border-border prose-pre:bg-white prose-pre:text-black prose-strong:text-black [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||
<div className={`prose prose-sm max-w-none text-black prose-a:text-black prose-code:rounded prose-code:bg-white prose-code:px-1 prose-code:py-0.5 prose-code:text-black prose-headings:text-black prose-headings:tracking-normal prose-li:text-black prose-p:text-black prose-pre:border prose-pre:border-border prose-pre:bg-white prose-pre:text-black prose-strong:text-black [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_*]:min-w-0 ${containedLongTextClass}`}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user