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
@@ -0,0 +1,37 @@
import { getCurrentAccount } from "@/lib/claw-auth";
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
export async function GET(req: Request) {
const account = await getCurrentAccount();
if (!account)
return Response.json({ error: "请先登录账号" }, { status: 401 });
const incoming = new URL(req.url);
const url = new URL(`${CLAW_API_URL}/api/context-budget`);
url.searchParams.set("account_id", account.id);
const sessionId = incoming.searchParams.get("session_id");
if (sessionId) url.searchParams.set("session_id", sessionId);
try {
const response = await fetch(url, { cache: "no-store" });
const payload = await response.text();
return new Response(payload, {
status: response.status,
headers: {
"content-type":
response.headers.get("content-type") ?? "application/json",
},
});
} catch (err) {
return Response.json(
{
error:
err instanceof Error
? `Unable to reach ZK Data Agent backend: ${err.message}`
: "Unable to reach ZK Data Agent backend",
},
{ status: 502 },
);
}
}
@@ -23,3 +23,52 @@ export async function GET(
},
});
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ sessionId: string }> },
) {
const account = await getCurrentAccount();
if (!account)
return Response.json({ error: "请先登录账号" }, { status: 401 });
const { sessionId } = await params;
const url = new URL(`${CLAW_API_URL}/api/sessions/${sessionId}`);
url.searchParams.set("account_id", account.id);
const response = await fetch(url, { method: "DELETE", cache: "no-store" });
const payload = await response.text();
return new Response(payload, {
status: response.status,
headers: {
"content-type":
response.headers.get("content-type") ?? "application/json",
},
});
}
export async function PATCH(
req: Request,
{ params }: { params: Promise<{ sessionId: string }> },
) {
const account = await getCurrentAccount();
if (!account)
return Response.json({ error: "请先登录账号" }, { status: 401 });
const { sessionId } = await params;
const url = new URL(`${CLAW_API_URL}/api/sessions/${sessionId}`);
url.searchParams.set("account_id", account.id);
const response = await fetch(url, {
method: "PATCH",
cache: "no-store",
headers: { "content-type": "application/json" },
body: await req.text(),
});
const payload = await response.text();
return new Response(payload, {
status: response.status,
headers: {
"content-type":
response.headers.get("content-type") ?? "application/json",
},
});
}
+3 -1
View File
@@ -23,6 +23,7 @@ import {
import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
import { ClawAccountGate, useClawAccount } from "./claw-account-gate";
import { ClawLlmSettings } from "./claw-llm-settings";
import { ClawThemeSwitcher } from "./claw-theme-switcher";
export const Assistant = () => {
const { account, isLoading, setAccount } = useClawAccount();
@@ -94,9 +95,10 @@ export const Assistant = () => {
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-sm"></div>
<div className="text-muted-foreground text-xs">
Claw Data Agent
ZK Data Agent
</div>
</div>
<ClawThemeSwitcher />
<ClawLlmSettings />
</header>
<div className="flex min-h-0 flex-1 overflow-hidden">
+2 -2
View File
@@ -74,9 +74,9 @@ export function ClawAccountGate({
<div className="flex min-h-dvh items-center justify-center bg-background px-4">
<div className="w-full max-w-sm rounded-xl border bg-card p-6 shadow-sm">
<div className="mb-6">
<h1 className="font-semibold text-xl">Claw Data Agent</h1>
<h1 className="font-semibold text-xl">ZK Data Agent</h1>
<p className="mt-1 text-muted-foreground text-sm">
</p>
</div>
<div className="grid gap-3">
+1 -1
View File
@@ -96,7 +96,7 @@ export function ClawLlmSettings() {
<DialogHeader>
<DialogTitle> LLM API</DialogTitle>
<DialogDescription>
Claw Agent 使 Base URL API Key
ZK Data Agent 使 Base URL API Key
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
+99
View File
@@ -0,0 +1,99 @@
"use client";
import { CheckIcon, MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { Popover as PopoverPrimitive } from "radix-ui";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
type ThemeMode = "light" | "dark" | "system";
const THEME_STORAGE_KEY = "claw.theme";
const THEME_OPTIONS: Array<{
mode: ThemeMode;
label: string;
icon: typeof SunIcon;
}> = [
{ mode: "light", label: "亮色", icon: SunIcon },
{ mode: "dark", label: "暗色", icon: MoonIcon },
{ mode: "system", label: "跟随系统", icon: MonitorIcon },
];
export function ClawThemeSwitcher() {
const [mode, setMode] = useState<ThemeMode>("system");
const activeOption =
THEME_OPTIONS.find((option) => option.mode === mode) ?? THEME_OPTIONS[2];
const ActiveIcon = activeOption.icon;
useEffect(() => {
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
const initialMode = isThemeMode(stored) ? stored : "system";
setMode(initialMode);
applyTheme(initialMode);
const media = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => {
if (window.localStorage.getItem(THEME_STORAGE_KEY) === "system") {
applyTheme("system");
}
};
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, []);
const updateMode = (nextMode: ThemeMode) => {
window.localStorage.setItem(THEME_STORAGE_KEY, nextMode);
setMode(nextMode);
applyTheme(nextMode);
};
return (
<PopoverPrimitive.Root>
<PopoverPrimitive.Trigger asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="rounded-full"
aria-label="切换主题"
title={`主题:${activeOption.label}`}
>
<ActiveIcon className="size-4" />
</Button>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align="end"
sideOffset={8}
className="z-50 min-w-36 rounded-xl border bg-popover p-1 text-popover-foreground shadow-lg"
>
{THEME_OPTIONS.map((option) => {
const Icon = option.icon;
return (
<button
key={option.mode}
type="button"
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition hover:bg-accent"
onClick={() => updateMode(option.mode)}
>
<Icon className="size-4 text-muted-foreground" />
<span className="min-w-0 flex-1">{option.label}</span>
{mode === option.mode ? <CheckIcon className="size-4" /> : null}
</button>
);
})}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
}
function applyTheme(mode: ThemeMode) {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const shouldUseDark = mode === "dark" || (mode === "system" && prefersDark);
document.documentElement.classList.toggle("dark", shouldUseDark);
}
function isThemeMode(value: string | null): value is ThemeMode {
return value === "light" || value === "dark" || value === "system";
}
+3 -3
View File
@@ -14,8 +14,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Claw Data Agent",
description: "assistant-ui based data agent workbench",
title: "ZK Data Agent",
description: "中控数据开发平台",
};
export default function RootLayout({
@@ -24,7 +24,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<html lang="zh-CN" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>