mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 00:34:22 +08:00
feat(agent): add task status and client-scoped operations
This commit is contained in:
@@ -151,6 +151,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
setAgentState({ connected: true, activity: "已连接", connectError: "", silentConnect: false, messages: useAgentStore.getState().messages.filter((item) => !isConnectionErrorMessage(item)) });
|
||||
if (!headless) message.success("本地 Agent 已连接");
|
||||
void postState(endpoint, token, clientId, canvasContextRef.current?.snapshot || null);
|
||||
if (document.visibilityState === "visible" && document.hasFocus()) void activateAgentClient(endpoint, token, clientId);
|
||||
});
|
||||
source.addEventListener("tool_call", (event) => {
|
||||
const data = parseEventData<AgentPendingToolCall>(event);
|
||||
@@ -200,6 +201,19 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
if (connected) void loadThreads();
|
||||
}, [connected, loadThreads]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected) return;
|
||||
const activate = () => void activateAgentClient(endpoint, token, clientIdRef.current);
|
||||
const activateVisible = () => {
|
||||
if (document.visibilityState === "visible") activate();
|
||||
};
|
||||
window.addEventListener("focus", activate);
|
||||
document.addEventListener("visibilitychange", activateVisible);
|
||||
return () => {
|
||||
window.removeEventListener("focus", activate);
|
||||
document.removeEventListener("visibilitychange", activateVisible);
|
||||
};
|
||||
}, [connected, endpoint, token]);
|
||||
const sendPrompt = async () => {
|
||||
const text = prompt.trim();
|
||||
const files = attachments;
|
||||
@@ -304,7 +318,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
|
||||
try {
|
||||
setAgentState({ activity: SITE_TOOL_LABELS[payload.name], waiting: true });
|
||||
addEventLog(toolName(payload.name), payload, payload);
|
||||
const result = await runSiteTool(payload.name, payload.input || {}, navigate);
|
||||
const result = await runSiteTool(payload.name, payload.input || {}, navigate, { canvasSnapshot: canvasContextRef.current?.snapshot || null });
|
||||
await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, result });
|
||||
setAgentState({ activity: "工具完成", waiting: true });
|
||||
addEventLog(`${toolName(payload.name)}完成`, result, result);
|
||||
@@ -955,6 +969,12 @@ async function postState(endpoint: string, token: string, clientId: string, snap
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function activateAgentClient(endpoint: string, token: string, clientId: string) {
|
||||
try {
|
||||
await fetch(`${endpoint}/canvas/activate?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}`, { method: "POST" });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function postToolResult(endpoint: string, token: string, clientId: string, body: { requestId: string; result?: unknown; error?: string }) {
|
||||
await fetch(`${endpoint}/canvas/result?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
||||
}
|
||||
@@ -1093,6 +1113,10 @@ function siteToolSummary(name: string, result: unknown) {
|
||||
if (name === "prompts_search") return `找到 ${numberField(data, "total")} 条提示词`;
|
||||
if (name === "assets_list") return `共 ${numberField(data, "total")} 个资产`;
|
||||
if (name === "assets_add") return "已加入我的资产";
|
||||
if (name === "generation_get_status") {
|
||||
const summary = data.summary && typeof data.summary === "object" ? (data.summary as Record<string, unknown>) : {};
|
||||
return `共 ${numberField(data, "total")} 个任务,排队 ${numberField(summary, "queued")},运行中 ${numberField(summary, "running")},成功 ${numberField(summary, "succeeded")},失败 ${numberField(summary, "failed")}`;
|
||||
}
|
||||
if (name === "workbench_image_generate" || name === "workbench_video_generate") return typeof data.note === "string" ? data.note : "已在工作台执行";
|
||||
if (name === "workbench_image_get_config" || name === "workbench_video_get_config") return "已读取工作台配置";
|
||||
return "已完成";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fetchPrompts } from "@/services/api/prompts";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { imageAspectOptions, imageQualityOptions } from "@/components/image-settings-panel";
|
||||
import { videoResolutionOptions, videoSecondOptions, videoSizeOptions } from "@/components/video-settings-panel";
|
||||
import type { CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useAssetStore } from "@/stores/use-asset-store";
|
||||
import { modelOptionLabel, modelOptionName, normalizeModelOptionValue, selectableModelsByCapability, useConfigStore } from "@/stores/use-config-store";
|
||||
@@ -14,6 +15,7 @@ import { useWorkbenchAgentStore } from "@/stores/use-workbench-agent-store";
|
||||
|
||||
export const SITE_TOOL_NAMES = [
|
||||
"canvas_list_projects",
|
||||
"generation_get_status",
|
||||
"workbench_image_get_config",
|
||||
"workbench_image_generate",
|
||||
"workbench_video_get_config",
|
||||
@@ -31,6 +33,7 @@ export function isSiteTool(name: string): name is SiteToolName {
|
||||
|
||||
export const SITE_TOOL_LABELS: Record<SiteToolName, string> = {
|
||||
canvas_list_projects: "画布列表",
|
||||
generation_get_status: "生成任务状态",
|
||||
workbench_image_get_config: "生图配置",
|
||||
workbench_image_generate: "生图工作台生成",
|
||||
workbench_video_get_config: "视频配置",
|
||||
@@ -41,11 +44,16 @@ export const SITE_TOOL_LABELS: Record<SiteToolName, string> = {
|
||||
};
|
||||
|
||||
type SiteToolInput = Record<string, unknown>;
|
||||
type SiteToolContext = { canvasSnapshot?: CanvasAgentSnapshot | null };
|
||||
type GenerationStatus = "idle" | "queued" | "running" | "succeeded" | "failed";
|
||||
type GenerationStatusItem = { id: string; source: "canvas" | "image" | "video"; status: GenerationStatus; kind?: string; title?: string; prompt?: string; projectId?: string; createdAt?: string; updatedAt?: string; successCount?: number; failCount?: number; error?: string };
|
||||
|
||||
export async function runSiteTool(name: SiteToolName, input: SiteToolInput, navigate: NavigateFunction): Promise<unknown> {
|
||||
export async function runSiteTool(name: SiteToolName, input: SiteToolInput, navigate: NavigateFunction, context: SiteToolContext = {}): Promise<unknown> {
|
||||
switch (name) {
|
||||
case "canvas_list_projects":
|
||||
return listCanvasProjects(input);
|
||||
case "generation_get_status":
|
||||
return getGenerationStatus(input, context.canvasSnapshot);
|
||||
case "workbench_image_get_config":
|
||||
return getImageConfig();
|
||||
case "workbench_image_generate":
|
||||
@@ -65,6 +73,56 @@ export async function runSiteTool(name: SiteToolName, input: SiteToolInput, navi
|
||||
}
|
||||
}
|
||||
|
||||
function getGenerationStatus(input: SiteToolInput, canvasSnapshot?: CanvasAgentSnapshot | null) {
|
||||
const scope = input.scope === "canvas" || input.scope === "image" || input.scope === "video" ? input.scope : "all";
|
||||
const taskId = typeof input.taskId === "string" ? input.taskId : "";
|
||||
const nodeIds = new Set(Array.isArray(input.nodeIds) ? input.nodeIds.filter((id): id is string => typeof id === "string") : []);
|
||||
const limit = Math.max(1, Math.min(100, Math.floor(Number(input.limit)) || 20));
|
||||
const tasks: GenerationStatusItem[] = [];
|
||||
const includeCanvas = (scope === "all" || scope === "canvas") && (!taskId || nodeIds.size > 0);
|
||||
const includeWorkbench = !nodeIds.size || Boolean(taskId);
|
||||
|
||||
if (includeCanvas && canvasSnapshot) {
|
||||
canvasSnapshot.nodes.forEach((node) => {
|
||||
const status = normalizeCanvasGenerationStatus(node.metadata?.status);
|
||||
if (!status || (nodeIds.size && !nodeIds.has(node.id))) return;
|
||||
const metadata = node.metadata || {};
|
||||
if (!nodeIds.size && node.type !== "config" && status !== "running" && status !== "failed" && !metadata.generationMode && !metadata.generationType && !metadata.model) return;
|
||||
tasks.push({ id: node.id, source: "canvas", status, kind: metadata.generationMode || node.type, title: node.title, prompt: compactPrompt(metadata.prompt || metadata.composerContent), projectId: canvasSnapshot.projectId, error: metadata.errorDetails });
|
||||
});
|
||||
}
|
||||
|
||||
if (includeWorkbench) {
|
||||
useWorkbenchAgentStore.getState().tasks.forEach((task) => {
|
||||
if ((scope === "image" || scope === "video") && task.kind !== scope) return;
|
||||
if (scope === "canvas" || (taskId && task.id !== taskId)) return;
|
||||
tasks.push({ ...task, source: task.kind, prompt: compactPrompt(task.prompt) });
|
||||
});
|
||||
}
|
||||
|
||||
tasks.sort((a, b) => generationStatusOrder(a.status) - generationStatusOrder(b.status) || (b.updatedAt || "").localeCompare(a.updatedAt || ""));
|
||||
const summary: Record<GenerationStatus, number> = { idle: 0, queued: 0, running: 0, succeeded: 0, failed: 0 };
|
||||
tasks.forEach((task) => (summary[task.status] += 1));
|
||||
return { total: tasks.length, summary, tasks: tasks.slice(0, limit) };
|
||||
}
|
||||
|
||||
function generationStatusOrder(status: GenerationStatus) {
|
||||
return status === "running" ? 0 : status === "queued" ? 1 : 2;
|
||||
}
|
||||
|
||||
function normalizeCanvasGenerationStatus(status: unknown): GenerationStatus | null {
|
||||
if (status === "idle") return "idle";
|
||||
if (status === "loading") return "running";
|
||||
if (status === "success") return "succeeded";
|
||||
if (status === "error") return "failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
function compactPrompt(prompt: unknown) {
|
||||
const value = typeof prompt === "string" ? prompt.trim() : "";
|
||||
return value ? `${value.slice(0, 200)}${value.length > 200 ? "..." : ""}` : undefined;
|
||||
}
|
||||
|
||||
function listCanvasProjects(input: SiteToolInput) {
|
||||
const { projects, hydrated } = useCanvasStore.getState();
|
||||
if (!hydrated) throw new Error("画布还在加载中,请稍后重试");
|
||||
@@ -118,8 +176,8 @@ function runImageWorkbench(input: SiteToolInput, navigate: NavigateFunction) {
|
||||
const prompt = typeof input.prompt === "string" ? input.prompt : undefined;
|
||||
const run = input.run !== false;
|
||||
navigate("/image");
|
||||
useWorkbenchAgentStore.getState().dispatchImage({ prompt, run });
|
||||
return { ok: true, navigated: "/image", prompt, run, applied, note: run ? "已跳转生图工作台并触发生成,结果请稍后在工作台查看" : "已跳转生图工作台并填入参数,未触发生成" };
|
||||
const taskId = useWorkbenchAgentStore.getState().dispatchImage({ prompt, run });
|
||||
return { ok: true, navigated: "/image", prompt, run, taskId, applied, note: run ? "已跳转生图工作台并触发生成,可用 generation_get_status 查询任务" : "已跳转生图工作台并填入参数,未触发生成" };
|
||||
}
|
||||
|
||||
function getVideoConfig() {
|
||||
@@ -173,8 +231,8 @@ function runVideoWorkbench(input: SiteToolInput, navigate: NavigateFunction) {
|
||||
const prompt = typeof input.prompt === "string" ? input.prompt : undefined;
|
||||
const run = input.run !== false;
|
||||
navigate("/video");
|
||||
useWorkbenchAgentStore.getState().dispatchVideo({ prompt, run });
|
||||
return { ok: true, navigated: "/video", prompt, run, applied, note: run ? "已跳转视频创作台并触发生成,结果请稍后在工作台查看" : "已跳转视频创作台并填入参数,未触发生成" };
|
||||
const taskId = useWorkbenchAgentStore.getState().dispatchVideo({ prompt, run });
|
||||
return { ok: true, navigated: "/video", prompt, run, taskId, applied, note: run ? "已跳转视频创作台并触发生成,可用 generation_get_status 查询任务" : "已跳转视频创作台并填入参数,未触发生成" };
|
||||
}
|
||||
|
||||
async function searchPrompts(input: SiteToolInput) {
|
||||
|
||||
@@ -92,7 +92,9 @@ export default function ImagePage() {
|
||||
const [autoRunToken, setAutoRunToken] = useState(0);
|
||||
const imageCommand = useWorkbenchAgentStore((state) => state.imageCommand);
|
||||
const clearImageCommand = useWorkbenchAgentStore((state) => state.clearImageCommand);
|
||||
const updateAgentTask = useWorkbenchAgentStore((state) => state.updateTask);
|
||||
const processedCommandRef = useRef(0);
|
||||
const agentTaskIdRef = useRef<string | undefined>(undefined);
|
||||
|
||||
const model = effectiveConfig.imageModel || effectiveConfig.model;
|
||||
const canGenerate = Boolean(prompt.trim());
|
||||
@@ -141,22 +143,30 @@ export default function ImagePage() {
|
||||
};
|
||||
|
||||
const generate = async () => {
|
||||
const agentTaskId = agentTaskIdRef.current;
|
||||
agentTaskIdRef.current = undefined;
|
||||
const text = prompt.trim();
|
||||
if (!text) {
|
||||
message.error("请输入生图提示词");
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "failed", error: "请输入生图提示词" });
|
||||
return;
|
||||
}
|
||||
if (!isAiConfigReady(effectiveConfig, model)) {
|
||||
message.warning("请先完成配置");
|
||||
openConfigDialog(true);
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "failed", error: "生图配置不完整" });
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = buildRequestSnapshot();
|
||||
if (!snapshot) return;
|
||||
if (!snapshot) {
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "failed", error: "生图参数无效" });
|
||||
return;
|
||||
}
|
||||
|
||||
setElapsedMs(0);
|
||||
setRunning(true);
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "running", error: undefined });
|
||||
setPreviewLog(null);
|
||||
setResults(Array.from({ length: generationCount }, () => ({ id: nanoid(), status: "pending" })));
|
||||
const batchStartedAt = performance.now();
|
||||
@@ -169,6 +179,8 @@ export default function ImagePage() {
|
||||
const successCount = successImages.length;
|
||||
const failCount = generationCount - successCount;
|
||||
const failed = result.find((item): item is PromiseRejectedResult => item.status === "rejected");
|
||||
const error = failed?.reason instanceof Error ? failed.reason.message : failCount ? "生成失败" : undefined;
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: successCount ? "succeeded" : "failed", successCount, failCount, error: successCount ? undefined : error });
|
||||
|
||||
try {
|
||||
const logImages = await Promise.all(
|
||||
@@ -202,8 +214,15 @@ export default function ImagePage() {
|
||||
processedCommandRef.current = imageCommand.nonce;
|
||||
clearImageCommand();
|
||||
if (typeof imageCommand.prompt === "string") setPrompt(imageCommand.prompt);
|
||||
if (imageCommand.run && !running) setAutoRunToken((value) => value + 1);
|
||||
}, [imageCommand, clearImageCommand, running]);
|
||||
if (imageCommand.run && running) {
|
||||
if (imageCommand.taskId) updateAgentTask(imageCommand.taskId, { status: "failed", error: "生图工作台已有任务正在运行" });
|
||||
return;
|
||||
}
|
||||
if (imageCommand.run) {
|
||||
agentTaskIdRef.current = imageCommand.taskId;
|
||||
setAutoRunToken((value) => value + 1);
|
||||
}
|
||||
}, [imageCommand, clearImageCommand, running, updateAgentTask]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRunToken) return;
|
||||
|
||||
@@ -97,7 +97,9 @@ export default function VideoPage() {
|
||||
const [autoRunToken, setAutoRunToken] = useState(0);
|
||||
const videoCommand = useWorkbenchAgentStore((state) => state.videoCommand);
|
||||
const clearVideoCommand = useWorkbenchAgentStore((state) => state.clearVideoCommand);
|
||||
const updateAgentTask = useWorkbenchAgentStore((state) => state.updateTask);
|
||||
const processedCommandRef = useRef(0);
|
||||
const agentTaskIdRef = useRef<string | undefined>(undefined);
|
||||
|
||||
const model = effectiveConfig.videoModel || effectiveConfig.model;
|
||||
const canGenerate = Boolean(prompt.trim());
|
||||
@@ -170,10 +172,16 @@ export default function VideoPage() {
|
||||
}
|
||||
};
|
||||
const generate = async () => {
|
||||
const agentTaskId = agentTaskIdRef.current;
|
||||
agentTaskIdRef.current = undefined;
|
||||
const snapshot = buildRequestSnapshot();
|
||||
if (!snapshot) return;
|
||||
if (!snapshot) {
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "failed", error: "视频生成参数无效" });
|
||||
return;
|
||||
}
|
||||
setElapsedMs(0);
|
||||
setRunning(true);
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "running", error: undefined });
|
||||
setPreviewLog(null);
|
||||
setResults([{ id: nanoid(), status: "pending" }]);
|
||||
const batchStartedAt = performance.now();
|
||||
@@ -181,11 +189,12 @@ export default function VideoPage() {
|
||||
try {
|
||||
const task = await createVideoGenerationTask(snapshot.config, snapshot.text, snapshot.references, snapshot.videoReferences, snapshot.audioReferences);
|
||||
const log = buildLog({ prompt: snapshot.text, model, config: snapshot.config, references: snapshot.references, videoReferences: snapshot.videoReferences, audioReferences: snapshot.audioReferences, durationMs: 0, status: "生成中", task });
|
||||
await saveLog(log);
|
||||
void pollGenerationLog(log, snapshot.config);
|
||||
await saveLog(log, false);
|
||||
void pollGenerationLog(log, snapshot.config, agentTaskId);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "生成失败";
|
||||
setResults([{ id: nanoid(), status: "failed", error: errorMessage }]);
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "failed", successCount: 0, failCount: 1, error: errorMessage });
|
||||
await saveLog(buildLog({ prompt: snapshot.text, model, config: snapshot.config, references: snapshot.references, videoReferences: snapshot.videoReferences, audioReferences: snapshot.audioReferences, durationMs: performance.now() - batchStartedAt, status: "失败", error: errorMessage }));
|
||||
message.error(errorMessage);
|
||||
setRunning(false);
|
||||
@@ -198,8 +207,15 @@ export default function VideoPage() {
|
||||
processedCommandRef.current = videoCommand.nonce;
|
||||
clearVideoCommand();
|
||||
if (typeof videoCommand.prompt === "string") setPrompt(videoCommand.prompt);
|
||||
if (videoCommand.run && !running) setAutoRunToken((value) => value + 1);
|
||||
}, [videoCommand, clearVideoCommand, running]);
|
||||
if (videoCommand.run && running) {
|
||||
if (videoCommand.taskId) updateAgentTask(videoCommand.taskId, { status: "failed", error: "视频工作台已有任务正在运行" });
|
||||
return;
|
||||
}
|
||||
if (videoCommand.run) {
|
||||
agentTaskIdRef.current = videoCommand.taskId;
|
||||
setAutoRunToken((value) => value + 1);
|
||||
}
|
||||
}, [videoCommand, clearVideoCommand, running, updateAgentTask]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRunToken) return;
|
||||
@@ -276,7 +292,7 @@ export default function VideoPage() {
|
||||
.filter((log) => selectedLogIds.includes(log.id))
|
||||
.map((log) => log.video?.storageKey)
|
||||
.filter((key): key is string => Boolean(key));
|
||||
void Promise.all([deleteStoredMedia(mediaKeys), ...selectedLogIds.map((id) => logStore.removeItem(id))]).then(refreshLogs);
|
||||
void Promise.all([deleteStoredMedia(mediaKeys), ...selectedLogIds.map((id) => logStore.removeItem(id))]).then(() => refreshLogs());
|
||||
if (previewLog && selectedLogIds.includes(previewLog.id)) {
|
||||
setPreviewLog(null);
|
||||
setResults([]);
|
||||
@@ -285,15 +301,15 @@ export default function VideoPage() {
|
||||
setDeleteConfirmOpen(false);
|
||||
};
|
||||
|
||||
const saveLog = async (log: GenerationLog) => {
|
||||
const saveLog = async (log: GenerationLog, resumePending = true) => {
|
||||
await logStore.setItem(log.id, serializeLog(log));
|
||||
await refreshLogs();
|
||||
await refreshLogs(resumePending);
|
||||
};
|
||||
|
||||
const refreshLogs = async () => {
|
||||
const refreshLogs = async (resumePending = true) => {
|
||||
const nextLogs = await readStoredLogs();
|
||||
setLogs(nextLogs);
|
||||
resumePendingLogs(nextLogs);
|
||||
if (resumePending) resumePendingLogs(nextLogs);
|
||||
return nextLogs;
|
||||
};
|
||||
|
||||
@@ -303,7 +319,7 @@ export default function VideoPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const pollGenerationLog = async (log: GenerationLog, configOverride?: AiConfig) => {
|
||||
const pollGenerationLog = async (log: GenerationLog, configOverride?: AiConfig, agentTaskId?: string) => {
|
||||
if (!log.task || activeLogIdsRef.current.has(log.id)) return;
|
||||
activeLogIdsRef.current.add(log.id);
|
||||
setRunning(true);
|
||||
@@ -326,6 +342,7 @@ export default function VideoPage() {
|
||||
mimeType: stored.mimeType,
|
||||
};
|
||||
setResults([{ id: nextVideo.id, status: "success", video: nextVideo }]);
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "succeeded", successCount: 1, failCount: 0, error: undefined });
|
||||
await saveLog({ ...log, status: "成功", durationMs: nextVideo.durationMs, video: nextVideo, error: undefined });
|
||||
message.success("视频已生成");
|
||||
return;
|
||||
@@ -337,6 +354,7 @@ export default function VideoPage() {
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "生成失败";
|
||||
setResults([{ id: log.id, status: "failed", error: errorMessage }]);
|
||||
if (agentTaskId) updateAgentTask(agentTaskId, { status: "failed", successCount: 0, failCount: 1, error: errorMessage });
|
||||
await saveLog({ ...log, status: "失败", durationMs: Date.now() - log.createdAt, error: errorMessage });
|
||||
message.error(errorMessage);
|
||||
} finally {
|
||||
|
||||
@@ -6,15 +6,30 @@ import { create } from "zustand";
|
||||
|
||||
export type WorkbenchCommand = {
|
||||
nonce: number;
|
||||
taskId?: string;
|
||||
prompt?: string;
|
||||
run: boolean;
|
||||
};
|
||||
|
||||
export type WorkbenchGenerationTask = {
|
||||
id: string;
|
||||
kind: "image" | "video";
|
||||
status: "queued" | "running" | "succeeded" | "failed";
|
||||
prompt?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
successCount?: number;
|
||||
failCount?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type WorkbenchAgentStore = {
|
||||
imageCommand: WorkbenchCommand | null;
|
||||
videoCommand: WorkbenchCommand | null;
|
||||
dispatchImage: (command: Omit<WorkbenchCommand, "nonce">) => void;
|
||||
dispatchVideo: (command: Omit<WorkbenchCommand, "nonce">) => void;
|
||||
tasks: WorkbenchGenerationTask[];
|
||||
dispatchImage: (command: Omit<WorkbenchCommand, "nonce" | "taskId">) => string | undefined;
|
||||
dispatchVideo: (command: Omit<WorkbenchCommand, "nonce" | "taskId">) => string | undefined;
|
||||
updateTask: (id: string, patch: Partial<Pick<WorkbenchGenerationTask, "status" | "successCount" | "failCount" | "error">>) => void;
|
||||
clearImageCommand: () => void;
|
||||
clearVideoCommand: () => void;
|
||||
};
|
||||
@@ -25,8 +40,25 @@ const nextNonce = () => (nonce += 1);
|
||||
export const useWorkbenchAgentStore = create<WorkbenchAgentStore>((set) => ({
|
||||
imageCommand: null,
|
||||
videoCommand: null,
|
||||
dispatchImage: (command) => set({ imageCommand: { ...command, nonce: nextNonce() } }),
|
||||
dispatchVideo: (command) => set({ videoCommand: { ...command, nonce: nextNonce() } }),
|
||||
tasks: [],
|
||||
dispatchImage: (command) => {
|
||||
const commandNonce = nextNonce();
|
||||
const task = command.run ? createTask("image", commandNonce, command.prompt) : undefined;
|
||||
set((state) => ({ imageCommand: { ...command, nonce: commandNonce, taskId: task?.id }, tasks: task ? [task, ...state.tasks].slice(0, 30) : state.tasks }));
|
||||
return task?.id;
|
||||
},
|
||||
dispatchVideo: (command) => {
|
||||
const commandNonce = nextNonce();
|
||||
const task = command.run ? createTask("video", commandNonce, command.prompt) : undefined;
|
||||
set((state) => ({ videoCommand: { ...command, nonce: commandNonce, taskId: task?.id }, tasks: task ? [task, ...state.tasks].slice(0, 30) : state.tasks }));
|
||||
return task?.id;
|
||||
},
|
||||
updateTask: (id, patch) => set((state) => ({ tasks: state.tasks.map((task) => (task.id === id ? { ...task, ...patch, updatedAt: new Date().toISOString() } : task)) })),
|
||||
clearImageCommand: () => set({ imageCommand: null }),
|
||||
clearVideoCommand: () => set({ videoCommand: null }),
|
||||
}));
|
||||
|
||||
function createTask(kind: "image" | "video", commandNonce: number, prompt?: string): WorkbenchGenerationTask {
|
||||
const now = new Date().toISOString();
|
||||
return { id: `${kind}-${commandNonce}`, kind, status: "queued", prompt, createdAt: now, updatedAt: now };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user