import type { ExportedMessageRepository } from "@assistant-ui/core"; import { ThreadListPrimitive } from "@assistant-ui/react"; import { BarChart3Icon, BotIcon, ChevronDownIcon, Clock3Icon, LogOutIcon, PanelLeftOpenIcon, SearchIcon, SquarePenIcon, UserIcon, } from "lucide-react"; import { Popover as PopoverPrimitive } from "radix-ui"; import type * as React from "react"; import { useEffect, useState } from "react"; import type { ClawAccount } from "@/app/claw-account-gate"; import { ClawLlmSettings } from "@/app/claw-llm-settings"; import { ClawThemeSwitcher } from "@/app/claw-theme-switcher"; import { fetchLatestRunStatus, ThreadList, toReplayRepository, } from "@/components/assistant-ui/thread-list"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; import { clearActiveSessionId, readActiveSessionId, writeActiveSessionId, } from "@/lib/claw-active-session"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; type ThreadListSidebarProps = React.ComponentProps & { account: ClawAccount; onLogout: () => void; }; type ClawState = { model?: string; session_directory?: string; active_session_id?: string | null; }; type ClawSessionSummary = { session_id: string; turns: number; tool_calls: number; modified_at: number; preview?: string; model?: string; usage?: UsageSummary; }; type UsageSummary = { input_tokens?: number; output_tokens?: number; total_tokens?: number; }; type ClawStoredSession = { session_id: string; messages?: unknown[]; usage?: UsageSummary; tool_calls?: number; turns?: number; model?: string; }; type ReplaySessionPayload = Parameters[0]; type SidebarSession = ClawSessionSummary & { preview: string; }; export function ThreadListSidebar({ account, onLogout, ...props }: ThreadListSidebarProps) { return (
ZK Data Agent 中控数据开发平台
); } function CollapsedSidebarRail({ account, onLogout, }: { account: ClawAccount; onLogout: () => void; }) { const { setOpen } = useSidebar(); return (
{ clearActiveSessionId(); window.dispatchEvent(new Event("claw-active-session-cleared")); }} >
); } function CollapsedIconButton({ label, children, onClick, }: { label: string; children: React.ReactNode; onClick?: () => void; }) { return ( ); } function CollapsedSessionSearch() { const { replaySession } = useClawSessionReplay(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const { sessions, loadingSessionId, openSession } = useSidebarSessions(open); const filteredSessions = sessions.filter((session) => { const keyword = query.trim().toLowerCase(); if (!keyword) return true; return ( session.preview.toLowerCase().includes(keyword) || session.session_id.toLowerCase().includes(keyword) ); }); return ( <> setOpen(true)}> 搜索任务 按任务标题或会话 ID 搜索,点击后恢复会话。 setQuery(event.target.value)} />
{filteredSessions.length ? (
{filteredSessions.map((session) => ( { await openSession(session.session_id, replaySession); setOpen(false); }} /> ))}
) : (

没有匹配的任务。

)}
); } function CollapsedRecentSessions() { const { replaySession } = useClawSessionReplay(); const [open, setOpen] = useState(false); const { sessions, loadingSessionId, openSession } = useSidebarSessions(open); return (
最近任务
{sessions.length ? (
{sessions.map((session) => ( { await openSession(session.session_id, replaySession); setOpen(false); }} /> ))}
) : (

暂无最近任务。

)}
); } function SessionResultButton({ session, loading, onClick, }: { session: SidebarSession; loading: boolean; onClick: () => void; }) { return ( ); } function useSidebarSessions(open: boolean) { const [sessions, setSessions] = useState([]); const [loadingSessionId, setLoadingSessionId] = useState(null); useEffect(() => { if (!open) return; let cancelled = false; fetch("/api/claw/sessions", { cache: "no-store" }) .then((res) => (res.ok ? res.json() : [])) .then((payload) => { if (cancelled || !Array.isArray(payload)) return; setSessions(dedupeSidebarSessions(payload)); }) .catch(() => { if (!cancelled) setSessions([]); }); return () => { cancelled = true; }; }, [open]); async function openSession( sessionId: string, replaySession: ( sessionId: string, repository: ExportedMessageRepository, ) => void, ) { const cleanSessionId = normalizeSessionId(sessionId); if (!cleanSessionId) return; writeActiveSessionId(cleanSessionId); setLoadingSessionId(sessionId); try { const response = await fetch(`/api/claw/sessions/${cleanSessionId}`, { cache: "no-store", }); if (!response.ok) return; const payload = (await response.json()) as ReplaySessionPayload; const runStatus = await fetchLatestRunStatus(cleanSessionId); replaySession( cleanSessionId, toReplayRepository(payload, cleanSessionId, runStatus), ); } finally { setLoadingSessionId(null); } } return { sessions, loadingSessionId, openSession }; } function dedupeSidebarSessions(payload: unknown[]) { const byId = new Map(); for (const item of payload) { if (!isSidebarSession(item)) continue; const previous = byId.get(item.session_id); if (!previous || item.modified_at >= previous.modified_at) { byId.set(item.session_id, item); } } return [...byId.values()].sort((a, b) => b.modified_at - a.modified_at); } function isSidebarSession(value: unknown): value is SidebarSession { if (!value || typeof value !== "object") return false; const item = value as Partial; return ( typeof item.session_id === "string" && typeof item.modified_at === "number" && typeof item.preview === "string" ); } function AccountMenu({ account, onLogout, compact = false, }: { account: ClawAccount; onLogout: () => void; compact?: boolean; }) { const [open, setOpen] = useState(false); const [state, setState] = useState(null); const [sessions, setSessions] = useState([]); const [activeSession, setActiveSession] = useState( null, ); const [modelUsageOpen, setModelUsageOpen] = useState(false); useEffect(() => { if (!open) return; Promise.all([ fetch("/api/claw/state", { cache: "no-store" }).then((res) => res.ok ? res.json() : null, ), fetch("/api/claw/sessions", { cache: "no-store" }).then((res) => res.ok ? res.json() : [], ), ]) .then(async ([statePayload, sessionsPayload]) => { const nextState = statePayload as ClawState | null; const nextSessions = Array.isArray(sessionsPayload) ? (sessionsPayload as ClawSessionSummary[]) : []; setState(nextState); setSessions(nextSessions); const activeSessionId = normalizeSessionId( readActiveSessionId() ?? nextState?.active_session_id, ); if (!activeSessionId) { setActiveSession(null); return; } writeActiveSessionId(activeSessionId); const response = await fetch(`/api/claw/sessions/${activeSessionId}`, { cache: "no-store", }); setActiveSession(response.ok ? await response.json() : null); }) .catch(() => { setState(null); setSessions([]); setActiveSession(null); }); }, [open]); async function logout() { await fetch("/api/claw/auth/logout", { method: "POST" }); clearActiveSessionId(); onLogout(); } const totalToolCalls = sessions.reduce( (sum, session) => sum + (session.tool_calls ?? 0), 0, ); const modelUsage = aggregateUsageByModel(sessions); const modelUsageTotalTokens = modelUsage.reduce( (sum, item) => sum + item.totalTokens, 0, ); return ( {compact ? ( ) : ( )}
{account.username}
{state?.model ?? "模型配置读取中"}
当前会话
输入 token {activeSession?.usage?.input_tokens ?? "暂无"} 输出 token {activeSession?.usage?.output_tokens ?? "暂无"} 工具调用 {activeSession?.tool_calls ?? "暂无"}
{modelUsageOpen && modelUsage.length ? (
{modelUsage.map((item) => (
{item.model} {formatCompactNumber(item.totalTokens)} tokens {item.sessions} 会话 · {item.toolCalls} 工具 in {formatCompactNumber(item.inputTokens)} / out{" "} {formatCompactNumber(item.outputTokens)}
))}
) : null} {modelUsageOpen && !modelUsage.length ? (
暂无可统计会话
) : null}
界面与模型配置
); } function normalizeSessionId(value: unknown) { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed || null; } function aggregateUsageByModel(sessions: ClawSessionSummary[]) { const byModel = new Map< string, { model: string; sessions: number; toolCalls: number; inputTokens: number; outputTokens: number; totalTokens: number; } >(); for (const session of sessions) { const model = session.model || "unknown"; const current = byModel.get(model) ?? { model, sessions: 0, toolCalls: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, }; current.sessions += 1; current.toolCalls += session.tool_calls ?? 0; current.inputTokens += session.usage?.input_tokens ?? 0; current.outputTokens += session.usage?.output_tokens ?? 0; current.totalTokens += session.usage?.total_tokens ?? (session.usage?.input_tokens ?? 0) + (session.usage?.output_tokens ?? 0); byModel.set(model, current); } return [...byModel.values()].sort((a, b) => b.totalTokens - a.totalTokens); } function formatCompactNumber(value: number) { return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1, }).format(value); } function AccountStat({ label, value, }: { label: string; value: string | number; }) { return (
{value}
{label}
); }