Improve data agent workspace UI

This commit is contained in:
武阳
2026-05-06 17:19:13 +08:00
parent e6e9968ede
commit d9a8110b7c
17 changed files with 910 additions and 201 deletions
+93 -32
View File
@@ -68,13 +68,6 @@ type SkillSummary = {
when_to_use?: string;
};
type UsageSummary = {
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
reasoning_tokens?: number;
};
type ClawState = {
model?: string;
active_session_id?: string | null;
@@ -83,13 +76,32 @@ type ClawState = {
type ClawStoredSession = {
session_id?: string;
model?: string;
usage?: UsageSummary;
};
type ModelOption = {
id: string;
};
type ContextBudget = {
model?: string;
context_window_tokens?: number;
projected_input_tokens?: number;
message_tokens?: number;
chat_overhead_tokens?: number;
reserved_output_tokens?: number;
reserved_compaction_buffer_tokens?: number;
reserved_schema_tokens?: number;
hard_input_limit_tokens?: number;
soft_input_limit_tokens?: number;
overflow_tokens?: number;
soft_overflow_tokens?: number;
exceeds_hard_limit?: boolean;
exceeds_soft_limit?: boolean;
token_counter_backend?: string;
token_counter_source?: string;
token_counter_accurate?: boolean;
};
export const Thread: FC = () => {
return (
<ThreadPrimitive.Root
@@ -158,10 +170,10 @@ const ThreadWelcome: FC = () => {
<div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
<div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200">
Hello there!
ZK Data Agent
</h1>
<p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground text-xl delay-75 duration-200">
How can I help you today?
</p>
</div>
</div>
@@ -261,9 +273,11 @@ const ComposerAction: FC = () => {
const ComposerContextStatus: FC = () => {
const status = useComposerContextStatus();
const totalTokens = status.usage?.total_tokens ?? 0;
const limit = inferContextLimit(status.model);
const percent = Math.min(100, Math.round((totalTokens / limit) * 100));
const totalTokens = status.contextBudget?.projected_input_tokens ?? 0;
const limit = status.contextBudget?.context_window_tokens ?? 0;
const percent = limit
? Math.min(100, Math.round((totalTokens / limit) * 100))
: 0;
return (
<div className="mx-2 hidden min-w-0 flex-1 items-center justify-end gap-1.5 sm:flex">
@@ -271,6 +285,7 @@ const ComposerContextStatus: FC = () => {
percent={percent}
totalTokens={totalTokens}
limit={limit}
contextBudget={status.contextBudget}
/>
<ModelPickerButton status={status} />
</div>
@@ -281,10 +296,12 @@ function ContextWindowButton({
percent,
totalTokens,
limit,
contextBudget,
}: {
percent: number;
totalTokens: number;
limit: number;
contextBudget?: ContextBudget;
}) {
const ringStyle = {
background: `conic-gradient(var(--primary) ${percent * 3.6}deg, var(--muted) 0deg)`,
@@ -335,8 +352,33 @@ function ContextWindowButton({
{formatCompactTokenCount(totalTokens)} {" "}
{formatCompactTokenCount(limit)}
</div>
<div className="mt-3 font-semibold text-sm">
Codex
<div className="mt-3 grid gap-1 text-muted-foreground text-xs">
<div>
{formatCompactTokenCount(
contextBudget?.soft_input_limit_tokens ?? 0,
)}
</div>
<div>
{formatCompactTokenCount(
contextBudget?.hard_input_limit_tokens ?? 0,
)}
</div>
<div>
{formatCompactTokenCount(
contextBudget?.reserved_output_tokens ?? 0,
)}
{formatCompactTokenCount(
contextBudget?.reserved_compaction_buffer_tokens ?? 0,
)}
</div>
<div>
{contextBudget?.token_counter_backend ?? "unknown"}
{contextBudget?.token_counter_accurate ? "(精确)" : "(估算)"}
</div>
</div>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
@@ -410,12 +452,10 @@ function useComposerContextStatus() {
const latestSessionId = useAuiState((s) =>
findLatestMessageMetadataValue(s.thread.messages, "sessionId"),
);
const latestUsage = useAuiState((s) =>
findLatestMessageMetadataValue(s.thread.messages, "usage"),
) as UsageSummary | undefined;
const wasRunningRef = useRef(false);
const [status, setStatus] = useState<{
model?: string;
usage?: UsageSummary;
contextBudget?: ContextBudget;
sessionId?: string | null;
models: ModelOption[];
setModel: (model: string) => Promise<void>;
@@ -427,9 +467,22 @@ function useComposerContextStatus() {
useEffect(() => {
if (typeof latestSessionId === "string" && latestSessionId) {
window.localStorage.setItem("claw.activeSessionId", latestSessionId);
window.setTimeout(() => {
window.dispatchEvent(new Event("claw-sessions-changed"));
}, 0);
}
}, [latestSessionId]);
useEffect(() => {
if (wasRunningRef.current && !isRunning) {
window.dispatchEvent(new Event("claw-sessions-changed"));
window.setTimeout(() => {
window.dispatchEvent(new Event("claw-sessions-changed"));
}, 1500);
}
wasRunningRef.current = isRunning;
}, [isRunning]);
useEffect(() => {
let cancelled = false;
@@ -447,6 +500,7 @@ function useComposerContextStatus() {
statePayload.active_session_id ||
null;
let sessionPayload: ClawStoredSession | null = null;
let contextBudget: ContextBudget | undefined;
if (sessionId) {
const sessionResponse = await fetch(
`/api/claw/sessions/${sessionId}`,
@@ -458,10 +512,20 @@ function useComposerContextStatus() {
? ((await sessionResponse.json()) as ClawStoredSession)
: null;
}
const budgetUrl = new URL(
"/api/claw/context-budget",
window.location.origin,
);
if (sessionId) budgetUrl.searchParams.set("session_id", sessionId);
const budgetResponse = await fetch(budgetUrl, { cache: "no-store" });
contextBudget = budgetResponse.ok
? ((await budgetResponse.json()) as ContextBudget)
: undefined;
if (cancelled) return;
setStatus((current) => ({
model: sessionPayload?.model ?? statePayload.model,
usage: latestUsage ?? sessionPayload?.usage,
model:
contextBudget?.model ?? sessionPayload?.model ?? statePayload.model,
contextBudget,
sessionId,
models: current.models,
setModel: current.setModel,
@@ -470,7 +534,6 @@ function useComposerContextStatus() {
if (!cancelled) {
setStatus((current) => ({
...current,
usage: latestUsage ?? current.usage,
}));
}
}
@@ -487,7 +550,7 @@ function useComposerContextStatus() {
window.removeEventListener("claw-active-session-cleared", onRefresh);
window.clearInterval(interval);
};
}, [isRunning, latestSessionId, latestUsage]);
}, [isRunning, latestSessionId]);
useEffect(() => {
let cancelled = false;
@@ -528,6 +591,7 @@ function useComposerContextStatus() {
setStatus((current) => ({
...current,
model: payload.model ?? model,
contextBudget: undefined,
}));
}, []);
@@ -562,14 +626,6 @@ function readMetadataValue(metadata: unknown, key: string): unknown {
return undefined;
}
function inferContextLimit(model?: string) {
const lower = (model ?? "").toLowerCase();
if (lower.includes("qwen3-32b")) return 128_000;
if (lower.includes("deepseek")) return 128_000;
if (lower.includes("mimo")) return 128_000;
return 128_000;
}
function formatCompactTokenCount(value: number) {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
@@ -849,7 +905,12 @@ const AssistantMessage: FC = () => {
/>
);
case "text":
return <MarkdownText />;
return (
<MarkdownText
text={part.text}
isRunning={part.status?.type === "running"}
/>
);
case "reasoning":
return <Reasoning {...part} />;
case "tool-call":