feat(frontend): detail auto-refresh, session list, pinned AI-assistant tab
Some checks failed
CI / test (push) Failing after 40s
Some checks failed
CI / test (push) Failing after 40s
- Detail page silently polls every 5s while planning/executing/pending_approval - Detail page shows per-session progress (persona/status/goal/turns), polled while executing - /openclaw tab pinned by default like the dashboard (tabStore now .tsx)
This commit is contained in:
parent
9cdbc41808
commit
6cc2efafb6
@ -74,6 +74,8 @@ const routeConfigs: RouteConfig[] = [
|
||||
const componentMap: Record<string, () => ReactNode> = {}
|
||||
routeConfigs.forEach((r) => { componentMap[r.path] = r.component })
|
||||
|
||||
const PINNED_TAB_PATHS = new Set(['/', '/openclaw'])
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: '仪表盘' },
|
||||
{ key: '/openclaw', icon: <RobotOutlined />, 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)
|
||||
|
||||
@ -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<IntelligentEvalSession[] | null>(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<unknown>, okMsg: string) => {
|
||||
setBusy(true)
|
||||
@ -177,15 +191,40 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{ev.status === 'executing' && (
|
||||
<Card size="small" title="执行进度" style={sectionCard}>
|
||||
<div style={{ fontSize: 13, color: colors.textSecondary, marginBottom: 8 }}>
|
||||
已完成会话 {ev.completed_sessions} / 已创建 {ev.session_count}
|
||||
{ev.plan?.estimated_sessions ? ` / 预估 ${ev.plan.estimated_sessions}` : ''}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: colors.textMuted }}>
|
||||
会话由 OpenClaw 按粗计划的时间分布自唤醒创建,最近活动于 {shortDateTime(ev.updated_at)}
|
||||
</div>
|
||||
{showSessions && (
|
||||
<Card size="small" title={`会话进度(${ev.completed_sessions}/${ev.session_count})`} style={sectionCard}>
|
||||
{sessions === null && <Spin size="small" />}
|
||||
{sessions !== null && 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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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<TabStore>((set, get) => ({
|
||||
tabs: [{ key: '/', title: '仪表盘', closable: false }],
|
||||
tabs: [
|
||||
{ key: '/', title: '仪表盘', closable: false },
|
||||
{ key: '/openclaw', title: 'AI 助手', icon: <RobotOutlined />, closable: false },
|
||||
],
|
||||
activeKey: '/',
|
||||
|
||||
openTab: (tab) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user