diff --git a/frontend/web/src/App.tsx b/frontend/web/src/App.tsx index 838146e..b868969 100644 --- a/frontend/web/src/App.tsx +++ b/frontend/web/src/App.tsx @@ -74,6 +74,8 @@ const routeConfigs: RouteConfig[] = [ const componentMap: Record ReactNode> = {} routeConfigs.forEach((r) => { componentMap[r.path] = r.component }) +const PINNED_TAB_PATHS = new Set(['/', '/openclaw']) + const menuItems: MenuProps['items'] = [ { key: '/', icon: , label: '仪表盘' }, { key: '/openclaw', icon: , label: 'AI 助手' }, @@ -147,7 +149,7 @@ function App() { key: config.path, title: config.name, icon: config.icon, - closable: config.path !== '/', + closable: !PINNED_TAB_PATHS.has(config.path), } openTab(tab) } else { @@ -163,7 +165,7 @@ function App() { key: config.path, title: config.name, icon: config.icon, - closable: config.path !== '/', + closable: !PINNED_TAB_PATHS.has(config.path), } openTab(tab) navigate(key) diff --git a/frontend/web/src/components/intelligent_eval/EvalDetail.tsx b/frontend/web/src/components/intelligent_eval/EvalDetail.tsx index adcf2ec..7faade1 100644 --- a/frontend/web/src/components/intelligent_eval/EvalDetail.tsx +++ b/frontend/web/src/components/intelligent_eval/EvalDetail.tsx @@ -1,12 +1,12 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Alert, Button, Card, Descriptions, Empty, Input, Modal, Popconfirm, Space, Spin, Tag, message, } from 'antd' import { ArrowLeftOutlined, FileTextOutlined, StopOutlined } from '@ant-design/icons' -import { intelligentEvalsApi, type IntelligentEval } from '../../api' +import { intelligentEvalsApi, type IntelligentEval, type IntelligentEvalSession } from '../../api' import { colors } from '../../tokens' import { formatDateTime, shortDateTime } from '../../utils/date' -import { EVAL_STATUS } from './status' +import { EVAL_STATUS, SESSION_STATUS } from './status' const sectionCard: React.CSSProperties = { marginBottom: 16 } @@ -71,7 +71,21 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha const [busy, setBusy] = useState(false) const [rejectOpen, setRejectOpen] = useState(false) const [feedback, setFeedback] = useState('') + const [sessions, setSessions] = useState(null) const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' } + const showSessions = ev.status === 'executing' || ev.status === 'completed' + + useEffect(() => { + if (!showSessions) { setSessions(null); return } + let cancelled = false + const load = () => intelligentEvalsApi.listSessions(ev.id) + .then((res) => { if (!cancelled) setSessions(res.data.sessions) }) + .catch(() => undefined) + void load() + if (ev.status !== 'executing') return () => { cancelled = true } + const timer = setInterval(load, 5000) + return () => { cancelled = true; clearInterval(timer) } + }, [ev.id, ev.status, showSessions]) const runAction = async (fn: () => Promise, okMsg: string) => { setBusy(true) @@ -177,15 +191,40 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha )} - {ev.status === 'executing' && ( - -
- 已完成会话 {ev.completed_sessions} / 已创建 {ev.session_count} - {ev.plan?.estimated_sessions ? ` / 预估 ${ev.plan.estimated_sessions}` : ''} -
-
- 会话由 OpenClaw 按粗计划的时间分布自唤醒创建,最近活动于 {shortDateTime(ev.updated_at)} -
+ {showSessions && ( + + {sessions === null && } + {sessions !== null && sessions.length === 0 && ( +
+ 等待 OpenClaw 按粗计划的时间分布创建会话… +
+ )} + {sessions?.map((s) => { + const sMeta = SESSION_STATUS[s.status] + return ( +
+ {personaLabel(s.persona)} + {sMeta.label} + {s.dimension && {s.dimension}} + {s.goal} + {s.turn_count} 轮 + {s.created_at && ( + {shortDateTime(s.created_at)} + )} +
+ ) + })} + {ev.status === 'executing' && ( +
+ 会话由 OpenClaw 按粗计划的时间分布自唤醒创建,本页每 5 秒自动刷新 +
+ )}
)} diff --git a/frontend/web/src/pages/IntelligentEvals.tsx b/frontend/web/src/pages/IntelligentEvals.tsx index f89689c..24b33f9 100644 --- a/frontend/web/src/pages/IntelligentEvals.tsx +++ b/frontend/web/src/pages/IntelligentEvals.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message, } from 'antd' @@ -45,11 +45,20 @@ export default function IntelligentEvalsPage() { { tabPath: '/intelligent-evals' }, ) - const { data: selected } = useResource( + const { data: selected, reload: reloadSelected } = useResource( () => (selectedId ? intelligentEvalsApi.get(selectedId).then((r) => r.data) : Promise.resolve(null)), { deps: [selectedId, detailTick] }, ) + const pollActive = view === 'detail' && selected != null + && (selected.status === 'planning' || selected.status === 'executing' || selected.status === 'pending_approval') + + useEffect(() => { + if (!pollActive) return + const timer = setInterval(() => void reloadSelected(true), 5000) + return () => clearInterval(timer) + }, [pollActive, reloadSelected]) + const targetName = (id: string) => targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8) diff --git a/frontend/web/src/stores/tabStore.tsx b/frontend/web/src/stores/tabStore.tsx index 240166d..c7ebf35 100644 --- a/frontend/web/src/stores/tabStore.tsx +++ b/frontend/web/src/stores/tabStore.tsx @@ -1,4 +1,5 @@ import { create } from 'zustand' +import { RobotOutlined } from '@ant-design/icons' export interface TabItem { key: string @@ -16,7 +17,10 @@ interface TabStore { } export const useTabStore = create((set, get) => ({ - tabs: [{ key: '/', title: '仪表盘', closable: false }], + tabs: [ + { key: '/', title: '仪表盘', closable: false }, + { key: '/openclaw', title: 'AI 助手', icon: , closable: false }, + ], activeKey: '/', openTab: (tab) => {