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> = {}
|
const componentMap: Record<string, () => ReactNode> = {}
|
||||||
routeConfigs.forEach((r) => { componentMap[r.path] = r.component })
|
routeConfigs.forEach((r) => { componentMap[r.path] = r.component })
|
||||||
|
|
||||||
|
const PINNED_TAB_PATHS = new Set(['/', '/openclaw'])
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = [
|
const menuItems: MenuProps['items'] = [
|
||||||
{ key: '/', icon: <DashboardOutlined />, label: '仪表盘' },
|
{ key: '/', icon: <DashboardOutlined />, label: '仪表盘' },
|
||||||
{ key: '/openclaw', icon: <RobotOutlined />, label: 'AI 助手' },
|
{ key: '/openclaw', icon: <RobotOutlined />, label: 'AI 助手' },
|
||||||
@ -147,7 +149,7 @@ function App() {
|
|||||||
key: config.path,
|
key: config.path,
|
||||||
title: config.name,
|
title: config.name,
|
||||||
icon: config.icon,
|
icon: config.icon,
|
||||||
closable: config.path !== '/',
|
closable: !PINNED_TAB_PATHS.has(config.path),
|
||||||
}
|
}
|
||||||
openTab(tab)
|
openTab(tab)
|
||||||
} else {
|
} else {
|
||||||
@ -163,7 +165,7 @@ function App() {
|
|||||||
key: config.path,
|
key: config.path,
|
||||||
title: config.name,
|
title: config.name,
|
||||||
icon: config.icon,
|
icon: config.icon,
|
||||||
closable: config.path !== '/',
|
closable: !PINNED_TAB_PATHS.has(config.path),
|
||||||
}
|
}
|
||||||
openTab(tab)
|
openTab(tab)
|
||||||
navigate(key)
|
navigate(key)
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Alert, Button, Card, Descriptions, Empty, Input, Modal, Popconfirm, Space, Spin, Tag, message,
|
Alert, Button, Card, Descriptions, Empty, Input, Modal, Popconfirm, Space, Spin, Tag, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { ArrowLeftOutlined, FileTextOutlined, StopOutlined } from '@ant-design/icons'
|
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 { colors } from '../../tokens'
|
||||||
import { formatDateTime, shortDateTime } from '../../utils/date'
|
import { formatDateTime, shortDateTime } from '../../utils/date'
|
||||||
import { EVAL_STATUS } from './status'
|
import { EVAL_STATUS, SESSION_STATUS } from './status'
|
||||||
|
|
||||||
const sectionCard: React.CSSProperties = { marginBottom: 16 }
|
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 [busy, setBusy] = useState(false)
|
||||||
const [rejectOpen, setRejectOpen] = useState(false)
|
const [rejectOpen, setRejectOpen] = useState(false)
|
||||||
const [feedback, setFeedback] = useState('')
|
const [feedback, setFeedback] = useState('')
|
||||||
|
const [sessions, setSessions] = useState<IntelligentEvalSession[] | null>(null)
|
||||||
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
|
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) => {
|
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
@ -177,15 +191,40 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ev.status === 'executing' && (
|
{showSessions && (
|
||||||
<Card size="small" title="执行进度" style={sectionCard}>
|
<Card size="small" title={`会话进度(${ev.completed_sessions}/${ev.session_count})`} style={sectionCard}>
|
||||||
<div style={{ fontSize: 13, color: colors.textSecondary, marginBottom: 8 }}>
|
{sessions === null && <Spin size="small" />}
|
||||||
已完成会话 {ev.completed_sessions} / 已创建 {ev.session_count}
|
{sessions !== null && sessions.length === 0 && (
|
||||||
{ev.plan?.estimated_sessions ? ` / 预估 ${ev.plan.estimated_sessions}` : ''}
|
<div style={{ fontSize: 13, color: colors.textSecondary }}>
|
||||||
</div>
|
等待 OpenClaw 按粗计划的时间分布创建会话…
|
||||||
<div style={{ fontSize: 12, color: colors.textMuted }}>
|
</div>
|
||||||
会话由 OpenClaw 按粗计划的时间分布自唤醒创建,最近活动于 {shortDateTime(ev.updated_at)}
|
)}
|
||||||
</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>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message,
|
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
@ -45,11 +45,20 @@ export default function IntelligentEvalsPage() {
|
|||||||
{ tabPath: '/intelligent-evals' },
|
{ tabPath: '/intelligent-evals' },
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: selected } = useResource(
|
const { data: selected, reload: reloadSelected } = useResource(
|
||||||
() => (selectedId ? intelligentEvalsApi.get(selectedId).then((r) => r.data) : Promise.resolve(null)),
|
() => (selectedId ? intelligentEvalsApi.get(selectedId).then((r) => r.data) : Promise.resolve(null)),
|
||||||
{ deps: [selectedId, detailTick] },
|
{ 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) =>
|
const targetName = (id: string) =>
|
||||||
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
import { RobotOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
export interface TabItem {
|
export interface TabItem {
|
||||||
key: string
|
key: string
|
||||||
@ -16,7 +17,10 @@ interface TabStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useTabStore = create<TabStore>((set, get) => ({
|
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: '/',
|
activeKey: '/',
|
||||||
|
|
||||||
openTab: (tab) => {
|
openTab: (tab) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user