@lokvis/ui-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AssetPanel.d.ts +12 -0
- package/dist/components/AssetPanel.d.ts.map +1 -0
- package/dist/components/AssetPanel.js +69 -0
- package/dist/components/AssetPanel.js.map +1 -0
- package/dist/components/Canvas.d.ts +12 -0
- package/dist/components/Canvas.d.ts.map +1 -0
- package/dist/components/Canvas.js +35 -0
- package/dist/components/Canvas.js.map +1 -0
- package/dist/components/HistoryPanel.d.ts +14 -0
- package/dist/components/HistoryPanel.d.ts.map +1 -0
- package/dist/components/HistoryPanel.js +68 -0
- package/dist/components/HistoryPanel.js.map +1 -0
- package/dist/components/Inspector.d.ts +12 -0
- package/dist/components/Inspector.d.ts.map +1 -0
- package/dist/components/Inspector.js +46 -0
- package/dist/components/Inspector.js.map +1 -0
- package/dist/components/ParamForm.d.ts +14 -0
- package/dist/components/ParamForm.d.ts.map +1 -0
- package/dist/components/ParamForm.js +28 -0
- package/dist/components/ParamForm.js.map +1 -0
- package/dist/components/PipelineBar.d.ts +12 -0
- package/dist/components/PipelineBar.d.ts.map +1 -0
- package/dist/components/PipelineBar.js +44 -0
- package/dist/components/PipelineBar.js.map +1 -0
- package/dist/components/StatusBar.d.ts +10 -0
- package/dist/components/StatusBar.d.ts.map +1 -0
- package/dist/components/StatusBar.js +22 -0
- package/dist/components/StatusBar.js.map +1 -0
- package/dist/components/Toolbar.d.ts +14 -0
- package/dist/components/Toolbar.d.ts.map +1 -0
- package/dist/components/Toolbar.js +36 -0
- package/dist/components/Toolbar.js.map +1 -0
- package/dist/components/Workspace.d.ts +24 -0
- package/dist/components/Workspace.d.ts.map +1 -0
- package/dist/components/Workspace.js +33 -0
- package/dist/components/Workspace.js.map +1 -0
- package/dist/hooks/useLokvis.d.ts +32 -0
- package/dist/hooks/useLokvis.d.ts.map +1 -0
- package/dist/hooks/useLokvis.js +62 -0
- package/dist/hooks/useLokvis.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/store/assets-slice.d.ts +11 -0
- package/dist/store/assets-slice.d.ts.map +1 -0
- package/dist/store/assets-slice.js +39 -0
- package/dist/store/assets-slice.js.map +1 -0
- package/dist/store/capabilities-slice.d.ts +11 -0
- package/dist/store/capabilities-slice.d.ts.map +1 -0
- package/dist/store/capabilities-slice.js +15 -0
- package/dist/store/capabilities-slice.js.map +1 -0
- package/dist/store/index.d.ts +4 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +24 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/runtime-slice.d.ts +13 -0
- package/dist/store/runtime-slice.d.ts.map +1 -0
- package/dist/store/runtime-slice.js +29 -0
- package/dist/store/runtime-slice.js.map +1 -0
- package/dist/store/types.d.ts +77 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +5 -0
- package/dist/store/types.js.map +1 -0
- package/dist/store/workflow-slice.d.ts +18 -0
- package/dist/store/workflow-slice.d.ts.map +1 -0
- package/dist/store/workflow-slice.js +133 -0
- package/dist/store/workflow-slice.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/components/AssetPanel.tsx +151 -0
- package/src/components/Canvas.tsx +123 -0
- package/src/components/HistoryPanel.tsx +127 -0
- package/src/components/Inspector.tsx +158 -0
- package/src/components/ParamForm.tsx +163 -0
- package/src/components/PipelineBar.tsx +125 -0
- package/src/components/StatusBar.tsx +63 -0
- package/src/components/Toolbar.tsx +87 -0
- package/src/components/Workspace.tsx +90 -0
- package/src/hooks/useLokvis.ts +78 -0
- package/src/index.ts +32 -0
- package/src/store/assets-slice.ts +57 -0
- package/src/store/capabilities-slice.ts +29 -0
- package/src/store/index.ts +27 -0
- package/src/store/runtime-slice.ts +53 -0
- package/src/store/types.ts +88 -0
- package/src/store/workflow-slice.ts +178 -0
- package/src/types.ts +46 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace - 完整工作台组件
|
|
3
|
+
*
|
|
4
|
+
* 标准三栏布局:Toolbar → [AssetPanel | Canvas | Inspector] → PipelineBar → StatusBar
|
|
5
|
+
* 通过 useLokvis 自动初始化 Runtime 与插件。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { Workspace } from '@lokvis/ui-react';
|
|
10
|
+
* import imageToolsPlugin from '@lokvis/plugin-image';
|
|
11
|
+
*
|
|
12
|
+
* <Workspace plugins={[imageToolsPlugin()]} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useLokvis, type UseLokvisOptions } from '../hooks/useLokvis.js';
|
|
17
|
+
import { Toolbar } from './Toolbar.js';
|
|
18
|
+
import { AssetPanel } from './AssetPanel.js';
|
|
19
|
+
import { Canvas } from './Canvas.js';
|
|
20
|
+
import { Inspector } from './Inspector.js';
|
|
21
|
+
import { PipelineBar } from './PipelineBar.js';
|
|
22
|
+
import { StatusBar } from './StatusBar.js';
|
|
23
|
+
|
|
24
|
+
export interface WorkspaceProps extends UseLokvisOptions {
|
|
25
|
+
/** 顶部标题 */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** 是否显示状态栏(默认 true) */
|
|
28
|
+
showStatusBar?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Workspace({
|
|
33
|
+
title,
|
|
34
|
+
showStatusBar = true,
|
|
35
|
+
className = '',
|
|
36
|
+
...lokvisOptions
|
|
37
|
+
}: WorkspaceProps) {
|
|
38
|
+
const { status, error } = useLokvis(lokvisOptions);
|
|
39
|
+
|
|
40
|
+
if (status === 'initializing') {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex h-full items-center justify-center">
|
|
43
|
+
<div className="flex flex-col items-center gap-4">
|
|
44
|
+
<div className="relative flex h-12 w-12 items-center justify-center">
|
|
45
|
+
<div className="absolute inset-0 rounded-full border-2 border-indigo-200 dark:border-indigo-800" />
|
|
46
|
+
<div className="h-8 w-8 animate-spin rounded-full border-2 border-transparent border-t-indigo-500" />
|
|
47
|
+
</div>
|
|
48
|
+
<div className="text-center">
|
|
49
|
+
<p className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Initializing Runtime</p>
|
|
50
|
+
<p className="mt-1 text-xs text-zinc-400">Loading plugins and capabilities...</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (status === 'error') {
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex h-full items-center justify-center px-6">
|
|
60
|
+
<div className="max-w-md rounded-xl border border-red-200 bg-red-50 p-6 text-center dark:border-red-800/50 dark:bg-red-950/20">
|
|
61
|
+
<div className="mb-3 flex h-10 w-10 mx-auto items-center justify-center rounded-full bg-red-100 dark:bg-red-900/40">
|
|
62
|
+
<svg className="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
63
|
+
</div>
|
|
64
|
+
<p className="font-semibold text-red-700 dark:text-red-400">Failed to initialize</p>
|
|
65
|
+
<p className="mt-1.5 text-sm text-red-600/80 dark:text-red-400/70">{error}</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={`flex h-full flex-col bg-white dark:bg-zinc-900 ${className}`}>
|
|
73
|
+
{/* Top: Toolbar */}
|
|
74
|
+
<Toolbar title={title} />
|
|
75
|
+
|
|
76
|
+
{/* Middle: Three-panel area */}
|
|
77
|
+
<div className="flex flex-1 overflow-hidden">
|
|
78
|
+
<AssetPanel />
|
|
79
|
+
<Canvas />
|
|
80
|
+
<Inspector />
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Pipeline bar */}
|
|
84
|
+
<PipelineBar />
|
|
85
|
+
|
|
86
|
+
{/* Bottom: StatusBar */}
|
|
87
|
+
{showStatusBar && <StatusBar />}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLokvis - 在 React 中初始化 Lokvis Runtime 并加载插件
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { useLokvis } from '@lokvis/ui-react';
|
|
7
|
+
* import imageToolsPlugin from '@lokvis/plugin-image';
|
|
8
|
+
*
|
|
9
|
+
* function App() {
|
|
10
|
+
* const { runtime, status, error } = useLokvis({
|
|
11
|
+
* plugins: [imageToolsPlugin()],
|
|
12
|
+
* });
|
|
13
|
+
* if (!runtime) return <p>Loading...</p>;
|
|
14
|
+
* return <Workspace />;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { createLokvis } from '@lokvis/sdk';
|
|
21
|
+
import type { LokvisRuntime, RuntimeConfig } from '@lokvis/runtime';
|
|
22
|
+
import type { PluginLoadEntry } from '@lokvis/sdk';
|
|
23
|
+
import { useWorkspaceStore } from '../store/index.js';
|
|
24
|
+
|
|
25
|
+
export interface UseLokvisOptions extends RuntimeConfig {
|
|
26
|
+
/** 预加载的插件列表 */
|
|
27
|
+
plugins?: PluginLoadEntry[];
|
|
28
|
+
/** 是否在挂载时自动初始化(默认 true) */
|
|
29
|
+
autoInit?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UseLokvisResult {
|
|
33
|
+
runtime: LokvisRuntime | null;
|
|
34
|
+
status: 'idle' | 'initializing' | 'ready' | 'error';
|
|
35
|
+
error: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useLokvis(options: UseLokvisOptions = {}): UseLokvisResult {
|
|
39
|
+
const { plugins = [], autoInit = true, ...runtimeConfig } = options;
|
|
40
|
+
const [runtime, setRuntime] = useState<LokvisRuntime | null>(null);
|
|
41
|
+
const [status, setStatus] = useState<UseLokvisResult['status']>('idle');
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const initRef = useRef(false);
|
|
44
|
+
|
|
45
|
+
const storeInit = useWorkspaceStore((s) => s.init);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!autoInit || initRef.current) return;
|
|
49
|
+
initRef.current = true;
|
|
50
|
+
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
(async () => {
|
|
53
|
+
try {
|
|
54
|
+
setStatus('initializing');
|
|
55
|
+
const rt = await createLokvis({
|
|
56
|
+
...runtimeConfig,
|
|
57
|
+
plugins,
|
|
58
|
+
});
|
|
59
|
+
if (cancelled) return;
|
|
60
|
+
setRuntime(rt);
|
|
61
|
+
await storeInit(rt);
|
|
62
|
+
if (cancelled) return;
|
|
63
|
+
setStatus('ready');
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (cancelled) return;
|
|
66
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
67
|
+
setStatus('error');
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return { runtime, status, error };
|
|
78
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lokvis/ui-react
|
|
3
|
+
*
|
|
4
|
+
* Lokvis Workspace UI - React workspace component library.
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* - `<Workspace />` - Ready-to-use local-first workspace
|
|
8
|
+
* - `<AssetPanel />` - Asset panel (import / select / preview)
|
|
9
|
+
* - `<Canvas />` - Canvas preview area
|
|
10
|
+
* - `<Inspector />` - Right panel for capability configuration
|
|
11
|
+
* - `<PipelineBar />` - Workflow pipeline step bar
|
|
12
|
+
* - `<Toolbar />` - Top toolbar
|
|
13
|
+
* - `<StatusBar />` - Bottom status bar
|
|
14
|
+
* - `useLokvis()` - React hook for initializing Runtime + plugins
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { useLokvis, type UseLokvisOptions, type UseLokvisResult } from './hooks/useLokvis.js';
|
|
18
|
+
export { useWorkspaceStore, type WorkspaceState, type WorkspaceActions } from './store/index.js';
|
|
19
|
+
|
|
20
|
+
export { Workspace, type WorkspaceProps } from './components/Workspace.js';
|
|
21
|
+
export { AssetPanel } from './components/AssetPanel.js';
|
|
22
|
+
export { Canvas } from './components/Canvas.js';
|
|
23
|
+
export { Inspector } from './components/Inspector.js';
|
|
24
|
+
export { PipelineBar } from './components/PipelineBar.js';
|
|
25
|
+
export { Toolbar } from './components/Toolbar.js';
|
|
26
|
+
export { StatusBar } from './components/StatusBar.js';
|
|
27
|
+
export { ParamForm, type ParamFormProps } from './components/ParamForm.js';
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
WorkspaceEvent,
|
|
31
|
+
NodeStatus,
|
|
32
|
+
} from './types.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assets slice —— 资产导入 / 选择 / 删除 / 缩略图
|
|
3
|
+
*/
|
|
4
|
+
import type { StateCreator } from 'zustand';
|
|
5
|
+
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
6
|
+
|
|
7
|
+
export interface AssetsSlice
|
|
8
|
+
extends Pick<WorkspaceState, 'assets' | 'selectedAssetId' | 'thumbnails'>,
|
|
9
|
+
Pick<
|
|
10
|
+
WorkspaceActions,
|
|
11
|
+
'refreshAssets' | 'importFiles' | 'selectAsset' | 'setThumbnail' | 'removeAsset'
|
|
12
|
+
> {}
|
|
13
|
+
|
|
14
|
+
export const createAssetsSlice: StateCreator<
|
|
15
|
+
WorkspaceStore,
|
|
16
|
+
[],
|
|
17
|
+
[],
|
|
18
|
+
AssetsSlice
|
|
19
|
+
> = (set, get) => ({
|
|
20
|
+
assets: [],
|
|
21
|
+
selectedAssetId: null,
|
|
22
|
+
thumbnails: {},
|
|
23
|
+
|
|
24
|
+
async refreshAssets() {
|
|
25
|
+
const { runtime } = get();
|
|
26
|
+
if (!runtime) return;
|
|
27
|
+
const assets = await runtime.listAssets();
|
|
28
|
+
set({ assets });
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async importFiles(files) {
|
|
32
|
+
const { runtime } = get();
|
|
33
|
+
if (!runtime) return;
|
|
34
|
+
set({ statusMessage: `Importing ${files.length} file(s)...` });
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
await runtime.importAsset({ kind: 'file', file });
|
|
37
|
+
}
|
|
38
|
+
await get().refreshAssets();
|
|
39
|
+
set({ statusMessage: `Imported ${files.length} file(s)` });
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
selectAsset(id) {
|
|
43
|
+
set({ selectedAssetId: id });
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
setThumbnail(id, url) {
|
|
47
|
+
set((state) => ({ thumbnails: { ...state.thumbnails, [id]: url } }));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async removeAsset(id) {
|
|
51
|
+
const { runtime } = get();
|
|
52
|
+
if (!runtime) return;
|
|
53
|
+
await runtime.removeAsset(id);
|
|
54
|
+
await get().refreshAssets();
|
|
55
|
+
if (get().selectedAssetId === id) set({ selectedAssetId: null });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capabilities slice —— 能力列表与映射
|
|
3
|
+
*/
|
|
4
|
+
import type { StateCreator } from 'zustand';
|
|
5
|
+
import type { CapabilityMap } from '../types.js';
|
|
6
|
+
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
7
|
+
|
|
8
|
+
export interface CapabilitiesSlice
|
|
9
|
+
extends Pick<WorkspaceState, 'capabilities' | 'capabilityMap'>,
|
|
10
|
+
Pick<WorkspaceActions, 'refreshCapabilities'> {}
|
|
11
|
+
|
|
12
|
+
export const createCapabilitiesSlice: StateCreator<
|
|
13
|
+
WorkspaceStore,
|
|
14
|
+
[],
|
|
15
|
+
[],
|
|
16
|
+
CapabilitiesSlice
|
|
17
|
+
> = (set, get) => ({
|
|
18
|
+
capabilities: [],
|
|
19
|
+
capabilityMap: {},
|
|
20
|
+
|
|
21
|
+
async refreshCapabilities() {
|
|
22
|
+
const { runtime } = get();
|
|
23
|
+
if (!runtime) return;
|
|
24
|
+
const capabilities = await runtime.capabilities();
|
|
25
|
+
const capabilityMap: CapabilityMap = {};
|
|
26
|
+
for (const cap of capabilities) capabilityMap[cap.name] = cap;
|
|
27
|
+
set({ capabilities, capabilityMap });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace UI 状态管理(Zustand)
|
|
3
|
+
*
|
|
4
|
+
* 按域拆分为 4 个 slice:
|
|
5
|
+
* - runtime-slice: Runtime 初始化、全局状态消息、错误
|
|
6
|
+
* - assets-slice: 资产导入 / 选择 / 删除 / 缩略图
|
|
7
|
+
* - capabilities-slice: 能力列表与映射
|
|
8
|
+
* - workflow-slice: 工作流节点编排与执行
|
|
9
|
+
*
|
|
10
|
+
* 各 slice 独立演进,W6/W7/W9 扩展时按 slice 添加 action 即可。
|
|
11
|
+
* Runtime 永远不知道 React,但 UI 层负责把 Runtime 事件桥接到 store。
|
|
12
|
+
*/
|
|
13
|
+
import { create } from 'zustand';
|
|
14
|
+
import type { WorkspaceStore } from './types.js';
|
|
15
|
+
import { createRuntimeSlice } from './runtime-slice.js';
|
|
16
|
+
import { createAssetsSlice } from './assets-slice.js';
|
|
17
|
+
import { createCapabilitiesSlice } from './capabilities-slice.js';
|
|
18
|
+
import { createWorkflowSlice } from './workflow-slice.js';
|
|
19
|
+
|
|
20
|
+
export const useWorkspaceStore = create<WorkspaceStore>()((...a) => ({
|
|
21
|
+
...createRuntimeSlice(...a),
|
|
22
|
+
...createAssetsSlice(...a),
|
|
23
|
+
...createCapabilitiesSlice(...a),
|
|
24
|
+
...createWorkflowSlice(...a),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
export type { WorkspaceState, WorkspaceActions, WorkspaceStore } from './types.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime slice —— Runtime 初始化与状态消息
|
|
3
|
+
*
|
|
4
|
+
* 负责 runtime 实例注入、初始化、全局状态消息与错误。
|
|
5
|
+
*/
|
|
6
|
+
import type { StateCreator } from 'zustand';
|
|
7
|
+
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
8
|
+
|
|
9
|
+
export interface RuntimeSlice
|
|
10
|
+
extends Pick<
|
|
11
|
+
WorkspaceState,
|
|
12
|
+
'runtime' | 'initializing' | 'initError' | 'running' | 'statusMessage' | 'error'
|
|
13
|
+
>,
|
|
14
|
+
Pick<
|
|
15
|
+
WorkspaceActions,
|
|
16
|
+
'init' | 'setStatus' | 'setError'
|
|
17
|
+
> {}
|
|
18
|
+
|
|
19
|
+
export const createRuntimeSlice: StateCreator<
|
|
20
|
+
WorkspaceStore,
|
|
21
|
+
[],
|
|
22
|
+
[],
|
|
23
|
+
RuntimeSlice
|
|
24
|
+
> = (set, get) => ({
|
|
25
|
+
runtime: null,
|
|
26
|
+
initializing: false,
|
|
27
|
+
initError: null,
|
|
28
|
+
running: false,
|
|
29
|
+
statusMessage: 'Idle',
|
|
30
|
+
error: null,
|
|
31
|
+
|
|
32
|
+
async init(runtime) {
|
|
33
|
+
set({ runtime, initializing: true, initError: null });
|
|
34
|
+
try {
|
|
35
|
+
await get().refreshAssets();
|
|
36
|
+
await get().refreshCapabilities();
|
|
37
|
+
set({ initializing: false, statusMessage: 'Ready' });
|
|
38
|
+
} catch (err) {
|
|
39
|
+
set({
|
|
40
|
+
initializing: false,
|
|
41
|
+
initError: err instanceof Error ? err.message : String(err),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
setStatus(message) {
|
|
47
|
+
set({ statusMessage: message });
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
setError(error) {
|
|
51
|
+
set({ error });
|
|
52
|
+
},
|
|
53
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Store 类型定义
|
|
3
|
+
*
|
|
4
|
+
* 把原 store.ts 顶部的 WorkspaceState / WorkspaceActions 抽出,
|
|
5
|
+
* 供各 slice 引用,避免循环依赖。
|
|
6
|
+
*/
|
|
7
|
+
import type { Asset, Capability } from '@lokvis/schema';
|
|
8
|
+
import type { LokvisRuntime } from '@lokvis/runtime';
|
|
9
|
+
import type { CapabilityMap, NodeStatus, WorkspaceNode } from '../types.js';
|
|
10
|
+
|
|
11
|
+
/** 工作台完整状态 */
|
|
12
|
+
export interface WorkspaceState {
|
|
13
|
+
/** Runtime 实例 */
|
|
14
|
+
runtime: LokvisRuntime | null;
|
|
15
|
+
/** 是否正在初始化 Runtime */
|
|
16
|
+
initializing: boolean;
|
|
17
|
+
/** 初始化错误 */
|
|
18
|
+
initError: string | null;
|
|
19
|
+
|
|
20
|
+
/** 所有已导入资产 */
|
|
21
|
+
assets: Asset[];
|
|
22
|
+
/** 当前选中的资产 ID */
|
|
23
|
+
selectedAssetId: string | null;
|
|
24
|
+
/** 资产缩略图(AssetId → ObjectURL) */
|
|
25
|
+
thumbnails: Record<string, string>;
|
|
26
|
+
|
|
27
|
+
/** 所有已注册能力 */
|
|
28
|
+
capabilities: Capability[];
|
|
29
|
+
/** 能力映射表(name → Capability) */
|
|
30
|
+
capabilityMap: CapabilityMap;
|
|
31
|
+
|
|
32
|
+
/** 工作流节点序列(线性链) */
|
|
33
|
+
nodes: WorkspaceNode[];
|
|
34
|
+
/** 当前选中的节点 ID */
|
|
35
|
+
selectedNodeId: string | null;
|
|
36
|
+
|
|
37
|
+
/** 是否正在执行工作流 */
|
|
38
|
+
running: boolean;
|
|
39
|
+
/** 全局状态消息 */
|
|
40
|
+
statusMessage: string;
|
|
41
|
+
/** 错误信息 */
|
|
42
|
+
error: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 工作台操作 */
|
|
46
|
+
export interface WorkspaceActions {
|
|
47
|
+
/** 初始化 Runtime */
|
|
48
|
+
init(runtime: LokvisRuntime): Promise<void>;
|
|
49
|
+
/** 刷新资产列表 */
|
|
50
|
+
refreshAssets(): Promise<void>;
|
|
51
|
+
/** 刷新能力列表 */
|
|
52
|
+
refreshCapabilities(): Promise<void>;
|
|
53
|
+
/** 导入文件 */
|
|
54
|
+
importFiles(files: File[]): Promise<void>;
|
|
55
|
+
/** 选择资产 */
|
|
56
|
+
selectAsset(id: string | null): void;
|
|
57
|
+
/** 设置缩略图 */
|
|
58
|
+
setThumbnail(id: string, url: string): void;
|
|
59
|
+
/** 删除资产 */
|
|
60
|
+
removeAsset(id: string): Promise<void>;
|
|
61
|
+
|
|
62
|
+
/** 添加工作流节点 */
|
|
63
|
+
addNode(capability: string): void;
|
|
64
|
+
/** 更新节点参数 */
|
|
65
|
+
updateNodeParams(id: string, params: Record<string, unknown>): void;
|
|
66
|
+
/** 删除节点 */
|
|
67
|
+
removeNode(id: string): void;
|
|
68
|
+
/** 选择节点 */
|
|
69
|
+
selectNode(id: string | null): void;
|
|
70
|
+
/** 设置节点状态 */
|
|
71
|
+
setNodeStatus(id: string, status: NodeStatus, error?: string, duration?: number): void;
|
|
72
|
+
|
|
73
|
+
/** 执行工作流 */
|
|
74
|
+
run(): Promise<Asset[]>;
|
|
75
|
+
/** 设置状态消息 */
|
|
76
|
+
setStatus(message: string): void;
|
|
77
|
+
/** 设置错误 */
|
|
78
|
+
setError(error: string | null): void;
|
|
79
|
+
/** 清空工作流 */
|
|
80
|
+
clearWorkflow(): void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type WorkspaceStore = WorkspaceState & WorkspaceActions;
|
|
84
|
+
|
|
85
|
+
/** 生成节点 ID */
|
|
86
|
+
export function genNodeId(): string {
|
|
87
|
+
return `node_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow slice —— 工作流节点编排与执行
|
|
3
|
+
*
|
|
4
|
+
* 负责:
|
|
5
|
+
* - 节点 CRUD(add / remove / updateParams / select / setNodeStatus)
|
|
6
|
+
* - 工作流执行(run):构建线性 Workflow → 调用 runtime.run → 更新节点状态
|
|
7
|
+
* - 清空(clearWorkflow)
|
|
8
|
+
*
|
|
9
|
+
* run() 内部依赖 buildLinearWorkflow(本地工具)把节点序列构建为 Workflow 定义。
|
|
10
|
+
*/
|
|
11
|
+
import type { StateCreator } from 'zustand';
|
|
12
|
+
import type { Asset, Workflow, WorkflowEdge, WorkflowNode } from '@lokvis/schema';
|
|
13
|
+
import type { WorkspaceNode } from '../types.js';
|
|
14
|
+
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
15
|
+
import { genNodeId } from './types.js';
|
|
16
|
+
|
|
17
|
+
export interface WorkflowSlice
|
|
18
|
+
extends Pick<WorkspaceState, 'nodes' | 'selectedNodeId'>,
|
|
19
|
+
Pick<
|
|
20
|
+
WorkspaceActions,
|
|
21
|
+
| 'addNode'
|
|
22
|
+
| 'updateNodeParams'
|
|
23
|
+
| 'removeNode'
|
|
24
|
+
| 'selectNode'
|
|
25
|
+
| 'setNodeStatus'
|
|
26
|
+
| 'run'
|
|
27
|
+
| 'clearWorkflow'
|
|
28
|
+
> {}
|
|
29
|
+
|
|
30
|
+
export const createWorkflowSlice: StateCreator<
|
|
31
|
+
WorkspaceStore,
|
|
32
|
+
[],
|
|
33
|
+
[],
|
|
34
|
+
WorkflowSlice
|
|
35
|
+
> = (set, get) => ({
|
|
36
|
+
nodes: [],
|
|
37
|
+
selectedNodeId: null,
|
|
38
|
+
|
|
39
|
+
addNode(capability) {
|
|
40
|
+
const node: WorkspaceNode = {
|
|
41
|
+
id: genNodeId(),
|
|
42
|
+
capability,
|
|
43
|
+
params: {},
|
|
44
|
+
status: 'idle',
|
|
45
|
+
};
|
|
46
|
+
set((state) => ({
|
|
47
|
+
nodes: [...state.nodes, node],
|
|
48
|
+
selectedNodeId: node.id,
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
updateNodeParams(id, params) {
|
|
53
|
+
set((state) => ({
|
|
54
|
+
nodes: state.nodes.map((n) => (n.id === id ? { ...n, params } : n)),
|
|
55
|
+
}));
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
removeNode(id) {
|
|
59
|
+
set((state) => ({
|
|
60
|
+
nodes: state.nodes.filter((n) => n.id !== id),
|
|
61
|
+
selectedNodeId: state.selectedNodeId === id ? null : state.selectedNodeId,
|
|
62
|
+
}));
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
selectNode(id) {
|
|
66
|
+
set({ selectedNodeId: id });
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
setNodeStatus(id, status, error, duration) {
|
|
70
|
+
set((state) => ({
|
|
71
|
+
nodes: state.nodes.map((n) =>
|
|
72
|
+
n.id === id ? { ...n, status, error, duration } : n
|
|
73
|
+
),
|
|
74
|
+
}));
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async run() {
|
|
78
|
+
const { runtime, nodes, selectedAssetId } = get();
|
|
79
|
+
if (!runtime) throw new Error('Runtime not initialized');
|
|
80
|
+
if (nodes.length === 0) throw new Error('Workflow is empty');
|
|
81
|
+
if (!selectedAssetId) throw new Error('No asset selected');
|
|
82
|
+
|
|
83
|
+
set({ running: true, error: null, statusMessage: 'Running...' });
|
|
84
|
+
|
|
85
|
+
// 重置所有节点状态
|
|
86
|
+
set((state) => ({
|
|
87
|
+
nodes: state.nodes.map((n) => ({ ...n, status: 'pending', error: undefined })),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// 构建 Workflow
|
|
92
|
+
const workflow = buildLinearWorkflow(nodes);
|
|
93
|
+
const input = await runtime.getAsset(selectedAssetId);
|
|
94
|
+
|
|
95
|
+
set({ statusMessage: 'Running workflow...' });
|
|
96
|
+
const result = await runtime.run(workflow, [input]);
|
|
97
|
+
|
|
98
|
+
// 更新节点状态
|
|
99
|
+
if (result.status === 'completed') {
|
|
100
|
+
for (const node of nodes) {
|
|
101
|
+
get().setNodeStatus(node.id, 'success', undefined, undefined);
|
|
102
|
+
}
|
|
103
|
+
} else if (result.status === 'failed') {
|
|
104
|
+
for (const node of nodes) {
|
|
105
|
+
if (get().nodes.find((n) => n.id === node.id)?.status === 'pending') {
|
|
106
|
+
get().setNodeStatus(node.id, 'failed', result.error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 加载输出资产
|
|
112
|
+
const outputs: Asset[] = [];
|
|
113
|
+
for (const id of result.outputs) {
|
|
114
|
+
try {
|
|
115
|
+
outputs.push(await runtime.getAsset(id));
|
|
116
|
+
} catch {
|
|
117
|
+
// skip
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
set({
|
|
122
|
+
running: false,
|
|
123
|
+
statusMessage: `Workflow ${result.status} in ${result.duration}ms`,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.status === 'failed') {
|
|
127
|
+
set({ error: result.error ?? 'Workflow failed' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await get().refreshAssets();
|
|
131
|
+
return outputs;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
134
|
+
set({
|
|
135
|
+
running: false,
|
|
136
|
+
error: message,
|
|
137
|
+
statusMessage: 'Failed',
|
|
138
|
+
});
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
clearWorkflow() {
|
|
144
|
+
set({ nodes: [], selectedNodeId: null });
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** 把工作台节点序列构建为线性 Workflow */
|
|
149
|
+
function buildLinearWorkflow(nodes: WorkspaceNode[]): Workflow {
|
|
150
|
+
if (nodes.length === 0) {
|
|
151
|
+
throw new Error('Workflow is empty');
|
|
152
|
+
}
|
|
153
|
+
const workflowNodes: WorkflowNode[] = nodes.map((n) => ({
|
|
154
|
+
id: n.id,
|
|
155
|
+
type: 'transform' as const,
|
|
156
|
+
capability: n.capability,
|
|
157
|
+
params: n.params,
|
|
158
|
+
}));
|
|
159
|
+
const edges: WorkflowEdge[] = [];
|
|
160
|
+
for (let i = 0; i < workflowNodes.length - 1; i++) {
|
|
161
|
+
edges.push({ from: workflowNodes[i]!.id, to: workflowNodes[i + 1]!.id });
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id: `wf_${Date.now().toString(36)}`,
|
|
165
|
+
name: 'Workspace Workflow',
|
|
166
|
+
version: '1',
|
|
167
|
+
description: 'Workspace linear workflow',
|
|
168
|
+
author: { id: 'local', name: 'Local User' },
|
|
169
|
+
category: 'image',
|
|
170
|
+
tags: [],
|
|
171
|
+
nodes: workflowNodes,
|
|
172
|
+
edges,
|
|
173
|
+
inputs: { type: 'image', multiple: true },
|
|
174
|
+
outputs: { type: 'image' },
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
updatedAt: Date.now(),
|
|
177
|
+
};
|
|
178
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace UI 类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Asset, Capability, WorkflowResult } from '@lokvis/schema';
|
|
6
|
+
|
|
7
|
+
/** 工作台事件(用于事件回调) */
|
|
8
|
+
export interface WorkspaceEvent {
|
|
9
|
+
type:
|
|
10
|
+
| 'asset:imported'
|
|
11
|
+
| 'asset:selected'
|
|
12
|
+
| 'asset:removed'
|
|
13
|
+
| 'capability:added'
|
|
14
|
+
| 'workflow:started'
|
|
15
|
+
| 'workflow:completed'
|
|
16
|
+
| 'workflow:failed';
|
|
17
|
+
payload?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 节点状态 */
|
|
21
|
+
export type NodeStatus = 'idle' | 'pending' | 'running' | 'success' | 'failed' | 'cancelled';
|
|
22
|
+
|
|
23
|
+
/** 已添加到工作台的工作流节点 */
|
|
24
|
+
export interface WorkspaceNode {
|
|
25
|
+
id: string;
|
|
26
|
+
capability: string;
|
|
27
|
+
params: Record<string, unknown>;
|
|
28
|
+
status: NodeStatus;
|
|
29
|
+
/** 上次执行的耗时(毫秒) */
|
|
30
|
+
duration?: number;
|
|
31
|
+
/** 错误信息 */
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 工作台运行结果摘要 */
|
|
36
|
+
export interface RunSummary {
|
|
37
|
+
result?: WorkflowResult;
|
|
38
|
+
outputs: Asset[];
|
|
39
|
+
duration: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 资产缩略图缓存 */
|
|
43
|
+
export type ThumbnailMap = Record<string, string>;
|
|
44
|
+
|
|
45
|
+
/** 能力映射表(按能力名索引) */
|
|
46
|
+
export type CapabilityMap = Record<string, Capability>;
|