✨ feat: add i18n and draggable workflow canvas
This commit is contained in:
parent
1cf1f45690
commit
8ff26b3816
2
Makefile
2
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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"test": "tsx --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xyflow/react": "^12.8.4",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
|
||||
@ -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(
|
||||
<React.StrictMode>
|
||||
<I18nProvider>
|
||||
<App apiBaseUrl={apiBaseUrl} />
|
||||
</I18nProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
30
apps/web/src/runtime/i18n.test.ts
Normal file
30
apps/web/src/runtime/i18n.test.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { localizeNodeDefinition, translate } from "./i18n.tsx";
|
||||
|
||||
test("translate returns chinese and english labels for shared frontend keys", () => {
|
||||
assert.equal(translate("en", "navWorkflows"), "Workflows");
|
||||
assert.equal(translate("zh", "navWorkflows"), "工作流");
|
||||
assert.equal(
|
||||
translate("en", "workflowCreatedName", { count: 3 }),
|
||||
"Delivery Normalize 3",
|
||||
);
|
||||
assert.equal(
|
||||
translate("zh", "workflowCreatedName", { count: 3 }),
|
||||
"交付标准化 3",
|
||||
);
|
||||
});
|
||||
|
||||
test("localize built-in node definitions into chinese labels", () => {
|
||||
const localized = localizeNodeDefinition("zh", {
|
||||
id: "validate-structure",
|
||||
name: "Validate Structure",
|
||||
description: "Validate required directories and metadata files.",
|
||||
category: "Inspect",
|
||||
});
|
||||
|
||||
assert.equal(localized.name, "校验目录结构");
|
||||
assert.equal(localized.category, "检查");
|
||||
assert.match(localized.description ?? "", /元数据/);
|
||||
});
|
||||
534
apps/web/src/runtime/i18n.tsx
Normal file
534
apps/web/src/runtime/i18n.tsx
Normal file
@ -0,0 +1,534 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
export type Language = "en" | "zh";
|
||||
|
||||
type TranslationKey =
|
||||
| "workspace"
|
||||
| "project"
|
||||
| "runs"
|
||||
| "localDev"
|
||||
| "navAssets"
|
||||
| "navWorkflows"
|
||||
| "navRuns"
|
||||
| "navExplore"
|
||||
| "navLabels"
|
||||
| "navAdmin"
|
||||
| "language"
|
||||
| "english"
|
||||
| "chinese"
|
||||
| "assetsTitle"
|
||||
| "assetsDescription"
|
||||
| "localPath"
|
||||
| "registerLocalPath"
|
||||
| "noAssetsYet"
|
||||
| "type"
|
||||
| "source"
|
||||
| "detected"
|
||||
| "pending"
|
||||
| "topLevelEntries"
|
||||
| "notAvailable"
|
||||
| "loadingAssetDetail"
|
||||
| "assetId"
|
||||
| "status"
|
||||
| "sourcePath"
|
||||
| "probeAgain"
|
||||
| "createExploreArtifact"
|
||||
| "openExploreView"
|
||||
| "probeSummary"
|
||||
| "warnings"
|
||||
| "none"
|
||||
| "recommendedNodes"
|
||||
| "noProbeReportYet"
|
||||
| "workflowsTitle"
|
||||
| "createWorkflow"
|
||||
| "noWorkflowsYet"
|
||||
| "latestVersion"
|
||||
| "workflowEditor"
|
||||
| "runAsset"
|
||||
| "saveWorkflowVersion"
|
||||
| "triggerWorkflowRun"
|
||||
| "reloadLatestSaved"
|
||||
| "openLatestRun"
|
||||
| "selectAssetBeforeRun"
|
||||
| "nodeLibrary"
|
||||
| "canvas"
|
||||
| "canvasHint"
|
||||
| "latestSavedVersions"
|
||||
| "draftStatus"
|
||||
| "draftSynced"
|
||||
| "draftUnsaved"
|
||||
| "nodeConfiguration"
|
||||
| "category"
|
||||
| "definition"
|
||||
| "executorType"
|
||||
| "runtimeTarget"
|
||||
| "artifactTitle"
|
||||
| "pythonCodeHook"
|
||||
| "nodeNoHook"
|
||||
| "selectNode"
|
||||
| "removeNode"
|
||||
| "workflowCreatedName"
|
||||
| "noAssetsAvailable"
|
||||
| "runsTitle"
|
||||
| "runsDescription"
|
||||
| "noRunsYet"
|
||||
| "createdAt"
|
||||
| "inputAssets"
|
||||
| "runDetail"
|
||||
| "workflow"
|
||||
| "startedAt"
|
||||
| "finishedAt"
|
||||
| "runDuration"
|
||||
| "runSummary"
|
||||
| "cancelRun"
|
||||
| "retryRun"
|
||||
| "runGraph"
|
||||
| "boundAssets"
|
||||
| "selectedTask"
|
||||
| "executor"
|
||||
| "executorConfig"
|
||||
| "codeHook"
|
||||
| "duration"
|
||||
| "summary"
|
||||
| "error"
|
||||
| "retryTask"
|
||||
| "artifacts"
|
||||
| "noTaskArtifactsYet"
|
||||
| "stdout"
|
||||
| "stderr"
|
||||
| "executionLog"
|
||||
| "resultPreview"
|
||||
| "noStdout"
|
||||
| "noStderr"
|
||||
| "noTaskLogs"
|
||||
| "noTasksCreated"
|
||||
| "loadingRun"
|
||||
| "exploreTitle"
|
||||
| "exploreEmpty"
|
||||
| "loadingArtifact"
|
||||
| "bootstrappingLocalWorkspace"
|
||||
| "failedLoadAssets"
|
||||
| "failedRegisterAsset"
|
||||
| "failedLoadWorkflows"
|
||||
| "failedLoadWorkflow"
|
||||
| "failedLoadRuns"
|
||||
| "failedLoadRunDetail"
|
||||
| "failedLoadTaskArtifacts"
|
||||
| "failedCancelRun"
|
||||
| "failedRetryRun"
|
||||
| "failedRetryTask"
|
||||
| "failedLoadArtifact"
|
||||
| "failedBootstrap"
|
||||
| "validatedAssetCount"
|
||||
| "loadedAssetCount"
|
||||
| "success"
|
||||
| "failed"
|
||||
| "running"
|
||||
| "queued"
|
||||
| "cancelled"
|
||||
| "totalTasks"
|
||||
| "stdoutLines"
|
||||
| "stderrLines"
|
||||
| "successCount"
|
||||
| "failedCount"
|
||||
| "runningCount"
|
||||
| "cancelledCount"
|
||||
| "artifactsCount"
|
||||
| "viaExecutor"
|
||||
| "assetCount"
|
||||
| "artifactCount";
|
||||
|
||||
const TRANSLATIONS: Record<Language, Record<TranslationKey, string>> = {
|
||||
en: {
|
||||
workspace: "Workspace",
|
||||
project: "Project",
|
||||
runs: "Runs",
|
||||
localDev: "Local Dev",
|
||||
navAssets: "Assets",
|
||||
navWorkflows: "Workflows",
|
||||
navRuns: "Runs",
|
||||
navExplore: "Explore",
|
||||
navLabels: "Labels",
|
||||
navAdmin: "Admin",
|
||||
language: "Language",
|
||||
english: "English",
|
||||
chinese: "中文",
|
||||
assetsTitle: "Assets",
|
||||
assetsDescription:
|
||||
"Register local folders, archives, or dataset files, then probe them into managed asset metadata.",
|
||||
localPath: "Local Path",
|
||||
registerLocalPath: "Register Local Path",
|
||||
noAssetsYet: "No assets have been registered yet.",
|
||||
type: "Type",
|
||||
source: "Source",
|
||||
detected: "Detected",
|
||||
pending: "pending",
|
||||
topLevelEntries: "Top-level entries",
|
||||
notAvailable: "n/a",
|
||||
loadingAssetDetail: "Loading asset detail...",
|
||||
assetId: "Asset ID",
|
||||
status: "Status",
|
||||
sourcePath: "Source path",
|
||||
probeAgain: "Probe Again",
|
||||
createExploreArtifact: "Create Explore Artifact",
|
||||
openExploreView: "Open Explore View",
|
||||
probeSummary: "Probe Summary",
|
||||
warnings: "Warnings",
|
||||
none: "none",
|
||||
recommendedNodes: "Recommended nodes",
|
||||
noProbeReportYet: "No probe report yet.",
|
||||
workflowsTitle: "Workflows",
|
||||
createWorkflow: "Create Workflow",
|
||||
noWorkflowsYet: "No workflows yet.",
|
||||
latestVersion: "Latest version",
|
||||
workflowEditor: "Workflow Editor",
|
||||
runAsset: "Run Asset",
|
||||
saveWorkflowVersion: "Save Workflow Version",
|
||||
triggerWorkflowRun: "Trigger Workflow Run",
|
||||
reloadLatestSaved: "Reload Latest Saved",
|
||||
openLatestRun: "Open Latest Run",
|
||||
selectAssetBeforeRun: "Select an asset before triggering a workflow run.",
|
||||
nodeLibrary: "Node Library",
|
||||
canvas: "Canvas",
|
||||
canvasHint: "Drag nodes freely, connect handles, zoom, and pan.",
|
||||
latestSavedVersions: "Latest saved versions",
|
||||
draftStatus: "Draft status",
|
||||
draftSynced: "synced",
|
||||
draftUnsaved: "unsaved changes",
|
||||
nodeConfiguration: "Node Configuration",
|
||||
category: "Category",
|
||||
definition: "Definition",
|
||||
executorType: "Executor Type",
|
||||
runtimeTarget: "Runtime Target",
|
||||
artifactTitle: "Artifact Title",
|
||||
pythonCodeHook: "Python Code Hook",
|
||||
nodeNoHook: "This node does not expose a code hook in V1.",
|
||||
selectNode: "Select a node.",
|
||||
removeNode: "Remove Node",
|
||||
workflowCreatedName: "Delivery Normalize {count}",
|
||||
noAssetsAvailable: "No assets available",
|
||||
runsTitle: "Runs",
|
||||
runsDescription: "Recent workflow executions for the current project.",
|
||||
noRunsYet: "No workflow runs yet.",
|
||||
createdAt: "Created at",
|
||||
inputAssets: "Input assets",
|
||||
runDetail: "Run Detail",
|
||||
workflow: "Workflow",
|
||||
startedAt: "Started at",
|
||||
finishedAt: "Finished at",
|
||||
runDuration: "Run duration",
|
||||
runSummary: "Run summary",
|
||||
cancelRun: "Cancel Run",
|
||||
retryRun: "Retry Run",
|
||||
runGraph: "Run Graph",
|
||||
boundAssets: "Bound assets",
|
||||
selectedTask: "Selected Task",
|
||||
executor: "Executor",
|
||||
executorConfig: "Executor config",
|
||||
codeHook: "Code hook",
|
||||
duration: "Duration",
|
||||
summary: "Summary",
|
||||
error: "Error",
|
||||
retryTask: "Retry Task",
|
||||
artifacts: "Artifacts",
|
||||
noTaskArtifactsYet: "No task artifacts yet.",
|
||||
stdout: "Stdout",
|
||||
stderr: "Stderr",
|
||||
executionLog: "Execution Log",
|
||||
resultPreview: "Result Preview",
|
||||
noStdout: "No stdout lines.",
|
||||
noStderr: "No stderr lines.",
|
||||
noTaskLogs: "No task logs yet.",
|
||||
noTasksCreated: "No tasks created.",
|
||||
loadingRun: "Loading run...",
|
||||
exploreTitle: "Explore",
|
||||
exploreEmpty: "Create an artifact from asset detail to inspect it here.",
|
||||
loadingArtifact: "Loading artifact...",
|
||||
bootstrappingLocalWorkspace: "Bootstrapping local workspace...",
|
||||
failedLoadAssets: "Failed to load assets",
|
||||
failedRegisterAsset: "Failed to register local asset",
|
||||
failedLoadWorkflows: "Failed to load workflows",
|
||||
failedLoadWorkflow: "Failed to load workflow",
|
||||
failedLoadRuns: "Failed to load runs",
|
||||
failedLoadRunDetail: "Failed to load run detail",
|
||||
failedLoadTaskArtifacts: "Failed to load task artifacts",
|
||||
failedCancelRun: "Failed to cancel run",
|
||||
failedRetryRun: "Failed to retry run",
|
||||
failedRetryTask: "Failed to retry task",
|
||||
failedLoadArtifact: "Failed to load artifact",
|
||||
failedBootstrap: "Failed to bootstrap local context",
|
||||
validatedAssetCount: "validated {count} asset{suffix}",
|
||||
loadedAssetCount: "loaded {count} bound asset{suffix}",
|
||||
success: "success",
|
||||
failed: "failed",
|
||||
running: "running",
|
||||
queued: "queued",
|
||||
cancelled: "cancelled",
|
||||
totalTasks: "{count} total tasks",
|
||||
stdoutLines: "{count} stdout lines",
|
||||
stderrLines: "{count} stderr lines",
|
||||
successCount: "{count} success",
|
||||
failedCount: "{count} failed",
|
||||
runningCount: "{count} running",
|
||||
cancelledCount: "{count} cancelled",
|
||||
artifactsCount: "artifacts {count}",
|
||||
viaExecutor: "{outcome} via {executor}",
|
||||
assetCount: "assets {count}",
|
||||
artifactCount: "artifacts {count}",
|
||||
},
|
||||
zh: {
|
||||
workspace: "工作空间",
|
||||
project: "项目",
|
||||
runs: "运行",
|
||||
localDev: "本地开发",
|
||||
navAssets: "数据资产",
|
||||
navWorkflows: "工作流",
|
||||
navRuns: "运行记录",
|
||||
navExplore: "查看",
|
||||
navLabels: "标注",
|
||||
navAdmin: "管理",
|
||||
language: "语言",
|
||||
english: "English",
|
||||
chinese: "中文",
|
||||
assetsTitle: "数据资产",
|
||||
assetsDescription: "注册本地目录、压缩包或数据集文件,并将其探测为受管资产元数据。",
|
||||
localPath: "本地路径",
|
||||
registerLocalPath: "注册本地路径",
|
||||
noAssetsYet: "还没有注册任何资产。",
|
||||
type: "类型",
|
||||
source: "来源",
|
||||
detected: "识别结果",
|
||||
pending: "待探测",
|
||||
topLevelEntries: "顶层条目",
|
||||
notAvailable: "暂无",
|
||||
loadingAssetDetail: "正在加载资产详情...",
|
||||
assetId: "资产 ID",
|
||||
status: "状态",
|
||||
sourcePath: "源路径",
|
||||
probeAgain: "重新探测",
|
||||
createExploreArtifact: "创建查看产物",
|
||||
openExploreView: "打开查看页",
|
||||
probeSummary: "探测摘要",
|
||||
warnings: "告警",
|
||||
none: "无",
|
||||
recommendedNodes: "推荐节点",
|
||||
noProbeReportYet: "还没有探测报告。",
|
||||
workflowsTitle: "工作流",
|
||||
createWorkflow: "新建工作流",
|
||||
noWorkflowsYet: "还没有工作流。",
|
||||
latestVersion: "最新版本",
|
||||
workflowEditor: "工作流编辑器",
|
||||
runAsset: "运行资产",
|
||||
saveWorkflowVersion: "保存工作流版本",
|
||||
triggerWorkflowRun: "触发工作流运行",
|
||||
reloadLatestSaved: "重新加载最新保存版本",
|
||||
openLatestRun: "打开最新运行",
|
||||
selectAssetBeforeRun: "触发工作流运行前请先选择资产。",
|
||||
nodeLibrary: "节点面板",
|
||||
canvas: "画布",
|
||||
canvasHint: "支持自由拖动节点、拖拽连线、缩放和平移。",
|
||||
latestSavedVersions: "最近保存版本",
|
||||
draftStatus: "草稿状态",
|
||||
draftSynced: "已同步",
|
||||
draftUnsaved: "有未保存修改",
|
||||
nodeConfiguration: "节点配置",
|
||||
category: "分类",
|
||||
definition: "定义",
|
||||
executorType: "执行器类型",
|
||||
runtimeTarget: "运行目标",
|
||||
artifactTitle: "产物标题",
|
||||
pythonCodeHook: "Python 代码钩子",
|
||||
nodeNoHook: "该节点在 V1 中不开放代码钩子。",
|
||||
selectNode: "请选择一个节点。",
|
||||
removeNode: "删除节点",
|
||||
workflowCreatedName: "交付标准化 {count}",
|
||||
noAssetsAvailable: "没有可用资产",
|
||||
runsTitle: "运行记录",
|
||||
runsDescription: "当前项目最近的工作流执行记录。",
|
||||
noRunsYet: "还没有工作流运行记录。",
|
||||
createdAt: "创建时间",
|
||||
inputAssets: "输入资产",
|
||||
runDetail: "运行详情",
|
||||
workflow: "工作流",
|
||||
startedAt: "开始时间",
|
||||
finishedAt: "结束时间",
|
||||
runDuration: "运行时长",
|
||||
runSummary: "运行摘要",
|
||||
cancelRun: "取消运行",
|
||||
retryRun: "重试运行",
|
||||
runGraph: "运行图",
|
||||
boundAssets: "绑定资产",
|
||||
selectedTask: "当前任务",
|
||||
executor: "执行器",
|
||||
executorConfig: "执行器配置",
|
||||
codeHook: "代码钩子",
|
||||
duration: "耗时",
|
||||
summary: "摘要",
|
||||
error: "错误",
|
||||
retryTask: "重试任务",
|
||||
artifacts: "产物",
|
||||
noTaskArtifactsYet: "当前任务还没有产物。",
|
||||
stdout: "标准输出",
|
||||
stderr: "标准错误",
|
||||
executionLog: "执行日志",
|
||||
resultPreview: "结果预览",
|
||||
noStdout: "没有标准输出。",
|
||||
noStderr: "没有标准错误。",
|
||||
noTaskLogs: "当前任务还没有日志。",
|
||||
noTasksCreated: "还没有生成任务。",
|
||||
loadingRun: "正在加载运行...",
|
||||
exploreTitle: "查看",
|
||||
exploreEmpty: "先在资产详情或运行详情里创建产物,再来这里查看。",
|
||||
loadingArtifact: "正在加载产物...",
|
||||
bootstrappingLocalWorkspace: "正在初始化本地工作空间...",
|
||||
failedLoadAssets: "加载资产失败",
|
||||
failedRegisterAsset: "注册本地资产失败",
|
||||
failedLoadWorkflows: "加载工作流失败",
|
||||
failedLoadWorkflow: "加载工作流失败",
|
||||
failedLoadRuns: "加载运行列表失败",
|
||||
failedLoadRunDetail: "加载运行详情失败",
|
||||
failedLoadTaskArtifacts: "加载任务产物失败",
|
||||
failedCancelRun: "取消运行失败",
|
||||
failedRetryRun: "重试运行失败",
|
||||
failedRetryTask: "重试任务失败",
|
||||
failedLoadArtifact: "加载产物失败",
|
||||
failedBootstrap: "初始化本地上下文失败",
|
||||
validatedAssetCount: "已校验 {count} 个资产",
|
||||
loadedAssetCount: "已加载 {count} 个绑定资产",
|
||||
success: "成功",
|
||||
failed: "失败",
|
||||
running: "运行中",
|
||||
queued: "排队中",
|
||||
cancelled: "已取消",
|
||||
totalTasks: "共 {count} 个任务",
|
||||
stdoutLines: "{count} 条标准输出",
|
||||
stderrLines: "{count} 条标准错误",
|
||||
successCount: "{count} 个成功",
|
||||
failedCount: "{count} 个失败",
|
||||
runningCount: "{count} 个运行中",
|
||||
cancelledCount: "{count} 个已取消",
|
||||
artifactsCount: "产物 {count}",
|
||||
viaExecutor: "{outcome},执行器 {executor}",
|
||||
assetCount: "资产 {count}",
|
||||
artifactCount: "产物 {count}",
|
||||
},
|
||||
};
|
||||
|
||||
const BUILTIN_NODE_TRANSLATIONS: Record<string, { en: { name: string; description: string }; zh: { name: string; description: string } }> = {
|
||||
"source-asset": {
|
||||
en: { name: "Source Asset", description: "Load an uploaded asset or registered storage path." },
|
||||
zh: { name: "数据源", description: "加载上传资产或已注册的存储路径。" },
|
||||
},
|
||||
"extract-archive": {
|
||||
en: { name: "Extract Archive", description: "Expand a compressed archive into a managed folder artifact." },
|
||||
zh: { name: "解压归档", description: "将压缩包展开成受管目录产物。" },
|
||||
},
|
||||
"rename-folder": {
|
||||
en: { name: "Rename Delivery Folder", description: "Rename the top-level delivery folder to the business naming convention." },
|
||||
zh: { name: "重命名交付目录", description: "将顶层交付目录重命名为业务命名规范。" },
|
||||
},
|
||||
"validate-structure": {
|
||||
en: { name: "Validate Structure", description: "Validate required directories and metadata files." },
|
||||
zh: { name: "校验目录结构", description: "校验必需目录和元数据文件。" },
|
||||
},
|
||||
"validate-metadata": {
|
||||
en: { name: "Validate Metadata", description: "Validate meta.json, intrinsics.json, and video_meta.json." },
|
||||
zh: { name: "校验元数据", description: "校验 meta.json、intrinsics.json 和 video_meta.json。" },
|
||||
},
|
||||
"export-delivery-package": {
|
||||
en: { name: "Export Delivery Package", description: "Produce the final delivery package artifact for upload." },
|
||||
zh: { name: "导出交付包", description: "生成最终交付包产物用于上传或交付。" },
|
||||
},
|
||||
};
|
||||
|
||||
type InterpolationValues = Record<string, string | number>;
|
||||
|
||||
function interpolate(template: string, values?: InterpolationValues) {
|
||||
if (!values) {
|
||||
return template;
|
||||
}
|
||||
return template.replace(/\{(\w+)\}/gu, (_match, key) => String(values[key] ?? ""));
|
||||
}
|
||||
|
||||
export function translate(
|
||||
language: Language,
|
||||
key: TranslationKey,
|
||||
values?: InterpolationValues,
|
||||
) {
|
||||
return interpolate(TRANSLATIONS[language][key], values);
|
||||
}
|
||||
|
||||
export function getInitialLanguage(): Language {
|
||||
if (typeof window === "undefined") {
|
||||
return "zh";
|
||||
}
|
||||
const stored = window.localStorage.getItem("emboflow-language");
|
||||
if (stored === "en" || stored === "zh") {
|
||||
return stored;
|
||||
}
|
||||
return window.navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en";
|
||||
}
|
||||
|
||||
export function localizeNodeDefinition<T extends { id: string; name?: string; description?: string; category?: string }>(
|
||||
language: Language,
|
||||
definition: T,
|
||||
) {
|
||||
const localized = BUILTIN_NODE_TRANSLATIONS[definition.id];
|
||||
const categoryMap = {
|
||||
Source: language === "zh" ? "数据源" : "Source",
|
||||
Transform: language === "zh" ? "处理" : "Transform",
|
||||
Inspect: language === "zh" ? "检查" : "Inspect",
|
||||
Annotate: language === "zh" ? "标注" : "Annotate",
|
||||
Export: language === "zh" ? "导出" : "Export",
|
||||
Utility: language === "zh" ? "工具" : "Utility",
|
||||
} as const;
|
||||
return {
|
||||
...definition,
|
||||
name: localized ? localized[language].name : definition.name,
|
||||
description: localized ? localized[language].description : definition.description,
|
||||
category:
|
||||
definition.category && definition.category in categoryMap
|
||||
? categoryMap[definition.category as keyof typeof categoryMap]
|
||||
: definition.category,
|
||||
};
|
||||
}
|
||||
|
||||
type I18nContextValue = {
|
||||
language: Language;
|
||||
setLanguage: (language: Language) => void;
|
||||
t: (key: TranslationKey, values?: InterpolationValues) => string;
|
||||
};
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
export function I18nProvider(props: React.PropsWithChildren) {
|
||||
const [language, setLanguage] = useState<Language>(() => getInitialLanguage());
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem("emboflow-language", language);
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
const value = useMemo<I18nContextValue>(
|
||||
() => ({
|
||||
language,
|
||||
setLanguage,
|
||||
t: (key, values) => translate(language, key, values),
|
||||
}),
|
||||
[language],
|
||||
);
|
||||
|
||||
return <I18nContext.Provider value={value}>{props.children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const context = useContext(I18nContext);
|
||||
if (!context) {
|
||||
throw new Error("useI18n must be used within I18nProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -3,11 +3,14 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
addNodeToDraft,
|
||||
connectNodesInDraft,
|
||||
createDefaultWorkflowDraft,
|
||||
getNodeRuntimeConfig,
|
||||
removeNodeFromDraft,
|
||||
serializeWorkflowDraft,
|
||||
setNodePosition,
|
||||
setNodeRuntimeConfig,
|
||||
setViewportInDraft,
|
||||
workflowDraftFromVersion,
|
||||
} from "./workflow-editor-state.ts";
|
||||
|
||||
@ -29,6 +32,7 @@ test("load persisted workflow version into an editable draft", () => {
|
||||
assert.equal(draft.logicGraph.edges.length, 1);
|
||||
assert.equal(draft.runtimeGraph.selectedPreset, "delivery-normalization");
|
||||
assert.deepEqual(draft.pluginRefs, ["builtin:delivery-nodes"]);
|
||||
assert.deepEqual(draft.visualGraph.nodePositions["source-asset"], { x: 120, y: 120 });
|
||||
});
|
||||
|
||||
test("add node appends a unique node id and a sequential edge by default", () => {
|
||||
@ -44,6 +48,10 @@ test("add node appends a unique node id and a sequential edge by default", () =>
|
||||
from: "validate-structure",
|
||||
to: "export-delivery-package-1",
|
||||
});
|
||||
assert.deepEqual(result.draft.visualGraph.nodePositions["export-delivery-package-1"], {
|
||||
x: 140,
|
||||
y: 300,
|
||||
});
|
||||
});
|
||||
|
||||
test("remove node prunes attached edges and serialize emits workflow version payload", () => {
|
||||
@ -71,6 +79,7 @@ test("remove node prunes attached edges and serialize emits workflow version pay
|
||||
assert.equal(next.logicGraph.edges.length, 0);
|
||||
assert.equal(payload.runtimeGraph.selectedPreset, "delivery-normalization");
|
||||
assert.equal(payload.logicGraph.nodes[1]?.id, "validate-structure");
|
||||
assert.equal(payload.visualGraph.nodePositions["rename-folder"], undefined);
|
||||
});
|
||||
|
||||
test("set per-node runtime config and keep it in the serialized workflow payload", () => {
|
||||
@ -102,3 +111,18 @@ test("set per-node runtime config and keep it in the serialized workflow payload
|
||||
"python",
|
||||
);
|
||||
});
|
||||
|
||||
test("update node positions, connect nodes, and persist viewport in the workflow draft", () => {
|
||||
const draft = createDefaultWorkflowDraft();
|
||||
const moved = setNodePosition(draft, "rename-folder", { x: 520, y: 240 });
|
||||
const connected = connectNodesInDraft(moved, "source-asset", "validate-structure");
|
||||
const next = setViewportInDraft(connected, { x: -120, y: 45, zoom: 1.35 });
|
||||
const payload = serializeWorkflowDraft(next);
|
||||
|
||||
assert.deepEqual(payload.visualGraph.nodePositions["rename-folder"], { x: 520, y: 240 });
|
||||
assert.deepEqual(payload.visualGraph.viewport, { x: -120, y: 45, zoom: 1.35 });
|
||||
assert.deepEqual(payload.logicGraph.edges.at(-1), {
|
||||
from: "source-asset",
|
||||
to: "validate-structure",
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,6 +8,20 @@ export type WorkflowLogicEdge = {
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type WorkflowPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type WorkflowViewport = WorkflowPoint & {
|
||||
zoom: number;
|
||||
};
|
||||
|
||||
export type WorkflowVisualGraph = {
|
||||
viewport: WorkflowViewport;
|
||||
nodePositions: Record<string, WorkflowPoint>;
|
||||
};
|
||||
|
||||
export type WorkflowNodeDefinitionSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -30,7 +44,7 @@ export type WorkflowNodeRuntimeConfig = {
|
||||
};
|
||||
|
||||
export type WorkflowDraft = {
|
||||
visualGraph: Record<string, unknown>;
|
||||
visualGraph: WorkflowVisualGraph;
|
||||
logicGraph: {
|
||||
nodes: WorkflowLogicNode[];
|
||||
edges: WorkflowLogicEdge[];
|
||||
@ -45,9 +59,44 @@ export type WorkflowDraft = {
|
||||
|
||||
type WorkflowVersionLike = Partial<WorkflowDraft>;
|
||||
|
||||
const DEFAULT_VIEWPORT: WorkflowViewport = { x: 0, y: 0, zoom: 1 };
|
||||
const DEFAULT_NODE_LAYOUT: Record<string, WorkflowPoint> = {
|
||||
"source-asset": { x: 120, y: 120 },
|
||||
"rename-folder": { x: 430, y: 280 },
|
||||
"validate-structure": { x: 760, y: 450 },
|
||||
};
|
||||
|
||||
function createDefaultNodePosition(index: number): WorkflowPoint {
|
||||
const column = index % 3;
|
||||
const row = Math.floor(index / 3);
|
||||
return {
|
||||
x: 140 + column * 280,
|
||||
y: 120 + row * 180,
|
||||
};
|
||||
}
|
||||
|
||||
function createVisualGraph(input?: Partial<WorkflowVisualGraph>, nodes: WorkflowLogicNode[] = []): WorkflowVisualGraph {
|
||||
const nodePositions = { ...(input?.nodePositions ?? {}) };
|
||||
nodes.forEach((node, index) => {
|
||||
nodePositions[node.id] ??= DEFAULT_NODE_LAYOUT[node.id] ?? createDefaultNodePosition(index);
|
||||
});
|
||||
return {
|
||||
viewport: input?.viewport ? { ...DEFAULT_VIEWPORT, ...input.viewport } : { ...DEFAULT_VIEWPORT },
|
||||
nodePositions,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneDraft(draft: WorkflowDraft): WorkflowDraft {
|
||||
return {
|
||||
visualGraph: { ...draft.visualGraph },
|
||||
visualGraph: {
|
||||
viewport: { ...draft.visualGraph.viewport },
|
||||
nodePositions: Object.fromEntries(
|
||||
Object.entries(draft.visualGraph.nodePositions).map(([nodeId, position]) => [
|
||||
nodeId,
|
||||
{ ...position },
|
||||
]),
|
||||
),
|
||||
},
|
||||
logicGraph: {
|
||||
nodes: draft.logicGraph.nodes.map((node) => ({ ...node })),
|
||||
edges: draft.logicGraph.edges.map((edge) => ({ ...edge })),
|
||||
@ -100,11 +149,7 @@ function inferDefinitionId(nodeId: string): string {
|
||||
}
|
||||
|
||||
export function createDefaultWorkflowDraft(): WorkflowDraft {
|
||||
return {
|
||||
visualGraph: {
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
},
|
||||
logicGraph: {
|
||||
const logicGraph = {
|
||||
nodes: [
|
||||
{ id: "source-asset", type: "source" },
|
||||
{ id: "rename-folder", type: "transform" },
|
||||
@ -114,7 +159,10 @@ export function createDefaultWorkflowDraft(): WorkflowDraft {
|
||||
{ from: "source-asset", to: "rename-folder" },
|
||||
{ from: "rename-folder", to: "validate-structure" },
|
||||
],
|
||||
},
|
||||
};
|
||||
return {
|
||||
visualGraph: createVisualGraph(undefined, logicGraph.nodes),
|
||||
logicGraph,
|
||||
runtimeGraph: {
|
||||
selectedPreset: "delivery-normalization",
|
||||
nodeBindings: {
|
||||
@ -156,7 +204,7 @@ export function workflowDraftFromVersion(version?: WorkflowVersionLike | null):
|
||||
}
|
||||
|
||||
return {
|
||||
visualGraph: { ...(version.visualGraph ?? { viewport: { x: 0, y: 0, zoom: 1 } }) },
|
||||
visualGraph: createVisualGraph(version.visualGraph, version.logicGraph.nodes),
|
||||
logicGraph: {
|
||||
nodes: version.logicGraph.nodes.map((node) => ({ ...node })),
|
||||
edges: (version.logicGraph.edges ?? []).map((edge) => ({ ...edge })),
|
||||
@ -195,6 +243,7 @@ export function addNodeToDraft(
|
||||
next.runtimeGraph.nodeBindings ??= {};
|
||||
next.runtimeGraph.nodeBindings[nodeId] = definition.id;
|
||||
next.runtimeGraph.nodeConfigs ??= {};
|
||||
next.visualGraph.nodePositions[nodeId] = createDefaultNodePosition(next.logicGraph.nodes.length - 1);
|
||||
|
||||
return { draft: next, nodeId };
|
||||
}
|
||||
@ -211,6 +260,7 @@ export function removeNodeFromDraft(draft: WorkflowDraft, nodeId: string): Workf
|
||||
if (next.runtimeGraph.nodeConfigs) {
|
||||
delete next.runtimeGraph.nodeConfigs[nodeId];
|
||||
}
|
||||
delete next.visualGraph.nodePositions[nodeId];
|
||||
return next;
|
||||
}
|
||||
|
||||
@ -237,6 +287,36 @@ export function setNodeRuntimeConfig(
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setNodePosition(draft: WorkflowDraft, nodeId: string, position: WorkflowPoint): WorkflowDraft {
|
||||
const next = cloneDraft(draft);
|
||||
next.visualGraph.nodePositions[nodeId] = { ...position };
|
||||
return next;
|
||||
}
|
||||
|
||||
export function connectNodesInDraft(
|
||||
draft: WorkflowDraft,
|
||||
sourceNodeId: string | null | undefined,
|
||||
targetNodeId: string | null | undefined,
|
||||
): WorkflowDraft {
|
||||
if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) {
|
||||
return draft;
|
||||
}
|
||||
const next = cloneDraft(draft);
|
||||
const exists = next.logicGraph.edges.some(
|
||||
(edge) => edge.from === sourceNodeId && edge.to === targetNodeId,
|
||||
);
|
||||
if (!exists) {
|
||||
next.logicGraph.edges.push({ from: sourceNodeId, to: targetNodeId });
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setViewportInDraft(draft: WorkflowDraft, viewport: WorkflowViewport): WorkflowDraft {
|
||||
const next = cloneDraft(draft);
|
||||
next.visualGraph.viewport = { ...viewport };
|
||||
return next;
|
||||
}
|
||||
|
||||
export function serializeWorkflowDraft(draft: WorkflowDraft): WorkflowDraft {
|
||||
return cloneDraft(draft);
|
||||
}
|
||||
|
||||
@ -50,6 +50,15 @@ textarea {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
padding: 6px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.app-header__pill {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
@ -113,6 +122,7 @@ textarea {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr) 320px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.list-grid {
|
||||
@ -199,6 +209,11 @@ textarea {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.button-secondary[data-active="true"] {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mono-block {
|
||||
border-radius: 12px;
|
||||
background: #0f172a;
|
||||
@ -213,6 +228,106 @@ textarea {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.workflow-canvas-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workflow-canvas-shell {
|
||||
height: 680px;
|
||||
margin-top: 12px;
|
||||
border: 1px solid #d4d4d8;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(14, 165, 233, 0.1), transparent 30%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.workflow-canvas-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
color: #52525b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.workflow-flow-node {
|
||||
min-width: 220px;
|
||||
padding: 0;
|
||||
border: 2px solid #111827;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.workflow-flow-node.selected,
|
||||
.workflow-flow-node:focus-visible {
|
||||
border-color: #0f766e;
|
||||
box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
.workflow-flow-node__body {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 18px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workflow-flow-node__body strong {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workflow-flow-node__body span {
|
||||
font-size: 12px;
|
||||
color: #52525b;
|
||||
}
|
||||
|
||||
.workflow-flow-node--source {
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.workflow-flow-node--transform {
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.workflow-flow-node--inspect {
|
||||
border-color: #047857;
|
||||
}
|
||||
|
||||
.workflow-flow-node--export {
|
||||
border-color: #b45309;
|
||||
}
|
||||
|
||||
.react-flow__panel {
|
||||
margin: 14px;
|
||||
}
|
||||
|
||||
.react-flow__controls {
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.16);
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.react-flow__minimap {
|
||||
border: 1px solid #d4d4d8;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.react-flow__edge-path {
|
||||
stroke: #3f3f46;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
@ -228,4 +343,12 @@ textarea {
|
||||
.two-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.workflow-canvas-shell {
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
.workflow-canvas-footer {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ Recommended global header content:
|
||||
|
||||
- workspace switcher
|
||||
- project switcher
|
||||
- language switcher for Chinese and English
|
||||
- search entry
|
||||
- run notifications
|
||||
- user menu
|
||||
@ -148,6 +149,18 @@ The current V1 implementation is simpler than the target canvas UX, but it alrea
|
||||
- save the current draft as a new workflow version
|
||||
- auto-save a dirty draft before triggering a run
|
||||
|
||||
The current runtime implementation now also renders the center surface as a real node canvas instead of a static placeholder list:
|
||||
|
||||
- free node dragging on the canvas
|
||||
- drag-to-connect edges between node handles
|
||||
- zoom and pan
|
||||
- dotted background grid
|
||||
- mini-map
|
||||
- canvas controls
|
||||
- persisted node positions and viewport in `visualGraph`
|
||||
|
||||
The runtime header also now exposes a visible `中文 / English` language toggle and the main shell plus workflow authoring surface are translated through a lightweight i18n layer.
|
||||
|
||||
### Right Configuration Panel
|
||||
|
||||
The right panel is schema-driven.
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
- `2026-03-27`: The current runtime-config pass freezes per-node executor config into `workflow_runs` and `run_tasks`, exposes runtime editing controls in the React workflow editor, and executes trusted-local Python code hooks from the task snapshot.
|
||||
- `2026-03-27`: The current Docker-runtime pass upgrades `executorType=docker` from a pure stub to a real local container execution path whenever `executorConfig.image` is provided, while retaining a compatibility fallback for older demo tasks without an image.
|
||||
- `2026-03-27`: The current built-in-node pass enriches the worker execution context with bound asset metadata and gives the default Python implementations for `source-asset` and `validate-structure` real delivery-oriented behavior instead of placeholder output.
|
||||
- `2026-03-27`: The current web-authoring pass adds a visible zh/en language switcher, a lightweight i18n layer for the runtime shell, and a real React Flow canvas with persisted node positions and viewport instead of the earlier static node list.
|
||||
|
||||
---
|
||||
|
||||
|
||||
183
pnpm-lock.yaml
generated
183
pnpm-lock.yaml
generated
@ -30,6 +30,9 @@ importers:
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
'@xyflow/react':
|
||||
specifier: ^12.8.4
|
||||
version: 12.10.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@ -330,6 +333,24 @@ packages:
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
'@types/d3-color@3.1.3':
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||
|
||||
'@types/d3-selection@3.0.11':
|
||||
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||
|
||||
'@types/webidl-conversions@7.0.3':
|
||||
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
|
||||
|
||||
@ -352,6 +373,15 @@ packages:
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
|
||||
'@xyflow/react@12.10.1':
|
||||
resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==}
|
||||
peerDependencies:
|
||||
react: '>=17'
|
||||
react-dom: '>=17'
|
||||
|
||||
'@xyflow/system@0.0.75':
|
||||
resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -443,6 +473,9 @@ packages:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
classcat@5.0.5:
|
||||
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
|
||||
@ -466,6 +499,44 @@ packages:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-dispatch@3.0.1:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-transition@3.0.1:
|
||||
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
d3-selection: 2 - 3
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -995,6 +1066,11 @@ packages:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -1057,6 +1133,21 @@ packages:
|
||||
resolution: {integrity: sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
zustand@4.5.7:
|
||||
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16.8'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@emnapi/core@1.9.1':
|
||||
@ -1222,6 +1313,27 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
'@types/d3-drag@3.0.7':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
dependencies:
|
||||
'@types/d3-color': 3.1.3
|
||||
|
||||
'@types/d3-selection@3.0.11': {}
|
||||
|
||||
'@types/d3-transition@3.0.9':
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/d3-zoom@3.0.8':
|
||||
dependencies:
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
|
||||
'@types/webidl-conversions@7.0.3': {}
|
||||
|
||||
'@types/whatwg-url@11.0.5':
|
||||
@ -1237,6 +1349,29 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||
vite: 8.0.3(esbuild@0.27.4)(tsx@4.21.0)
|
||||
|
||||
'@xyflow/react@12.10.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@xyflow/system': 0.0.75
|
||||
classcat: 5.0.5
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
zustand: 4.5.7(react@19.2.4)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- immer
|
||||
|
||||
'@xyflow/system@0.0.75':
|
||||
dependencies:
|
||||
'@types/d3-drag': 3.0.7
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
'@types/d3-transition': 3.0.9
|
||||
'@types/d3-zoom': 3.0.8
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
@ -1316,6 +1451,8 @@ snapshots:
|
||||
|
||||
camelcase@6.3.0: {}
|
||||
|
||||
classcat@5.0.5: {}
|
||||
|
||||
commondir@1.0.1: {}
|
||||
|
||||
content-disposition@1.0.1: {}
|
||||
@ -1331,6 +1468,42 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-dispatch@3.0.1: {}
|
||||
|
||||
d3-drag@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
|
||||
d3-selection@3.0.0: {}
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-dispatch: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
d3-zoom@3.0.0:
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -1910,6 +2083,10 @@ snapshots:
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite@8.0.3(esbuild@0.27.4)(tsx@4.21.0):
|
||||
@ -1937,3 +2114,9 @@ snapshots:
|
||||
dependencies:
|
||||
buffer-crc32: 0.2.13
|
||||
pend: 1.2.0
|
||||
|
||||
zustand@4.5.7(react@19.2.4):
|
||||
dependencies:
|
||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
|
||||
@ -54,6 +54,8 @@ def classify(path_text: str) -> str:
|
||||
lower = path_text.lower()
|
||||
path = Path(path_text)
|
||||
|
||||
if path.name == "Makefile":
|
||||
return "config"
|
||||
if any(token in lower for token in TEST_HINTS):
|
||||
return "tests"
|
||||
if any(token in lower for token in DOC_PATTERNS) or path.suffix == ".md":
|
||||
|
||||
@ -40,6 +40,9 @@ class DocCodeSyncAssessmentTests(unittest.TestCase):
|
||||
def test_classifies_env_example_as_config(self):
|
||||
self.assertEqual(MODULE.classify(".env.example"), "config")
|
||||
|
||||
def test_classifies_makefile_as_config(self):
|
||||
self.assertEqual(MODULE.classify("Makefile"), "config")
|
||||
|
||||
def test_extract_status_paths_preserves_first_character(self):
|
||||
self.assertEqual(
|
||||
MODULE.extract_status_paths([" M apps/api/package.json"]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user