refactor(intelligent-eval): split list page into dedicated pages
All checks were successful
CI / test (push) Successful in 4m7s
All checks were successful
CI / test (push) Successful in 4m7s
评估列表页此前用 Drawer 嵌套承载详情/报告/任务队列/新建,功能页面过多。 按 keep-alive 多页模式拆为独立页面(静态路由 + intelligentEvalNav store 传递选中): - 评估列表 (/intelligent-evals):只留列表 + 新建;详情/报告/任务队列改为导航 - 任务队列 (/intelligent-evals/tasks):TaskQueueMonitor 独立页,二级菜单项 - 评估详情 (/intelligent-evals/detail):新独立页,页内 Tabs 承载概览/决策过程/ 配置历史/报告(completed 才显示报告 tab),取代 Drawer 嵌套;审批/打回/取消 提到页面头部统一管理 - EvalDetail 拆为纯展示的 EvalOverview;DecisionProcess/ConfigSnapshots/EvalReport 的 onBack 改可选(tab 环境不显示返回按钮) - index.css 加 intelligent-detail-tabs 高度链(绕开 Ant CSS-in-JS 高度覆盖) tsc 0 错误, vitest 19 passed
This commit is contained in:
parent
2dd023fdd9
commit
a69fa8a797
@ -17,6 +17,7 @@ import {
|
||||
ExperimentOutlined,
|
||||
SettingOutlined,
|
||||
ToolOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import TabBar from './components/TabBar'
|
||||
import LoginPage from './pages/Login'
|
||||
@ -38,6 +39,8 @@ const OpenClawPage = lazy(() => import('./pages/OpenClaw'))
|
||||
const FilesPage = lazy(() => import('./pages/Files'))
|
||||
const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs'))
|
||||
const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals'))
|
||||
const IntelligentEvalTasksPage = lazy(() => import('./pages/IntelligentEvalTasks'))
|
||||
const IntelligentEvalDetailPage = lazy(() => import('./pages/IntelligentEvalDetail'))
|
||||
|
||||
function PageLoader({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
@ -67,6 +70,8 @@ const routeConfigs: RouteConfig[] = [
|
||||
{ path: '/campaigns', name: '评估活动', icon: <ScheduleOutlined />, component: () => <PageLoader><CampaignsPage /></PageLoader> },
|
||||
{ path: '/reports', name: '评测报告', icon: <BarChartOutlined />, component: () => <PageLoader><ReportsPage /></PageLoader> },
|
||||
{ path: '/intelligent-evals', name: '智能评估', icon: <BulbOutlined />, component: () => <PageLoader><IntelligentEvalsPage /></PageLoader> },
|
||||
{ path: '/intelligent-evals/tasks', name: '任务队列', icon: <UnorderedListOutlined />, component: () => <PageLoader><IntelligentEvalTasksPage /></PageLoader> },
|
||||
{ path: '/intelligent-evals/detail', name: '评估详情', icon: <FileTextOutlined />, component: () => <PageLoader><IntelligentEvalDetailPage /></PageLoader> },
|
||||
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
|
||||
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
|
||||
]
|
||||
@ -97,6 +102,7 @@ const menuItems: MenuProps['items'] = [
|
||||
label: '智能评估',
|
||||
children: [
|
||||
{ key: '/intelligent-evals', icon: <BulbOutlined />, label: '评估列表' },
|
||||
{ key: '/intelligent-evals/tasks', icon: <UnorderedListOutlined />, label: '任务队列' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -16,7 +16,8 @@ const SNAPSHOT_TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
||||
|
||||
interface ConfigSnapshotsProps {
|
||||
evalId: string
|
||||
onBack: () => void
|
||||
/** 独立页内作为子视图 tab 使用时可不传(tab 切换代替返回)。 */
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps) {
|
||||
@ -240,7 +241,7 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
|
||||
return (
|
||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回</Button>
|
||||
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回</Button>}
|
||||
<span style={{ fontSize: 16, fontWeight: 600 }}>配置历史</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Button
|
||||
|
||||
@ -16,7 +16,8 @@ const DECISION_TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
||||
|
||||
interface DecisionProcessProps {
|
||||
evalId: string
|
||||
onBack: () => void
|
||||
/** 独立页内作为子视图 tab 使用时可不传(tab 切换代替返回)。 */
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps) {
|
||||
@ -105,7 +106,7 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
|
||||
return (
|
||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回</Button>
|
||||
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回</Button>}
|
||||
<span style={{ fontSize: 16, fontWeight: 600 }}>决策过程</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Select
|
||||
|
||||
@ -1,267 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Alert, Button, Card, Col, Descriptions, Empty, Input, Modal, Popconfirm, Progress, Row, Space, Spin, Tag, message,
|
||||
} from 'antd'
|
||||
import { FileTextOutlined, HistoryOutlined, NodeIndexOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import { intelligentEvalsApi, type IntelligentEval } from '../../api'
|
||||
import { colors } from '../../tokens'
|
||||
import { formatDateTime, shortDateTime } from '../../utils/date'
|
||||
import { EVAL_STATUS, SESSION_STATUS } from './status'
|
||||
import ConfigSnapshots from './ConfigSnapshots'
|
||||
import DecisionProcess from './DecisionProcess'
|
||||
|
||||
const sectionCard: React.CSSProperties = { marginBottom: 16 }
|
||||
|
||||
function personaLabel(persona: Record<string, unknown>): string {
|
||||
if (typeof persona.name === 'string' && persona.name) return persona.name
|
||||
if (typeof persona.background === 'string' && persona.background) return persona.background
|
||||
return JSON.stringify(persona)
|
||||
}
|
||||
|
||||
function PlanView({ ev }: { ev: IntelligentEval }) {
|
||||
const plan = ev.plan
|
||||
if (!plan) return <Empty description="暂无粗计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
return (
|
||||
<div>
|
||||
<Descriptions column={3} size="small" bordered style={{ marginBottom: 12 }}>
|
||||
<Descriptions.Item label="预估会话数">{plan.estimated_sessions}</Descriptions.Item>
|
||||
<Descriptions.Item label="单会话最大轮数">{plan.budget?.max_turns_per_session ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="总轮数上限">{plan.budget?.total_max_turns ?? '—'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>评测维度</div>
|
||||
<Space size={6} wrap style={{ marginBottom: 12 }}>
|
||||
{(plan.dimensions ?? []).map((d) => <Tag key={d} color="blue">{d}</Tag>)}
|
||||
</Space>
|
||||
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>虚拟用户</div>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
|
||||
{(plan.virtual_users ?? []).map((u, i) => (
|
||||
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
<b style={{ color: colors.text }}>{personaLabel(u.persona)}</b> · 目标:{u.goal}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>时间分布</div>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
|
||||
{(plan.time_distribution ?? []).map((t, i) => (
|
||||
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
<Tag style={{ margin: 0 }}>{t.time_slot}</Tag> {t.sessions} 个会话 · {t.scenario}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
{plan.completion_criteria && (
|
||||
<div style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
<b style={{ color: colors.text }}>完成标准:</b>{plan.completion_criteria}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface EvalDetailProps {
|
||||
ev: IntelligentEval
|
||||
targetName: string
|
||||
onOpenReport: () => void
|
||||
onChanged: () => void
|
||||
}
|
||||
|
||||
export default function EvalDetail({ ev, targetName, onOpenReport, onChanged }: EvalDetailProps) {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [rejectOpen, setRejectOpen] = useState(false)
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [showConfigHistory, setShowConfigHistory] = useState(false)
|
||||
const [showDecisionProcess, setShowDecisionProcess] = useState(false)
|
||||
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
|
||||
const showSessions = ev.status === 'executing' || ev.status === 'completed'
|
||||
const sessions = showSessions ? ev.sessions ?? [] : []
|
||||
|
||||
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
|
||||
setBusy(true)
|
||||
try {
|
||||
await fn()
|
||||
message.success(okMsg)
|
||||
onChanged()
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const approve = () => runAction(() => intelligentEvalsApi.approve(ev.id), '已批准,进入执行')
|
||||
const cancel = () => runAction(() => intelligentEvalsApi.cancel(ev.id), '已取消')
|
||||
const submitReject = () => runAction(async () => {
|
||||
await intelligentEvalsApi.reject(ev.id, feedback)
|
||||
setRejectOpen(false)
|
||||
setFeedback('')
|
||||
}, '已打回,等待重新规划')
|
||||
|
||||
if (showConfigHistory) {
|
||||
return <ConfigSnapshots evalId={ev.id} onBack={() => setShowConfigHistory(false)} />
|
||||
}
|
||||
|
||||
if (showDecisionProcess) {
|
||||
return <DecisionProcess evalId={ev.id} onBack={() => setShowDecisionProcess(false)} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>{ev.name}</span>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Space>
|
||||
<Button icon={<HistoryOutlined />} onClick={() => setShowConfigHistory(true)}>配置历史</Button>
|
||||
<Button icon={<NodeIndexOutlined />} onClick={() => setShowDecisionProcess(true)}>决策过程</Button>
|
||||
{ev.status === 'completed' && (
|
||||
<Button type="primary" icon={<FileTextOutlined />} onClick={onOpenReport}>查看报告</Button>
|
||||
)}
|
||||
{(ev.status === 'pending_approval' || ev.status === 'executing') && (
|
||||
<Popconfirm title="取消该智能评估?" onConfirm={cancel}>
|
||||
<Button danger icon={<StopOutlined />} loading={busy}>取消</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={9}>
|
||||
<Card size="small" title="基本信息" style={sectionCard}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="评测对象">{targetName}</Descriptions.Item>
|
||||
<Descriptions.Item label="时间窗口">{ev.time_window_hours} 小时</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{formatDateTime(ev.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">{formatDateTime(ev.started_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="完成时间">{formatDateTime(ev.completed_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="会话进度">{ev.completed_sessions}/{ev.session_count}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="用户输入" style={sectionCard}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="评估目标">{ev.goal || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="考察意图">{ev.intent || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="角色描述">
|
||||
<span style={{ whiteSpace: 'pre-wrap' }}>{ev.role_description || '—'}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="种子集">
|
||||
{Object.keys(ev.seeds ?? {}).length === 0
|
||||
? '—'
|
||||
: (
|
||||
<pre style={{
|
||||
margin: 0, fontSize: 12, background: colors.bgSubtle,
|
||||
padding: 8, borderRadius: 6, overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(ev.seeds, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={15}>
|
||||
{ev.status === 'planning' && (
|
||||
ev.plan_feedback ? (
|
||||
<Alert
|
||||
style={sectionCard}
|
||||
type="warning"
|
||||
showIcon
|
||||
message="计划已打回,OpenClaw 正在根据反馈重新规划"
|
||||
description={ev.plan_feedback}
|
||||
/>
|
||||
) : (
|
||||
<Card size="small" style={sectionCard}>
|
||||
<Spin size="small" />
|
||||
<span style={{ marginLeft: 10, color: colors.textSecondary }}>
|
||||
OpenClaw 正在规划,产出粗计划后会自动进入待审批…
|
||||
</span>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
|
||||
{(ev.status === 'pending_approval' || ev.status === 'executing' || ev.status === 'completed') && (
|
||||
<Card
|
||||
size="small"
|
||||
title="粗计划"
|
||||
style={sectionCard}
|
||||
extra={ev.status === 'pending_approval' && (
|
||||
<Space>
|
||||
<Button size="small" danger onClick={() => setRejectOpen(true)}>打回</Button>
|
||||
<Button size="small" type="primary" loading={busy} onClick={approve}>批准并执行</Button>
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
<PlanView ev={ev} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showSessions && (
|
||||
<Card
|
||||
size="small"
|
||||
title={`会话进度(${ev.completed_sessions}/${ev.session_count})`}
|
||||
style={sectionCard}
|
||||
extra={(
|
||||
<Progress
|
||||
percent={ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0}
|
||||
size="small"
|
||||
style={{ width: 140 }}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{sessions.length === 0 && (
|
||||
<div style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
等待 OpenClaw 按粗计划的时间分布创建会话…
|
||||
</div>
|
||||
)}
|
||||
{sessions.map((s) => {
|
||||
const sMeta = SESSION_STATUS[s.status]
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
||||
padding: '6px 0', borderBottom: `1px solid ${colors.border}`, fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<b style={{ color: colors.text }}>{personaLabel(s.persona)}</b>
|
||||
<Tag color={sMeta.color} style={{ margin: 0 }}>{sMeta.label}</Tag>
|
||||
{s.dimension && <Tag style={{ margin: 0 }}>{s.dimension}</Tag>}
|
||||
<span style={{ color: colors.textSecondary, flex: 1, minWidth: 120 }}>{s.goal}</span>
|
||||
<span style={{ color: colors.textMuted }}>{s.turn_count} 轮</span>
|
||||
{s.created_at && (
|
||||
<span style={{ color: colors.textMuted }}>{shortDateTime(s.created_at)}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{ev.status === 'executing' && (
|
||||
<div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}>
|
||||
会话由 OpenClaw 按粗计划的时间分布自唤醒创建,本页每 5 秒自动刷新
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Modal
|
||||
title="打回计划"
|
||||
open={rejectOpen}
|
||||
onCancel={() => setRejectOpen(false)}
|
||||
onOk={submitReject}
|
||||
okText="打回"
|
||||
okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
placeholder="请填写打回原因与改进方向,OpenClaw 会据此重新规划"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
190
frontend/web/src/components/intelligent_eval/EvalOverview.tsx
Normal file
190
frontend/web/src/components/intelligent_eval/EvalOverview.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import {
|
||||
Alert, Card, Col, Descriptions, Empty, Progress, Row, Space, Spin, Tag,
|
||||
} from 'antd'
|
||||
import type { IntelligentEval } from '../../api'
|
||||
import { colors } from '../../tokens'
|
||||
import { formatDateTime, shortDateTime } from '../../utils/date'
|
||||
import { EVAL_STATUS, SESSION_STATUS } from './status'
|
||||
|
||||
const sectionCard: React.CSSProperties = { marginBottom: 16 }
|
||||
|
||||
function personaLabel(persona: Record<string, unknown>): string {
|
||||
if (typeof persona.name === 'string' && persona.name) return persona.name
|
||||
if (typeof persona.background === 'string' && persona.background) return persona.background
|
||||
return JSON.stringify(persona)
|
||||
}
|
||||
|
||||
function PlanView({ ev }: { ev: IntelligentEval }) {
|
||||
const plan = ev.plan
|
||||
if (!plan) return <Empty description="暂无粗计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
return (
|
||||
<div>
|
||||
<Descriptions column={3} size="small" bordered style={{ marginBottom: 12 }}>
|
||||
<Descriptions.Item label="预估会话数">{plan.estimated_sessions}</Descriptions.Item>
|
||||
<Descriptions.Item label="单会话最大轮数">{plan.budget?.max_turns_per_session ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="总轮数上限">{plan.budget?.total_max_turns ?? '—'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>评测维度</div>
|
||||
<Space size={6} wrap style={{ marginBottom: 12 }}>
|
||||
{(plan.dimensions ?? []).map((d) => <Tag key={d} color="blue">{d}</Tag>)}
|
||||
</Space>
|
||||
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>虚拟用户</div>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
|
||||
{(plan.virtual_users ?? []).map((u, i) => (
|
||||
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
<b style={{ color: colors.text }}>{personaLabel(u.persona)}</b> · 目标:{u.goal}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>时间分布</div>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
|
||||
{(plan.time_distribution ?? []).map((t, i) => (
|
||||
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
<Tag style={{ margin: 0 }}>{t.time_slot}</Tag> {t.sessions} 个会话 · {t.scenario}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
{plan.completion_criteria && (
|
||||
<div style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
<b style={{ color: colors.text }}>完成标准:</b>{plan.completion_criteria}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface EvalOverviewProps {
|
||||
ev: IntelligentEval
|
||||
targetName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估详情"概览"子视图(纯展示):基本信息、用户输入、粗计划、会话进度。
|
||||
* 审批/取消等动作由详情页(IntelligentEvalDetail)统一管理。
|
||||
*/
|
||||
export default function EvalOverview({ ev, targetName }: EvalOverviewProps) {
|
||||
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
|
||||
const showSessions = ev.status === 'executing' || ev.status === 'completed'
|
||||
const sessions = showSessions ? ev.sessions ?? [] : []
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col span={9}>
|
||||
<Card size="small" title="基本信息" style={sectionCard}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="评测对象">{targetName}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="时间窗口">{ev.time_window_hours} 小时</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{formatDateTime(ev.created_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">{formatDateTime(ev.started_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="完成时间">{formatDateTime(ev.completed_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="会话进度">{ev.completed_sessions}/{ev.session_count}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="用户输入" style={sectionCard}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="评估目标">{ev.goal || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="考察意图">{ev.intent || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="角色描述">
|
||||
<span style={{ whiteSpace: 'pre-wrap' }}>{ev.role_description || '—'}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="种子集">
|
||||
{Object.keys(ev.seeds ?? {}).length === 0
|
||||
? '—'
|
||||
: (
|
||||
<pre style={{
|
||||
margin: 0, fontSize: 12, background: colors.bgSubtle,
|
||||
padding: 8, borderRadius: 6, overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(ev.seeds, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={15}>
|
||||
{ev.status === 'planning' && (
|
||||
ev.plan_feedback ? (
|
||||
<Alert
|
||||
style={sectionCard}
|
||||
type="warning"
|
||||
showIcon
|
||||
message="计划已打回,OpenClaw 正在根据反馈重新规划"
|
||||
description={ev.plan_feedback}
|
||||
/>
|
||||
) : (
|
||||
<Card size="small" style={sectionCard}>
|
||||
<Spin size="small" />
|
||||
<span style={{ marginLeft: 10, color: colors.textSecondary }}>
|
||||
OpenClaw 正在规划,产出粗计划后会自动进入待审批…
|
||||
</span>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
|
||||
{(ev.status === 'pending_approval' || ev.status === 'executing' || ev.status === 'completed') && (
|
||||
<Card size="small" title="粗计划" style={sectionCard}>
|
||||
<PlanView ev={ev} />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showSessions && (
|
||||
<Card
|
||||
size="small"
|
||||
title={`会话进度(${ev.completed_sessions}/${ev.session_count})`}
|
||||
style={sectionCard}
|
||||
extra={(
|
||||
<Progress
|
||||
percent={ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0}
|
||||
size="small"
|
||||
style={{ width: 140 }}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{sessions.length === 0 && (
|
||||
<div style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||
等待 OpenClaw 按粗计划的时间分布创建会话…
|
||||
</div>
|
||||
)}
|
||||
{sessions.map((s) => {
|
||||
const sMeta = SESSION_STATUS[s.status]
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
||||
padding: '6px 0', borderBottom: `1px solid ${colors.border}`, fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<b style={{ color: colors.text }}>{personaLabel(s.persona)}</b>
|
||||
<Tag color={sMeta.color} style={{ margin: 0 }}>{sMeta.label}</Tag>
|
||||
{s.dimension && <Tag style={{ margin: 0 }}>{s.dimension}</Tag>}
|
||||
<span style={{ color: colors.textSecondary, flex: 1, minWidth: 120 }}>{s.goal}</span>
|
||||
<span style={{ color: colors.textMuted }}>{s.turn_count} 轮</span>
|
||||
{s.created_at && (
|
||||
<span style={{ color: colors.textMuted }}>{shortDateTime(s.created_at)}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{ev.status === 'executing' && (
|
||||
<div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}>
|
||||
会话由 OpenClaw 按粗计划的时间分布自唤醒创建,本页每 5 秒自动刷新
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@ -106,7 +106,8 @@ function SessionMessages({ evalId, sessionId, verdict }: {
|
||||
|
||||
interface EvalReportProps {
|
||||
ev: IntelligentEval
|
||||
onBack: () => void
|
||||
/** 独立页内作为子视图 tab 使用时可不传(tab 切换代替返回)。 */
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
export default function EvalReport({ ev, onBack }: EvalReportProps) {
|
||||
@ -175,7 +176,7 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
|
||||
return (
|
||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回详情</Button>
|
||||
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回详情</Button>}
|
||||
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>
|
||||
{ev.name} · 评估报告
|
||||
</span>
|
||||
|
||||
@ -64,6 +64,26 @@ body {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* intelligent-detail-tabs:评估详情页的 Tabs 需承载内容面板(概览/决策过程/
|
||||
* 配置历史/报告)。建立高度链:Tabs 撑满父容器,content-holder 占剩余空间,
|
||||
* 面板内的子组件(height:100% + overflowY:auto)才能正确滚动。
|
||||
*/
|
||||
.ant-tabs.intelligent-detail-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.ant-tabs.intelligent-detail-tabs .ant-tabs-content-holder {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-tabs.intelligent-detail-tabs .ant-tabs-content,
|
||||
.ant-tabs.intelligent-detail-tabs .ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
* flex-tabs(保留兼容,当前 Runs 页改用 nav-only-tabs)
|
||||
*/
|
||||
|
||||
193
frontend/web/src/pages/IntelligentEvalDetail.tsx
Normal file
193
frontend/web/src/pages/IntelligentEvalDetail.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Button, Empty, Input, Modal, Popconfirm, Space, Spin, Tabs, Tag, message,
|
||||
} from 'antd'
|
||||
import type { TabsProps } from 'antd'
|
||||
import { StopOutlined } from '@ant-design/icons'
|
||||
import { intelligentEvalsApi, targetsApi, type IntelligentEval } from '../api'
|
||||
import PageWrapper from '../components/PageWrapper'
|
||||
import EvalOverview from '../components/intelligent_eval/EvalOverview'
|
||||
import DecisionProcess from '../components/intelligent_eval/DecisionProcess'
|
||||
import ConfigSnapshots from '../components/intelligent_eval/ConfigSnapshots'
|
||||
import EvalReport from '../components/intelligent_eval/EvalReport'
|
||||
import { EVAL_STATUS } from '../components/intelligent_eval/status'
|
||||
import { useResource } from '../hooks/useResource'
|
||||
import { usePolling } from '../hooks/usePolling'
|
||||
import { useIntelligentEvalNav, type EvalDetailTab } from '../stores/intelligentEvalNav'
|
||||
import { colors } from '../tokens'
|
||||
|
||||
const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing'])
|
||||
|
||||
function isActive(status: string | undefined): boolean {
|
||||
return status != null && ACTIVE_STATUSES.has(status)
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估详情独立页(keep-alive 多页模式下的子 tab 页)。
|
||||
*
|
||||
* 由评估列表页点击"详情/报告"进入:选中评估 id 通过 intelligentEvalNav store
|
||||
* 传递,本页读取并加载,用页内 Tabs 承载概览/决策过程/配置历史/报告子视图,
|
||||
* 取代原列表页内的 Drawer 嵌套。
|
||||
*/
|
||||
export default function IntelligentEvalDetailPage() {
|
||||
const evalId = useIntelligentEvalNav((s) => s.selectedEvalId)
|
||||
const initialTab = useIntelligentEvalNav((s) => s.detailTab)
|
||||
const [detail, setDetail] = useState<IntelligentEval | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [rejectOpen, setRejectOpen] = useState(false)
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<EvalDetailTab>(initialTab)
|
||||
const { data: targets } = useResource(
|
||||
() => targetsApi.list().then((r) => r.data),
|
||||
{ tabPath: '/intelligent-evals/detail' },
|
||||
)
|
||||
|
||||
const load = async () => {
|
||||
if (!evalId) return
|
||||
try {
|
||||
const res = await intelligentEvalsApi.get(evalId)
|
||||
setDetail(res.data)
|
||||
} catch {
|
||||
message.error('加载评估详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (evalId) {
|
||||
setActiveTab(initialTab)
|
||||
void load()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [evalId, initialTab])
|
||||
|
||||
usePolling(() => { void load() }, 5000, evalId != null && isActive(detail?.status))
|
||||
|
||||
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
|
||||
setBusy(true)
|
||||
try {
|
||||
await fn()
|
||||
message.success(okMsg)
|
||||
await load()
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const approve = () => runAction(() => intelligentEvalsApi.approve(evalId!), '已批准,进入执行')
|
||||
const cancel = () => runAction(() => intelligentEvalsApi.cancel(evalId!), '已取消')
|
||||
const submitReject = () => runAction(async () => {
|
||||
await intelligentEvalsApi.reject(evalId!, feedback)
|
||||
setRejectOpen(false)
|
||||
setFeedback('')
|
||||
}, '已打回,等待重新规划')
|
||||
|
||||
if (!evalId) {
|
||||
return (
|
||||
<PageWrapper title="评估详情" inline fullHeight>
|
||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Empty description="从「智能评估 → 评估列表」点击某条评估进入详情" />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
if (!detail) {
|
||||
return (
|
||||
<PageWrapper title="评估详情" inline fullHeight>
|
||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = EVAL_STATUS[detail.status] ?? { label: detail.status, color: 'default' }
|
||||
const targetName = (id: string) =>
|
||||
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
||||
const canApprove = detail.status === 'pending_approval'
|
||||
const canCancel = detail.status === 'pending_approval' || detail.status === 'executing'
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: 'overview',
|
||||
label: '概览',
|
||||
children: (
|
||||
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
|
||||
<EvalOverview ev={detail} targetName={targetName(detail.target_id)} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'decision',
|
||||
label: '决策过程',
|
||||
children: <DecisionProcess evalId={evalId} />,
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
label: '配置历史',
|
||||
children: <ConfigSnapshots evalId={evalId} />,
|
||||
},
|
||||
]
|
||||
if (detail.status === 'completed') {
|
||||
items.push({
|
||||
key: 'report',
|
||||
label: '评估报告',
|
||||
children: <EvalReport ev={detail} />,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper title="评估详情" inline fullHeight>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
||||
padding: '8px 16px', background: colors.bgContainer, borderBottom: `1px solid ${colors.border}`,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: colors.text }}>{detail.name}</span>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Space>
|
||||
{canApprove && (
|
||||
<>
|
||||
<Button danger loading={busy} onClick={() => setRejectOpen(true)}>打回</Button>
|
||||
<Button type="primary" loading={busy} onClick={approve}>批准并执行</Button>
|
||||
</>
|
||||
)}
|
||||
{canCancel && (
|
||||
<Popconfirm title="取消该智能评估?" onConfirm={cancel}>
|
||||
<Button danger icon={<StopOutlined />} loading={busy}>取消</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, padding: '0 16px', background: colors.bgLayout }}>
|
||||
<Tabs
|
||||
className="intelligent-detail-tabs"
|
||||
activeKey={activeTab}
|
||||
onChange={(k) => setActiveTab(k as EvalDetailTab)}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title="打回计划"
|
||||
open={rejectOpen}
|
||||
onCancel={() => setRejectOpen(false)}
|
||||
onOk={submitReject}
|
||||
okText="打回"
|
||||
okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
placeholder="请填写打回原因与改进方向,OpenClaw 会据此重新规划"
|
||||
/>
|
||||
</Modal>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
23
frontend/web/src/pages/IntelligentEvalTasks.tsx
Normal file
23
frontend/web/src/pages/IntelligentEvalTasks.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import PageWrapper from '../components/PageWrapper'
|
||||
import TaskQueueMonitor from '../components/intelligent_eval/TaskQueueMonitor'
|
||||
|
||||
/**
|
||||
* 任务队列独立页(方案③可视化)。
|
||||
*
|
||||
* 智能评估的定时触发(scan loop 每 60s 扫描入队 + 触发 worker)由本页监控:
|
||||
* 状态分布卡 + 状态筛选 + 明细表,5s 轮询。
|
||||
*/
|
||||
export default function IntelligentEvalTasksPage() {
|
||||
return (
|
||||
<PageWrapper
|
||||
title="任务队列"
|
||||
description="平台每 60 秒扫描 executing 评估入队,有任务时触发 OpenClaw worker 执行"
|
||||
inline
|
||||
fullHeight
|
||||
>
|
||||
<div style={{ height: '100%', padding: '0 16px 16px' }}>
|
||||
<TaskQueueMonitor />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Spin, Table, Tag, Tooltip, message,
|
||||
Button, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, Tooltip, message,
|
||||
} from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import {
|
||||
@ -8,21 +9,18 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import FormDrawer from '../components/FormDrawer'
|
||||
import PageWrapper from '../components/PageWrapper'
|
||||
import EvalDetail from '../components/intelligent_eval/EvalDetail'
|
||||
import EvalReport from '../components/intelligent_eval/EvalReport'
|
||||
import TaskQueueMonitor from '../components/intelligent_eval/TaskQueueMonitor'
|
||||
import { EVAL_STATUS } from '../components/intelligent_eval/status'
|
||||
import { useResource } from '../hooks/useResource'
|
||||
import {
|
||||
intelligentEvalsApi, targetsApi,
|
||||
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
|
||||
} from '../api'
|
||||
import { useIntelligentEvalNav, type EvalDetailTab } from '../stores/intelligentEvalNav'
|
||||
import { useTabStore, type TabItem } from '../stores/tabStore'
|
||||
import { colors } from '../tokens'
|
||||
import { formatDateTime } from '../utils/date'
|
||||
import { useIntelligentEvalRead } from '../read/useIntelligentEvalRead'
|
||||
|
||||
type DrawerView = 'detail' | 'report'
|
||||
|
||||
interface CreateFormValues {
|
||||
name: string
|
||||
target_id: string
|
||||
@ -33,17 +31,24 @@ interface CreateFormValues {
|
||||
time_window_hours: number
|
||||
}
|
||||
|
||||
const DETAIL_TAB: TabItem = {
|
||||
key: '/intelligent-evals/detail', title: '评估详情', icon: <EyeOutlined />, closable: true,
|
||||
}
|
||||
const TASKS_TAB: TabItem = {
|
||||
key: '/intelligent-evals/tasks', title: '任务队列', icon: <UnorderedListOutlined />, closable: true,
|
||||
}
|
||||
|
||||
export default function IntelligentEvalsPage() {
|
||||
const [drawerView, setDrawerView] = useState<DrawerView>('detail')
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const navigate = useNavigate()
|
||||
const openTab = useTabStore((s) => s.openTab)
|
||||
const openDetailNav = useIntelligentEvalNav((s) => s.openDetail)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [taskQueueOpen, setTaskQueueOpen] = useState(false)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [form] = Form.useForm<CreateFormValues>()
|
||||
|
||||
const { list, detail, reloadList, reloadDetail } = useIntelligentEvalRead(selectedId, undefined, page, pageSize)
|
||||
const { list, reloadList } = useIntelligentEvalRead(null, undefined, page, pageSize)
|
||||
const evals = list.value
|
||||
const loading = list.phase === 'loading'
|
||||
const { data: targets } = useResource(
|
||||
@ -51,25 +56,20 @@ export default function IntelligentEvalsPage() {
|
||||
{ tabPath: '/intelligent-evals' },
|
||||
)
|
||||
|
||||
const selected = selectedId != null && detail.value?.id === selectedId ? detail.value : null
|
||||
|
||||
const targetName = (id: string) =>
|
||||
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
||||
|
||||
const openDetail = (id: string) => {
|
||||
setSelectedId(id)
|
||||
setDrawerView('detail')
|
||||
const openDetail = (id: string, tab: EvalDetailTab = 'overview') => {
|
||||
openDetailNav(id, tab)
|
||||
openTab(DETAIL_TAB)
|
||||
navigate('/intelligent-evals/detail')
|
||||
}
|
||||
|
||||
const openReport = (id: string) => {
|
||||
setSelectedId(id)
|
||||
setDrawerView('report')
|
||||
}
|
||||
const openReport = (id: string) => openDetail(id, 'report')
|
||||
|
||||
const closeDrawer = () => {
|
||||
setSelectedId(null)
|
||||
setDrawerView('detail')
|
||||
void reloadList()
|
||||
const openTasks = () => {
|
||||
openTab(TASKS_TAB)
|
||||
navigate('/intelligent-evals/tasks')
|
||||
}
|
||||
|
||||
const submitCreate = async () => {
|
||||
@ -160,7 +160,7 @@ export default function IntelligentEvalsPage() {
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void reloadList()} />
|
||||
<Button icon={<UnorderedListOutlined />} onClick={() => setTaskQueueOpen(true)}>
|
||||
<Button icon={<UnorderedListOutlined />} onClick={openTasks}>
|
||||
任务队列
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
|
||||
@ -191,38 +191,6 @@ export default function IntelligentEvalsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
title={drawerView === 'report' ? '评估报告' : '评估详情'}
|
||||
open={selectedId != null}
|
||||
onClose={closeDrawer}
|
||||
width={1040}
|
||||
destroyOnClose
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{selected == null ? (
|
||||
<div style={{ padding: 48, textAlign: 'center' }}><Spin /></div>
|
||||
) : drawerView === 'report' ? (
|
||||
<EvalReport ev={selected} onBack={() => setDrawerView('detail')} />
|
||||
) : (
|
||||
<EvalDetail
|
||||
ev={selected}
|
||||
targetName={targetName(selected.target_id)}
|
||||
onOpenReport={() => setDrawerView('report')}
|
||||
onChanged={() => void reloadDetail()}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Drawer
|
||||
title="任务队列(定时触发监控)"
|
||||
open={taskQueueOpen}
|
||||
onClose={() => setTaskQueueOpen(false)}
|
||||
width={1000}
|
||||
destroyOnClose
|
||||
>
|
||||
<TaskQueueMonitor />
|
||||
</Drawer>
|
||||
|
||||
<FormDrawer
|
||||
title="新建智能评估"
|
||||
open={createOpen}
|
||||
|
||||
22
frontend/web/src/stores/intelligentEvalNav.ts
Normal file
22
frontend/web/src/stores/intelligentEvalNav.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export type EvalDetailTab = 'overview' | 'decision' | 'history' | 'report'
|
||||
|
||||
interface IntelligentEvalNav {
|
||||
/** 当前详情页展示的评估 id(由评估列表页进入时设置)。 */
|
||||
selectedEvalId: string | null
|
||||
/** 详情页初始激活的子视图 tab。 */
|
||||
detailTab: EvalDetailTab
|
||||
openDetail: (id: string, tab?: EvalDetailTab) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估列表 → 详情页 之间的选中状态桥(keep-alive 多页模式)。
|
||||
* 详情页是独立 tab(/intelligent-evals/detail),与列表页同时挂载,
|
||||
* 选中哪个评估由这里传递;列表页导航前调用 openDetail 再 openTab + navigate。
|
||||
*/
|
||||
export const useIntelligentEvalNav = create<IntelligentEvalNav>((set) => ({
|
||||
selectedEvalId: null,
|
||||
detailTab: 'overview',
|
||||
openDetail: (id, tab = 'overview') => set({ selectedEvalId: id, detailTab: tab }),
|
||||
}))
|
||||
Loading…
Reference in New Issue
Block a user