@lokvis/ui-react 0.2.2 → 0.4.1
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.map +1 -1
- package/dist/components/AssetPanel.js +10 -54
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +4 -4
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +4 -3
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +5 -4
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +1 -8
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts +40 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +52 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/ExifPanel.js +1 -1
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +10 -3
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +10 -7
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +8 -5
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +17 -26
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +3 -12
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/StatusDot.d.ts +12 -0
- package/dist/components/StatusDot.d.ts.map +1 -0
- package/dist/components/StatusDot.js +15 -0
- package/dist/components/StatusDot.js.map +1 -0
- package/dist/components/ThemeToggle.js +1 -1
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/ToolRunner.d.ts +53 -0
- package/dist/components/ToolRunner.d.ts.map +1 -0
- package/dist/components/ToolRunner.js +142 -0
- package/dist/components/ToolRunner.js.map +1 -0
- package/dist/components/Toolbar.js +2 -2
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +31 -40
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +15 -5
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +23 -1
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +60 -13
- package/dist/components/Workspace.js.map +1 -1
- package/dist/hooks/useCustomPresets.d.ts +98 -0
- package/dist/hooks/useCustomPresets.d.ts.map +1 -0
- package/dist/hooks/useCustomPresets.js +210 -0
- package/dist/hooks/useCustomPresets.js.map +1 -0
- package/dist/hooks/useLokvis.d.ts +19 -1
- package/dist/hooks/useLokvis.d.ts.map +1 -1
- package/dist/hooks/useLokvis.js +30 -9
- package/dist/hooks/useLokvis.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/store/assets-slice.d.ts +8 -1
- package/dist/store/assets-slice.d.ts.map +1 -1
- package/dist/store/assets-slice.js +101 -54
- package/dist/store/assets-slice.js.map +1 -1
- package/dist/store/capabilities-slice.d.ts +1 -1
- package/dist/store/capabilities-slice.d.ts.map +1 -1
- package/dist/store/capabilities-slice.js +8 -1
- package/dist/store/capabilities-slice.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-slice.d.ts.map +1 -1
- package/dist/store/runtime-slice.js +22 -16
- package/dist/store/runtime-slice.js.map +1 -1
- package/dist/store/subscribe-utils.d.ts +45 -0
- package/dist/store/subscribe-utils.d.ts.map +1 -0
- package/dist/store/subscribe-utils.js +69 -0
- package/dist/store/subscribe-utils.js.map +1 -0
- package/dist/store/types.d.ts +19 -11
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/types.js +0 -10
- package/dist/store/types.js.map +1 -1
- package/dist/store/workflow-slice.d.ts +1 -1
- package/dist/store/workflow-slice.d.ts.map +1 -1
- package/dist/store/workflow-slice.js +19 -51
- package/dist/store/workflow-slice.js.map +1 -1
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +31 -0
- package/dist/utils.js.map +1 -0
- package/package.json +7 -6
- package/src/components/AssetPanel.tsx +18 -58
- package/src/components/Canvas.tsx +11 -7
- package/src/components/CompareSlider.tsx +13 -10
- package/src/components/DownloadPanel.tsx +1 -7
- package/src/components/ErrorBoundary.tsx +101 -0
- package/src/components/ExifPanel.tsx +1 -1
- package/src/components/GlobalDropzone.tsx +8 -3
- package/src/components/Inspector.tsx +24 -16
- package/src/components/ParamForm.tsx +12 -9
- package/src/components/PipelineBar.tsx +20 -27
- package/src/components/StatusBar.tsx +3 -10
- package/src/components/StatusDot.tsx +27 -0
- package/src/components/ThemeToggle.tsx +1 -1
- package/src/components/ToolRunner.tsx +352 -0
- package/src/components/Toolbar.tsx +2 -2
- package/src/components/WorkflowEditor.tsx +50 -43
- package/src/components/WorkflowTemplates.tsx +27 -4
- package/src/components/Workspace.tsx +97 -10
- package/src/hooks/useCustomPresets.ts +284 -0
- package/src/hooks/useLokvis.ts +43 -10
- package/src/index.ts +3 -0
- package/src/store/assets-slice.ts +106 -52
- package/src/store/capabilities-slice.ts +10 -2
- package/src/store/index.ts +1 -1
- package/src/store/runtime-slice.ts +20 -15
- package/src/store/subscribe-utils.ts +82 -0
- package/src/store/types.ts +19 -12
- package/src/store/workflow-slice.ts +19 -53
- package/src/utils.ts +41 -0
- package/dist/store.d.ts +0 -76
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -217
- package/dist/store.js.map +0 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Assets slice —— 资产导入 / 选择 / 删除 / 缩略图
|
|
3
|
+
*
|
|
4
|
+
* TD-5.1 长期方案:缩略图 ObjectURL 的创建 / 替换 / 释放统一在 store 内管理,
|
|
5
|
+
* 消除原先"创建在 AssetPanel effect、释放在 removeAsset action"的跨边界脆弱性。
|
|
6
|
+
* - ensureThumbnails:iterate assets → exportAsset → createObjectURL → setThumbnail
|
|
7
|
+
* - setThumbnail:替换时自动 revoke 旧 ObjectURL
|
|
8
|
+
* - removeAsset:删除时 revoke ObjectURL + 清理 entry
|
|
9
|
+
* inflight 去重通过闭包捕获的 Set(非响应式,不触发额外 re-render)。
|
|
3
10
|
*/
|
|
4
11
|
import type { StateCreator } from 'zustand';
|
|
5
12
|
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
@@ -8,7 +15,12 @@ export interface AssetsSlice
|
|
|
8
15
|
extends Pick<WorkspaceState, 'assets' | 'selectedAssetId' | 'thumbnails'>,
|
|
9
16
|
Pick<
|
|
10
17
|
WorkspaceActions,
|
|
11
|
-
|
|
18
|
+
| 'refreshAssets'
|
|
19
|
+
| 'importFiles'
|
|
20
|
+
| 'selectAsset'
|
|
21
|
+
| 'setThumbnail'
|
|
22
|
+
| 'ensureThumbnails'
|
|
23
|
+
| 'removeAsset'
|
|
12
24
|
> {}
|
|
13
25
|
|
|
14
26
|
export const createAssetsSlice: StateCreator<
|
|
@@ -16,61 +28,103 @@ export const createAssetsSlice: StateCreator<
|
|
|
16
28
|
[],
|
|
17
29
|
[],
|
|
18
30
|
AssetsSlice
|
|
19
|
-
> = (set, get) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
thumbnails: {},
|
|
31
|
+
> = (set, get) => {
|
|
32
|
+
// TD-5.1:per-store inflight 缩略图去重(闭包捕获,非响应式,不触发 re-render)
|
|
33
|
+
const inflightThumbnails = new Set<string>();
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
set({ assets });
|
|
29
|
-
},
|
|
35
|
+
return {
|
|
36
|
+
assets: [],
|
|
37
|
+
selectedAssetId: null,
|
|
38
|
+
thumbnails: {},
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
await get().refreshAssets();
|
|
39
|
-
await get().refreshStorageUsage();
|
|
40
|
-
set({ statusMessage: `Imported ${files.length} file(s)` });
|
|
41
|
-
},
|
|
40
|
+
async refreshAssets() {
|
|
41
|
+
const { runtime } = get();
|
|
42
|
+
if (!runtime) return;
|
|
43
|
+
const assets = await runtime.listAssets();
|
|
44
|
+
set({ assets });
|
|
45
|
+
},
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
async importFiles(files) {
|
|
48
|
+
const { runtime } = get();
|
|
49
|
+
if (!runtime) return;
|
|
50
|
+
set({ statusMessage: `Importing ${files.length} file(s)...` });
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
await runtime.importAsset({ kind: 'file', file });
|
|
53
|
+
}
|
|
54
|
+
await get().refreshAssets();
|
|
55
|
+
await get().refreshStorageUsage();
|
|
56
|
+
set({ statusMessage: `Imported ${files.length} file(s)` });
|
|
57
|
+
},
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await runtime.removeAsset(id);
|
|
55
|
-
await get().refreshAssets();
|
|
56
|
-
await get().refreshStorageUsage();
|
|
57
|
-
if (get().selectedAssetId === id) set({ selectedAssetId: null });
|
|
59
|
+
selectAsset(id) {
|
|
60
|
+
set({ selectedAssetId: id });
|
|
61
|
+
},
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// 同时从 thumbnails map 中删除该 entry,避免 stale ref
|
|
62
|
-
const oldUrl = get().thumbnails[id];
|
|
63
|
-
if (oldUrl) {
|
|
64
|
-
try {
|
|
65
|
-
URL.revokeObjectURL(oldUrl);
|
|
66
|
-
} catch {
|
|
67
|
-
/* URL 已失效或非 ObjectURL,忽略 */
|
|
68
|
-
}
|
|
63
|
+
setThumbnail(id, url) {
|
|
64
|
+
// TD-5.1:替换时 revoke 旧 ObjectURL,集中管理释放,避免孤儿 URL
|
|
69
65
|
set((state) => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
const old = state.thumbnails[id];
|
|
67
|
+
if (old) {
|
|
68
|
+
try {
|
|
69
|
+
URL.revokeObjectURL(old);
|
|
70
|
+
} catch {
|
|
71
|
+
/* URL 已失效或非 ObjectURL,忽略 */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { thumbnails: { ...state.thumbnails, [id]: url } };
|
|
73
75
|
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
ensureThumbnails() {
|
|
79
|
+
const { runtime, assets, thumbnails } = get();
|
|
80
|
+
if (!runtime) return;
|
|
81
|
+
for (const asset of assets) {
|
|
82
|
+
if (thumbnails[asset.id]) continue;
|
|
83
|
+
if (asset.type !== 'image') continue;
|
|
84
|
+
if (inflightThumbnails.has(asset.id)) continue;
|
|
85
|
+
inflightThumbnails.add(asset.id);
|
|
86
|
+
void (async () => {
|
|
87
|
+
try {
|
|
88
|
+
const blob = await runtime.exportAsset(asset.id);
|
|
89
|
+
const url = URL.createObjectURL(blob);
|
|
90
|
+
// 资产可能在 await 期间被删除:若已不在 assets 中,revoke URL 不入 store
|
|
91
|
+
if (!get().assets.some((a) => a.id === asset.id)) {
|
|
92
|
+
URL.revokeObjectURL(url);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
get().setThumbnail(asset.id, url);
|
|
96
|
+
} catch {
|
|
97
|
+
// export 失败,不留缩略图(下次 ensureThumbnails 会重试)
|
|
98
|
+
} finally {
|
|
99
|
+
inflightThumbnails.delete(asset.id);
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async removeAsset(id) {
|
|
106
|
+
const { runtime } = get();
|
|
107
|
+
if (!runtime) return;
|
|
108
|
+
await runtime.removeAsset(id);
|
|
109
|
+
await get().refreshAssets();
|
|
110
|
+
await get().refreshStorageUsage();
|
|
111
|
+
if (get().selectedAssetId === id) set({ selectedAssetId: null });
|
|
112
|
+
|
|
113
|
+
// TD-5.1:删除资产时 revoke 缩略图 ObjectURL + 清理 entry(与 setThumbnail
|
|
114
|
+
// 的 replace-revoke 一起,构成"创建/替换/释放"全在 store 内的闭环)
|
|
115
|
+
const oldUrl = get().thumbnails[id];
|
|
116
|
+
if (oldUrl) {
|
|
117
|
+
try {
|
|
118
|
+
URL.revokeObjectURL(oldUrl);
|
|
119
|
+
} catch {
|
|
120
|
+
/* URL 已失效或非 ObjectURL,忽略 */
|
|
121
|
+
}
|
|
122
|
+
set((state) => {
|
|
123
|
+
const next = { ...state.thumbnails };
|
|
124
|
+
delete next[id];
|
|
125
|
+
return { thumbnails: next };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -6,7 +6,7 @@ import type { CapabilityMap } from '../types.js';
|
|
|
6
6
|
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
7
7
|
|
|
8
8
|
export interface CapabilitiesSlice
|
|
9
|
-
extends Pick<WorkspaceState, 'capabilities' | 'capabilityMap'>,
|
|
9
|
+
extends Pick<WorkspaceState, 'capabilities' | 'capabilityMap' | 'stubCapabilities'>,
|
|
10
10
|
Pick<WorkspaceActions, 'refreshCapabilities'> {}
|
|
11
11
|
|
|
12
12
|
export const createCapabilitiesSlice: StateCreator<
|
|
@@ -17,6 +17,7 @@ export const createCapabilitiesSlice: StateCreator<
|
|
|
17
17
|
> = (set, get) => ({
|
|
18
18
|
capabilities: [],
|
|
19
19
|
capabilityMap: {},
|
|
20
|
+
stubCapabilities: new Set<string>(),
|
|
20
21
|
|
|
21
22
|
async refreshCapabilities() {
|
|
22
23
|
const { runtime } = get();
|
|
@@ -24,6 +25,13 @@ export const createCapabilitiesSlice: StateCreator<
|
|
|
24
25
|
const capabilities = await runtime.capabilities();
|
|
25
26
|
const capabilityMap: CapabilityMap = {};
|
|
26
27
|
for (const cap of capabilities) capabilityMap[cap.name] = cap;
|
|
27
|
-
|
|
28
|
+
// A7: 探测 stub-only 能力,供 UI 显示 "Coming Soon" 标记
|
|
29
|
+
const stubCapabilities = new Set<string>();
|
|
30
|
+
await Promise.all(
|
|
31
|
+
capabilities.map(async (cap) => {
|
|
32
|
+
if (await runtime.isStubOnly(cap.name)) stubCapabilities.add(cap.name);
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
set({ capabilities, capabilityMap, stubCapabilities });
|
|
28
36
|
},
|
|
29
37
|
});
|
package/src/store/index.ts
CHANGED
|
@@ -27,4 +27,4 @@ export const useWorkspaceStore = create<WorkspaceStore>()((...a) => ({
|
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
29
|
export type { WorkspaceState, WorkspaceActions, WorkspaceStore } from './types.js';
|
|
30
|
-
export { MAX_WORKFLOW_STEPS } from '
|
|
30
|
+
export { MAX_WORKFLOW_STEPS } from '@lokvis/schema';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { StateCreator } from 'zustand';
|
|
8
8
|
import type { LokvisEvent } from '@lokvis/schema';
|
|
9
9
|
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
10
|
+
import { reuseSubscription } from './subscribe-utils.js';
|
|
10
11
|
|
|
11
12
|
export interface RuntimeSlice
|
|
12
13
|
extends Pick<
|
|
@@ -38,8 +39,23 @@ export const createRuntimeSlice: StateCreator<
|
|
|
38
39
|
[],
|
|
39
40
|
RuntimeSlice
|
|
40
41
|
> = (set, get) => {
|
|
41
|
-
/**
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* 当前 store 实例的历史订阅(TD-6.2 重入安全订阅)。
|
|
44
|
+
* reuseSubscription 在 subscribe() 时自动清理上一个订阅,避免重复订阅泄漏。
|
|
45
|
+
* 每个闭包持有独立实例,多个 store 实例(测试 / 多 Workspace)互不干扰。
|
|
46
|
+
*/
|
|
47
|
+
const historySub = reuseSubscription(() => {
|
|
48
|
+
const { runtime } = get();
|
|
49
|
+
if (!runtime) return () => {};
|
|
50
|
+
return runtime.eventBus.on('history:changed', (e: LokvisEvent) => {
|
|
51
|
+
if (e.type !== 'history:changed') return;
|
|
52
|
+
set({
|
|
53
|
+
historyEntries: e.entries,
|
|
54
|
+
historyCursor: e.currentIndex,
|
|
55
|
+
historyWorkflowId: e.workflowId,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
43
59
|
|
|
44
60
|
return {
|
|
45
61
|
runtime: null,
|
|
@@ -52,25 +68,14 @@ export const createRuntimeSlice: StateCreator<
|
|
|
52
68
|
storageUsage: null,
|
|
53
69
|
|
|
54
70
|
async init(runtime) {
|
|
55
|
-
// 重入 init 时先清理上一次的历史订阅(避免重复订阅泄漏)
|
|
56
|
-
if (offHistoryChanged) {
|
|
57
|
-
offHistoryChanged();
|
|
58
|
-
offHistoryChanged = null;
|
|
59
|
-
}
|
|
60
71
|
set({ runtime, initializing: true, initError: null });
|
|
61
72
|
try {
|
|
62
73
|
// W7.1: 订阅 history:changed,把 runtime 历史事件桥接到 store,
|
|
63
74
|
// 使 HistoryPanel 无需各自订阅,store 成为历史状态单一来源。
|
|
64
75
|
// workspace 单工作流场景下直接采用事件携带的 workflowId/entries/cursor
|
|
65
76
|
// 覆盖 store,无需额外过滤。
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
set({
|
|
69
|
-
historyEntries: e.entries,
|
|
70
|
-
historyCursor: e.currentIndex,
|
|
71
|
-
historyWorkflowId: e.workflowId,
|
|
72
|
-
});
|
|
73
|
-
});
|
|
77
|
+
// TD-6.2: reuseSubscription 在重入 init 时自动清理上一次订阅
|
|
78
|
+
historySub.subscribe();
|
|
74
79
|
await get().refreshAssets();
|
|
75
80
|
await get().refreshCapabilities();
|
|
76
81
|
await get().refreshStorageUsage();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事件订阅工具(TD-6.1 / TD-6.2 长期方案)
|
|
3
|
+
*
|
|
4
|
+
* 提供两个工具消除 ui-react store 内事件订阅 cleanup 的样板代码:
|
|
5
|
+
* - subscribeAll:多事件一次性订阅 + 统一卸载(TD-6.1)
|
|
6
|
+
* - reuseSubscription:重入安全订阅,新订阅自动清理上一个(TD-6.2)
|
|
7
|
+
*/
|
|
8
|
+
import type { EventBus, LokvisEvent, LokvisEventType } from '@lokvis/schema';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 多事件一次性订阅,返回统一卸载函数。
|
|
12
|
+
*
|
|
13
|
+
* 替代手写多个 eventBus.on(...) + finally 内逐个 off() 的样板。
|
|
14
|
+
* 每个卸载用 try/catch 隔离,单个 off 异常不阻断后续卸载。
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const offAll = subscribeAll(runtime.eventBus, {
|
|
18
|
+
* 'node:started': (e) => get().setNodeStatus(e.nodeId, 'running'),
|
|
19
|
+
* 'node:finished': (e) => get().setNodeStatus(e.nodeId, 'success', undefined, e.duration),
|
|
20
|
+
* 'node:failed': (e) => get().setNodeStatus(e.nodeId, 'failed', errMsg(e.error)),
|
|
21
|
+
* });
|
|
22
|
+
* try { await runtime.run(workflow, [input]); }
|
|
23
|
+
* finally { offAll(); }
|
|
24
|
+
*/
|
|
25
|
+
export function subscribeAll<T extends LokvisEventType>(
|
|
26
|
+
eventBus: EventBus,
|
|
27
|
+
handlers: { [K in T]: (event: Extract<LokvisEvent, { type: K }>) => void }
|
|
28
|
+
): () => void {
|
|
29
|
+
const offs = (Object.keys(handlers) as T[]).map((type) =>
|
|
30
|
+
eventBus.on(type, handlers[type])
|
|
31
|
+
);
|
|
32
|
+
return () => {
|
|
33
|
+
for (const off of offs) {
|
|
34
|
+
try {
|
|
35
|
+
off();
|
|
36
|
+
} catch {
|
|
37
|
+
// 单个 off 异常不阻断后续卸载(与 EventBus.emit 的 handler 隔离策略一致)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 重入安全订阅工厂。
|
|
45
|
+
*
|
|
46
|
+
* 用于"重复调用 init 时需先清理上一次订阅"的场景(TD-6.2)。
|
|
47
|
+
* 持有当前订阅的卸载函数,subscribe() 会先清理上一个再建立新的,dispose() 清理当前。
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const historySub = reuseSubscription(() => runtime.eventBus.on('history:changed', handler));
|
|
51
|
+
* historySub.subscribe(); // 建立订阅
|
|
52
|
+
* historySub.subscribe(); // 自动清理上一个,再建立新的
|
|
53
|
+
* historySub.dispose(); // 清理当前订阅
|
|
54
|
+
*/
|
|
55
|
+
export function reuseSubscription(
|
|
56
|
+
factory: () => () => void
|
|
57
|
+
): { subscribe: () => void; dispose: () => void } {
|
|
58
|
+
let off: (() => void) | null = null;
|
|
59
|
+
return {
|
|
60
|
+
subscribe() {
|
|
61
|
+
if (off) {
|
|
62
|
+
try {
|
|
63
|
+
off();
|
|
64
|
+
} catch {
|
|
65
|
+
// 旧订阅卸载异常不阻断新订阅建立
|
|
66
|
+
}
|
|
67
|
+
off = null;
|
|
68
|
+
}
|
|
69
|
+
off = factory();
|
|
70
|
+
},
|
|
71
|
+
dispose() {
|
|
72
|
+
if (off) {
|
|
73
|
+
try {
|
|
74
|
+
off();
|
|
75
|
+
} catch {
|
|
76
|
+
// 卸载异常不阻断调用方后续逻辑
|
|
77
|
+
}
|
|
78
|
+
off = null;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/store/types.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface WorkspaceState {
|
|
|
28
28
|
capabilities: Capability[];
|
|
29
29
|
/** 能力映射表(name → Capability) */
|
|
30
30
|
capabilityMap: CapabilityMap;
|
|
31
|
+
/**
|
|
32
|
+
* 仅有 stub 实现的能力名集合(A7)。
|
|
33
|
+
* UI 据此为 stub-only 能力显示 "Coming Soon" 标记,
|
|
34
|
+
* 避免用户选择后在工作流执行阶段才收到 stub error。
|
|
35
|
+
*/
|
|
36
|
+
stubCapabilities: Set<string>;
|
|
31
37
|
|
|
32
38
|
/** 工作流节点序列(线性链) */
|
|
33
39
|
nodes: WorkspaceNode[];
|
|
@@ -78,8 +84,20 @@ export interface WorkspaceActions {
|
|
|
78
84
|
importFiles(files: File[]): Promise<void>;
|
|
79
85
|
/** 选择资产 */
|
|
80
86
|
selectAsset(id: string | null): void;
|
|
81
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* 设置缩略图(TD-5.1:替换时自动 revoke 旧 ObjectURL,集中管理生命周期)。
|
|
89
|
+
* 主要由 ensureThumbnails 内部调用,外部一般不直接使用。
|
|
90
|
+
*/
|
|
82
91
|
setThumbnail(id: string, url: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* 为所有缺少缩略图的 image 资产异步生成 ObjectURL 缩略图(TD-5.1 长期方案)。
|
|
94
|
+
*
|
|
95
|
+
* 集中管理缩略图 ObjectURL 的创建:iterate assets → exportAsset →
|
|
96
|
+
* createObjectURL → setThumbnail(revoke 旧 URL)。inflight 去重 + 资产
|
|
97
|
+
* 存在性检查避免孤儿 URL。AssetPanel 只需在 effect 中调用此方法并读取
|
|
98
|
+
* store.thumbnails,创建/替换/释放全部在 store 内统一。
|
|
99
|
+
*/
|
|
100
|
+
ensureThumbnails(): void;
|
|
83
101
|
/** 删除资产 */
|
|
84
102
|
removeAsset(id: string): Promise<void>;
|
|
85
103
|
|
|
@@ -141,17 +159,6 @@ export interface WorkspaceActions {
|
|
|
141
159
|
|
|
142
160
|
export type WorkspaceStore = WorkspaceState & WorkspaceActions;
|
|
143
161
|
|
|
144
|
-
/**
|
|
145
|
-
* 工作流最大节点数(M1 MVP 约束)。
|
|
146
|
-
*
|
|
147
|
-
* UI 层独立定义此常量而非 import runtime 包的 MAX_WORKFLOW_STEPS,
|
|
148
|
-
* 因为 ui-react → runtime 的方向依赖违反五层架构约束(UI → Workflow → Runtime)。
|
|
149
|
-
* 两层各自维护同值常量,通过测试 + 文档约定保持同步
|
|
150
|
-
* (runtime 测试断言 MAX_WORKFLOW_STEPS === 5,UI 层此常量也为 5)。
|
|
151
|
-
* 调整上限时需同步修改两处 + 相关注释。
|
|
152
|
-
*/
|
|
153
|
-
export const MAX_WORKFLOW_STEPS = 5;
|
|
154
|
-
|
|
155
162
|
/** 生成节点 ID */
|
|
156
163
|
export function genNodeId(): string {
|
|
157
164
|
return `node_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
* - 工作流执行(run):构建线性 Workflow → 调用 runtime.run → 更新节点状态
|
|
7
7
|
* - 清空(clearWorkflow)
|
|
8
8
|
*
|
|
9
|
-
* run() 内部依赖 buildLinearWorkflow(
|
|
9
|
+
* run() 内部依赖 buildLinearWorkflow(@lokvis/workflow)把节点序列构建为 Workflow 定义。
|
|
10
10
|
*/
|
|
11
11
|
import type { StateCreator } from 'zustand';
|
|
12
|
-
import
|
|
12
|
+
import { MAX_WORKFLOW_STEPS, type Asset } from '@lokvis/schema';
|
|
13
|
+
import { buildLinearWorkflow } from '@lokvis/workflow';
|
|
13
14
|
import type { WorkspaceNode } from '../types.js';
|
|
14
15
|
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
15
|
-
import {
|
|
16
|
+
import { subscribeAll } from './subscribe-utils.js';
|
|
17
|
+
import { genNodeId } from './types.js';
|
|
16
18
|
|
|
17
19
|
export interface WorkflowSlice
|
|
18
20
|
extends Pick<WorkspaceState, 'nodes' | 'selectedNodeId' | 'lastOutputIds' | 'selectedOutputId' | 'currentRunId'>,
|
|
@@ -46,7 +48,7 @@ export const createWorkflowSlice: StateCreator<
|
|
|
46
48
|
currentRunId: null,
|
|
47
49
|
|
|
48
50
|
addNode(capability) {
|
|
49
|
-
// W10.1/W10.3: 节点数上限(
|
|
51
|
+
// W10.1/W10.3: 节点数上限(MAX_WORKFLOW_STEPS,单一源 @lokvis/schema)
|
|
50
52
|
if (get().nodes.length >= MAX_WORKFLOW_STEPS) {
|
|
51
53
|
get().setError(`工作流最多 ${MAX_WORKFLOW_STEPS} 个节点(M1 MVP 限制),请先删除不需要的节点`);
|
|
52
54
|
return;
|
|
@@ -168,7 +170,7 @@ export const createWorkflowSlice: StateCreator<
|
|
|
168
170
|
},
|
|
169
171
|
|
|
170
172
|
async run() {
|
|
171
|
-
const { runtime, nodes, selectedAssetId, running } = get();
|
|
173
|
+
const { runtime, nodes, selectedAssetId, running, assets } = get();
|
|
172
174
|
if (!runtime) throw new Error('Runtime not initialized');
|
|
173
175
|
if (nodes.length === 0) throw new Error('Workflow is empty');
|
|
174
176
|
if (!selectedAssetId) throw new Error('No asset selected');
|
|
@@ -193,20 +195,21 @@ export const createWorkflowSlice: StateCreator<
|
|
|
193
195
|
//
|
|
194
196
|
// 修复:订阅 node:started / node:finished / node:failed 事件实时更新节点状态,
|
|
195
197
|
// 这样 completed/failed/cancelled 三种情况下节点状态都精确
|
|
196
|
-
const
|
|
197
|
-
get().setNodeStatus(e.nodeId, 'running')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const errMsg = e.error instanceof Error ? e.error.message : String(e.error);
|
|
204
|
-
get().setNodeStatus(e.nodeId, 'failed', errMsg);
|
|
198
|
+
const offAll = subscribeAll(runtime.eventBus, {
|
|
199
|
+
'node:started': (e) => get().setNodeStatus(e.nodeId, 'running'),
|
|
200
|
+
'node:finished': (e) => get().setNodeStatus(e.nodeId, 'success', undefined, e.duration),
|
|
201
|
+
'node:failed': (e) => {
|
|
202
|
+
const errMsg = e.error instanceof Error ? e.error.message : String(e.error);
|
|
203
|
+
get().setNodeStatus(e.nodeId, 'failed', errMsg);
|
|
204
|
+
},
|
|
205
205
|
});
|
|
206
206
|
|
|
207
207
|
try {
|
|
208
|
+
// 从选中资产推导输入类型(去硬编码 'image')
|
|
209
|
+
const selectedAsset = assets.find((a) => a.id === selectedAssetId);
|
|
210
|
+
if (!selectedAsset) throw new Error('Selected asset not found');
|
|
208
211
|
// 构建 Workflow
|
|
209
|
-
const workflow = buildLinearWorkflow(nodes);
|
|
212
|
+
const workflow = buildLinearWorkflow(nodes, selectedAsset.type);
|
|
210
213
|
// W11.6: 记录当前 workflowId 供 cancelRun() 使用
|
|
211
214
|
set({ currentRunId: workflow.id });
|
|
212
215
|
const input = await runtime.getAsset(selectedAssetId);
|
|
@@ -281,9 +284,7 @@ export const createWorkflowSlice: StateCreator<
|
|
|
281
284
|
} finally {
|
|
282
285
|
// W11.6: 清除 currentRunId(成功/失败/取消都应清除)
|
|
283
286
|
set({ currentRunId: null });
|
|
284
|
-
|
|
285
|
-
offFinished();
|
|
286
|
-
offNodeFailed();
|
|
287
|
+
offAll();
|
|
287
288
|
}
|
|
288
289
|
},
|
|
289
290
|
|
|
@@ -299,38 +300,3 @@ export const createWorkflowSlice: StateCreator<
|
|
|
299
300
|
set({ lastOutputIds: [], selectedOutputId: null });
|
|
300
301
|
},
|
|
301
302
|
});
|
|
302
|
-
|
|
303
|
-
/** 把工作台节点序列构建为线性 Workflow */
|
|
304
|
-
function buildLinearWorkflow(nodes: WorkspaceNode[]): Workflow {
|
|
305
|
-
if (nodes.length === 0) {
|
|
306
|
-
throw new Error('Workflow is empty');
|
|
307
|
-
}
|
|
308
|
-
const workflowNodes: WorkflowNode[] = nodes.map((n) => ({
|
|
309
|
-
id: n.id,
|
|
310
|
-
type: 'transform' as const,
|
|
311
|
-
capability: n.capability,
|
|
312
|
-
params: n.params,
|
|
313
|
-
}));
|
|
314
|
-
const edges: WorkflowEdge[] = [];
|
|
315
|
-
for (let i = 0; i < workflowNodes.length - 1; i++) {
|
|
316
|
-
const fromNode = workflowNodes[i];
|
|
317
|
-
const toNode = workflowNodes[i + 1];
|
|
318
|
-
if (!fromNode || !toNode) continue;
|
|
319
|
-
edges.push({ from: fromNode.id, to: toNode.id });
|
|
320
|
-
}
|
|
321
|
-
return {
|
|
322
|
-
id: `wf_${Date.now().toString(36)}`,
|
|
323
|
-
name: 'Workspace Workflow',
|
|
324
|
-
version: '1',
|
|
325
|
-
description: 'Workspace linear workflow',
|
|
326
|
-
author: { id: 'local', name: 'Local User' },
|
|
327
|
-
category: 'image',
|
|
328
|
-
tags: [],
|
|
329
|
-
nodes: workflowNodes,
|
|
330
|
-
edges,
|
|
331
|
-
inputs: { type: 'image', multiple: true },
|
|
332
|
-
outputs: { type: 'image' },
|
|
333
|
-
createdAt: Date.now(),
|
|
334
|
-
updatedAt: Date.now(),
|
|
335
|
-
};
|
|
336
|
-
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-react 共享工具函数
|
|
3
|
+
*
|
|
4
|
+
* 抽取自多个组件的重复逻辑(formatBytes / filterCapabilities),
|
|
5
|
+
* 避免散落在各组件中的同名实现 drift。
|
|
6
|
+
*/
|
|
7
|
+
import type { Capability } from '@lokvis/schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 字节数格式化为人类可读字符串(B / KB / MB / GB)。
|
|
11
|
+
*
|
|
12
|
+
* 统一使用 1024 进制,保留合理小数位:
|
|
13
|
+
* - B:整数
|
|
14
|
+
* - KB:1 位小数
|
|
15
|
+
* - MB:1 位小数
|
|
16
|
+
* - GB:2 位小数
|
|
17
|
+
*/
|
|
18
|
+
export function formatBytes(bytes: number): string {
|
|
19
|
+
if (bytes < 0) return '0 B';
|
|
20
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
21
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
22
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
23
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 按搜索关键词过滤能力列表(匹配 name 或 description,大小写不敏感)。
|
|
28
|
+
*
|
|
29
|
+
* 用于 Inspector 能力列表与 WorkflowEditor InsertConnector 搜索。
|
|
30
|
+
*/
|
|
31
|
+
export function filterCapabilities(
|
|
32
|
+
capabilities: Capability[],
|
|
33
|
+
query: string
|
|
34
|
+
): Capability[] {
|
|
35
|
+
const q = query.toLowerCase();
|
|
36
|
+
return capabilities.filter(
|
|
37
|
+
(c) =>
|
|
38
|
+
c.name.toLowerCase().includes(q) ||
|
|
39
|
+
c.description.toLowerCase().includes(q)
|
|
40
|
+
);
|
|
41
|
+
}
|
package/dist/store.d.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace UI 状态管理
|
|
3
|
-
*
|
|
4
|
-
* 使用 Zustand 管理工作台状态。Runtime 永远不知道 React,
|
|
5
|
-
* 但 UI 层负责把 Runtime 事件桥接到 store。
|
|
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
|
-
export interface WorkspaceState {
|
|
12
|
-
/** Runtime 实例 */
|
|
13
|
-
runtime: LokvisRuntime | null;
|
|
14
|
-
/** 是否正在初始化 Runtime */
|
|
15
|
-
initializing: boolean;
|
|
16
|
-
/** 初始化错误 */
|
|
17
|
-
initError: string | null;
|
|
18
|
-
/** 所有已导入资产 */
|
|
19
|
-
assets: Asset[];
|
|
20
|
-
/** 当前选中的资产 ID */
|
|
21
|
-
selectedAssetId: string | null;
|
|
22
|
-
/** 资产缩略图(AssetId → ObjectURL) */
|
|
23
|
-
thumbnails: Record<string, string>;
|
|
24
|
-
/** 所有已注册能力 */
|
|
25
|
-
capabilities: Capability[];
|
|
26
|
-
/** 能力映射表(name → Capability) */
|
|
27
|
-
capabilityMap: CapabilityMap;
|
|
28
|
-
/** 工作流节点序列(线性链) */
|
|
29
|
-
nodes: WorkspaceNode[];
|
|
30
|
-
/** 当前选中的节点 ID */
|
|
31
|
-
selectedNodeId: string | null;
|
|
32
|
-
/** 是否正在执行工作流 */
|
|
33
|
-
running: boolean;
|
|
34
|
-
/** 全局状态消息 */
|
|
35
|
-
statusMessage: string;
|
|
36
|
-
/** 错误信息 */
|
|
37
|
-
error: string | null;
|
|
38
|
-
}
|
|
39
|
-
/** 工作台操作 */
|
|
40
|
-
export interface WorkspaceActions {
|
|
41
|
-
/** 初始化 Runtime */
|
|
42
|
-
init(runtime: LokvisRuntime): Promise<void>;
|
|
43
|
-
/** 刷新资产列表 */
|
|
44
|
-
refreshAssets(): Promise<void>;
|
|
45
|
-
/** 刷新能力列表 */
|
|
46
|
-
refreshCapabilities(): Promise<void>;
|
|
47
|
-
/** 导入文件 */
|
|
48
|
-
importFiles(files: File[]): Promise<void>;
|
|
49
|
-
/** 选择资产 */
|
|
50
|
-
selectAsset(id: string | null): void;
|
|
51
|
-
/** 设置缩略图 */
|
|
52
|
-
setThumbnail(id: string, url: string): void;
|
|
53
|
-
/** 删除资产 */
|
|
54
|
-
removeAsset(id: string): Promise<void>;
|
|
55
|
-
/** 添加工作流节点 */
|
|
56
|
-
addNode(capability: string): void;
|
|
57
|
-
/** 更新节点参数 */
|
|
58
|
-
updateNodeParams(id: string, params: Record<string, unknown>): void;
|
|
59
|
-
/** 删除节点 */
|
|
60
|
-
removeNode(id: string): void;
|
|
61
|
-
/** 选择节点 */
|
|
62
|
-
selectNode(id: string | null): void;
|
|
63
|
-
/** 设置节点状态 */
|
|
64
|
-
setNodeStatus(id: string, status: NodeStatus, error?: string, duration?: number): void;
|
|
65
|
-
/** 执行工作流 */
|
|
66
|
-
run(): Promise<Asset[]>;
|
|
67
|
-
/** 设置状态消息 */
|
|
68
|
-
setStatus(message: string): void;
|
|
69
|
-
/** 设置错误 */
|
|
70
|
-
setError(error: string | null): void;
|
|
71
|
-
/** 清空工作流 */
|
|
72
|
-
clearWorkflow(): void;
|
|
73
|
-
}
|
|
74
|
-
export type WorkspaceStore = WorkspaceState & WorkspaceActions;
|
|
75
|
-
export declare const useWorkspaceStore: import("zustand").UseBoundStore<import("zustand").StoreApi<WorkspaceStore>>;
|
|
76
|
-
//# sourceMappingURL=store.d.ts.map
|
package/dist/store.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,cAAc;AACd,MAAM,WAAW,cAAc;IAC7B,iBAAiB;IACjB,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,sBAAsB;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAEzB,cAAc;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,iBAAiB;IACjB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEnC,cAAc;IACd,YAAY,EAAE,UAAU,EAAE,CAAC;IAC3B,+BAA+B;IAC/B,aAAa,EAAE,aAAa,CAAC;IAE7B,mBAAmB;IACnB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,iBAAiB;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAE9B,gBAAgB;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,YAAY;AACZ,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB;IAClB,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,aAAa;IACb,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,aAAa;IACb,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,WAAW;IACX,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,WAAW;IACX,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACrC,YAAY;IACZ,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,WAAW;IACX,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,cAAc;IACd,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa;IACb,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACpE,WAAW;IACX,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW;IACX,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACpC,aAAa;IACb,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAEvF,YAAY;IACZ,GAAG,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACxB,aAAa;IACb,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW;IACX,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACrC,YAAY;IACZ,aAAa,IAAI,IAAI,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAO/D,eAAO,MAAM,iBAAiB,6EA+L3B,CAAC"}
|