Improve Jupyter workspace bootstrap

This commit is contained in:
wuyang6
2026-05-12 22:33:15 +08:00
parent 295d5d1ea1
commit 86f49134a4
5 changed files with 858 additions and 44 deletions
@@ -32,7 +32,14 @@ import {
WrenchIcon,
} from "lucide-react";
import { Popover as PopoverPrimitive } from "radix-ui";
import type { ComponentProps, FC, KeyboardEvent, ReactNode } from "react";
import type {
ComponentProps,
Dispatch,
FC,
KeyboardEvent,
ReactNode,
SetStateAction,
} from "react";
import {
useCallback,
useEffect,
@@ -359,6 +366,20 @@ type JupyterWorkspaceStatus = {
error?: string;
};
type WorkspaceSwitchProgress = {
percent: number;
label: string;
};
const WORKSPACE_SWITCH_STEPS = [
"登录 Jupyter",
"初始化目录",
"创建 Python 环境",
"配置 pip 源",
"同步 Skill",
"链接工作区",
];
function WorkspaceSwitchDialog({
open,
onOpenChange,
@@ -376,6 +397,9 @@ function WorkspaceSwitchDialog({
const [status, setStatus] = useState<JupyterWorkspaceStatus | null>(null);
const [submitting, setSubmitting] = useState(false);
const [message, setMessage] = useState("");
const [progress, setProgress] = useState<WorkspaceSwitchProgress | null>(
null,
);
const fieldId = useId();
const effectiveSessionId =
sessionId ??
@@ -405,21 +429,19 @@ function WorkspaceSwitchDialog({
async function submit() {
setMessage("");
if (!effectiveSessionId) {
setMessage("请先发送一条消息创建 session,再切换工作区。");
return;
}
const sessionIdForRequest = ensureWorkspaceSessionId(effectiveSessionId);
if (!jupyterUrl.trim() || !password) {
setMessage("请填写 Jupyter 地址和密码。");
return;
}
setSubmitting(true);
const progressTimer = startWorkspaceProgress(setProgress);
try {
const response = await fetch("/api/claw/jupyter", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
session_id: effectiveSessionId,
session_id: sessionIdForRequest,
base_url: jupyterUrl.trim(),
password,
workspace_root: workspaceRoot.trim() || "/root/zk_agent_workspaces",
@@ -432,11 +454,15 @@ function WorkspaceSwitchDialog({
setMessage(payload.detail ?? payload.error ?? "切换工作区失败");
return;
}
writeActiveSessionId(sessionIdForRequest);
setStatus(payload as JupyterWorkspaceStatus);
setProgress({ percent: 100, label: "切换完成" });
setMessage("工作区切换成功,后续工具会在远端 Jupyter 工作区执行。");
setPassword("");
} finally {
window.clearInterval(progressTimer);
setSubmitting(false);
window.setTimeout(() => setProgress(null), 1200);
}
}
@@ -475,6 +501,7 @@ function WorkspaceSwitchDialog({
value={jupyterUrl}
onChange={(event) => setJupyterUrl(event.target.value)}
placeholder="https://...-jupyter.../lab"
disabled={submitting}
/>
</label>
<label className="grid gap-1.5" htmlFor={`${fieldId}-password`}>
@@ -485,6 +512,7 @@ function WorkspaceSwitchDialog({
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Jupyter 登录密码"
disabled={submitting}
/>
</label>
<label className="grid gap-1.5" htmlFor={`${fieldId}-root`}>
@@ -493,8 +521,25 @@ function WorkspaceSwitchDialog({
id={`${fieldId}-root`}
value={workspaceRoot}
onChange={(event) => setWorkspaceRoot(event.target.value)}
disabled={submitting}
/>
</label>
{progress ? (
<div className="rounded-md border bg-muted/30 px-3 py-2">
<div className="mb-2 flex items-center justify-between gap-3 text-xs">
<span className="text-muted-foreground">{progress.label}</span>
<span className="font-medium tabular-nums">
{Math.round(progress.percent)}%
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all duration-500"
style={{ width: `${progress.percent}%` }}
/>
</div>
</div>
) : null}
{message ? (
<div className="rounded-md bg-muted px-3 py-2 text-muted-foreground">
{message}
@@ -518,6 +563,43 @@ function WorkspaceSwitchDialog({
);
}
function ensureWorkspaceSessionId(current: string | null) {
if (current) return current;
const generated =
typeof crypto !== "undefined" && "randomUUID" in crypto
? `__LOCALID_${crypto.randomUUID().replace(/-/g, "").slice(0, 8)}`
: `__LOCALID_${Math.random().toString(36).slice(2, 10)}`;
writeActiveSessionId(generated);
scheduleSessionListRefresh();
return generated;
}
function startWorkspaceProgress(
setProgress: Dispatch<SetStateAction<WorkspaceSwitchProgress | null>>,
) {
setProgress({ percent: 6, label: WORKSPACE_SWITCH_STEPS[0] });
return window.setInterval(() => {
setProgress((current) => {
const previous = current ?? {
percent: 6,
label: WORKSPACE_SWITCH_STEPS[0],
};
const nextPercent = Math.min(
92,
previous.percent + (previous.percent < 55 ? 9 : 4),
);
const stepIndex = Math.min(
WORKSPACE_SWITCH_STEPS.length - 1,
Math.floor((nextPercent / 100) * WORKSPACE_SWITCH_STEPS.length),
);
return {
percent: nextPercent,
label: WORKSPACE_SWITCH_STEPS[stepIndex],
};
});
}, 1200);
}
const ThreadMessage: FC = () => {
const role = useAuiState((s) => s.message.role);
const isEditing = useAuiState((s) => s.message.composer.isEditing);