Improve data agent workspace UI
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user