import { BarChart3Icon, BotIcon, ChevronDownIcon, LogOutIcon, 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 { ThreadList } from "@/components/assistant-ui/thread-list"; import { Button } from "@/components/ui/button"; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar"; 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; model?: string; usage?: UsageSummary; }; type UsageSummary = { input_tokens?: number; output_tokens?: number; total_tokens?: number; }; type ClawStoredSession = { session_id: string; usage?: UsageSummary; tool_calls?: number; turns?: number; model?: string; }; export function ThreadListSidebar({ account, onLogout, ...props }: ThreadListSidebarProps) { return (
Claw Data Agent 数据开发工作台
); } function AccountMenu({ account, onLogout, }: { account: ClawAccount; onLogout: () => void; }) { 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 = window.localStorage.getItem("claw.activeSessionId") ?? nextState?.active_session_id; if (!activeSessionId) { setActiveSession(null); return; } 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" }); window.localStorage.removeItem("claw.activeSessionId"); 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 (
{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 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}
); }