From 8ff26b3816e0fb0070ef55a4f6c2919745c08e8e Mon Sep 17 00:00:00 2001 From: eust-w Date: Fri, 27 Mar 2026 11:48:00 +0800 Subject: [PATCH] :sparkles: feat: add i18n and draggable workflow canvas --- Makefile | 2 +- README.md | 2 + apps/web/package.json | 1 + apps/web/src/main.tsx | 6 +- apps/web/src/runtime/app.tsx | 530 +++++++++++------ apps/web/src/runtime/i18n.test.ts | 30 + apps/web/src/runtime/i18n.tsx | 534 ++++++++++++++++++ .../src/runtime/workflow-editor-state.test.ts | 24 + apps/web/src/runtime/workflow-editor-state.ts | 114 +++- apps/web/src/styles.css | 123 ++++ ...nformation-architecture-and-key-screens.md | 13 + ...26-03-26-emboflow-v1-foundation-and-mvp.md | 1 + pnpm-lock.yaml | 183 ++++++ scripts/check_doc_code_sync.py | 2 + tests/test_doc_code_sync.py | 3 + 15 files changed, 1376 insertions(+), 192 deletions(-) create mode 100644 apps/web/src/runtime/i18n.test.ts create mode 100644 apps/web/src/runtime/i18n.tsx diff --git a/Makefile b/Makefile index 104bcc4..2b59ebb 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ bootstrap: test: python3 -m unittest discover -s tests -p 'test_*.py' pnpm --filter api test - pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx + pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx src/runtime/workflow-editor-state.test.ts src/runtime/i18n.test.ts pnpm --filter web build pnpm --filter worker test diff --git a/README.md b/README.md index 788053f..9a8db24 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ The local validation path currently used for embodied data testing is: You can register that directory from the Assets page or via `POST /api/assets/register`. The workflow editor currently requires selecting at least one registered asset before a run can be created. The editor now also persists per-node runtime config in workflow versions, including executor overrides, optional artifact title overrides, and Python code-hook source for inspect and transform style nodes. +The runtime web shell now exposes a visible `中文 / English` language toggle. The core workspace shell and workflow authoring surface are translated through a lightweight i18n layer. +The workflow editor center panel now uses a real draggable node canvas with zoom, pan, mini-map, dotted background, handle-based edge creation, and persisted node positions instead of a static list of node cards. The Runs workspace now shows project-scoped run history, run-level aggregated summaries, cancel/retry controls, and run detail views with persisted task summaries, stdout/stderr sections, result previews, and artifact links into Explore. Selected run tasks now expose the frozen node definition id, executor config snapshot, and code-hook metadata that were captured when the run was created. When a node uses `executorType=docker` and provides `executorConfig.image`, the worker now runs a real local Docker container with mounted `input.json` / `output.json` exchange files. If no image is configured, the executor falls back to the lightweight simulated behavior used by older demo tasks. diff --git a/apps/web/package.json b/apps/web/package.json index 2b2b566..c6ddf75 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ "test": "tsx --test" }, "dependencies": { + "@xyflow/react": "^12.8.4", "react": "^19.2.4", "react-dom": "^19.2.4" }, diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 6992c82..640a00a 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -2,6 +2,8 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { App } from "./runtime/app.tsx"; +import { I18nProvider } from "./runtime/i18n.tsx"; +import "@xyflow/react/dist/style.css"; import "./styles.css"; export function createWebApp() { @@ -21,7 +23,9 @@ export function mountWebApp() { : "http://127.0.0.1:3001"; createRoot(target).render( - + + + , ); } diff --git a/apps/web/src/runtime/app.tsx b/apps/web/src/runtime/app.tsx index ddbf562..8b8a7ce 100644 --- a/apps/web/src/runtime/app.tsx +++ b/apps/web/src/runtime/app.tsx @@ -1,13 +1,29 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Background, + BackgroundVariant, + Controls, + MiniMap, + ReactFlow, + type Connection, + type Edge, + type Node, + type NodeChange, + type Viewport, +} from "@xyflow/react"; import { ApiClient } from "./api-client.ts"; +import { localizeNodeDefinition, useI18n } from "./i18n.tsx"; import { addNodeToDraft, + connectNodesInDraft, createDefaultWorkflowDraft, getNodeRuntimeConfig, removeNodeFromDraft, + setNodePosition, resolveDefinitionIdForNode, serializeWorkflowDraft, + setViewportInDraft, setNodeRuntimeConfig, workflowDraftFromVersion, type WorkflowDraft, @@ -26,18 +42,36 @@ type AppProps = { apiBaseUrl: string; }; -function formatTaskSummary(task: any) { +function translateStatus(status: string | undefined, t: ReturnType["t"]) { + switch (status) { + case "success": + return t("success"); + case "failed": + return t("failed"); + case "running": + return t("running"); + case "queued": + case "pending": + return t("queued"); + case "cancelled": + return t("cancelled"); + default: + return status ?? "unknown"; + } +} + +function formatTaskSummary(task: any, t: ReturnType["t"]) { if (task?.summary?.errorMessage) { return task.summary.errorMessage; } - const outcome = task?.summary?.outcome ?? task?.status ?? "unknown"; + const outcome = translateStatus(task?.summary?.outcome ?? task?.status, t); const executor = task?.summary?.executorType ?? task?.executorType ?? "unknown"; const assetCount = task?.summary?.assetCount ?? task?.assetIds?.length ?? 0; const artifactCount = task?.summary?.artifactIds?.length ?? task?.outputArtifactIds?.length ?? 0; - return `${outcome} via ${executor}; assets ${assetCount}; artifacts ${artifactCount}`; + return `${t("viaExecutor", { outcome, executor })}; ${t("assetCount", { count: assetCount })}; ${t("artifactCount", { count: artifactCount })}`; } -function formatRunSummary(run: any) { +function formatRunSummary(run: any, t: ReturnType["t"]) { const totalTaskCount = run?.summary?.totalTaskCount ?? 0; const successCount = run?.summary?.taskCounts?.success ?? 0; const failedCount = run?.summary?.taskCounts?.failed ?? 0; @@ -45,7 +79,15 @@ function formatRunSummary(run: any) { const cancelledCount = run?.summary?.taskCounts?.cancelled ?? 0; const stdoutLineCount = run?.summary?.stdoutLineCount ?? 0; const stderrLineCount = run?.summary?.stderrLineCount ?? 0; - return `${successCount} success, ${failedCount} failed, ${runningCount} running, ${cancelledCount} cancelled, ${stdoutLineCount} stdout lines, ${stderrLineCount} stderr lines, ${totalTaskCount} total tasks`; + return [ + t("successCount", { count: successCount }), + t("failedCount", { count: failedCount }), + t("runningCount", { count: runningCount }), + t("cancelledCount", { count: cancelledCount }), + t("stdoutLines", { count: stdoutLineCount }), + t("stderrLines", { count: stderrLineCount }), + t("totalTasks", { count: totalTaskCount }), + ].join(", "); } function formatExecutorConfigLabel(config?: Record) { @@ -75,13 +117,14 @@ function AppShell(props: { active: NavItem; children: React.ReactNode; }) { - const navItems: Array<{ label: NavItem; href: string }> = [ - { label: "Assets", href: "/assets" }, - { label: "Workflows", href: "/workflows" }, - { label: "Runs", href: "/runs" }, - { label: "Explore", href: "/explore" }, - { label: "Labels", href: "/labels" }, - { label: "Admin", href: "/admin" }, + const { language, setLanguage, t } = useI18n(); + const navItems: Array<{ label: NavItem; href: string; key: "navAssets" | "navWorkflows" | "navRuns" | "navExplore" | "navLabels" | "navAdmin" }> = [ + { label: "Assets", href: "/assets", key: "navAssets" }, + { label: "Workflows", href: "/workflows", key: "navWorkflows" }, + { label: "Runs", href: "/runs", key: "navRuns" }, + { label: "Explore", href: "/explore", key: "navExplore" }, + { label: "Labels", href: "/labels", key: "navLabels" }, + { label: "Admin", href: "/admin", key: "navAdmin" }, ]; return ( @@ -89,17 +132,35 @@ function AppShell(props: {
- Workspace + {t("workspace")} {props.workspaceName}
- Project + {t("project")} {props.projectName}
- Runs - Local Dev +
+ + +
+ {t("runs")} + {t("localDev")}