@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,133 @@
|
|
|
1
|
+
import { genNodeId } from './types.js';
|
|
2
|
+
export const createWorkflowSlice = (set, get) => ({
|
|
3
|
+
nodes: [],
|
|
4
|
+
selectedNodeId: null,
|
|
5
|
+
addNode(capability) {
|
|
6
|
+
const node = {
|
|
7
|
+
id: genNodeId(),
|
|
8
|
+
capability,
|
|
9
|
+
params: {},
|
|
10
|
+
status: 'idle',
|
|
11
|
+
};
|
|
12
|
+
set((state) => ({
|
|
13
|
+
nodes: [...state.nodes, node],
|
|
14
|
+
selectedNodeId: node.id,
|
|
15
|
+
}));
|
|
16
|
+
},
|
|
17
|
+
updateNodeParams(id, params) {
|
|
18
|
+
set((state) => ({
|
|
19
|
+
nodes: state.nodes.map((n) => (n.id === id ? { ...n, params } : n)),
|
|
20
|
+
}));
|
|
21
|
+
},
|
|
22
|
+
removeNode(id) {
|
|
23
|
+
set((state) => ({
|
|
24
|
+
nodes: state.nodes.filter((n) => n.id !== id),
|
|
25
|
+
selectedNodeId: state.selectedNodeId === id ? null : state.selectedNodeId,
|
|
26
|
+
}));
|
|
27
|
+
},
|
|
28
|
+
selectNode(id) {
|
|
29
|
+
set({ selectedNodeId: id });
|
|
30
|
+
},
|
|
31
|
+
setNodeStatus(id, status, error, duration) {
|
|
32
|
+
set((state) => ({
|
|
33
|
+
nodes: state.nodes.map((n) => n.id === id ? { ...n, status, error, duration } : n),
|
|
34
|
+
}));
|
|
35
|
+
},
|
|
36
|
+
async run() {
|
|
37
|
+
const { runtime, nodes, selectedAssetId } = get();
|
|
38
|
+
if (!runtime)
|
|
39
|
+
throw new Error('Runtime not initialized');
|
|
40
|
+
if (nodes.length === 0)
|
|
41
|
+
throw new Error('Workflow is empty');
|
|
42
|
+
if (!selectedAssetId)
|
|
43
|
+
throw new Error('No asset selected');
|
|
44
|
+
set({ running: true, error: null, statusMessage: 'Running...' });
|
|
45
|
+
// 重置所有节点状态
|
|
46
|
+
set((state) => ({
|
|
47
|
+
nodes: state.nodes.map((n) => ({ ...n, status: 'pending', error: undefined })),
|
|
48
|
+
}));
|
|
49
|
+
try {
|
|
50
|
+
// 构建 Workflow
|
|
51
|
+
const workflow = buildLinearWorkflow(nodes);
|
|
52
|
+
const input = await runtime.getAsset(selectedAssetId);
|
|
53
|
+
set({ statusMessage: 'Running workflow...' });
|
|
54
|
+
const result = await runtime.run(workflow, [input]);
|
|
55
|
+
// 更新节点状态
|
|
56
|
+
if (result.status === 'completed') {
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
get().setNodeStatus(node.id, 'success', undefined, undefined);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (result.status === 'failed') {
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
if (get().nodes.find((n) => n.id === node.id)?.status === 'pending') {
|
|
64
|
+
get().setNodeStatus(node.id, 'failed', result.error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 加载输出资产
|
|
69
|
+
const outputs = [];
|
|
70
|
+
for (const id of result.outputs) {
|
|
71
|
+
try {
|
|
72
|
+
outputs.push(await runtime.getAsset(id));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// skip
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
set({
|
|
79
|
+
running: false,
|
|
80
|
+
statusMessage: `Workflow ${result.status} in ${result.duration}ms`,
|
|
81
|
+
});
|
|
82
|
+
if (result.status === 'failed') {
|
|
83
|
+
set({ error: result.error ?? 'Workflow failed' });
|
|
84
|
+
}
|
|
85
|
+
await get().refreshAssets();
|
|
86
|
+
return outputs;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
set({
|
|
91
|
+
running: false,
|
|
92
|
+
error: message,
|
|
93
|
+
statusMessage: 'Failed',
|
|
94
|
+
});
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
clearWorkflow() {
|
|
99
|
+
set({ nodes: [], selectedNodeId: null });
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
/** 把工作台节点序列构建为线性 Workflow */
|
|
103
|
+
function buildLinearWorkflow(nodes) {
|
|
104
|
+
if (nodes.length === 0) {
|
|
105
|
+
throw new Error('Workflow is empty');
|
|
106
|
+
}
|
|
107
|
+
const workflowNodes = nodes.map((n) => ({
|
|
108
|
+
id: n.id,
|
|
109
|
+
type: 'transform',
|
|
110
|
+
capability: n.capability,
|
|
111
|
+
params: n.params,
|
|
112
|
+
}));
|
|
113
|
+
const edges = [];
|
|
114
|
+
for (let i = 0; i < workflowNodes.length - 1; i++) {
|
|
115
|
+
edges.push({ from: workflowNodes[i].id, to: workflowNodes[i + 1].id });
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: `wf_${Date.now().toString(36)}`,
|
|
119
|
+
name: 'Workspace Workflow',
|
|
120
|
+
version: '1',
|
|
121
|
+
description: 'Workspace linear workflow',
|
|
122
|
+
author: { id: 'local', name: 'Local User' },
|
|
123
|
+
category: 'image',
|
|
124
|
+
tags: [],
|
|
125
|
+
nodes: workflowNodes,
|
|
126
|
+
edges,
|
|
127
|
+
inputs: { type: 'image', multiple: true },
|
|
128
|
+
outputs: { type: 'image' },
|
|
129
|
+
createdAt: Date.now(),
|
|
130
|
+
updatedAt: Date.now(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=workflow-slice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-slice.js","sourceRoot":"","sources":["../../src/store/workflow-slice.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAevC,MAAM,CAAC,MAAM,mBAAmB,GAK5B,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACjB,KAAK,EAAE,EAAE;IACT,cAAc,EAAE,IAAI;IAEpB,OAAO,CAAC,UAAU;QAChB,MAAM,IAAI,GAAkB;YAC1B,EAAE,EAAE,SAAS,EAAE;YACf,UAAU;YACV,MAAM,EAAE,EAAE;YACV,MAAM,EAAE,MAAM;SACf,CAAC;QACF,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACd,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC;YAC7B,cAAc,EAAE,IAAI,CAAC,EAAE;SACxB,CAAC,CAAC,CAAC;IACN,CAAC;IAED,gBAAgB,CAAC,EAAE,EAAE,MAAM;QACzB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACd,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACpE,CAAC,CAAC,CAAC;IACN,CAAC;IAED,UAAU,CAAC,EAAE;QACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACd,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;YAC7C,cAAc,EAAE,KAAK,CAAC,cAAc,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc;SAC1E,CAAC,CAAC,CAAC;IACN,CAAC;IAED,UAAU,CAAC,EAAE;QACX,GAAG,CAAC,EAAE,cAAc,EAAE,EAAE,EAAE,CAAC,CAAC;IAC9B,CAAC;IAED,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ;QACvC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACd,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAC3B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CACpD;SACF,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,GAAG;QACP,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,GAAG,EAAE,CAAC;QAClD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC7D,IAAI,CAAC,eAAe;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAE3D,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC;QAEjE,WAAW;QACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACd,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;SAC/E,CAAC,CAAC,CAAC;QAEJ,IAAI,CAAC;YACH,cAAc;YACd,MAAM,QAAQ,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YAC5C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;YAEtD,GAAG,CAAC,EAAE,aAAa,EAAE,qBAAqB,EAAE,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;YAEpD,SAAS;YACT,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;gBAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;gBAChE,CAAC;YACH,CAAC;iBAAM,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,IAAI,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;wBACpE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;oBACvD,CAAC;gBACH,CAAC;YACH,CAAC;YAED,SAAS;YACT,MAAM,OAAO,GAAY,EAAE,CAAC;YAC5B,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAChC,IAAI,CAAC;oBACH,OAAO,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC3C,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO;gBACT,CAAC;YACH,CAAC;YAED,GAAG,CAAC;gBACF,OAAO,EAAE,KAAK;gBACd,aAAa,EAAE,YAAY,MAAM,CAAC,MAAM,OAAO,MAAM,CAAC,QAAQ,IAAI;aACnE,CAAC,CAAC;YAEH,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC/B,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,iBAAiB,EAAE,CAAC,CAAC;YACpD,CAAC;YAED,MAAM,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC;YAC5B,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,GAAG,CAAC;gBACF,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE,OAAO;gBACd,aAAa,EAAE,QAAQ;aACxB,CAAC,CAAC;YACH,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,aAAa;QACX,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;CACF,CAAC,CAAC;AAEH,6BAA6B;AAC7B,SAAS,mBAAmB,CAAC,KAAsB;IACjD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,aAAa,GAAmB,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtD,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,WAAoB;QAC1B,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,MAAM,EAAE,CAAC,CAAC,MAAM;KACjB,CAAC,CAAC,CAAC;IACJ,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAE,CAAC,EAAE,EAAE,EAAE,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO;QACL,EAAE,EAAE,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE;QACnC,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,GAAG;QACZ,WAAW,EAAE,2BAA2B;QACxC,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE;QAC3C,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,EAAE;QACR,KAAK,EAAE,aAAa;QACpB,KAAK;QACL,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;QACzC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;QAC1B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACtB,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace UI 类型定义
|
|
3
|
+
*/
|
|
4
|
+
import type { Asset, Capability, WorkflowResult } from '@lokvis/schema';
|
|
5
|
+
/** 工作台事件(用于事件回调) */
|
|
6
|
+
export interface WorkspaceEvent {
|
|
7
|
+
type: 'asset:imported' | 'asset:selected' | 'asset:removed' | 'capability:added' | 'workflow:started' | 'workflow:completed' | 'workflow:failed';
|
|
8
|
+
payload?: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** 节点状态 */
|
|
11
|
+
export type NodeStatus = 'idle' | 'pending' | 'running' | 'success' | 'failed' | 'cancelled';
|
|
12
|
+
/** 已添加到工作台的工作流节点 */
|
|
13
|
+
export interface WorkspaceNode {
|
|
14
|
+
id: string;
|
|
15
|
+
capability: string;
|
|
16
|
+
params: Record<string, unknown>;
|
|
17
|
+
status: NodeStatus;
|
|
18
|
+
/** 上次执行的耗时(毫秒) */
|
|
19
|
+
duration?: number;
|
|
20
|
+
/** 错误信息 */
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/** 工作台运行结果摘要 */
|
|
24
|
+
export interface RunSummary {
|
|
25
|
+
result?: WorkflowResult;
|
|
26
|
+
outputs: Asset[];
|
|
27
|
+
duration: number;
|
|
28
|
+
}
|
|
29
|
+
/** 资产缩略图缓存 */
|
|
30
|
+
export type ThumbnailMap = Record<string, string>;
|
|
31
|
+
/** 能力映射表(按能力名索引) */
|
|
32
|
+
export type CapabilityMap = Record<string, Capability>;
|
|
33
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAExE,oBAAoB;AACpB,MAAM,WAAW,cAAc;IAC7B,IAAI,EACA,gBAAgB,GAChB,gBAAgB,GAChB,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,WAAW;AACX,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE7F,oBAAoB;AACpB,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,UAAU,CAAC;IACnB,kBAAkB;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,gBAAgB;AAChB,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,KAAK,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,cAAc;AACd,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD,oBAAoB;AACpB,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lokvis/ui-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lokvis Workspace UI - React components for Image/Video/PDF Workspace",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"!**/__tests__",
|
|
20
|
+
"!**/*.test.ts",
|
|
21
|
+
"!**/*.test.tsx",
|
|
22
|
+
"!**/*.spec.ts",
|
|
23
|
+
"!**/*.spec.tsx"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^19.0.0",
|
|
27
|
+
"react-dom": "^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"zustand": "^5.0.0",
|
|
31
|
+
"@lokvis/schema": "0.1.0",
|
|
32
|
+
"@lokvis/runtime": "0.1.0",
|
|
33
|
+
"@lokvis/sdk": "0.1.0",
|
|
34
|
+
"@lokvis/ui-core": "0.1.0",
|
|
35
|
+
"@lokvis/capability": "0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^19.0.0",
|
|
39
|
+
"@types/react-dom": "^19.0.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/lokvis/lokvis.git",
|
|
49
|
+
"directory": "packages/ui-react"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p tsconfig.json",
|
|
53
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"clean": "rm -rf dist"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssetPanel - 左侧资产面板
|
|
3
|
+
*
|
|
4
|
+
* 紧凑布局:section header + 上传按钮 + 资产列表。
|
|
5
|
+
* 支持拖拽导入、点击选择、缩略图预览。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { useWorkspaceStore } from '../store/index.js';
|
|
10
|
+
|
|
11
|
+
export interface AssetPanelProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AssetPanel({ className = '' }: AssetPanelProps) {
|
|
16
|
+
const assets = useWorkspaceStore((s) => s.assets);
|
|
17
|
+
const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
|
|
18
|
+
const thumbnails = useWorkspaceStore((s) => s.thumbnails);
|
|
19
|
+
const importFiles = useWorkspaceStore((s) => s.importFiles);
|
|
20
|
+
const selectAsset = useWorkspaceStore((s) => s.selectAsset);
|
|
21
|
+
const removeAsset = useWorkspaceStore((s) => s.removeAsset);
|
|
22
|
+
const setThumbnail = useWorkspaceStore((s) => s.setThumbnail);
|
|
23
|
+
const runtime = useWorkspaceStore((s) => s.runtime);
|
|
24
|
+
|
|
25
|
+
const [dragging, setDragging] = React.useState(false);
|
|
26
|
+
|
|
27
|
+
async function handleFiles(files: FileList | File[] | null) {
|
|
28
|
+
if (!files) return;
|
|
29
|
+
const arr = Array.from(files);
|
|
30
|
+
if (arr.length > 0) await importFiles(arr);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 自动生成缩略图
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!runtime) return;
|
|
36
|
+
for (const asset of assets) {
|
|
37
|
+
if (thumbnails[asset.id]) continue;
|
|
38
|
+
if (asset.type !== 'image') continue;
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const blob = await runtime.exportAsset(asset.id);
|
|
42
|
+
const url = URL.createObjectURL(blob);
|
|
43
|
+
setThumbnail(asset.id, url);
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
}
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
}, [assets, runtime]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<aside
|
|
54
|
+
className={`flex w-56 shrink-0 flex-col border-r border-zinc-200 dark:border-zinc-800 ${className}`}
|
|
55
|
+
onDragOver={(e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
setDragging(true);
|
|
58
|
+
}}
|
|
59
|
+
onDragLeave={() => setDragging(false)}
|
|
60
|
+
onDrop={(e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
setDragging(false);
|
|
63
|
+
void handleFiles(e.dataTransfer.files);
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{/* Header */}
|
|
67
|
+
<div className="flex items-center justify-between px-3 h-10 shrink-0 border-b border-zinc-200 dark:border-zinc-800">
|
|
68
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
|
|
69
|
+
Assets
|
|
70
|
+
</span>
|
|
71
|
+
<span className="text-[11px] tabular-nums text-zinc-400">{assets.length}</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Upload zone */}
|
|
75
|
+
<label
|
|
76
|
+
className={`m-2 block cursor-pointer rounded-lg border border-dashed px-2 py-3 text-center transition-all ${
|
|
77
|
+
dragging
|
|
78
|
+
? 'border-indigo-400 bg-indigo-50 text-indigo-600 dark:bg-indigo-950/30'
|
|
79
|
+
: 'border-zinc-200 text-zinc-400 hover:border-indigo-300 hover:bg-indigo-50/50 hover:text-indigo-500 dark:border-zinc-700 dark:hover:border-indigo-800 dark:hover:bg-indigo-950/20'
|
|
80
|
+
}`}
|
|
81
|
+
>
|
|
82
|
+
<input
|
|
83
|
+
type="file"
|
|
84
|
+
multiple
|
|
85
|
+
className="hidden"
|
|
86
|
+
onChange={(e) => handleFiles(e.target.files)}
|
|
87
|
+
/>
|
|
88
|
+
<svg className="mx-auto h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
89
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
90
|
+
</svg>
|
|
91
|
+
<span className="mt-1 block text-[10px] font-medium">
|
|
92
|
+
{dragging ? 'Drop here' : 'Add files'}
|
|
93
|
+
</span>
|
|
94
|
+
</label>
|
|
95
|
+
|
|
96
|
+
{/* Asset list */}
|
|
97
|
+
<div className="flex-1 overflow-y-auto px-2 pb-2">
|
|
98
|
+
{assets.length === 0 ? (
|
|
99
|
+
<p className="px-1 py-4 text-center text-[10px] text-zinc-400">
|
|
100
|
+
No assets imported
|
|
101
|
+
</p>
|
|
102
|
+
) : (
|
|
103
|
+
<ul className="space-y-1">
|
|
104
|
+
{assets.map((asset) => {
|
|
105
|
+
const selected = asset.id === selectedAssetId;
|
|
106
|
+
const thumb = thumbnails[asset.id];
|
|
107
|
+
return (
|
|
108
|
+
<li
|
|
109
|
+
key={asset.id}
|
|
110
|
+
onClick={() => selectAsset(asset.id)}
|
|
111
|
+
className={`group flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs transition-all ${
|
|
112
|
+
selected
|
|
113
|
+
? 'bg-indigo-50 ring-1 ring-indigo-200 dark:bg-indigo-950/30 dark:ring-indigo-800'
|
|
114
|
+
: 'hover:bg-zinc-50 dark:hover:bg-zinc-800'
|
|
115
|
+
}`}
|
|
116
|
+
>
|
|
117
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded bg-zinc-100 dark:bg-zinc-800">
|
|
118
|
+
{thumb ? (
|
|
119
|
+
<img src={thumb} alt="" className="h-full w-full object-cover" />
|
|
120
|
+
) : (
|
|
121
|
+
<span className="text-[10px] font-medium text-zinc-400">{asset.metadata.format.slice(0, 3).toUpperCase()}</span>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
<div className="min-w-0 flex-1">
|
|
125
|
+
<div className="truncate text-[11px] font-medium">
|
|
126
|
+
{asset.metadata.format}
|
|
127
|
+
</div>
|
|
128
|
+
<div className="text-[10px] text-zinc-400">
|
|
129
|
+
{(asset.metadata.size / 1024).toFixed(1)} KB
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={(e) => {
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
void removeAsset(asset.id);
|
|
137
|
+
}}
|
|
138
|
+
className="shrink-0 rounded p-0.5 text-zinc-300 opacity-0 transition-all hover:bg-red-50 hover:text-red-500 group-hover:opacity-100 dark:text-zinc-600 dark:hover:bg-red-950/30"
|
|
139
|
+
aria-label="Remove asset"
|
|
140
|
+
>
|
|
141
|
+
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
142
|
+
</button>
|
|
143
|
+
</li>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</ul>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</aside>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas - 中央画布(纯预览)
|
|
3
|
+
*
|
|
4
|
+
* 只负责展示当前选中资产的预览,以及空状态引导。
|
|
5
|
+
* 工作流节点链已移至独立的 PipelineBar 组件。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { useWorkspaceStore } from '../store/index.js';
|
|
10
|
+
|
|
11
|
+
export interface CanvasProps {
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Canvas({ className = '' }: CanvasProps) {
|
|
16
|
+
const selectedAssetId = useWorkspaceStore((s) => s.selectedAssetId);
|
|
17
|
+
const assets = useWorkspaceStore((s) => s.assets);
|
|
18
|
+
const thumbnails = useWorkspaceStore((s) => s.thumbnails);
|
|
19
|
+
const importFiles = useWorkspaceStore((s) => s.importFiles);
|
|
20
|
+
|
|
21
|
+
const selected = assets.find((a) => a.id === selectedAssetId);
|
|
22
|
+
const preview = selected ? thumbnails[selected.id] : undefined;
|
|
23
|
+
|
|
24
|
+
// 全局拖拽支持(直接拖到画布区)
|
|
25
|
+
const [dragOver, setDragOver] = React.useState(false);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<main
|
|
29
|
+
className={`relative flex flex-1 flex-col overflow-hidden ${className}`}
|
|
30
|
+
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
|
31
|
+
onDragLeave={() => setDragOver(false)}
|
|
32
|
+
onDrop={(e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setDragOver(false);
|
|
35
|
+
if (e.dataTransfer.files.length > 0) {
|
|
36
|
+
void importFiles(Array.from(e.dataTransfer.files));
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{/* Preview area */}
|
|
41
|
+
<div className="relative flex flex-1 items-center justify-center bg-zinc-50 dark:bg-zinc-950">
|
|
42
|
+
{/* Subtle grid pattern */}
|
|
43
|
+
<div
|
|
44
|
+
className="absolute inset-0 opacity-[0.35]"
|
|
45
|
+
style={{
|
|
46
|
+
backgroundImage: 'radial-gradient(circle, #d4d4d8 1px, transparent 1px)',
|
|
47
|
+
backgroundSize: '20px 20px',
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
{dragOver && (
|
|
52
|
+
<div className="absolute inset-4 z-10 rounded-xl border-2 border-dashed border-indigo-400 bg-indigo-50/80 flex items-center justify-center dark:border-indigo-600 dark:bg-indigo-950/60">
|
|
53
|
+
<div className="text-center">
|
|
54
|
+
<svg className="mx-auto h-8 w-8 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"/></svg>
|
|
55
|
+
<p className="mt-2 text-sm font-medium text-indigo-600 dark:text-indigo-400">Drop files to import</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{selected ? (
|
|
61
|
+
preview ? (
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<img
|
|
64
|
+
src={preview}
|
|
65
|
+
alt={selected.metadata.format}
|
|
66
|
+
className="max-h-[calc(100%-4rem)] max-w-[calc(100%-4rem)] rounded-lg object-contain shadow-2xl shadow-black/10 ring-1 ring-black/5 dark:shadow-black/30"
|
|
67
|
+
/>
|
|
68
|
+
{/* Metadata overlay */}
|
|
69
|
+
<div className="absolute bottom-2 left-2 rounded-lg bg-white/90 px-2.5 py-1 text-[11px] text-zinc-600 shadow-sm backdrop-blur-sm dark:bg-zinc-900/90 dark:text-zinc-300">
|
|
70
|
+
<div className="flex items-center gap-2">
|
|
71
|
+
<span className="font-medium">{selected.metadata.format.toUpperCase()}</span>
|
|
72
|
+
{selected.metadata.dimensions && (
|
|
73
|
+
<>
|
|
74
|
+
<span className="text-zinc-300">·</span>
|
|
75
|
+
<span>{selected.metadata.dimensions.width} x {selected.metadata.dimensions.height}</span>
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
<span className="text-zinc-300">·</span>
|
|
79
|
+
<span>{(selected.metadata.size / 1024).toFixed(1)} KB</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="flex flex-col items-center gap-3">
|
|
85
|
+
<div className="h-28 w-28 animate-pulse rounded-xl bg-zinc-200 dark:bg-zinc-800" />
|
|
86
|
+
<span className="text-xs text-zinc-400">Loading preview...</span>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
) : (
|
|
90
|
+
<div className="relative flex flex-col items-center gap-5 px-6 text-center">
|
|
91
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200/80 dark:bg-zinc-800 dark:ring-zinc-700">
|
|
92
|
+
<svg className="h-9 w-9 text-zinc-300 dark:text-zinc-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
93
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
|
94
|
+
</svg>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<p className="text-sm font-medium text-zinc-500 dark:text-zinc-400">No asset selected</p>
|
|
98
|
+
<p className="mt-1.5 text-xs text-zinc-400 dark:text-zinc-500">
|
|
99
|
+
{assets.length === 0
|
|
100
|
+
? 'Import files to get started, or drag them here'
|
|
101
|
+
: 'Select an asset from the left panel'}
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
{assets.length === 0 && (
|
|
105
|
+
<label className="mt-2 inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-xs font-medium text-white shadow-sm cursor-pointer transition-colors hover:bg-indigo-500">
|
|
106
|
+
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
|
|
107
|
+
Import Files
|
|
108
|
+
<input
|
|
109
|
+
type="file"
|
|
110
|
+
multiple
|
|
111
|
+
className="hidden"
|
|
112
|
+
onChange={(e) => {
|
|
113
|
+
if (e.target.files) void importFiles(Array.from(e.target.files));
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
</label>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</main>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HistoryPanel - 处理历史面板
|
|
3
|
+
*
|
|
4
|
+
* 监听 Runtime 事件,记录最近处理过的资产与工作流执行。
|
|
5
|
+
* MVP 仅做只读展示,撤销/重做通过 Runtime API(已留接口)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import type { LokvisEvent } from '@lokvis/schema';
|
|
10
|
+
import { useWorkspaceStore } from '../store/index.js';
|
|
11
|
+
|
|
12
|
+
interface HistoryItem {
|
|
13
|
+
id: string;
|
|
14
|
+
type: string;
|
|
15
|
+
message: string;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HistoryPanelProps {
|
|
20
|
+
className?: string;
|
|
21
|
+
/** 最大记录数 */
|
|
22
|
+
maxItems?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function HistoryPanel({ className = '', maxItems = 50 }: HistoryPanelProps) {
|
|
26
|
+
const runtime = useWorkspaceStore((s) => s.runtime);
|
|
27
|
+
const [items, setItems] = React.useState<HistoryItem[]>([]);
|
|
28
|
+
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
if (!runtime) return;
|
|
31
|
+
const push = (e: LokvisEvent) => {
|
|
32
|
+
const item: HistoryItem = {
|
|
33
|
+
id: `${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
34
|
+
type: e.type,
|
|
35
|
+
message: formatEvent(e),
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
setItems((prev) => [item, ...prev].slice(0, maxItems));
|
|
39
|
+
};
|
|
40
|
+
// 使用 onAny 订阅所有事件,避免枚举所有类型
|
|
41
|
+
const off = runtime.eventBus.onAny(push);
|
|
42
|
+
return () => {
|
|
43
|
+
off();
|
|
44
|
+
};
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [runtime]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<aside
|
|
50
|
+
className={`flex h-32 flex-col border-t border-zinc-200 bg-zinc-50/50 dark:border-zinc-800 dark:bg-zinc-950/50 ${className}`}
|
|
51
|
+
>
|
|
52
|
+
<div className="flex items-center justify-between border-b border-zinc-200 px-4 py-1.5 dark:border-zinc-800">
|
|
53
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
|
|
54
|
+
History
|
|
55
|
+
</h3>
|
|
56
|
+
{items.length > 0 && (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => setItems([])}
|
|
60
|
+
className="text-xs text-zinc-400 hover:text-zinc-600"
|
|
61
|
+
>
|
|
62
|
+
Clear
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex-1 overflow-x-auto overflow-y-hidden px-2 py-1.5">
|
|
67
|
+
{items.length === 0 ? (
|
|
68
|
+
<p className="px-2 text-xs text-zinc-400">No history yet</p>
|
|
69
|
+
) : (
|
|
70
|
+
<ul className="flex h-full items-stretch gap-1.5">
|
|
71
|
+
{items.map((item) => (
|
|
72
|
+
<li
|
|
73
|
+
key={item.id}
|
|
74
|
+
className="flex w-56 flex-none flex-col justify-between rounded border border-zinc-200 bg-white px-2 py-1 text-xs dark:border-zinc-800 dark:bg-zinc-900"
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-center justify-between gap-2">
|
|
77
|
+
<span className="font-mono text-[10px] text-zinc-400">
|
|
78
|
+
{new Date(item.timestamp).toLocaleTimeString()}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="font-mono text-[10px] text-zinc-400">{item.type}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="truncate text-zinc-600 dark:text-zinc-300" title={item.message}>
|
|
83
|
+
{item.message}
|
|
84
|
+
</div>
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</aside>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatEvent(e: LokvisEvent): string {
|
|
95
|
+
switch (e.type) {
|
|
96
|
+
case 'asset:imported':
|
|
97
|
+
return `Imported ${e.metadata.mimeType}`;
|
|
98
|
+
case 'asset:removed':
|
|
99
|
+
return `Removed asset ${e.assetId.slice(0, 8)}`;
|
|
100
|
+
case 'workflow:started':
|
|
101
|
+
return `Workflow started: ${e.workflowId.slice(0, 8)}`;
|
|
102
|
+
case 'workflow:completed':
|
|
103
|
+
return `Workflow ${e.result.status} in ${e.result.duration}ms`;
|
|
104
|
+
case 'workflow:cancelled':
|
|
105
|
+
return 'Workflow cancelled';
|
|
106
|
+
case 'workflow:paused':
|
|
107
|
+
return 'Workflow paused';
|
|
108
|
+
case 'workflow:resumed':
|
|
109
|
+
return 'Workflow resumed';
|
|
110
|
+
case 'node:started':
|
|
111
|
+
return `Node started: ${e.nodeId.slice(0, 8)}`;
|
|
112
|
+
case 'node:finished':
|
|
113
|
+
return `Node finished: ${e.nodeId.slice(0, 8)} (${e.duration}ms)`;
|
|
114
|
+
case 'node:failed':
|
|
115
|
+
return `Node failed: ${e.error.message}`;
|
|
116
|
+
case 'plugin:loaded':
|
|
117
|
+
return `Plugin loaded: ${e.name}@${e.version}`;
|
|
118
|
+
case 'export:completed':
|
|
119
|
+
return `Exported ${e.format} (${(e.size / 1024).toFixed(1)} KB)`;
|
|
120
|
+
case 'capability:registered':
|
|
121
|
+
return `Capability registered: ${e.capability} (${e.engine})`;
|
|
122
|
+
case 'history:changed':
|
|
123
|
+
return `History changed (${e.entries.length} entries)`;
|
|
124
|
+
default:
|
|
125
|
+
return (e as { type: string }).type;
|
|
126
|
+
}
|
|
127
|
+
}
|