entropia-template-ui 0.2.4 → 0.2.5

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.
Files changed (88) hide show
  1. package/dist/App.d.ts +2 -0
  2. package/dist/App.d.ts.map +1 -0
  3. package/dist/areas.d.ts +55 -0
  4. package/dist/areas.d.ts.map +1 -0
  5. package/dist/components/AboutPanel.d.ts +4 -0
  6. package/dist/components/AboutPanel.d.ts.map +1 -0
  7. package/dist/components/BrandLogo.d.ts +6 -0
  8. package/dist/components/BrandLogo.d.ts.map +1 -0
  9. package/dist/components/CodeEditor.d.ts +2 -0
  10. package/dist/components/CodeEditor.d.ts.map +1 -0
  11. package/dist/components/CodePreview.d.ts +6 -0
  12. package/dist/components/CodePreview.d.ts.map +1 -0
  13. package/dist/components/ContextMenu.d.ts +10 -0
  14. package/dist/components/ContextMenu.d.ts.map +1 -0
  15. package/dist/components/DocsPanel.d.ts +2 -0
  16. package/dist/components/DocsPanel.d.ts.map +1 -0
  17. package/dist/components/FileExplorer.d.ts +4 -0
  18. package/dist/components/FileExplorer.d.ts.map +1 -0
  19. package/dist/components/FileManager.d.ts +2 -0
  20. package/dist/components/FileManager.d.ts.map +1 -0
  21. package/dist/components/FilePicker.d.ts +5 -0
  22. package/dist/components/FilePicker.d.ts.map +1 -0
  23. package/dist/components/FloatingWindow.d.ts +10 -0
  24. package/dist/components/FloatingWindow.d.ts.map +1 -0
  25. package/dist/components/GraphCanvas.d.ts +2 -0
  26. package/dist/components/GraphCanvas.d.ts.map +1 -0
  27. package/dist/components/HandwritingPad.d.ts +2 -0
  28. package/dist/components/HandwritingPad.d.ts.map +1 -0
  29. package/dist/components/ImportFolderPanel.d.ts +4 -0
  30. package/dist/components/ImportFolderPanel.d.ts.map +1 -0
  31. package/dist/components/LossPanel.d.ts +2 -0
  32. package/dist/components/LossPanel.d.ts.map +1 -0
  33. package/dist/components/MenuBar.d.ts +13 -0
  34. package/dist/components/MenuBar.d.ts.map +1 -0
  35. package/dist/components/NodeCard.d.ts +4 -0
  36. package/dist/components/NodeCard.d.ts.map +1 -0
  37. package/dist/components/NodeLibrary.d.ts +2 -0
  38. package/dist/components/NodeLibrary.d.ts.map +1 -0
  39. package/dist/components/Panel.d.ts +39 -0
  40. package/dist/components/Panel.d.ts.map +1 -0
  41. package/dist/components/PluginPanel.d.ts +2 -0
  42. package/dist/components/PluginPanel.d.ts.map +1 -0
  43. package/dist/components/PopupMenu.d.ts +19 -0
  44. package/dist/components/PopupMenu.d.ts.map +1 -0
  45. package/dist/components/PreferencesPanel.d.ts +4 -0
  46. package/dist/components/PreferencesPanel.d.ts.map +1 -0
  47. package/dist/components/ProjectPanel.d.ts +2 -0
  48. package/dist/components/ProjectPanel.d.ts.map +1 -0
  49. package/dist/components/SideInspector.d.ts +7 -0
  50. package/dist/components/SideInspector.d.ts.map +1 -0
  51. package/dist/components/Splitter.d.ts +7 -0
  52. package/dist/components/Splitter.d.ts.map +1 -0
  53. package/dist/components/StatusPanel.d.ts +6 -0
  54. package/dist/components/StatusPanel.d.ts.map +1 -0
  55. package/dist/components/Titlebar.d.ts +6 -0
  56. package/dist/components/Titlebar.d.ts.map +1 -0
  57. package/dist/components/ToastStack.d.ts +2 -0
  58. package/dist/components/ToastStack.d.ts.map +1 -0
  59. package/dist/components/Toolbar.d.ts +2 -0
  60. package/dist/components/Toolbar.d.ts.map +1 -0
  61. package/dist/components/WelcomePanel.d.ts +4 -0
  62. package/dist/components/WelcomePanel.d.ts.map +1 -0
  63. package/dist/components/WorkspaceTabs.d.ts +15 -0
  64. package/dist/components/WorkspaceTabs.d.ts.map +1 -0
  65. package/dist/docs/Markdown.d.ts +2 -0
  66. package/dist/docs/Markdown.d.ts.map +1 -0
  67. package/dist/index.d.ts +51 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +3817 -0
  70. package/dist/nodes/nodeDefinitions.d.ts +30 -0
  71. package/dist/nodes/nodeDefinitions.d.ts.map +1 -0
  72. package/dist/store/graphStore.d.ts +167 -0
  73. package/dist/store/graphStore.d.ts.map +1 -0
  74. package/dist/{assets/index-BkpZ6AP9.css → style.css} +1 -1
  75. package/dist/subgraphActions.d.ts +4 -0
  76. package/dist/subgraphActions.d.ts.map +1 -0
  77. package/dist/theme.d.ts +11 -0
  78. package/dist/theme.d.ts.map +1 -0
  79. package/dist/version.d.ts +2 -0
  80. package/dist/version.d.ts.map +1 -0
  81. package/package.json +3 -2
  82. package/dist/assets/index-CPQMMXDH.js +0 -70
  83. package/dist/brand/apple-touch-icon.png +0 -0
  84. package/dist/brand/favicon.png +0 -0
  85. package/dist/brand/hero.jpg +0 -0
  86. package/dist/brand/logo.ico +0 -0
  87. package/dist/brand/logo.png +0 -0
  88. package/dist/index.html +0 -16
package/dist/index.js ADDED
@@ -0,0 +1,3817 @@
1
+ import { jsxs as p, Fragment as H, jsx as s } from "react/jsx-runtime";
2
+ import { useState as F, useMemo as X, useRef as M, useEffect as B, Fragment as $e } from "react";
3
+ import { create as Le } from "zustand";
4
+ import { addEdge as Xe, applyEdgeChanges as Qe, applyNodeChanges as Ze, Handle as xe, Position as Se, ReactFlowProvider as en, useReactFlow as nn, ReactFlow as tn, Background as sn, ViewportPortal as an } from "@xyflow/react";
5
+ import { createPortal as De } from "react-dom";
6
+ const on = [
7
+ {
8
+ type: "constant",
9
+ label: "Constant",
10
+ category: "Inputs",
11
+ inputs: [],
12
+ outputs: [{ name: "value", label: "Value", dataKind: "tensor", direction: "out" }],
13
+ parameters: [{ name: "value", label: "Value", kind: "any", default: 1, required: !0, dtype: "float" }]
14
+ },
15
+ {
16
+ type: "add",
17
+ label: "Add",
18
+ category: "Math",
19
+ inputs: [
20
+ { name: "left", label: "Left", dataKind: "tensor", direction: "in" },
21
+ { name: "right", label: "Right", dataKind: "tensor", direction: "in" }
22
+ ],
23
+ outputs: [{ name: "result", label: "Result", dataKind: "tensor", direction: "out" }],
24
+ parameters: []
25
+ },
26
+ {
27
+ type: "multiply",
28
+ label: "Multiply",
29
+ category: "Math",
30
+ inputs: [
31
+ { name: "left", label: "Left", dataKind: "tensor", direction: "in" },
32
+ { name: "right", label: "Right", dataKind: "tensor", direction: "in" }
33
+ ],
34
+ outputs: [{ name: "result", label: "Result", dataKind: "tensor", direction: "out" }],
35
+ parameters: []
36
+ }
37
+ ], te = "0.2.5";
38
+ let rn = 0;
39
+ function ue(n) {
40
+ return n.charAt(0).toUpperCase() + n.slice(1);
41
+ }
42
+ function ln(n) {
43
+ return {
44
+ type: n.type,
45
+ label: n.label,
46
+ category: n.category,
47
+ inputs: n.inputs.map((e) => ({
48
+ name: e.name,
49
+ label: ue(e.name),
50
+ dataKind: e.data_kind,
51
+ direction: "in"
52
+ })),
53
+ outputs: n.outputs.map((e) => ({
54
+ name: e.name,
55
+ label: ue(e.name),
56
+ dataKind: e.data_kind,
57
+ direction: "out"
58
+ })),
59
+ parameters: n.parameters.map((e) => ({
60
+ name: e.name,
61
+ label: ue(e.name),
62
+ kind: e.kind,
63
+ default: e.default,
64
+ required: e.required,
65
+ dtype: e.dtype ?? void 0,
66
+ browse: e.browse ?? void 0
67
+ }))
68
+ };
69
+ }
70
+ let Ce = 0;
71
+ function Y(n) {
72
+ return Ce += 1, `${n}_${Ce}`;
73
+ }
74
+ function G(n, e) {
75
+ let t = "untitled", a = "", o = "";
76
+ try {
77
+ t = g.getState().activeFileName ?? "untitled", a = localStorage.getItem("entropia_riko_theme") ?? "", o = localStorage.getItem("entropia_riko_background") ?? "";
78
+ } catch {
79
+ }
80
+ return {
81
+ version: "1.0",
82
+ metadata: { name: t, app: "entropia-riko", appVersion: te },
83
+ nodes: n.map((r) => ({
84
+ id: r.id,
85
+ type_name: r.data.type,
86
+ label: r.data.label,
87
+ category: r.data.category,
88
+ position: [r.position.x, r.position.y],
89
+ parameters: r.data.params,
90
+ inputs: [],
91
+ outputs: []
92
+ })),
93
+ edges: e.map((r) => ({
94
+ id: r.id,
95
+ source_node: r.source,
96
+ source_port: r.sourceHandle,
97
+ target_node: r.target,
98
+ target_port: r.targetHandle
99
+ })),
100
+ settings: { theme: a, backgroundImage: o }
101
+ };
102
+ }
103
+ function se(n, e) {
104
+ const t = (n.nodes || []).map((o) => {
105
+ var i, d;
106
+ const r = e.find((h) => h.type === o.type_name);
107
+ return {
108
+ id: o.id,
109
+ type: "thnNode",
110
+ position: { x: ((i = o.position) == null ? void 0 : i[0]) ?? 0, y: ((d = o.position) == null ? void 0 : d[1]) ?? 0 },
111
+ data: {
112
+ type: o.type_name,
113
+ label: o.label || (r == null ? void 0 : r.label) || o.type_name,
114
+ category: o.category || (r == null ? void 0 : r.category) || "",
115
+ params: o.parameters || {},
116
+ inputs: (r == null ? void 0 : r.inputs) || [],
117
+ outputs: (r == null ? void 0 : r.outputs) || [],
118
+ hasError: !1
119
+ }
120
+ };
121
+ }), a = (n.edges || []).map((o) => ({
122
+ id: o.id,
123
+ source: o.source_node,
124
+ target: o.target_node,
125
+ sourceHandle: o.source_port,
126
+ targetHandle: o.target_port
127
+ }));
128
+ return { nodes: t, edges: a };
129
+ }
130
+ async function cn(n) {
131
+ const e = (n || "").trim();
132
+ if (!e) return null;
133
+ const t = [];
134
+ (e.includes("/") || e.endsWith(".riko") || e.endsWith(".ric")) && t.push(e), t.push(`examples/models/${e}.riko`), t.push(`examples/${e}.riko`), t.push(`workflows/${e}.riko`), t.push(`${e}.riko`);
135
+ for (const a of t)
136
+ try {
137
+ if ((await (await fetch(`/api/files/content?path=${encodeURIComponent(a)}`)).json()).status === "success") return a;
138
+ } catch {
139
+ }
140
+ return null;
141
+ }
142
+ const Ae = "entropia_riko_recent";
143
+ function dn() {
144
+ try {
145
+ const n = localStorage.getItem(Ae), e = n ? JSON.parse(n) : [];
146
+ return Array.isArray(e) ? e.filter((t) => typeof t == "string") : [];
147
+ } catch {
148
+ return [];
149
+ }
150
+ }
151
+ function pn(n) {
152
+ try {
153
+ localStorage.setItem(Ae, JSON.stringify(n.slice(0, 10)));
154
+ } catch {
155
+ }
156
+ }
157
+ const g = Le((n, e) => ({
158
+ nodeDefs: on,
159
+ workflows: [],
160
+ activeWorkflowId: null,
161
+ nodes: [],
162
+ edges: [],
163
+ frames: [],
164
+ graphStack: [{ name: "root", path: "", nodes: [], edges: [] }],
165
+ selectedNodeId: null,
166
+ status: "idle",
167
+ logs: [],
168
+ results: {},
169
+ fileList: [],
170
+ activeFileName: null,
171
+ activeFilePath: null,
172
+ dirty: !1,
173
+ losses: [],
174
+ recentFiles: dn(),
175
+ welcomeOpen: !0,
176
+ prefsOpen: !1,
177
+ aboutOpen: !1,
178
+ importFolderOpen: !1,
179
+ filePickerOpen: !1,
180
+ filePickerMode: "import",
181
+ projectVersion: 0,
182
+ codeBuffer: "",
183
+ toasts: [],
184
+ onNodesChange: (t) => {
185
+ const a = Ze(t, e().nodes), o = t.some((r) => r.type !== "select");
186
+ n(o ? { nodes: a, dirty: !0 } : { nodes: a });
187
+ },
188
+ onEdgesChange: (t) => {
189
+ const a = Qe(t, e().edges), o = t.some((r) => r.type !== "select");
190
+ n(o ? { edges: a, dirty: !0 } : { edges: a });
191
+ },
192
+ onConnect: (t) => {
193
+ if (!t.source || !t.target) return;
194
+ const a = Y("edge");
195
+ n({ edges: Xe({ ...t, id: a }, e().edges), dirty: !0 }), e().log(`连接 ${t.source}.${t.sourceHandle} → ${t.target}.${t.targetHandle}`);
196
+ },
197
+ addNode: (t, a) => {
198
+ const o = e().nodeDefs.find((h) => h.type === t), r = Y(t), i = {};
199
+ if (o)
200
+ for (const h of o.parameters) i[h.name] = h.default;
201
+ const d = {
202
+ id: r,
203
+ type: "thnNode",
204
+ position: a,
205
+ data: {
206
+ type: t,
207
+ label: (o == null ? void 0 : o.label) ?? t,
208
+ category: (o == null ? void 0 : o.category) ?? "Other",
209
+ params: i,
210
+ inputs: (o == null ? void 0 : o.inputs) ?? [],
211
+ outputs: (o == null ? void 0 : o.outputs) ?? [{ name: "result", label: "Result", dataKind: "tensor", direction: "out" }],
212
+ hasError: !1
213
+ }
214
+ };
215
+ n({ nodes: [...e().nodes, d], selectedNodeId: r, dirty: !0 }), e().log(`添加节点 ${(o == null ? void 0 : o.label) ?? t} (${r})`);
216
+ },
217
+ onSelectNode: (t) => n({ selectedNodeId: t }),
218
+ updateParam: (t, a, o) => n({
219
+ nodes: e().nodes.map(
220
+ (r) => r.id === t ? { ...r, data: { ...r.data, params: { ...r.data.params, [a]: o } } } : r
221
+ ),
222
+ dirty: !0
223
+ }),
224
+ removeNode: (t) => n({
225
+ nodes: e().nodes.filter((a) => a.id !== t),
226
+ edges: e().edges.filter((a) => a.source !== t && a.target !== t),
227
+ selectedNodeId: e().selectedNodeId === t ? null : e().selectedNodeId,
228
+ dirty: !0
229
+ }),
230
+ addFrame: (t, a) => {
231
+ const o = Y("frame");
232
+ n({
233
+ frames: [
234
+ ...e().frames,
235
+ { id: o, title: t.trim() || "Frame", x: a.x, y: a.y, width: 360, height: 220 }
236
+ ],
237
+ dirty: !0
238
+ });
239
+ },
240
+ updateFrame: (t, a) => n({
241
+ frames: e().frames.map((o) => o.id === t ? { ...o, ...a } : o),
242
+ dirty: !0
243
+ }),
244
+ removeFrame: (t) => n({ frames: e().frames.filter((a) => a.id !== t), dirty: !0 }),
245
+ enterSubgraph: async (t) => {
246
+ var i;
247
+ const a = e().nodes.find((d) => d.id === t);
248
+ if (!a) return;
249
+ const o = a.data.params.file ?? a.data.params.module;
250
+ if (!o) {
251
+ e().pushToast("error", "该节点没有 file/module 参数");
252
+ return;
253
+ }
254
+ const r = await cn(String(o));
255
+ if (!r) {
256
+ e().pushToast("error", `找不到子图: ${o}`);
257
+ return;
258
+ }
259
+ try {
260
+ const h = await (await fetch(`/api/files/content?path=${encodeURIComponent(r)}`)).json();
261
+ if (h.status !== "success") {
262
+ e().pushToast("error", h.error ?? "打开子图失败");
263
+ return;
264
+ }
265
+ const { nodes: c, edges: m } = se(h.doc, e().nodeDefs), u = [...e().graphStack], y = u[u.length - 1];
266
+ u[u.length - 1] = { ...y, nodes: e().nodes, edges: e().edges };
267
+ const l = ((i = r.split("/").pop()) == null ? void 0 : i.replace(/\.(riko|ric)$/i, "")) ?? r;
268
+ u.push({ name: l, path: r, nodes: c, edges: m }), n({
269
+ graphStack: u,
270
+ nodes: c,
271
+ edges: m,
272
+ selectedNodeId: null,
273
+ results: {},
274
+ status: "idle",
275
+ dirty: !1
276
+ }), e().log(`进入子图 ${r}`);
277
+ } catch (d) {
278
+ e().pushToast("error", d instanceof Error ? d.message : String(d));
279
+ }
280
+ },
281
+ exitToLevel: (t) => {
282
+ const a = e().graphStack.slice(0, t + 1), o = a[t];
283
+ o && (n({
284
+ graphStack: a,
285
+ nodes: o.nodes,
286
+ edges: o.edges,
287
+ selectedNodeId: null,
288
+ results: {},
289
+ status: "idle",
290
+ dirty: !1
291
+ }), e().log(`返回层级 ${o.name}`));
292
+ },
293
+ run: async () => {
294
+ n({ status: "running" }), e().log("开始执行图(调用 Python runtime)...");
295
+ try {
296
+ const t = G(e().nodes, e().edges), o = await (await fetch("/api/execute", {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify(t)
300
+ })).json();
301
+ if (o.status === "success") {
302
+ const r = {}, i = o.outputs;
303
+ for (const [d, h] of Object.entries(i)) {
304
+ r[d] = {};
305
+ for (const [c, m] of Object.entries(h))
306
+ r[d][c] = {
307
+ summary: m.summary,
308
+ shape: m.shape,
309
+ dtype: m.dtype,
310
+ device: m.device,
311
+ dataPreview: m.data === null || m.data === void 0 ? "" : JSON.stringify(m.data).slice(0, 120),
312
+ dataKind: m.data_kind,
313
+ image: m.image
314
+ };
315
+ }
316
+ n({
317
+ results: r,
318
+ status: "success",
319
+ nodes: e().nodes.map((d) => ({
320
+ ...d,
321
+ data: { ...d.data, hasError: !1, errorMsg: void 0 }
322
+ }))
323
+ }), e().log("执行成功");
324
+ } else {
325
+ n({ status: "error" });
326
+ for (const r of o.errors)
327
+ e().log(`错误: ${r}`), e().pushToast("error", r);
328
+ }
329
+ } catch (t) {
330
+ const a = t instanceof Error ? t.message : String(t);
331
+ n({ status: "error" }), e().log(`请求失败: ${a}(请确认 API server 已启动)`), e().pushToast("error", `请求失败: ${a}`);
332
+ }
333
+ },
334
+ loadNodeDefs: async () => {
335
+ try {
336
+ const o = (await (await fetch("/api/nodes")).json()).nodes.map(ln);
337
+ o.length > 0 && (n({ nodeDefs: o }), e().log(`从 API 加载 ${o.length} 个节点定义`));
338
+ } catch (t) {
339
+ e().log(`API 节点加载失败: ${t instanceof Error ? t.message : String(t)}。请启动 API server: uvicorn src.server.app:app --port 8000`);
340
+ }
341
+ },
342
+ save: () => {
343
+ const t = G(e().nodes, e().edges), a = new Blob([JSON.stringify(t, null, 2)], {
344
+ type: "application/json"
345
+ }), o = URL.createObjectURL(a), r = document.createElement("a");
346
+ r.href = o, r.download = "workflow.riko", r.click(), URL.revokeObjectURL(o), n({ dirty: !1 }), e().log("已保存工作流 (workflow.riko)");
347
+ },
348
+ exportRiko: () => {
349
+ const t = G(e().nodes, e().edges), a = (e().activeFileName ?? "workflow").replace(/\.(riko|ric)$/i, ""), o = new Blob([JSON.stringify(t, null, 2)], { type: "application/json" }), r = URL.createObjectURL(o), i = document.createElement("a");
350
+ i.href = r, i.download = `${a}.riko`, i.click(), URL.revokeObjectURL(r), n({ dirty: !1 }), e().log(`已导出 ${a}.riko`);
351
+ },
352
+ exportBinary: async () => {
353
+ try {
354
+ const t = G(e().nodes, e().edges), o = await (await fetch("/api/export_binary", {
355
+ method: "POST",
356
+ headers: { "Content-Type": "application/json" },
357
+ body: JSON.stringify(t)
358
+ })).json();
359
+ if (o.status !== "success") throw new Error(o.error);
360
+ const r = (e().activeFileName ?? "workflow").replace(/\.(riko|ric)$/i, ""), i = Uint8Array.from(atob(o.base64), (m) => m.charCodeAt(0)), d = new Blob([i], { type: "application/octet-stream" }), h = URL.createObjectURL(d), c = document.createElement("a");
361
+ c.href = h, c.download = `${r}.ric`, c.click(), URL.revokeObjectURL(h), n({ dirty: !1 }), e().log(`已导出 ${r}.ric`);
362
+ } catch (t) {
363
+ e().log(`导出 .ric 失败: ${t instanceof Error ? t.message : String(t)}`);
364
+ }
365
+ },
366
+ exportProjectTo: async (t) => {
367
+ const a = G(e().nodes, e().edges);
368
+ try {
369
+ const r = await (await fetch("/api/export_project", {
370
+ method: "POST",
371
+ headers: { "Content-Type": "application/json" },
372
+ body: JSON.stringify({ doc: a, dir: t })
373
+ })).json();
374
+ r.status === "success" ? (e().pushToast("success", `Exported project → ${r.root}`), e().log(`已导出多文件 PyTorch 工程: ${r.files.join(", ")}`)) : e().pushToast("error", r.error ?? "导出工程失败");
375
+ } catch (o) {
376
+ e().pushToast("error", o instanceof Error ? o.message : String(o));
377
+ }
378
+ },
379
+ load: async (t) => {
380
+ try {
381
+ let a;
382
+ if (t.name.toLowerCase().endsWith(".ric")) {
383
+ const i = await t.arrayBuffer(), h = await (await fetch("/api/files/decode", {
384
+ method: "POST",
385
+ headers: { "Content-Type": "application/octet-stream" },
386
+ body: i
387
+ })).json();
388
+ if (h.status !== "success") throw new Error(h.error);
389
+ a = h.doc;
390
+ } else {
391
+ const i = await new Promise((d, h) => {
392
+ const c = new FileReader();
393
+ c.onload = () => d(String(c.result)), c.onerror = () => h(c.error), c.readAsText(t);
394
+ });
395
+ a = JSON.parse(i);
396
+ }
397
+ const { nodes: o, edges: r } = se(a, e().nodeDefs);
398
+ n({ nodes: o, edges: r, frames: [], graphStack: [{ name: t.name.replace(/\.(riko|ric)$/i, "") || "workflow", path: t.name, nodes: o, edges: [] }], selectedNodeId: null, results: {}, status: "idle", dirty: !1 }), e().log(`已加载工作流(${o.length} 节点, ${r.length} 连接)`);
399
+ } catch (a) {
400
+ e().log(`加载失败: ${a instanceof Error ? a.message : String(a)}`);
401
+ }
402
+ },
403
+ exportPython: async () => {
404
+ const t = G(e().nodes, e().edges);
405
+ try {
406
+ const o = await (await fetch("/api/export_python", {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: JSON.stringify(t)
410
+ })).json();
411
+ if (o.status === "success") {
412
+ const r = new Blob([o.code], { type: "text/x-python" }), i = URL.createObjectURL(r), d = document.createElement("a");
413
+ d.href = i, d.download = "workflow.py", d.click(), URL.revokeObjectURL(i), e().log("已导出 Python (workflow.py)");
414
+ } else
415
+ e().log(`导出失败: ${o.error}`);
416
+ } catch (a) {
417
+ e().log(`导出失败: ${a instanceof Error ? a.message : String(a)}`);
418
+ }
419
+ },
420
+ exportKeras: async () => {
421
+ const t = G(e().nodes, e().edges);
422
+ try {
423
+ const o = await (await fetch("/api/export_keras", {
424
+ method: "POST",
425
+ headers: { "Content-Type": "application/json" },
426
+ body: JSON.stringify(t)
427
+ })).json();
428
+ if (o.status === "success") {
429
+ const r = new Blob([o.code], { type: "text/x-python" }), i = URL.createObjectURL(r), d = document.createElement("a");
430
+ d.href = i, d.download = "workflow_keras.py", d.click(), URL.revokeObjectURL(i), e().log("已导出 Keras (workflow_keras.py)");
431
+ } else
432
+ e().log(`导出失败: ${o.error}`);
433
+ } catch (a) {
434
+ e().log(`导出失败: ${a instanceof Error ? a.message : String(a)}`);
435
+ }
436
+ },
437
+ generatePythonCode: async () => {
438
+ const t = G(e().nodes, e().edges);
439
+ try {
440
+ const o = await (await fetch("/api/export_python", {
441
+ method: "POST",
442
+ headers: { "Content-Type": "application/json" },
443
+ body: JSON.stringify(t)
444
+ })).json();
445
+ o.status === "success" ? (n({ codeBuffer: o.code }), e().log("已生成 PyTorch 代码(代码编辑器窗口)")) : e().pushToast("error", o.error ?? "代码生成失败");
446
+ } catch (a) {
447
+ e().pushToast("error", a instanceof Error ? a.message : String(a));
448
+ }
449
+ },
450
+ previewAssetCode: async (t) => {
451
+ try {
452
+ const o = await (await fetch(`/api/files/content?path=${encodeURIComponent(t)}`)).json();
453
+ if (o.status !== "success") {
454
+ e().pushToast("error", o.error ?? "读取失败");
455
+ return;
456
+ }
457
+ const i = await (await fetch("/api/export_python", {
458
+ method: "POST",
459
+ headers: { "Content-Type": "application/json" },
460
+ body: JSON.stringify(o.doc)
461
+ })).json();
462
+ i.status === "success" ? (n({ codeBuffer: i.code }), e().pushToast("success", "Code → Code Editor")) : e().pushToast("error", i.error ?? "代码生成失败");
463
+ } catch (a) {
464
+ e().pushToast("error", a instanceof Error ? a.message : String(a));
465
+ }
466
+ },
467
+ newWorkflow: (t) => {
468
+ const a = e().activeWorkflowId, o = a ? e().workflows.map(
469
+ (i) => i.id === a ? { ...i, nodes: e().nodes, edges: e().edges } : i
470
+ ) : e().workflows, r = Y("wf");
471
+ n({
472
+ workflows: [...o, { id: r, name: t, nodes: [], edges: [] }],
473
+ activeWorkflowId: r,
474
+ nodes: [],
475
+ edges: [],
476
+ selectedNodeId: null,
477
+ results: {},
478
+ status: "idle",
479
+ dirty: !1
480
+ }), e().log(`新建工作流: ${t} (${r})`);
481
+ },
482
+ switchWorkflow: (t) => {
483
+ const a = e().activeWorkflowId;
484
+ let o = e().workflows;
485
+ a && (o = o.map(
486
+ (i) => i.id === a ? { ...i, nodes: e().nodes, edges: e().edges } : i
487
+ ));
488
+ const r = o.find((i) => i.id === t);
489
+ r && (n({
490
+ workflows: o,
491
+ activeWorkflowId: t,
492
+ nodes: r.nodes,
493
+ edges: r.edges,
494
+ selectedNodeId: null,
495
+ results: {},
496
+ status: "idle",
497
+ dirty: !1
498
+ }), e().log(`切换到工作流: ${r.name}`));
499
+ },
500
+ refreshFiles: async () => {
501
+ try {
502
+ const a = await (await fetch("/api/files")).json();
503
+ Array.isArray(a.files) ? n({ fileList: a.files }) : e().log("文件列表格式错误");
504
+ } catch (t) {
505
+ e().log(`文件列表加载失败: ${t instanceof Error ? t.message : String(t)}`);
506
+ }
507
+ },
508
+ openFile: async (t) => {
509
+ var a, o, r, i;
510
+ try {
511
+ const h = await (await fetch(`/api/files/content?path=${encodeURIComponent(t)}`)).json();
512
+ if (h.status === "success") {
513
+ const { nodes: c, edges: m } = se(h.doc, e().nodeDefs);
514
+ n({
515
+ nodes: c,
516
+ edges: m,
517
+ selectedNodeId: null,
518
+ results: {},
519
+ status: "idle",
520
+ activeFileName: ((o = (a = h.doc) == null ? void 0 : a.metadata) == null ? void 0 : o.name) ?? t,
521
+ activeFilePath: t,
522
+ graphStack: [{ name: ((i = (r = h.doc) == null ? void 0 : r.metadata) == null ? void 0 : i.name) ?? t.split("/").pop() ?? t, path: t, nodes: c, edges: [] }],
523
+ dirty: !1
524
+ });
525
+ const u = [t, ...e().recentFiles.filter((y) => y !== t)].slice(0, 10);
526
+ pn(u), n({ recentFiles: u }), e().log(`已打开 ${t}`);
527
+ } else
528
+ e().log(`打开失败: ${h.error}`);
529
+ } catch (d) {
530
+ e().log(`打开失败: ${d instanceof Error ? d.message : String(d)}`);
531
+ }
532
+ },
533
+ openProjectFile: async (t) => {
534
+ var a;
535
+ try {
536
+ const r = await (await fetch(`/api/project/open?path=${encodeURIComponent(t)}`)).json();
537
+ if (r.status === "success") {
538
+ const { nodes: i, edges: d } = se(r.doc, e().nodeDefs), h = ((a = t.split("/").pop()) == null ? void 0 : a.replace(/\.(riko|ric)$/, "")) ?? t;
539
+ n({
540
+ nodes: i,
541
+ edges: d,
542
+ selectedNodeId: null,
543
+ results: {},
544
+ status: "idle",
545
+ activeFileName: h,
546
+ activeFilePath: t,
547
+ graphStack: [{ name: h, path: t, nodes: i, edges: [] }],
548
+ dirty: !1
549
+ }), e().log(`已打开 ${t}`);
550
+ } else
551
+ e().log(`打开失败: ${r.error}`);
552
+ } catch (o) {
553
+ e().log(`打开失败: ${o instanceof Error ? o.message : String(o)}`);
554
+ }
555
+ },
556
+ importFileInline: async (t) => {
557
+ try {
558
+ const o = await (await fetch(`/api/files/content?path=${encodeURIComponent(t)}`)).json();
559
+ if (o.status !== "success") {
560
+ e().log(`展开失败: ${o.error}`);
561
+ return;
562
+ }
563
+ const r = o.doc, i = e().nodes;
564
+ let d = 0;
565
+ for (const u of i) d = Math.max(d, u.position.y + 120);
566
+ const h = {}, c = (r.nodes ?? []).map((u) => {
567
+ var v, f;
568
+ const y = Y("n");
569
+ h[u.id] = y;
570
+ const l = e().nodeDefs.find((C) => C.type === u.type_name);
571
+ return {
572
+ id: y,
573
+ type: "thnNode",
574
+ position: { x: ((v = u.position) == null ? void 0 : v[0]) ?? 0, y: (((f = u.position) == null ? void 0 : f[1]) ?? 0) + d },
575
+ data: {
576
+ type: u.type_name,
577
+ label: u.label || (l == null ? void 0 : l.label) || u.type_name,
578
+ category: u.category || (l == null ? void 0 : l.category) || "",
579
+ params: u.parameters || {},
580
+ inputs: (l == null ? void 0 : l.inputs) ?? [],
581
+ outputs: (l == null ? void 0 : l.outputs) ?? [],
582
+ hasError: !1
583
+ }
584
+ };
585
+ }), m = (r.edges ?? []).map((u) => ({
586
+ id: Y("edge"),
587
+ source: h[u.source_node],
588
+ target: h[u.target_node],
589
+ sourceHandle: u.source_port,
590
+ targetHandle: u.target_port
591
+ })).filter((u) => u.source && u.target);
592
+ n({
593
+ nodes: [...i, ...c],
594
+ edges: [...e().edges, ...m],
595
+ dirty: !0
596
+ }), e().log(`已展开 ${t}(${c.length} 节点, ${m.length} 连接)`);
597
+ } catch (a) {
598
+ e().log(`展开失败: ${a instanceof Error ? a.message : String(a)}`);
599
+ }
600
+ },
601
+ saveFileToDisk: async (t, a = "ascii") => {
602
+ const r = { ...G(e().nodes, e().edges), metadata: { name: t, description: "" } };
603
+ try {
604
+ const d = await (await fetch("/api/files/save", {
605
+ method: "POST",
606
+ headers: { "Content-Type": "application/json" },
607
+ body: JSON.stringify({ name: t, format: a, doc: r })
608
+ })).json();
609
+ d.status === "success" ? (n({ activeFileName: t, dirty: !1 }), e().log(`已保存到 ${d.path} (${d.format === "binary" ? ".ric 二进制" : ".riko ASCII"})`), e().refreshFiles()) : e().log(`保存失败: ${d.error ?? "未知错误"}`);
610
+ } catch (i) {
611
+ e().log(`保存失败: ${i instanceof Error ? i.message : String(i)}`);
612
+ }
613
+ },
614
+ saveToPath: async (t, a = "ascii") => {
615
+ var r;
616
+ const o = G(e().nodes, e().edges);
617
+ try {
618
+ const d = await (await fetch("/api/fs/save", {
619
+ method: "POST",
620
+ headers: { "Content-Type": "application/json" },
621
+ body: JSON.stringify({ path: t, format: a, doc: o })
622
+ })).json();
623
+ if (d.status === "success") {
624
+ const h = d.name ?? ((r = t.split("/").pop()) == null ? void 0 : r.replace(/\.(riko|ric)$/i, "")) ?? "workflow";
625
+ n({ activeFileName: h, activeFilePath: t, dirty: !1 }), e().log(`已保存到 ${d.path}`), e().pushToast("success", `Saved ${h}`), e().refreshFiles();
626
+ } else
627
+ e().pushToast("error", d.error ?? "保存失败");
628
+ } catch (i) {
629
+ e().pushToast("error", i instanceof Error ? i.message : String(i));
630
+ }
631
+ },
632
+ importModule: (t, a) => {
633
+ const o = e().nodeDefs.find((h) => h.type === "import") ?? e().nodeDefs.find((h) => h.type === "graph_reference");
634
+ if (!o) {
635
+ e().log("未找到 import / graph_reference 节点定义,请确认 API server 已启动");
636
+ return;
637
+ }
638
+ const r = Y(o.type), i = {};
639
+ for (const h of o.parameters) i[h.name] = h.default;
640
+ i[o.type === "import" ? "module" : "file"] = t;
641
+ const d = {
642
+ id: r,
643
+ type: "thnNode",
644
+ position: a,
645
+ data: {
646
+ type: o.type,
647
+ label: o.label,
648
+ category: o.category,
649
+ params: i,
650
+ inputs: o.inputs,
651
+ outputs: o.outputs,
652
+ hasError: !1
653
+ }
654
+ };
655
+ n({ nodes: [...e().nodes, d], selectedNodeId: r, dirty: !0 }), e().log(`导入 ${t} (${o.type})`);
656
+ },
657
+ train: async (t = 20, a = 1e-3) => {
658
+ n({ status: "running", losses: [] }), e().log(`开始训练 (${t} 步, lr=${a})...`);
659
+ try {
660
+ const o = G(e().nodes, e().edges), r = await fetch("/api/train/stream", {
661
+ method: "POST",
662
+ headers: { "Content-Type": "application/json" },
663
+ body: JSON.stringify({ doc: o, steps: t, lr: a })
664
+ });
665
+ if (!r.body) throw new Error("响应无流 body");
666
+ const i = r.body.getReader(), d = new TextDecoder();
667
+ let h = "";
668
+ for (; ; ) {
669
+ const { done: m, value: u } = await i.read();
670
+ if (m) break;
671
+ h += d.decode(u, { stream: !0 });
672
+ const y = h.split(`
673
+ `);
674
+ h = y.pop() ?? "";
675
+ for (const l of y) {
676
+ const v = l.trim();
677
+ if (!v) continue;
678
+ let f;
679
+ try {
680
+ f = JSON.parse(v);
681
+ } catch {
682
+ continue;
683
+ }
684
+ if (f.error) {
685
+ e().log(`训练失败: ${f.error}`), e().pushToast("error", `训练失败: ${f.error}`), n({ status: "error" });
686
+ return;
687
+ }
688
+ typeof f.loss == "number" && n({ losses: [...e().losses, f.loss] }), f.done && n({ status: "success" });
689
+ }
690
+ }
691
+ const c = e().losses[e().losses.length - 1];
692
+ e().log(`训练完成,最终 loss=${(c == null ? void 0 : c.toFixed(4)) ?? "n/a"}`), n({ status: "success" });
693
+ } catch (o) {
694
+ n({ status: "error" }), e().log(`训练失败: ${o instanceof Error ? o.message : String(o)}`), e().pushToast("error", `训练失败: ${o instanceof Error ? o.message : String(o)}`);
695
+ }
696
+ },
697
+ newBlank: () => {
698
+ n({
699
+ nodes: [],
700
+ edges: [],
701
+ frames: [],
702
+ graphStack: [{ name: "root", path: "", nodes: [], edges: [] }],
703
+ selectedNodeId: null,
704
+ results: {},
705
+ status: "idle",
706
+ activeFileName: null,
707
+ activeFilePath: null,
708
+ dirty: !1
709
+ }), e().log("新建空白工作流");
710
+ },
711
+ setWelcomeOpen: (t) => n({ welcomeOpen: t }),
712
+ setPrefsOpen: (t) => n({ prefsOpen: t }),
713
+ setAboutOpen: (t) => n({ aboutOpen: t }),
714
+ setImportFolderOpen: (t) => n({ importFolderOpen: t }),
715
+ setFilePicker: (t, a) => n({ filePickerOpen: t, filePickerMode: a }),
716
+ bumpProject: () => n({ projectVersion: e().projectVersion + 1 }),
717
+ setCodeBuffer: (t) => n({ codeBuffer: t }),
718
+ pushToast: (t, a) => {
719
+ const o = ++rn;
720
+ n({ toasts: [...e().toasts, { id: o, kind: t, message: a }].slice(-6) }), t !== "error" && setTimeout(() => e().dismissToast(o), 6e3);
721
+ },
722
+ dismissToast: (t) => n({ toasts: e().toasts.filter((a) => a.id !== t) }),
723
+ log: (t) => {
724
+ const a = (/* @__PURE__ */ new Date()).toLocaleTimeString();
725
+ n({ logs: [...e().logs, `[${a}] ${t}`].slice(-80) });
726
+ }
727
+ })), Me = "entropia_riko_theme", fe = "entropia_riko_background";
728
+ function un() {
729
+ try {
730
+ const n = localStorage.getItem(Me);
731
+ return n === "light" || n === "dark" || n === "glass" ? n : "system";
732
+ } catch {
733
+ return "system";
734
+ }
735
+ }
736
+ function We(n) {
737
+ n === "system" ? delete document.documentElement.dataset.theme : document.documentElement.dataset.theme = n;
738
+ }
739
+ function Be(n) {
740
+ const e = document.body;
741
+ n ? (e.style.backgroundImage = `url("${n}")`, e.style.backgroundSize = "cover", e.style.backgroundPosition = "center") : (e.style.backgroundImage = "", e.style.backgroundSize = "", e.style.backgroundPosition = "");
742
+ }
743
+ function mn() {
744
+ try {
745
+ return localStorage.getItem(fe) ?? "";
746
+ } catch {
747
+ return "";
748
+ }
749
+ }
750
+ const q = Le((n) => ({
751
+ theme: un(),
752
+ setTheme: (e) => {
753
+ We(e);
754
+ try {
755
+ localStorage.setItem(Me, e);
756
+ } catch {
757
+ }
758
+ n({ theme: e });
759
+ },
760
+ backgroundImage: mn(),
761
+ setBackgroundImage: (e) => {
762
+ Be(e);
763
+ try {
764
+ e ? localStorage.setItem(fe, e) : localStorage.removeItem(fe);
765
+ } catch {
766
+ }
767
+ n({ backgroundImage: e });
768
+ }
769
+ }));
770
+ We(q.getState().theme);
771
+ Be(q.getState().backgroundImage);
772
+ function oe({
773
+ entries: n,
774
+ x: e,
775
+ y: t,
776
+ onClose: a,
777
+ searchable: o = !1,
778
+ maxWidth: r = 280
779
+ }) {
780
+ const [i, d] = F(""), [h, c] = F({}), m = X(() => {
781
+ const l = i.trim().toLowerCase(), v = l ? n.filter(
782
+ (b) => !b.separator && (b.label.toLowerCase().includes(l) || (b.description ?? "").toLowerCase().includes(l))
783
+ ) : n, f = {}, C = [];
784
+ for (const b of v) {
785
+ const P = b.category ?? "";
786
+ P in f || (f[P] = [], C.push(P)), f[P].push(b);
787
+ }
788
+ return { map: f, order: C };
789
+ }, [n, i]), { map: u, order: y } = m;
790
+ return De(
791
+ /* @__PURE__ */ p(H, { children: [
792
+ /* @__PURE__ */ s(
793
+ "div",
794
+ {
795
+ className: "popup-overlay",
796
+ onClick: a,
797
+ onContextMenu: (l) => {
798
+ l.preventDefault(), a();
799
+ }
800
+ }
801
+ ),
802
+ /* @__PURE__ */ p("div", { className: "popup-menu", style: { left: e, top: t, maxWidth: r }, children: [
803
+ o && /* @__PURE__ */ s("div", { className: "popup-search", children: /* @__PURE__ */ s(
804
+ "input",
805
+ {
806
+ autoFocus: !0,
807
+ placeholder: "Search…",
808
+ value: i,
809
+ onChange: (l) => d(l.target.value)
810
+ }
811
+ ) }),
812
+ /* @__PURE__ */ p("div", { className: "popup-list", children: [
813
+ y.map((l) => {
814
+ const v = !i && !h[l];
815
+ return /* @__PURE__ */ p("div", { className: "popup-category", children: [
816
+ l !== "" && /* @__PURE__ */ p(
817
+ "div",
818
+ {
819
+ className: "popup-cat-header",
820
+ onClick: () => c((f) => ({ ...f, [l]: !f[l] })),
821
+ children: [
822
+ /* @__PURE__ */ s("span", { className: "popup-cat-toggle", children: v ? "▸" : "▾" }),
823
+ /* @__PURE__ */ s("span", { className: "popup-cat-label", children: l }),
824
+ /* @__PURE__ */ s("span", { className: "popup-cat-count", children: u[l].length })
825
+ ]
826
+ }
827
+ ),
828
+ !v && u[l].map(
829
+ (f) => f.separator ? /* @__PURE__ */ s("div", { className: "popup-sep" }, f.id) : /* @__PURE__ */ p(
830
+ "div",
831
+ {
832
+ className: `popup-item ${f.danger ? "danger" : ""} ${f.disabled ? "disabled" : ""}`,
833
+ onClick: () => {
834
+ var C;
835
+ f.disabled || ((C = f.onSelect) == null || C.call(f), a());
836
+ },
837
+ children: [
838
+ /* @__PURE__ */ s("span", { className: "popup-item-label", children: f.label }),
839
+ f.description && /* @__PURE__ */ s("span", { className: "popup-item-desc", children: f.description })
840
+ ]
841
+ },
842
+ f.id
843
+ )
844
+ )
845
+ ] }, l);
846
+ }),
847
+ y.length === 0 && /* @__PURE__ */ s("div", { className: "popup-item disabled", children: "No matches" })
848
+ ] })
849
+ ] })
850
+ ] }),
851
+ document.body
852
+ );
853
+ }
854
+ let hn = 0;
855
+ const Ue = () => `area_${++hn}`, I = (n) => ({
856
+ kind: "leaf",
857
+ id: Ue(),
858
+ type: n
859
+ }), O = (n, e, t, a = 0.5) => ({ kind: "split", id: Ue(), direction: n, first: e, second: t, ratio: a });
860
+ function fn() {
861
+ const n = O("column", I("nodes"), I("files"), 0.72), e = O(
862
+ "row",
863
+ I("canvas"),
864
+ O("column", I("project"), I("inspector"), 0.3),
865
+ 0.68
866
+ ), t = O("row", n, e, 0.26), a = O("row", I("status"), I("loss"), 0.55);
867
+ return O("column", t, a, 0.82);
868
+ }
869
+ function J(n) {
870
+ const e = n.leftExtra ? O("column", I("nodes"), I(n.leftExtra), 0.7) : I("nodes");
871
+ let t = I(n.right);
872
+ n.rightTop && (t = O("column", I(n.rightTop), t, n.rightTopRatio ?? 0.28));
873
+ const a = O("row", I("canvas"), t, n.canvasRatio ?? 0.68), o = O("row", e, a, n.leftRatio ?? 0.26), r = n.bottom.length === 1 ? I(n.bottom[0]) : O("row", I(n.bottom[0]), I(n.bottom[1]), 0.55);
874
+ return O("column", o, r, n.colRatio ?? 0.84);
875
+ }
876
+ function gn() {
877
+ const n = O("column", I("nodes"), I("files"), 0.72), e = O("column", I("loss"), I("inspector"), 0.42), t = O("row", I("canvas"), e, 0.68), a = O("row", n, t, 0.24);
878
+ return O("column", a, I("status"), 0.86);
879
+ }
880
+ function yn() {
881
+ const n = O("column", I("nodes"), I("docs"), 0.65), e = O("column", I("loss"), I("inspector"), 0.5), t = O("row", I("canvas"), e, 0.7), a = O("row", n, t, 0.24);
882
+ return O("column", a, I("status"), 0.86);
883
+ }
884
+ function bn() {
885
+ const n = O("column", I("files"), I("project"), 0.62), e = O("column", I("docs"), I("inspector"), 0.45), t = O("row", I("code"), e, 0.7);
886
+ return O("row", n, t, 0.26);
887
+ }
888
+ function vn() {
889
+ const n = O("column", I("plugins"), I("docs"), 0.45), e = O("row", I("code"), I("status"), 0.78);
890
+ return O("row", n, e, 0.28);
891
+ }
892
+ function Nn() {
893
+ const n = O("column", I("nodes"), I("files"), 0.72), e = O("column", I("code"), I("inspector"), 0.52), t = O("row", I("canvas"), e, 0.6), a = O("row", n, t, 0.24);
894
+ return O("column", a, I("status"), 0.87);
895
+ }
896
+ function kn() {
897
+ const n = O("column", I("files"), I("project"), 0.6), e = O("column", I("code"), I("docs"), 0.55), t = O("row", I("canvas"), e, 0.64);
898
+ return O("row", n, t, 0.3);
899
+ }
900
+ const ge = [
901
+ // ---- primary project workspaces (the default tabs) ----
902
+ {
903
+ id: "layout",
904
+ label: "Layout",
905
+ category: "Project",
906
+ description: "Full IDE: nodes, assets, canvas, inspector, loss.",
907
+ build: fn
908
+ },
909
+ {
910
+ id: "train",
911
+ label: "Train",
912
+ category: "Project",
913
+ description: "train.py — canvas, live loss curve, inspector.",
914
+ build: gn
915
+ },
916
+ {
917
+ id: "eval",
918
+ label: "Eval",
919
+ category: "Project",
920
+ description: "eval.py — canvas, loss, docs for metrics.",
921
+ build: yn
922
+ },
923
+ {
924
+ id: "config",
925
+ label: "Config",
926
+ category: "Project",
927
+ description: "config.py — code editor + file manager + docs.",
928
+ build: bn
929
+ },
930
+ {
931
+ id: "utils",
932
+ label: "Utils",
933
+ category: "Project",
934
+ description: "utils.py — code editor + plugins.",
935
+ build: vn
936
+ },
937
+ {
938
+ id: "models",
939
+ label: "Models",
940
+ category: "Project",
941
+ description: "models/ — node library, model code, canvas.",
942
+ build: Nn
943
+ },
944
+ {
945
+ id: "datasets",
946
+ label: "Datasets",
947
+ category: "Project",
948
+ description: "datasets/ — asset library, dataset code, canvas.",
949
+ build: kn
950
+ },
951
+ // ---- extra workspaces (available from the "+" menu) ----
952
+ {
953
+ id: "code",
954
+ label: "Code",
955
+ category: "General",
956
+ description: "Big code editor + canvas for previewing exported PyTorch.",
957
+ build: () => J({ right: "code", bottom: ["status", "loss"], leftExtra: "files", canvasRatio: 0.6 })
958
+ },
959
+ {
960
+ id: "inference",
961
+ label: "Inference",
962
+ category: "General",
963
+ description: "Large canvas + inspector, no training.",
964
+ build: () => J({ right: "inspector", bottom: ["status", "docs"], leftExtra: "files", canvasRatio: 0.74 })
965
+ },
966
+ {
967
+ id: "tuning",
968
+ label: "Hyperparameter Tuning",
969
+ category: "Training",
970
+ description: "Loss + docs; inspector below.",
971
+ build: () => J({ right: "loss", bottom: ["inspector", "docs"], canvasRatio: 0.62 })
972
+ },
973
+ {
974
+ id: "mnist_studio",
975
+ label: "MNIST Studio",
976
+ category: "Vision",
977
+ description: "Handwriting pad above the inspector — draw a digit and infer.",
978
+ build: () => J({ right: "inspector", rightTop: "pad", bottom: ["status", "loss"], leftExtra: "files", canvasRatio: 0.58 })
979
+ },
980
+ {
981
+ id: "image_classifier",
982
+ label: "Image Classifier",
983
+ category: "Vision",
984
+ description: "Image previews + loss.",
985
+ build: () => J({ right: "inspector", bottom: ["status", "loss"], leftExtra: "files", canvasRatio: 0.68 })
986
+ },
987
+ {
988
+ id: "image_gen",
989
+ label: "Text → Image",
990
+ category: "Vision",
991
+ description: "Project explorer above inspector for Diffusers generation.",
992
+ build: () => J({ right: "inspector", rightTop: "project", bottom: ["status"], canvasRatio: 0.62 })
993
+ },
994
+ {
995
+ id: "detection",
996
+ label: "Object Detection",
997
+ category: "Vision",
998
+ description: "Canvas + inspector + docs.",
999
+ build: () => J({ right: "inspector", bottom: ["status", "docs"], leftExtra: "files", canvasRatio: 0.7 })
1000
+ },
1001
+ {
1002
+ id: "text_classifier",
1003
+ label: "Text Classifier",
1004
+ category: "Language",
1005
+ description: "Text previews + docs.",
1006
+ build: () => J({ right: "inspector", bottom: ["status", "docs"], leftExtra: "files", canvasRatio: 0.7 })
1007
+ },
1008
+ {
1009
+ id: "text_gen",
1010
+ label: "Text Gen",
1011
+ category: "Language",
1012
+ description: "Text preview for Transformers.",
1013
+ build: () => J({ right: "inspector", bottom: ["status"], rightTop: "project", canvasRatio: 0.68 })
1014
+ },
1015
+ {
1016
+ id: "embeddings",
1017
+ label: "Embeddings / JEPA",
1018
+ category: "Language",
1019
+ description: "Pretrained encoder embeddings.",
1020
+ build: () => J({ right: "inspector", bottom: ["status", "docs"], canvasRatio: 0.72 })
1021
+ }
1022
+ ], wn = (n, e, t) => Math.min(t, Math.max(e, n));
1023
+ function re(n, e, t) {
1024
+ return n.kind === "leaf" ? n.id === e ? O(t, n, I(n.type), 0.5) : n : {
1025
+ ...n,
1026
+ first: re(n.first, e, t),
1027
+ second: re(n.second, e, t)
1028
+ };
1029
+ }
1030
+ function ye(n, e) {
1031
+ return n.kind === "split" ? n.first.id === e ? n.second : n.second.id === e ? n.first : {
1032
+ ...n,
1033
+ first: ye(n.first, e),
1034
+ second: ye(n.second, e)
1035
+ } : n;
1036
+ }
1037
+ function be(n, e) {
1038
+ return n.kind === "split" ? n.first.id === e ? n.first : n.second.id === e ? n.second : {
1039
+ ...n,
1040
+ first: be(n.first, e),
1041
+ second: be(n.second, e)
1042
+ } : n;
1043
+ }
1044
+ function ve(n, e) {
1045
+ if (n.kind === "split") {
1046
+ if (n.first.id === e) return n.second.id;
1047
+ if (n.second.id === e) return n.first.id;
1048
+ const t = ve(n.first, e);
1049
+ return t !== null ? t : ve(n.second, e);
1050
+ }
1051
+ return null;
1052
+ }
1053
+ function ie(n, e, t) {
1054
+ return n.kind === "split" ? n.id === e ? { ...n, ratio: wn(n.ratio + t, 0.08, 0.92) } : {
1055
+ ...n,
1056
+ first: ie(n.first, e, t),
1057
+ second: ie(n.second, e, t)
1058
+ } : n;
1059
+ }
1060
+ function Ne(n, e, t) {
1061
+ return n.kind === "leaf" ? n.id === e ? { ...n, type: t } : n : {
1062
+ ...n,
1063
+ first: Ne(n.first, e, t),
1064
+ second: Ne(n.second, e, t)
1065
+ };
1066
+ }
1067
+ function ke(n) {
1068
+ return n.kind === "leaf" ? 1 : ke(n.first) + ke(n.second);
1069
+ }
1070
+ function _n({
1071
+ workspaces: n,
1072
+ activeId: e,
1073
+ onSwitch: t,
1074
+ onAdd: a,
1075
+ onRemove: o,
1076
+ onRename: r,
1077
+ onDuplicate: i,
1078
+ onMove: d,
1079
+ onReorder: h,
1080
+ presets: c
1081
+ }) {
1082
+ const [m, u] = F(!1), [y, l] = F(null), [v, f] = F(null), [C, b] = F(!1), [P, _] = F(null), $ = M(null);
1083
+ B(() => {
1084
+ const j = $.current;
1085
+ if (!j) return;
1086
+ const R = () => b(j.scrollWidth > j.clientWidth + 1);
1087
+ return R(), window.addEventListener("resize", R), () => window.removeEventListener("resize", R);
1088
+ }, [n]);
1089
+ const A = n.length > 1, w = X(
1090
+ () => (c ?? ge).map((j) => ({
1091
+ id: j.id,
1092
+ label: j.label,
1093
+ description: j.description,
1094
+ category: j.category,
1095
+ onSelect: () => a(j.id)
1096
+ })),
1097
+ [a]
1098
+ ), x = (j) => {
1099
+ const R = j.currentTarget.getBoundingClientRect();
1100
+ l({ x: R.left, y: R.bottom + 6 }), u(!0);
1101
+ }, k = (j) => {
1102
+ const R = n.find((V) => V.id === j), K = window.prompt("Rename workspace", (R == null ? void 0 : R.name) ?? "");
1103
+ K && K.trim() && r(j, K.trim());
1104
+ }, L = (j) => [
1105
+ { id: "rename", label: "Rename", onSelect: () => k(j) },
1106
+ { id: "duplicate", label: "Duplicate", onSelect: () => i(j) },
1107
+ { id: "sep1", label: "", separator: !0 },
1108
+ { id: "left", label: "← Move Left", onSelect: () => d(j, -1) },
1109
+ { id: "right", label: "Move Right →", onSelect: () => d(j, 1) },
1110
+ { id: "sep2", label: "", separator: !0 },
1111
+ ...A ? [{ id: "close", label: "Close", danger: !0, onSelect: () => o(j) }] : []
1112
+ ];
1113
+ return /* @__PURE__ */ p("div", { className: "workspace-tabs-wrap", children: [
1114
+ /* @__PURE__ */ s("div", { ref: $, className: `workspace-tabs ${C ? "overflowing" : ""}`, children: n.map((j) => /* @__PURE__ */ p(
1115
+ "div",
1116
+ {
1117
+ className: `workspace-tab ${j.id === e ? "active" : ""} ${j.id === P ? "dragging" : ""}`,
1118
+ draggable: !0,
1119
+ onDragStart: (R) => {
1120
+ _(j.id), R.dataTransfer.effectAllowed = "move";
1121
+ },
1122
+ onDragOver: (R) => {
1123
+ R.preventDefault(), R.dataTransfer.dropEffect = "move";
1124
+ },
1125
+ onDrop: (R) => {
1126
+ R.preventDefault(), P && P !== j.id && h(P, j.id), _(null);
1127
+ },
1128
+ onDragEnd: () => _(null),
1129
+ onClick: () => t(j.id),
1130
+ onContextMenu: (R) => {
1131
+ R.preventDefault(), f({ id: j.id, x: R.clientX, y: R.clientY });
1132
+ },
1133
+ children: [
1134
+ /* @__PURE__ */ s("span", { children: j.name }),
1135
+ A && /* @__PURE__ */ s(
1136
+ "span",
1137
+ {
1138
+ className: "workspace-tab-close",
1139
+ title: "Close workspace",
1140
+ onClick: (R) => {
1141
+ R.stopPropagation(), o(j.id);
1142
+ },
1143
+ children: "✕"
1144
+ }
1145
+ )
1146
+ ]
1147
+ },
1148
+ j.id
1149
+ )) }),
1150
+ /* @__PURE__ */ s("button", { className: "workspace-add", title: "Add workspace", onClick: x, children: "+" }),
1151
+ m && y && /* @__PURE__ */ s(oe, { entries: w, x: y.x, y: y.y, onClose: () => u(!1) }),
1152
+ v && /* @__PURE__ */ s(
1153
+ oe,
1154
+ {
1155
+ entries: L(v.id),
1156
+ x: v.x,
1157
+ y: v.y,
1158
+ onClose: () => f(null)
1159
+ }
1160
+ )
1161
+ ] });
1162
+ }
1163
+ function ce({
1164
+ title: n,
1165
+ onClose: e,
1166
+ x: t = 140,
1167
+ y: a = 90,
1168
+ width: o = 520,
1169
+ zIndex: r = 1e3,
1170
+ children: i
1171
+ }) {
1172
+ const [d, h] = F({ x: t, y: a }), c = M(null), m = (u) => {
1173
+ c.current = { dx: u.clientX - d.x, dy: u.clientY - d.y };
1174
+ const y = (v) => {
1175
+ c.current && h({ x: v.clientX - c.current.dx, y: v.clientY - c.current.dy });
1176
+ }, l = () => {
1177
+ c.current = null, document.removeEventListener("mousemove", y), document.removeEventListener("mouseup", l);
1178
+ };
1179
+ document.addEventListener("mousemove", y), document.addEventListener("mouseup", l);
1180
+ };
1181
+ return /* @__PURE__ */ p("div", { className: "float-window", style: { left: d.x, top: d.y, width: o, zIndex: r }, children: [
1182
+ /* @__PURE__ */ p("div", { className: "float-titlebar", onMouseDown: m, children: [
1183
+ /* @__PURE__ */ s("span", { className: "float-title", children: n }),
1184
+ /* @__PURE__ */ s("button", { className: "float-close", onClick: e, children: "✕" })
1185
+ ] }),
1186
+ /* @__PURE__ */ s("div", { className: "float-body", children: i })
1187
+ ] });
1188
+ }
1189
+ let ne = null, Ge = "";
1190
+ function Q(n, e) {
1191
+ return Ge = (e == null ? void 0 : e.defaultName) ?? "", g.getState().setFilePicker(!0, n), new Promise((t) => {
1192
+ ne = t;
1193
+ });
1194
+ }
1195
+ function ee(n) {
1196
+ g.getState().setFilePicker(!1, "import"), ne == null || ne(n), ne = null;
1197
+ }
1198
+ const He = "entropia_riko_recent_folders";
1199
+ function we() {
1200
+ try {
1201
+ const n = localStorage.getItem(He), e = n ? JSON.parse(n) : [];
1202
+ return Array.isArray(e) ? e.filter((t) => typeof t == "string") : [];
1203
+ } catch {
1204
+ return [];
1205
+ }
1206
+ }
1207
+ function xn(n) {
1208
+ try {
1209
+ const e = [n, ...we().filter((t) => t !== n)].slice(0, 8);
1210
+ localStorage.setItem(He, JSON.stringify(e));
1211
+ } catch {
1212
+ }
1213
+ }
1214
+ function Sn() {
1215
+ const n = g((k) => k.filePickerOpen), e = g((k) => k.filePickerMode), t = g((k) => k.pushToast), [a, o] = F("~"), [r, i] = F([]), [d, h] = F(null), [c, m] = F("~"), [u, y] = F(we()), [l, v] = F([]), [f, C] = F([]), [b, P] = F(""), _ = async (k, L = !0) => {
1216
+ const R = await (await fetch(`/api/fs/list?path=${encodeURIComponent(k)}`)).json();
1217
+ R.status === "success" ? (L && (v((K) => [...K, a]), C([])), o(R.path), i(R.entries ?? []), h(null), xn(R.path), y(we())) : t("error", R.error ?? "list failed");
1218
+ };
1219
+ if (B(() => {
1220
+ n && (e === "save" && P(Ge), _("~", !1), fetch("/api/project/tree").then((k) => k.json()).then((k) => {
1221
+ typeof k.root == "string" && m(k.root);
1222
+ }).catch(() => {
1223
+ }));
1224
+ }, [n]), !n) return null;
1225
+ const $ = [
1226
+ { label: "Home", path: "~" },
1227
+ { label: "Desktop", path: "~/Desktop" },
1228
+ { label: "Documents", path: "~/Documents" },
1229
+ { label: "Downloads", path: "~/Downloads" },
1230
+ { label: "Working Folder", path: c }
1231
+ ], A = () => {
1232
+ const k = l[l.length - 1];
1233
+ k && (v(l.slice(0, -1)), C((L) => [a, ...L]), _(k, !1));
1234
+ }, w = () => {
1235
+ const k = f[0];
1236
+ k && (C(f.slice(1)), v((L) => [...L, a]), _(k, !1));
1237
+ }, x = () => {
1238
+ if (e === "save") {
1239
+ const k = b.trim() || "untitled";
1240
+ return a.endsWith("/") ? a + k : a + "/" + k;
1241
+ }
1242
+ return e === "export" ? d && d.type === "dir" ? d.path : a : d ? d.path : a;
1243
+ };
1244
+ return /* @__PURE__ */ s(
1245
+ ce,
1246
+ {
1247
+ title: e === "import" ? "Import File / Folder" : e === "export" ? "Export To Folder" : "Save File",
1248
+ onClose: () => ee(null),
1249
+ width: 780,
1250
+ zIndex: 1002,
1251
+ children: /* @__PURE__ */ p("div", { className: "picker-inner", children: [
1252
+ /* @__PURE__ */ p("div", { className: "picker-side", children: [
1253
+ /* @__PURE__ */ s("div", { className: "picker-side-title", children: "Quick access" }),
1254
+ $.map((k) => /* @__PURE__ */ p("div", { className: "picker-side-item", onClick: () => _(k.path), children: [
1255
+ /* @__PURE__ */ s("span", { className: "picker-side-icon", children: "▦" }),
1256
+ k.label
1257
+ ] }, k.label)),
1258
+ /* @__PURE__ */ s("div", { className: "picker-side-title", style: { marginTop: 12 }, children: "Recent" }),
1259
+ u.map((k) => /* @__PURE__ */ p("div", { className: "picker-side-item", title: k, onClick: () => _(k), children: [
1260
+ /* @__PURE__ */ s("span", { className: "picker-side-icon", children: "🕘" }),
1261
+ /* @__PURE__ */ s("span", { className: "picker-side-name", children: k })
1262
+ ] }, k))
1263
+ ] }),
1264
+ /* @__PURE__ */ p("div", { className: "picker-main", children: [
1265
+ /* @__PURE__ */ p("div", { className: "picker-topbar", children: [
1266
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: A, title: "Back", children: "←" }),
1267
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: w, title: "Forward", children: "→" }),
1268
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: () => _(a, !1), title: "Refresh", children: "↻" }),
1269
+ /* @__PURE__ */ s(
1270
+ "input",
1271
+ {
1272
+ className: "prefs-input",
1273
+ value: a,
1274
+ onChange: (k) => o(k.target.value),
1275
+ onKeyDown: (k) => {
1276
+ k.key === "Enter" && _(a);
1277
+ }
1278
+ }
1279
+ )
1280
+ ] }),
1281
+ /* @__PURE__ */ p("div", { className: "picker-list", children: [
1282
+ r.map((k) => /* @__PURE__ */ p(
1283
+ "div",
1284
+ {
1285
+ className: `picker-row ${(d == null ? void 0 : d.path) === k.path ? "sel" : ""}`,
1286
+ onClick: () => h(k),
1287
+ onDoubleClick: () => k.type === "dir" && _(k.path),
1288
+ children: [
1289
+ /* @__PURE__ */ s("span", { className: "explorer-icon", children: k.type === "dir" ? "▦" : "▤" }),
1290
+ /* @__PURE__ */ s("span", { className: "explorer-name", children: k.name }),
1291
+ /* @__PURE__ */ s("span", { className: "explorer-size", children: k.type === "file" ? `${Math.round(k.size / 1024)} KB` : "" })
1292
+ ]
1293
+ },
1294
+ k.path
1295
+ )),
1296
+ r.length === 0 && /* @__PURE__ */ s("div", { className: "welcome-empty", children: "Empty folder." })
1297
+ ] }),
1298
+ /* @__PURE__ */ p("div", { className: "picker-bottombar", children: [
1299
+ /* @__PURE__ */ s("span", { className: "picker-filename-label", children: "File name:" }),
1300
+ /* @__PURE__ */ s(
1301
+ "input",
1302
+ {
1303
+ className: "prefs-input",
1304
+ value: e === "save" ? b : (d == null ? void 0 : d.name) ?? "",
1305
+ onChange: (k) => {
1306
+ e === "save" && P(k.target.value);
1307
+ },
1308
+ onKeyDown: (k) => {
1309
+ k.key === "Enter" && ee(x()), k.key === "Escape" && ee(null);
1310
+ },
1311
+ readOnly: e !== "save",
1312
+ placeholder: e === "save" ? "name.riko / name.ric" : "(select a file or folder)"
1313
+ }
1314
+ ),
1315
+ /* @__PURE__ */ s("button", { className: "btn btn-primary", onClick: () => ee(x()), children: "Save" }),
1316
+ /* @__PURE__ */ s("button", { className: "btn", onClick: () => ee(null), children: "Cancel" })
1317
+ ] })
1318
+ ] })
1319
+ ] })
1320
+ }
1321
+ );
1322
+ }
1323
+ const Te = ["/brand/logo.ico", "/brand/logo.png", "/brand/logo.svg"];
1324
+ function de({
1325
+ className: n,
1326
+ alt: e = "Entropia Riko logo",
1327
+ title: t
1328
+ }) {
1329
+ const [a, o] = F(0);
1330
+ return /* @__PURE__ */ s(
1331
+ "img",
1332
+ {
1333
+ className: n,
1334
+ src: Te[a],
1335
+ alt: e,
1336
+ title: t,
1337
+ onError: () => o((r) => Math.min(r + 1, Te.length - 1))
1338
+ }
1339
+ );
1340
+ }
1341
+ function Cn({
1342
+ workspaces: n,
1343
+ activeWorkspaceId: e,
1344
+ onSwitchWorkspace: t,
1345
+ onAddWorkspace: a,
1346
+ onRemoveWorkspace: o,
1347
+ onRenameWorkspace: r,
1348
+ onDuplicateWorkspace: i,
1349
+ onMoveWorkspace: d,
1350
+ onReorderWorkspace: h
1351
+ }) {
1352
+ const [c, m] = F(null), u = g((T) => T.status), y = g((T) => T.run), l = g((T) => T.train);
1353
+ g((T) => T.save), g((T) => T.exportRiko), g((T) => T.exportBinary);
1354
+ const v = g((T) => T.load), f = g((T) => T.exportPython), C = g((T) => T.exportKeras), b = g((T) => T.newWorkflow), P = g((T) => T.addNode), _ = g((T) => T.pushToast), $ = q((T) => T.theme), A = q((T) => T.setTheme), w = M(null), x = u === "running", k = async () => {
1355
+ const T = await Q("import");
1356
+ if (!T) return;
1357
+ const E = await (await fetch("/api/fs/import", {
1358
+ method: "POST",
1359
+ headers: { "Content-Type": "application/json" },
1360
+ body: JSON.stringify({ src: T })
1361
+ })).json();
1362
+ E.status === "success" ? (_("success", `Imported ${E.path}`), g.getState().bumpProject()) : _("error", E.error ?? "import failed");
1363
+ }, L = async () => {
1364
+ const T = g.getState().activeFilePath;
1365
+ if (!T) {
1366
+ _("error", "No open file to export");
1367
+ return;
1368
+ }
1369
+ const N = await Q("export");
1370
+ if (!N) return;
1371
+ const S = await (await fetch("/api/fs/export", {
1372
+ method: "POST",
1373
+ headers: { "Content-Type": "application/json" },
1374
+ body: JSON.stringify({ src: T, dest: N })
1375
+ })).json();
1376
+ S.status === "success" ? _("success", `Exported to ${S.path}`) : _("error", S.error ?? "export failed");
1377
+ }, j = async (T) => {
1378
+ const E = (g.getState().activeFileName ?? "untitled") + (T === "binary" ? ".ric" : ".riko"), S = await Q("save", { defaultName: E });
1379
+ S && await g.getState().saveToPath(S, T);
1380
+ };
1381
+ return /* @__PURE__ */ p("div", { className: "menubar", onClick: (T) => T.stopPropagation(), children: [
1382
+ /* @__PURE__ */ p(
1383
+ "div",
1384
+ {
1385
+ className: `menu-item app-logo-item ${c === "__app__" ? "active" : ""}`,
1386
+ onClick: () => m(c === "__app__" ? null : "__app__"),
1387
+ title: "Entropia Riko",
1388
+ children: [
1389
+ /* @__PURE__ */ s(de, { className: "menubar-logo", alt: "Entropia Riko" }),
1390
+ c === "__app__" && /* @__PURE__ */ p("div", { className: "dropdown app-menu", onClick: (T) => T.stopPropagation(), children: [
1391
+ /* @__PURE__ */ s(
1392
+ "div",
1393
+ {
1394
+ className: "dropdown-item",
1395
+ onClick: () => {
1396
+ g.getState().setAboutOpen(!0), m(null);
1397
+ },
1398
+ children: "About Entropia Riko"
1399
+ }
1400
+ ),
1401
+ /* @__PURE__ */ s(
1402
+ "div",
1403
+ {
1404
+ className: "dropdown-item",
1405
+ onClick: () => {
1406
+ g.getState().setWelcomeOpen(!0), m(null);
1407
+ },
1408
+ children: "Welcome Screen"
1409
+ }
1410
+ ),
1411
+ /* @__PURE__ */ s(
1412
+ "div",
1413
+ {
1414
+ className: "dropdown-item",
1415
+ onClick: () => {
1416
+ g.getState().setPrefsOpen(!0), m(null);
1417
+ },
1418
+ children: "Preferences…"
1419
+ }
1420
+ )
1421
+ ] })
1422
+ ]
1423
+ }
1424
+ ),
1425
+ [
1426
+ {
1427
+ label: "File",
1428
+ items: [
1429
+ { label: "New Workflow", onClick: () => b("Untitled") },
1430
+ { label: "New Project…", onClick: async () => {
1431
+ const T = await Q("save", { defaultName: "my_project" });
1432
+ if (!T) return;
1433
+ const E = await (await fetch("/api/project/new", {
1434
+ method: "POST",
1435
+ headers: { "Content-Type": "application/json" },
1436
+ body: JSON.stringify({ dir: T })
1437
+ })).json();
1438
+ E.status === "success" ? (g.getState().bumpProject(), _("success", `Created project ${E.root}`)) : _("error", E.error ?? "new project failed");
1439
+ } },
1440
+ { label: "Save .riko…", onClick: () => j("ascii"), disabled: x },
1441
+ { label: "Save .ric…", onClick: () => j("binary"), disabled: x },
1442
+ {
1443
+ label: "Import / Export",
1444
+ submenu: [
1445
+ { label: "Import…", onClick: k },
1446
+ { label: "Export…", onClick: L },
1447
+ { label: "Import Working Folder…", onClick: () => g.getState().setImportFolderOpen(!0) }
1448
+ ]
1449
+ },
1450
+ {
1451
+ label: "Export Code",
1452
+ submenu: [
1453
+ { label: "Export .py (PyTorch)", onClick: f, disabled: x },
1454
+ { label: "Export .py (Keras/TF)", onClick: C, disabled: x },
1455
+ { label: "Export Project… (multi-file)", onClick: async () => {
1456
+ const T = g.getState().activeFileName ?? "graph_project", N = await Q("save", { defaultName: T });
1457
+ N && await g.getState().exportProjectTo(N);
1458
+ }, disabled: x }
1459
+ ]
1460
+ },
1461
+ { label: "Preferences…", onClick: () => g.getState().setPrefsOpen(!0) }
1462
+ ]
1463
+ },
1464
+ {
1465
+ label: "Run",
1466
+ items: [
1467
+ { label: "Inference", onClick: y, disabled: x },
1468
+ { label: "Train (20 steps)", onClick: () => l(20, 1e-3), disabled: x },
1469
+ { label: "Train (100 steps)", onClick: () => l(100, 1e-3), disabled: x }
1470
+ ]
1471
+ },
1472
+ {
1473
+ label: "Data",
1474
+ items: [
1475
+ { label: "MNIST Loader", onClick: () => P("mnist_loader", { x: 200, y: 150 }) },
1476
+ { label: "CIFAR10 Loader", onClick: () => P("cifar10_loader", { x: 200, y: 150 }) },
1477
+ { label: "CSV Loader", onClick: () => P("csv_loader", { x: 200, y: 150 }) },
1478
+ { label: "Image Folder Loader", onClick: () => P("image_folder_loader", { x: 200, y: 150 }) },
1479
+ { label: "Tensor File Loader", onClick: () => P("tensor_file_loader", { x: 200, y: 150 }) },
1480
+ { label: "Generic Data Loader", onClick: () => P("dataloader", { x: 200, y: 150 }) }
1481
+ ]
1482
+ },
1483
+ {
1484
+ label: "View",
1485
+ items: [
1486
+ { label: `Theme: Light ${$ === "light" ? "✓" : ""}`, onClick: () => A("light") },
1487
+ { label: `Theme: Dark ${$ === "dark" ? "✓" : ""}`, onClick: () => A("dark") },
1488
+ { label: `Theme: System ${$ === "system" ? "✓" : ""}`, onClick: () => A("system") },
1489
+ { label: `Theme: Liquid Glass ${$ === "glass" ? "✓" : ""}`, onClick: () => A("glass") }
1490
+ ]
1491
+ },
1492
+ {
1493
+ label: "Help",
1494
+ items: [
1495
+ { label: "Welcome Screen", onClick: () => g.getState().setWelcomeOpen(!0) },
1496
+ { label: "About Entropia Riko", onClick: () => g.getState().setAboutOpen(!0) }
1497
+ ]
1498
+ }
1499
+ ].map((T) => /* @__PURE__ */ p(
1500
+ "div",
1501
+ {
1502
+ className: `menu-item ${c === T.label ? "active" : ""}`,
1503
+ onClick: () => m(c === T.label ? null : T.label),
1504
+ children: [
1505
+ T.label,
1506
+ c === T.label && /* @__PURE__ */ s("div", { className: "dropdown", onClick: (N) => N.stopPropagation(), children: T.items.map(
1507
+ (N, E) => N.submenu ? /* @__PURE__ */ p("div", { className: "dropdown-item has-submenu", children: [
1508
+ /* @__PURE__ */ p("span", { className: "submenu-label", children: [
1509
+ N.label,
1510
+ " ▸"
1511
+ ] }),
1512
+ /* @__PURE__ */ s("div", { className: "submenu", children: N.submenu.map((S, D) => /* @__PURE__ */ s(
1513
+ "div",
1514
+ {
1515
+ className: `dropdown-item ${S.disabled ? "disabled" : ""}`,
1516
+ onClick: () => {
1517
+ var W;
1518
+ S.disabled || ((W = S.onClick) == null || W.call(S), m(null));
1519
+ },
1520
+ children: S.label
1521
+ },
1522
+ D
1523
+ )) })
1524
+ ] }, E) : /* @__PURE__ */ s(
1525
+ "div",
1526
+ {
1527
+ className: `dropdown-item ${N.disabled ? "disabled" : ""}`,
1528
+ onClick: () => {
1529
+ var S;
1530
+ N.disabled || ((S = N.onClick) == null || S.call(N), m(null));
1531
+ },
1532
+ children: N.label
1533
+ },
1534
+ E
1535
+ )
1536
+ ) })
1537
+ ]
1538
+ },
1539
+ T.label
1540
+ )),
1541
+ /* @__PURE__ */ s("div", { className: "menubar-sep" }),
1542
+ /* @__PURE__ */ s(
1543
+ _n,
1544
+ {
1545
+ workspaces: n,
1546
+ activeId: e,
1547
+ onSwitch: t,
1548
+ onAdd: a,
1549
+ onRemove: o,
1550
+ onRename: r,
1551
+ onDuplicate: i,
1552
+ onMove: d,
1553
+ onReorder: h
1554
+ }
1555
+ ),
1556
+ /* @__PURE__ */ s(
1557
+ "input",
1558
+ {
1559
+ ref: w,
1560
+ type: "file",
1561
+ accept: ".riko,.ric,.json,application/json,application/octet-stream",
1562
+ style: { display: "none" },
1563
+ onChange: (T) => {
1564
+ var E;
1565
+ const N = (E = T.target.files) == null ? void 0 : E[0];
1566
+ N && v(N), T.target.value = "";
1567
+ }
1568
+ }
1569
+ ),
1570
+ /* @__PURE__ */ s("div", { className: "spacer" }),
1571
+ /* @__PURE__ */ p("span", { className: `status-pill ${u}`, children: [
1572
+ u,
1573
+ u !== "idle" && /* @__PURE__ */ s(
1574
+ "button",
1575
+ {
1576
+ className: "status-pill-close",
1577
+ title: "Dismiss",
1578
+ onClick: () => g.setState({ status: "idle" }),
1579
+ children: "✕"
1580
+ }
1581
+ )
1582
+ ] })
1583
+ ] });
1584
+ }
1585
+ function Tn({ title: n, appName: e = "Riko", version: t }) {
1586
+ const a = g((r) => r.dirty), o = g((r) => r.activeFileName);
1587
+ return /* @__PURE__ */ p("div", { className: "titlebar", children: [
1588
+ /* @__PURE__ */ s("span", { className: `titlebar-dirty ${a ? "visible" : ""}`, "aria-hidden": !a, children: "*" }),
1589
+ /* @__PURE__ */ s("span", { className: "titlebar-file", children: n ?? o ?? "Untitled" }),
1590
+ /* @__PURE__ */ p("span", { className: "titlebar-app", children: [
1591
+ " - ",
1592
+ e
1593
+ ] }),
1594
+ /* @__PURE__ */ s("span", { className: "spacer" }),
1595
+ /* @__PURE__ */ p("span", { className: "titlebar-version", children: [
1596
+ "v",
1597
+ t ?? te
1598
+ ] })
1599
+ ] });
1600
+ }
1601
+ function Ke(n, e) {
1602
+ const t = M(null);
1603
+ return (o) => {
1604
+ o.preventDefault(), t.current = { x: o.clientX, y: o.clientY };
1605
+ const r = (d) => {
1606
+ t.current && (n(d.clientX - t.current.x, d.clientY - t.current.y), t.current = { x: d.clientX, y: d.clientY });
1607
+ }, i = () => {
1608
+ document.removeEventListener("mousemove", r), document.removeEventListener("mouseup", i), document.body.style.cursor = "";
1609
+ };
1610
+ document.body.style.cursor = e, document.addEventListener("mousemove", r), document.addEventListener("mouseup", i);
1611
+ };
1612
+ }
1613
+ function Pn({ onDrag: n }) {
1614
+ const e = M(null), t = Ke((a) => {
1615
+ var r, i;
1616
+ const o = ((i = (r = e.current) == null ? void 0 : r.parentElement) == null ? void 0 : i.clientWidth) ?? 1;
1617
+ n(a, o);
1618
+ }, "col-resize");
1619
+ return /* @__PURE__ */ s("div", { ref: e, className: "splitter-v", onMouseDown: t });
1620
+ }
1621
+ function Fn({ onDrag: n }) {
1622
+ const e = M(null), t = Ke((a, o) => {
1623
+ var i, d;
1624
+ const r = ((d = (i = e.current) == null ? void 0 : i.parentElement) == null ? void 0 : d.clientHeight) ?? 1;
1625
+ n(o, r);
1626
+ }, "row-resize");
1627
+ return /* @__PURE__ */ s("div", { ref: e, className: "splitter-h", onMouseDown: t });
1628
+ }
1629
+ function En() {
1630
+ const [n, e] = F(""), t = g((d) => d.nodeDefs), a = g((d) => d.addNode), o = g((d) => d.addFrame), r = X(() => {
1631
+ const d = t.filter(
1632
+ (c) => c.label.toLowerCase().includes(n.toLowerCase()) || c.type.toLowerCase().includes(n.toLowerCase())
1633
+ ), h = {};
1634
+ for (const c of d) {
1635
+ const m = c.category || "Other";
1636
+ (h[m] ?? (h[m] = [])).push(c);
1637
+ }
1638
+ return h;
1639
+ }, [t, n]), i = Object.keys(r).sort();
1640
+ return /* @__PURE__ */ p("div", { className: "node-library", children: [
1641
+ /* @__PURE__ */ p("div", { className: "search", children: [
1642
+ /* @__PURE__ */ s(
1643
+ "input",
1644
+ {
1645
+ placeholder: "Search nodes…",
1646
+ value: n,
1647
+ onChange: (d) => e(d.target.value)
1648
+ }
1649
+ ),
1650
+ /* @__PURE__ */ s(
1651
+ "button",
1652
+ {
1653
+ className: "btn btn-sm",
1654
+ style: { width: "100%", marginTop: 4 },
1655
+ onClick: () => o("Frame", { x: 200, y: 120 }),
1656
+ children: "+ Frame"
1657
+ }
1658
+ )
1659
+ ] }),
1660
+ /* @__PURE__ */ p("div", { className: "node-list", children: [
1661
+ i.map((d) => /* @__PURE__ */ s(
1662
+ In,
1663
+ {
1664
+ category: d,
1665
+ nodes: r[d],
1666
+ onAdd: (h) => a(h, { x: 200 + Math.random() * 200, y: 120 + Math.random() * 100 })
1667
+ },
1668
+ d
1669
+ )),
1670
+ i.length === 0 && /* @__PURE__ */ s("div", { className: "category", style: { padding: 8 }, children: "No matching nodes" }),
1671
+ t.length < 10 && /* @__PURE__ */ p("div", { style: { padding: "8px", color: "var(--color-error)", fontSize: 12, lineHeight: 1.5 }, children: [
1672
+ "⚠ 仅 ",
1673
+ t.length,
1674
+ " 个节点。请确保 API server 已启动。"
1675
+ ] })
1676
+ ] })
1677
+ ] });
1678
+ }
1679
+ function In({
1680
+ category: n,
1681
+ nodes: e,
1682
+ onAdd: t
1683
+ }) {
1684
+ const [a, o] = F(!0);
1685
+ return /* @__PURE__ */ p("div", { className: "node-category", children: [
1686
+ /* @__PURE__ */ p(
1687
+ "div",
1688
+ {
1689
+ className: "category-header",
1690
+ onClick: () => o(!a),
1691
+ children: [
1692
+ /* @__PURE__ */ s("span", { className: "category-toggle", children: a ? "▼" : "▶" }),
1693
+ /* @__PURE__ */ s("span", { className: "category-name", children: n }),
1694
+ /* @__PURE__ */ s("span", { className: "category-count", children: e.length })
1695
+ ]
1696
+ }
1697
+ ),
1698
+ a && e.map((r) => /* @__PURE__ */ s(
1699
+ "div",
1700
+ {
1701
+ className: "node-item",
1702
+ onClick: () => t(r.type),
1703
+ children: /* @__PURE__ */ s("div", { className: "name", children: r.label })
1704
+ },
1705
+ r.type
1706
+ ))
1707
+ ] });
1708
+ }
1709
+ function On() {
1710
+ const n = g((u) => u.fileList), e = g((u) => u.activeFileName), t = g((u) => u.refreshFiles), a = g((u) => u.openFile), o = g((u) => u.saveFileToDisk), r = g((u) => u.importFileInline), i = g((u) => u.previewAssetCode), [d, h] = F("");
1711
+ B(() => {
1712
+ t();
1713
+ }, [t]);
1714
+ const c = X(() => {
1715
+ const u = d.trim().toLowerCase(), y = {}, l = [];
1716
+ for (const v of n) {
1717
+ if (u && !v.name.toLowerCase().includes(u) && !v.path.toLowerCase().includes(u)) continue;
1718
+ const f = v.path.includes("/") ? v.path.slice(0, v.path.indexOf("/")) : "(root)";
1719
+ f in y || (y[f] = [], l.push(f)), y[f].push(v);
1720
+ }
1721
+ return l.sort(), { map: y, order: l };
1722
+ }, [n, d]);
1723
+ return /* @__PURE__ */ p("div", { className: "file-manager", children: [
1724
+ /* @__PURE__ */ p("div", { className: "panel-head", children: [
1725
+ /* @__PURE__ */ s("span", { className: "panel-head-title", children: "Asset Library" }),
1726
+ /* @__PURE__ */ s("span", { className: "spacer" }),
1727
+ /* @__PURE__ */ s("button", { className: "panel-btn", title: "Save current graph", onClick: () => {
1728
+ const u = window.prompt("Save current graph as", e ?? "untitled");
1729
+ u && u.trim() && o(u.trim());
1730
+ }, children: "💾" }),
1731
+ /* @__PURE__ */ s("button", { className: "panel-btn", title: "Refresh", onClick: t, children: "↻" })
1732
+ ] }),
1733
+ /* @__PURE__ */ s("div", { className: "asset-search", children: /* @__PURE__ */ s(
1734
+ "input",
1735
+ {
1736
+ placeholder: "Search assets…",
1737
+ value: d,
1738
+ onChange: (u) => h(u.target.value)
1739
+ }
1740
+ ) }),
1741
+ /* @__PURE__ */ p("div", { className: "fm-tree", children: [
1742
+ c.order.map((u) => /* @__PURE__ */ p("div", { className: "fm-group", children: [
1743
+ /* @__PURE__ */ s("div", { className: "fm-group-header", children: u }),
1744
+ c.map[u].map((y) => /* @__PURE__ */ p(
1745
+ "div",
1746
+ {
1747
+ className: `fm-file ${e === y.name ? "active" : ""}`,
1748
+ onClick: () => a(y.path),
1749
+ title: y.path,
1750
+ children: [
1751
+ /* @__PURE__ */ s("span", { className: "fm-toggle", children: "▤" }),
1752
+ /* @__PURE__ */ s("span", { className: "fm-name", children: y.name }),
1753
+ y.format === "binary" && /* @__PURE__ */ s("span", { className: "fm-format-badge", title: "binary .ric", children: "BIN" }),
1754
+ /* @__PURE__ */ s(
1755
+ "button",
1756
+ {
1757
+ className: "fm-btn",
1758
+ title: "Preview PyTorch code",
1759
+ onClick: (l) => {
1760
+ l.stopPropagation(), i(y.path);
1761
+ },
1762
+ children: "⤡"
1763
+ }
1764
+ ),
1765
+ /* @__PURE__ */ s(
1766
+ "button",
1767
+ {
1768
+ className: "fm-btn",
1769
+ title: "Expand full nodes into canvas",
1770
+ onClick: (l) => {
1771
+ l.stopPropagation(), r(y.path);
1772
+ },
1773
+ children: "⤢"
1774
+ }
1775
+ )
1776
+ ]
1777
+ },
1778
+ y.path
1779
+ ))
1780
+ ] }, u)),
1781
+ n.length === 0 && /* @__PURE__ */ s("div", { className: "category", style: { padding: 8 }, children: "No .riko files found." }),
1782
+ n.length > 0 && c.order.length === 0 && /* @__PURE__ */ p("div", { className: "category", style: { padding: 8 }, children: [
1783
+ "No matches for “",
1784
+ d,
1785
+ "”."
1786
+ ] })
1787
+ ] })
1788
+ ] });
1789
+ }
1790
+ let le = [];
1791
+ function jn(n) {
1792
+ le = n;
1793
+ }
1794
+ function Pe() {
1795
+ return le;
1796
+ }
1797
+ let Fe = 0;
1798
+ function Ee(n) {
1799
+ return Fe += 1, `${n}_${Date.now()}_${Fe}`;
1800
+ }
1801
+ async function Rn() {
1802
+ const n = g.getState(), e = le.filter((l) => n.nodes.some((v) => v.id === l));
1803
+ if (e.length === 0) {
1804
+ n.pushToast("error", "先框选一些节点再创建子图");
1805
+ return;
1806
+ }
1807
+ const t = window.prompt("Subgraph name", "subgraph");
1808
+ if (!t || !t.trim()) return;
1809
+ const a = new Set(e), o = n.nodes.filter((l) => a.has(l.id)), r = n.edges.filter((l) => a.has(l.source) && a.has(l.target)), i = n.edges.filter((l) => !a.has(l.source) && a.has(l.target)), d = n.edges.filter((l) => a.has(l.source) && !a.has(l.target)), h = [], c = [], m = (l) => l === 0 ? "input" : `input_${l + 1}`, u = (l) => l === 0 ? "output" : `output_${l + 1}`;
1810
+ i.forEach((l, v) => {
1811
+ const f = m(v), C = `gin_${v}`;
1812
+ h.push({
1813
+ id: C,
1814
+ type_name: "graph_input",
1815
+ label: `Input ${v}`,
1816
+ category: "Subgraph",
1817
+ position: [0, v * 90],
1818
+ parameters: { name: f, data_kind: "tensor" },
1819
+ inputs: [],
1820
+ outputs: []
1821
+ }), c.push({
1822
+ id: `be_in_${v}`,
1823
+ source_node: C,
1824
+ source_port: "value",
1825
+ target_node: l.target,
1826
+ target_port: l.targetHandle
1827
+ });
1828
+ }), d.forEach((l, v) => {
1829
+ const f = u(v), C = `gout_${v}`;
1830
+ h.push({
1831
+ id: C,
1832
+ type_name: "graph_output",
1833
+ label: `Output ${v}`,
1834
+ category: "Subgraph",
1835
+ position: [1200, v * 90],
1836
+ parameters: { name: f, data_kind: "tensor" },
1837
+ inputs: [],
1838
+ outputs: []
1839
+ }), c.push({
1840
+ id: `be_out_${v}`,
1841
+ source_node: l.source,
1842
+ source_port: l.sourceHandle,
1843
+ target_node: C,
1844
+ target_port: "value"
1845
+ });
1846
+ });
1847
+ const y = {
1848
+ version: "1.0",
1849
+ metadata: { name: t.trim(), description: "" },
1850
+ nodes: [
1851
+ ...o.map((l) => ({
1852
+ id: l.id,
1853
+ type_name: l.data.type,
1854
+ label: l.data.label,
1855
+ category: l.data.category,
1856
+ position: [l.position.x, l.position.y],
1857
+ parameters: l.data.params,
1858
+ inputs: [],
1859
+ outputs: []
1860
+ })),
1861
+ ...h
1862
+ ],
1863
+ edges: [
1864
+ ...r.map((l) => ({
1865
+ id: l.id,
1866
+ source_node: l.source,
1867
+ source_port: l.sourceHandle,
1868
+ target_node: l.target,
1869
+ target_port: l.targetHandle
1870
+ })),
1871
+ ...c
1872
+ ],
1873
+ settings: {}
1874
+ };
1875
+ try {
1876
+ const v = await (await fetch("/api/files/save", {
1877
+ method: "POST",
1878
+ headers: { "Content-Type": "application/json" },
1879
+ body: JSON.stringify({ name: t.trim(), format: "ascii", doc: y })
1880
+ })).json();
1881
+ if (v.status !== "success") {
1882
+ n.pushToast("error", v.error ?? "子图保存失败");
1883
+ return;
1884
+ }
1885
+ const f = o.reduce((x, k) => x + k.position.x, 0) / o.length, C = o.reduce((x, k) => x + k.position.y, 0) / o.length, b = n.nodeDefs.find((x) => x.type === "import") ?? n.nodeDefs.find((x) => x.type === "graph_reference"), P = {
1886
+ id: `imp_${Date.now()}`,
1887
+ type: "thnNode",
1888
+ position: { x: f, y: C },
1889
+ data: {
1890
+ type: (b == null ? void 0 : b.type) ?? "import",
1891
+ label: t.trim(),
1892
+ category: "Subgraph",
1893
+ params: (b == null ? void 0 : b.type) === "import" ? { module: t.trim() } : { file: `${t.trim()}.riko` },
1894
+ inputs: (b == null ? void 0 : b.inputs) ?? [],
1895
+ outputs: (b == null ? void 0 : b.outputs) ?? [],
1896
+ hasError: !1
1897
+ }
1898
+ }, _ = n.edges.filter(
1899
+ (x) => !a.has(x.source) && !a.has(x.target)
1900
+ ), $ = 3;
1901
+ i.slice(0, $).forEach((x, k) => {
1902
+ _.push({
1903
+ id: Ee("rwin"),
1904
+ source: x.source,
1905
+ target: P.id,
1906
+ sourceHandle: x.sourceHandle,
1907
+ targetHandle: m(k)
1908
+ });
1909
+ }), d.slice(0, $).forEach((x, k) => {
1910
+ _.push({
1911
+ id: Ee("rwout"),
1912
+ source: P.id,
1913
+ target: x.target,
1914
+ sourceHandle: u(k),
1915
+ targetHandle: x.targetHandle
1916
+ });
1917
+ }), g.setState({
1918
+ nodes: [...n.nodes.filter((x) => !a.has(x.id)), P],
1919
+ edges: _,
1920
+ selectedNodeId: null,
1921
+ dirty: !0
1922
+ }), le = [];
1923
+ const A = Math.max(0, i.length - $), w = Math.max(0, d.length - $);
1924
+ A || w ? n.pushToast(
1925
+ "info",
1926
+ `子图已创建,但超过 3 入/3 出的边界连线有 ${A} 入/${w} 出 未保留`
1927
+ ) : n.pushToast("success", `已创建子图 ${t.trim()}(边界连线已保留)`), n.refreshFiles();
1928
+ } catch (l) {
1929
+ n.pushToast("error", l instanceof Error ? l.message : String(l));
1930
+ }
1931
+ }
1932
+ function $n({ id: n, data: e, selected: t }) {
1933
+ const a = e, o = g((r) => r.results[n]);
1934
+ return /* @__PURE__ */ p("div", { className: `thn-node ${t ? "selected" : ""}`, children: [
1935
+ /* @__PURE__ */ s("div", { className: "header", children: a.label }),
1936
+ /* @__PURE__ */ p("div", { className: "ports", children: [
1937
+ a.inputs.map((r) => /* @__PURE__ */ p("div", { className: "port in", children: [
1938
+ /* @__PURE__ */ s(
1939
+ xe,
1940
+ {
1941
+ type: "target",
1942
+ position: Se.Left,
1943
+ id: r.name,
1944
+ style: { left: -5 }
1945
+ }
1946
+ ),
1947
+ /* @__PURE__ */ s("span", { className: "label", children: r.label })
1948
+ ] }, r.name)),
1949
+ a.outputs.map((r) => /* @__PURE__ */ p("div", { className: "port out", children: [
1950
+ /* @__PURE__ */ s("span", { className: "label", children: r.label }),
1951
+ /* @__PURE__ */ s(
1952
+ xe,
1953
+ {
1954
+ type: "source",
1955
+ position: Se.Right,
1956
+ id: r.name,
1957
+ style: { right: -5 }
1958
+ }
1959
+ )
1960
+ ] }, r.name))
1961
+ ] }),
1962
+ a.hasError && /* @__PURE__ */ p(
1963
+ "div",
1964
+ {
1965
+ className: "result",
1966
+ style: { color: "var(--color-error)" },
1967
+ children: [
1968
+ "⚠ ",
1969
+ a.errorMsg || "error"
1970
+ ]
1971
+ }
1972
+ ),
1973
+ o && Object.entries(o).map(([r, i]) => /* @__PURE__ */ p("div", { className: "result", children: [
1974
+ r,
1975
+ " = ",
1976
+ i.summary
1977
+ ] }, r))
1978
+ ] });
1979
+ }
1980
+ function Ln({ state: n, onClose: e }) {
1981
+ const t = g((i) => i.nodeDefs), a = g((i) => i.addNode), o = g((i) => i.addFrame), r = X(
1982
+ () => [
1983
+ {
1984
+ id: "create_subgraph",
1985
+ label: `⤵ Create Subgraph (${Pe().length} selected)`,
1986
+ description: "collapse selection into an import node",
1987
+ category: "Subgraph",
1988
+ disabled: Pe().length === 0,
1989
+ onSelect: () => Rn()
1990
+ },
1991
+ {
1992
+ id: "add_frame",
1993
+ label: "+ Add Frame",
1994
+ description: "visual grouping box",
1995
+ category: "Frame",
1996
+ onSelect: () => o("Frame", { x: n.x - 200, y: n.y - 150 })
1997
+ },
1998
+ ...t.map((i) => ({
1999
+ id: i.type,
2000
+ label: i.label,
2001
+ description: i.type,
2002
+ category: i.category || "Other",
2003
+ onSelect: () => a(i.type, { x: n.x - 200, y: n.y - 150 })
2004
+ }))
2005
+ ],
2006
+ [t, a, o, n.x, n.y]
2007
+ );
2008
+ return n.visible ? /* @__PURE__ */ s(oe, { entries: r, x: n.x, y: n.y, onClose: e, searchable: !0 }) : null;
2009
+ }
2010
+ const Dn = { thnNode: $n };
2011
+ function An() {
2012
+ const n = g((f) => f.nodes), e = g((f) => f.edges), t = g((f) => f.frames), a = g((f) => f.updateFrame), o = g((f) => f.removeFrame), r = g((f) => f.onNodesChange), i = g((f) => f.onEdgesChange), d = g((f) => f.onConnect), h = g((f) => f.onSelectNode), c = g((f) => f.enterSubgraph), { zoomIn: m, zoomOut: u, fitView: y, getViewport: l } = nn(), v = (f, C) => {
2013
+ if (f.target.closest("button")) return;
2014
+ f.stopPropagation();
2015
+ const b = f.clientX, P = f.clientY, _ = C.x, $ = C.y, A = (x) => {
2016
+ const k = l().zoom || 1;
2017
+ a(C.id, {
2018
+ x: _ + (x.clientX - b) / k,
2019
+ y: $ + (x.clientY - P) / k
2020
+ });
2021
+ }, w = () => {
2022
+ document.removeEventListener("mousemove", A), document.removeEventListener("mouseup", w);
2023
+ };
2024
+ document.addEventListener("mousemove", A), document.addEventListener("mouseup", w);
2025
+ };
2026
+ return /* @__PURE__ */ p(H, { children: [
2027
+ /* @__PURE__ */ p(
2028
+ tn,
2029
+ {
2030
+ nodes: n,
2031
+ edges: e,
2032
+ nodeTypes: Dn,
2033
+ onNodesChange: r,
2034
+ onEdgesChange: i,
2035
+ onConnect: d,
2036
+ onNodeClick: (f, C) => h(C.id),
2037
+ onNodeDoubleClick: (f, C) => {
2038
+ (C.data.type === "graph_reference" || C.data.type === "import") && c(C.id);
2039
+ },
2040
+ onPaneClick: () => h(null),
2041
+ onSelectionChange: ({ nodes: f }) => jn(f.map((C) => C.id)),
2042
+ selectionOnDrag: !0,
2043
+ panOnDrag: [2],
2044
+ fitView: !0,
2045
+ children: [
2046
+ /* @__PURE__ */ s(sn, {}),
2047
+ /* @__PURE__ */ s(an, { children: t.map((f) => /* @__PURE__ */ s(
2048
+ "div",
2049
+ {
2050
+ className: "graph-frame",
2051
+ style: { left: f.x, top: f.y, width: f.width, height: f.height },
2052
+ onMouseDown: (C) => v(C, f),
2053
+ onDoubleClick: () => {
2054
+ const C = window.prompt("Frame title", f.title);
2055
+ C !== null && a(f.id, { title: C });
2056
+ },
2057
+ children: /* @__PURE__ */ p("div", { className: "graph-frame-title", children: [
2058
+ f.title,
2059
+ /* @__PURE__ */ s("button", { className: "graph-frame-close", onClick: () => o(f.id), children: "✕" })
2060
+ ] })
2061
+ },
2062
+ f.id
2063
+ )) })
2064
+ ]
2065
+ }
2066
+ ),
2067
+ /* @__PURE__ */ p("div", { className: "graph-zoom", children: [
2068
+ /* @__PURE__ */ s("button", { onClick: () => m(), title: "Zoom in", children: "+" }),
2069
+ /* @__PURE__ */ s("button", { onClick: () => y(), title: "Fit view", children: "⤢" }),
2070
+ /* @__PURE__ */ s("button", { onClick: () => u(), title: "Zoom out", children: "−" })
2071
+ ] })
2072
+ ] });
2073
+ }
2074
+ function Mn() {
2075
+ const [n, e] = F({ x: 0, y: 0, visible: !1 }), t = g((o) => o.graphStack), a = g((o) => o.exitToLevel);
2076
+ return /* @__PURE__ */ p(
2077
+ "div",
2078
+ {
2079
+ className: "graph-canvas",
2080
+ onContextMenu: (o) => {
2081
+ o.preventDefault(), e({ x: o.clientX, y: o.clientY, visible: !0 });
2082
+ },
2083
+ children: [
2084
+ /* @__PURE__ */ s(en, { children: /* @__PURE__ */ s(An, {}) }),
2085
+ /* @__PURE__ */ s("div", { className: "graph-breadcrumb", children: t.map((o, r) => /* @__PURE__ */ p("span", { className: "graph-breadcrumb-wrap", children: [
2086
+ r > 0 && /* @__PURE__ */ s("span", { className: "graph-breadcrumb-sep", children: "/" }),
2087
+ /* @__PURE__ */ s(
2088
+ "span",
2089
+ {
2090
+ className: `graph-breadcrumb-crumb ${r === t.length - 1 ? "current" : ""}`,
2091
+ onClick: () => {
2092
+ r < t.length - 1 && a(r);
2093
+ },
2094
+ children: o.name || "root"
2095
+ }
2096
+ )
2097
+ ] }, r)) }),
2098
+ /* @__PURE__ */ s(Ln, { state: n, onClose: () => e({ x: 0, y: 0, visible: !1 }) })
2099
+ ]
2100
+ }
2101
+ );
2102
+ }
2103
+ const Wn = [
2104
+ { id: "summary", label: "Summary", icon: "▤" },
2105
+ { id: "params", label: "Params", icon: "⚙" },
2106
+ { id: "inputs", label: "Inputs", icon: "→" },
2107
+ { id: "outputs", label: "Outputs", icon: "←" },
2108
+ { id: "preview", label: "Preview", icon: "◉" }
2109
+ ];
2110
+ function Bn({ style: n }) {
2111
+ const e = g((m) => m.selectedNodeId), t = g((m) => m.nodes), a = g((m) => m.results), o = g((m) => m.updateParam), r = g((m) => m.removeNode), i = g((m) => m.nodeDefs), [d, h] = F("summary"), c = t.find((m) => m.id === e);
2112
+ return /* @__PURE__ */ p("div", { className: "side-inspector", style: n, children: [
2113
+ /* @__PURE__ */ s("div", { className: "inspector-tags", children: Wn.map((m) => /* @__PURE__ */ p(
2114
+ "button",
2115
+ {
2116
+ className: `inspector-tag ${d === m.id ? "active" : ""}`,
2117
+ onClick: () => h(m.id),
2118
+ title: m.label,
2119
+ children: [
2120
+ /* @__PURE__ */ s("span", { className: "inspector-tag-icon", children: m.icon }),
2121
+ /* @__PURE__ */ s("span", { className: "inspector-tag-label", children: m.label })
2122
+ ]
2123
+ },
2124
+ m.id
2125
+ )) }),
2126
+ /* @__PURE__ */ s("div", { className: "inspector-body", children: c ? /* @__PURE__ */ p(H, { children: [
2127
+ d === "summary" && /* @__PURE__ */ s(Un, { node: c, onDelete: () => r(c.id) }),
2128
+ d === "params" && /* @__PURE__ */ s(
2129
+ Gn,
2130
+ {
2131
+ node: c,
2132
+ def: i.find((m) => m.type === c.data.type),
2133
+ onParam: (m, u) => o(c.id, m, u)
2134
+ }
2135
+ ),
2136
+ d === "inputs" && /* @__PURE__ */ s(Ie, { title: "Inputs", ports: c.data.inputs, dir: "in" }),
2137
+ d === "outputs" && /* @__PURE__ */ s(Ie, { title: "Outputs", ports: c.data.outputs, dir: "out" }),
2138
+ d === "preview" && /* @__PURE__ */ s(Hn, { result: a[c.id] })
2139
+ ] }) : /* @__PURE__ */ s("div", { className: "empty", children: "Select a node to edit its parameters" }) })
2140
+ ] });
2141
+ }
2142
+ function Un({
2143
+ node: n,
2144
+ onDelete: e
2145
+ }) {
2146
+ const t = n.data;
2147
+ return /* @__PURE__ */ p("div", { className: "section", children: [
2148
+ /* @__PURE__ */ s("h3", { children: "Summary" }),
2149
+ /* @__PURE__ */ s("div", { children: t.label }),
2150
+ /* @__PURE__ */ p("div", { className: "port-kind", children: [
2151
+ t.type,
2152
+ " · ",
2153
+ t.category
2154
+ ] }),
2155
+ /* @__PURE__ */ s(
2156
+ "button",
2157
+ {
2158
+ className: "btn",
2159
+ style: { marginTop: 8, color: "var(--color-error)", borderColor: "var(--color-error)" },
2160
+ onClick: e,
2161
+ children: "Delete"
2162
+ }
2163
+ )
2164
+ ] });
2165
+ }
2166
+ function Gn({
2167
+ node: n,
2168
+ def: e,
2169
+ onParam: t
2170
+ }) {
2171
+ return /* @__PURE__ */ p("div", { className: "section", children: [
2172
+ /* @__PURE__ */ s("h3", { children: "Parameters" }),
2173
+ e && e.parameters.length > 0 ? e.parameters.map((a) => /* @__PURE__ */ p("div", { className: "field", children: [
2174
+ /* @__PURE__ */ s("label", { children: a.label }),
2175
+ a.kind === "path" ? /* @__PURE__ */ p("div", { className: "path-field", style: { display: "flex", gap: 6 }, children: [
2176
+ /* @__PURE__ */ s(
2177
+ "input",
2178
+ {
2179
+ value: String(n.data.params[a.name] ?? ""),
2180
+ onChange: (o) => t(a.name, o.target.value),
2181
+ style: { flex: 1 }
2182
+ }
2183
+ ),
2184
+ /* @__PURE__ */ s(
2185
+ "button",
2186
+ {
2187
+ className: "btn",
2188
+ onClick: async () => {
2189
+ const o = a.browse === "save", r = await Q(
2190
+ o ? "save" : "import",
2191
+ o ? { defaultName: "model.safetensors" } : void 0
2192
+ );
2193
+ r && t(a.name, r);
2194
+ },
2195
+ children: "Browse"
2196
+ }
2197
+ )
2198
+ ] }) : /* @__PURE__ */ s(
2199
+ "input",
2200
+ {
2201
+ value: String(n.data.params[a.name] ?? ""),
2202
+ onChange: (o) => {
2203
+ const r = a.dtype === "float" || a.dtype === "int" ? Number(o.target.value) : o.target.value;
2204
+ t(a.name, r);
2205
+ }
2206
+ }
2207
+ )
2208
+ ] }, a.name)) : /* @__PURE__ */ s("div", { className: "port-kind", children: "No parameters" })
2209
+ ] });
2210
+ }
2211
+ function Ie({
2212
+ title: n,
2213
+ ports: e,
2214
+ dir: t
2215
+ }) {
2216
+ return /* @__PURE__ */ p("div", { className: "section", children: [
2217
+ /* @__PURE__ */ s("h3", { children: n }),
2218
+ e.length === 0 ? /* @__PURE__ */ s("div", { className: "port-kind", children: "None" }) : e.map((a) => /* @__PURE__ */ p("div", { className: "port-row", children: [
2219
+ /* @__PURE__ */ s("span", { children: a.label }),
2220
+ /* @__PURE__ */ p("span", { className: "port-kind", children: [
2221
+ t,
2222
+ " · ",
2223
+ a.dataKind
2224
+ ] })
2225
+ ] }, a.name))
2226
+ ] });
2227
+ }
2228
+ function Hn({ result: n }) {
2229
+ return /* @__PURE__ */ p("div", { className: "section", children: [
2230
+ /* @__PURE__ */ s("h3", { children: "Preview" }),
2231
+ n ? /* @__PURE__ */ s("div", { className: "preview", children: Object.entries(n).map(([e, t]) => /* @__PURE__ */ p("div", { style: { marginBottom: 6 }, children: [
2232
+ /* @__PURE__ */ p("div", { children: [
2233
+ /* @__PURE__ */ s("strong", { children: e }),
2234
+ ":",
2235
+ " ",
2236
+ t.shape.length === 0 ? "scalar" : t.shape.join("×"),
2237
+ " ",
2238
+ t.dtype,
2239
+ " · ",
2240
+ t.device
2241
+ ] }),
2242
+ t.image ? /* @__PURE__ */ s(
2243
+ "img",
2244
+ {
2245
+ src: t.image,
2246
+ alt: "preview",
2247
+ style: { maxWidth: "100%", marginTop: 4, borderRadius: 4, border: "1px solid var(--color-border)" }
2248
+ }
2249
+ ) : /* @__PURE__ */ p(H, { children: [
2250
+ /* @__PURE__ */ s("div", { children: t.summary }),
2251
+ t.dataPreview && /* @__PURE__ */ p("div", { style: { color: "var(--color-text-secondary)", whiteSpace: "pre-wrap" }, children: [
2252
+ t.dataPreview,
2253
+ t.dataPreview.length >= 120 && "…"
2254
+ ] })
2255
+ ] })
2256
+ ] }, e)) }) : /* @__PURE__ */ s("div", { className: "port-kind", children: "Not executed" })
2257
+ ] });
2258
+ }
2259
+ function Kn({ style: n }) {
2260
+ const [e, t] = F("logs"), a = g((i) => i.logs), o = g((i) => i.nodes), r = g((i) => i.status);
2261
+ return /* @__PURE__ */ p("div", { className: "status-panel", style: n, children: [
2262
+ /* @__PURE__ */ p("div", { className: "tabs", children: [
2263
+ /* @__PURE__ */ s(
2264
+ "div",
2265
+ {
2266
+ className: `tab ${e === "logs" ? "active" : ""}`,
2267
+ onClick: () => t("logs"),
2268
+ children: "Logs"
2269
+ }
2270
+ ),
2271
+ /* @__PURE__ */ s(
2272
+ "div",
2273
+ {
2274
+ className: `tab ${e === "queue" ? "active" : ""}`,
2275
+ onClick: () => t("queue"),
2276
+ children: "Queue"
2277
+ }
2278
+ )
2279
+ ] }),
2280
+ /* @__PURE__ */ s("div", { className: "content", children: e === "logs" ? a.length === 0 ? /* @__PURE__ */ s("div", { className: "log-line", children: "No logs yet. Add nodes and click Run." }) : a.map((i, d) => /* @__PURE__ */ s("div", { className: "log-line", children: i }, d)) : o.length === 0 ? /* @__PURE__ */ s("div", { className: "log-line", children: "Graph is empty" }) : o.map((i) => /* @__PURE__ */ p("div", { className: "queue-item", children: [
2281
+ /* @__PURE__ */ s("span", { children: i.data.label }),
2282
+ /* @__PURE__ */ s("span", { className: "port-kind", children: r })
2283
+ ] }, i.id)) })
2284
+ ] });
2285
+ }
2286
+ function Jn() {
2287
+ var u;
2288
+ const n = g((y) => y.losses), e = g((y) => y.train), t = g((y) => y.status), [a, o] = F(20), [r, i] = F(1e-3), d = 300, h = 160, c = 10, m = (() => {
2289
+ if (n.length === 0) return "";
2290
+ const y = Math.min(...n), v = Math.max(...n) - y || 1;
2291
+ return n.map((f, C) => {
2292
+ const b = c + C / Math.max(n.length - 1, 1) * (d - c * 2), P = c + (1 - (f - y) / v) * (h - c * 2);
2293
+ return `${b.toFixed(1)},${P.toFixed(1)}`;
2294
+ }).join(" ");
2295
+ })();
2296
+ return /* @__PURE__ */ p("div", { className: "loss-panel", children: [
2297
+ /* @__PURE__ */ p("div", { className: "loss-controls", children: [
2298
+ /* @__PURE__ */ p("label", { className: "loss-field", children: [
2299
+ "Steps",
2300
+ /* @__PURE__ */ s(
2301
+ "input",
2302
+ {
2303
+ type: "number",
2304
+ min: 1,
2305
+ step: 1,
2306
+ value: a,
2307
+ onChange: (y) => o(Math.max(1, Math.floor(Number(y.target.value) || 1)))
2308
+ }
2309
+ )
2310
+ ] }),
2311
+ /* @__PURE__ */ p("label", { className: "loss-field", children: [
2312
+ "LR",
2313
+ /* @__PURE__ */ s(
2314
+ "input",
2315
+ {
2316
+ type: "number",
2317
+ min: 0,
2318
+ step: "any",
2319
+ value: r,
2320
+ onChange: (y) => i(Number(y.target.value) || 0)
2321
+ }
2322
+ )
2323
+ ] }),
2324
+ /* @__PURE__ */ s(
2325
+ "button",
2326
+ {
2327
+ className: "btn btn-sm",
2328
+ onClick: () => e(a, r),
2329
+ disabled: t === "running",
2330
+ children: t === "running" ? "Training…" : `Train ${a} steps`
2331
+ }
2332
+ )
2333
+ ] }),
2334
+ n.length === 0 ? /* @__PURE__ */ s("div", { className: "port-kind", style: { padding: 8 }, children: "No loss history. Add a loss node (e.g. cross_entropy_loss) + a data loader, set steps / LR above, then click Train." }) : /* @__PURE__ */ p(H, { children: [
2335
+ /* @__PURE__ */ s("svg", { viewBox: `0 0 ${d} ${h}`, className: "loss-svg", preserveAspectRatio: "none", children: /* @__PURE__ */ s(
2336
+ "polyline",
2337
+ {
2338
+ points: m,
2339
+ fill: "none",
2340
+ stroke: "var(--color-primary)",
2341
+ strokeWidth: "2"
2342
+ }
2343
+ ) }),
2344
+ /* @__PURE__ */ p("div", { className: "loss-meta", children: [
2345
+ /* @__PURE__ */ p("span", { children: [
2346
+ "steps: ",
2347
+ n.length
2348
+ ] }),
2349
+ /* @__PURE__ */ p("span", { children: [
2350
+ "last loss: ",
2351
+ (u = n[n.length - 1]) == null ? void 0 : u.toFixed(4)
2352
+ ] })
2353
+ ] })
2354
+ ] })
2355
+ ] });
2356
+ }
2357
+ const zn = '# Entropia Riko 用户指南\n\n> 版本:v0.1 · 节点数:195+(含插件)· 后端:PyTorch(默认)+ TensorFlow/Keras(可选)\n> 本文档是面向用户的完整使用手册,覆盖界面、节点、构图、训练、导出与开发。\n\n---\n\n## 目录\n\n1. [项目简介](#1-项目简介)\n2. [快速开始](#2-快速开始)\n3. [界面总览](#3-界面总览)\n4. [模块化面板(Blender 风格)](#4-模块化面板blender-风格)\n5. [欢迎窗口](#5-欢迎窗口)\n6. [文件与插件](#6-文件与插件)\n7. [节点系统(191 节点)](#7-节点系统191-节点)\n8. [构图与执行](#8-构图与执行)\n9. [数据输入与预览节点](#9-数据输入与预览节点)\n10. [Wrangle 代码节点](#10-wrangle-代码节点)\n11. [训练与实时 Loss 曲线](#11-训练与实时-loss-曲线)\n12. [导出代码](#12-导出代码)\n13. [.riko 文件格式](#13-tns-文件格式)\n14. [API 端点](#14-api-端点)\n15. [后端架构](#15-后端架构)\n16. [开发与测试](#16-开发与测试)\n17. [已知限制与后续方向](#17-已知限制与后续方向)\n\n---\n\n## 1. 项目简介\n\nEntropia Riko 是一个**独立的节点图编辑器**,用 ComfyUI 风格的画布搭建并运行\nPyTorch(及可选 TensorFlow/Keras)张量工作流。它不是 Houdini 插件。\n\n核心特性:\n\n- **191 个节点**:数学、张量操作、神经网络激活/层、注意力、归一化、归约、形状、\n 创建、损失、数据加载、模型推理、子图引用、Wrangle 代码、预览,以及 TF/Keras。\n- **Blender 风格模块化面板**:拖角分离/合并、任意切换窗口类型、实时缩放。\n- **实时训练 + Loss 曲线**:自包含图(数据加载 + 损失)可直接训练并流式显示损失。\n- **代码导出**:导出干净的 `torch.nn.Module` 或 `tf.keras.Model` 脚本。\n- **子图 import**:一个 `.riko` 图可像 `import xx` 一样引用另一个图。\n- **数据预览**:图片缩略图、文本、JSON 直接在检查器里显示。\n\n---\n\n## 2. 快速开始\n\n### 前置条件\n- Python 3.10+\n- Node.js 18+\n- PyTorch(CPU 版即可;CUDA/MPS 可选)\n\n### 启动(两个终端)\n\n```bash\ncd ~/Documents/torch-node/entropia-riko\n\n# 终端 1:API 服务器(必须先启动)\n.venv/bin/python -m uvicorn src.server.app:app --reload --port 8000\n\n# 终端 2:前端开发服务器\nnpm run dev # → http://localhost:5173\n```\n\n打开 **http://localhost:5173**(务必通过 5173 访问,`/api` 走 Vite 代理)。\n\n> ⚠️ 若看不到 190+ 节点,说明 API 服务器没启动或旧进程仍在(无 `--reload` 的旧进程\n> 不会加载新代码,需杀掉重启)。改代码后浏览器需硬刷新 `Cmd+Shift+R`。\n\n---\n\n## 3. 界面总览\n\n```\n┌ 标题栏(* 未保存 · 文件名 - Riko · 版本号)────────────────────┐\n├ 菜单栏(logo · File / Run / Data / Help,右侧状态徽章)────────┤\n├ 模块化面板区域(Blender 风格区域树)──────────────────────────┤\n│ [节点库] [Asset Library] [New File] [画布] [检查器] │\n│ [状态/日志] [Loss 曲线] │\n└──────────────────────────────────────────────────────────────┘\n```\n\n- **标题栏**:未保存时显示 `*`,随后 `文件名 - Riko`,右侧版本号。\n- **菜单栏**:左上角 **logo**(点开 About / Welcome / Preferences);File(Import/Export\n → 内置 **File Explorer** 导入导出文件/文件夹;Export Code 导出 .py)、Run、Data、\n View(含 **Liquid Glass** 主题)、Help。\n- **节点库**:按分类分组、可折叠、可搜索,点击节点加入画布。\n- **画布**:React Flow 节点图,右键弹搜索式节点菜单。\n- **检查器**:左侧 tag 切换(Summary/Params/Inputs/Outputs/Preview)。\n- **New File**:右上角 mini 文件管理器(右键新建/删除/预览代码/展开节点)。\n- **Asset Library**:`.riko`/`.ric` 资产树(打开 / **展开完整节点** / 保存)。\n- **Code Editor**:代码编辑器窗口(类似记事本,编辑/复制/全选/保存/生成)。\n- **Handwriting Pad**:28×28 手写板,画数字 → 生成 `constant` 节点喂 MNIST。\n- **状态栏**:Logs(日志)与 Queue(节点队列)两个标签。\n- **Loss 曲线**:训练损失的实时 SVG 折线图。\n- **Plugins**:插件管理面板(右上角 `+` 从 `.py` 加载,每个插件带启用/禁用开关)。\n\n---\n\n## 4. 模块化面板(Blender 风格)\n\n每个面板右上角有一个**类型下拉**(切换该窗口显示的内容)和 **✕**(关闭当前面板);\n右下角有一个**角抓手**。\n\n| 操作 | 手势 | 效果 |\n|------|------|------|\n| 切换窗口类型 | 点右上角 `类型 ▾` | Node Library / New File / Asset Library / Graph / Inspector / Status / Loss Curve / Docs / Plugins / Code Editor / Handwriting Pad 十一选一 |\n| 分离(split) | 角抓手**向左/向上拖** | 蓝色预览线跟随鼠标(只在该面板内),松手在 50% 处分出新面板,方向=拖动轴向 |\n| 合并(merge) | 角抓手**向右/向下拖** | 相邻窗口被**蓝色阴影 + 加粗边框**高亮,松手它被并掉、当前面板扩大 |\n| 关闭当前面板 | 点 **✕** | 当前面板被移除,邻居吸收空间 |\n| 缩放 | 拖分隔条 | 所有分隔条可同时独立拖动 |\n\n默认布局:`[节点库/Asset Library 上下] | [画布 | (New File 上 / 检查器)]`,下方\n`[状态 | Loss 曲线]`(New File 只占据最右列、检查器上方,不占用画布区域)。\n\n---\n\n## 5. 欢迎窗口\n\n启动时弹出 Blender 风格欢迎窗口:\n\n- **头图**:上半部为可替换 **JPEG 位图**(`public/brand/hero.jpg`,960×220),右上角显示\n 版本号;左下角为 logo(`public/brand/logo.svg`)。\n- **New File**:`General`(空白图)+ 从 `examples/` 读出的**预制模板**\n (cnn / classifier / mlp / mnist_cnn / transformer / …),点击即载入。\n- **Recent Files**:`localStorage` 记录最近打开过的 `.riko`(去重、最多 10 条)。\n\n点 ✕ 或选择任意项关闭;**Help → Welcome Screen** 或 logo 菜单可随时重新打开。\n\n---\n\n## 6. 文件与插件\n\n**New File**(最右列、检查器上方)是一个 **mini 文件管理器**,映射**工作目录**的\n目录树(可展开/折叠子目录)。**右键**弹出菜单:新建文件/新建文件夹/重命名/删除、\n打开、**预览 PyTorch 代码**(`/api/project/code` 导出 `nn.Module`)、导入为节点。\n**File → Import Working Folder…** 可把工作目录指向任意本地文件夹,并在其中创建\n`.riko` 缓存文件夹存放工具自身状态。\n\n**Asset Library**(原 Files 面板,Blender-outliner 风格资产树)在其基础上增加保存与\nimport 依赖视图,每个文件可展开显示其 `import` 依赖(✓ 已解析 / ✗ 未解析)。\n\n- **打开**:点文件名载入画布。\n- **导入为节点**:点 `⤵` 添加一个引用该文件的 `import` 节点。\n- **保存**:在输入框命名后点 `+ Save .riko`(或 `⇩ .ric`),存到 `workflows/<name>`。\n\n**子图引用**:一个 `.riko` 可被 `graph_reference`(按 `file` 路径)或 `import`\n(按模块名,如 `mlp`,在 `workflows/`、`examples/`、`examples/models/` 中解析)引用。\n被引用图需含 `graph_input(name="input")` 与 `graph_output(name="output")`。\n\n**Plugins 面板**:右上角 `+` 从本地 `.py` 文件加载插件(安装到 `plugins/<name>/`),\n每个插件带启用/禁用开关(禁用即从节点库移除其节点)。内置插件:\n`example_plugin`(`plugin_double`)、`math_extra`(`plugin_square`、`plugin_scale`)、\n`stat_extra`(`plugin_shift_scale`、`plugin_neg`)。\n\n---\n\n## 7. 节点系统(195+ 节点)\n\n### 输入\n`constant`\n\n### 数学(纯 Python)\n`add`, `multiply`\n\n### 数学(torch)\n`torch_add`, `torch_multiply`, `sub`, `div`, `pow`, `matmul`, `mm`, `maximum`,\n`minimum`, `fmod`, `remainder`, `atan2`\n\n### 张量(单目)\n`abs`, `exp`, `log`, `log2`, `log10`, `log1p`, `expm1`, `sqrt`, `erf`, `erfc`,\n`neg`, `sign`, `reciprocal`, `floor`, `ceil`, `round`, `square`, `cos`, `sin`\n\n### 神经网络(激活)\n`relu`, `leaky_relu`, `sigmoid`, `tanh`, `gelu`, `silu`, `selu`, `elu`, `mish`,\n`hardswish`, `hardsigmoid`, `hardtanh`, `softsign`, `tanhshrink`, `softplus`,\n`relu6`, `glu`, `softmax`, `log_softmax`, `log_sigmoid`\n\n### 注意力\n`multihead_attention`, `sdpa`(scaled dot-product attention)\n\n### 神经网络(层)\n`linear`, `conv2d`, `conv1d`, `conv_transpose2d`, `maxpool2d`, `avgpool2d`,\n`embedding`, `dropout`, `batchnorm1d`, `batchnorm2d`, `layernorm`, `groupnorm`,\n`rmsnorm`, `transformer_encoder`\n\n### 归约\n`sum`, `mean`, `amax`, `amin`, `logsumexp`, `median`, `max_reduce`, `min_reduce`,\n`prod`, `std`, `var`, `norm`, `argmax`, `argmin`, `cumsum`, `topk`, `sort`\n\n### 形状\n`reshape`, `view`, `transpose`, `swapaxes`, `movedim`, `permute`, `flatten`,\n`squeeze`, `unsqueeze`, `concat`, `stack`, `flip`, `expand`, `expand_as`,\n`broadcast_to`, `tile`, `repeat`, `repeat_interleave`, `tril`, `triu`,\n`diagonal`, `narrow`, `roll`, `index_select`, `gather`, `interpolate`\n\n### 创建\n`zeros`, `ones`, `rand`, `randn`, `randint`, `randperm`, `empty`, `zeros_like`,\n`ones_like`, `randn_like`, `rand_like`, `eye`, `arange`, `linspace`, `full`,\n`positional_encoding`\n\n### 张量算子\n`bmm`, `dot`, `outer`, `xlogy`, `cross`, `addmm`, `einsum`, `clamp`,\n`contiguous`, `clone`, `detach`, `masked_fill`, `where`\n\n### 损失\n`mse_loss`, `cross_entropy_loss`, `l1_loss`, `smooth_l1_loss`,\n`binary_cross_entropy`, `kl_div`, `nll_loss`, `hinge_embedding_loss`,\n`cosine_embedding_loss`\n\n### 设备\n`to_device`\n\n### 模型\n`model_loader`(加载 .pt)、`inference`(跑 forward)\n\n### 子图\n`graph_input`, `graph_output`, `graph_reference`(按路径), `import`(按模块名)\n\n### 数据(数据集加载)\n`mnist_loader`, `cifar10_loader`, `csv_loader`, `image_folder_loader`,\n`tensor_file_loader`, `dataloader`, `text_loader`, `json_loader`, `image_loader`\n\n### Wrangle 与预览\n`wrangle`(内联 Python 代码), `text_preview`, `json_preview`, `image_preview`\n\n### TensorFlow / Keras(可选后端)\n`keras_dense`, `keras_conv2d`, `keras_flatten`, `keras_embedding`,\n`keras_layernorm`, `keras_dropout`, `keras_relu`, `keras_sigmoid`, `keras_tanh`,\n`keras_gelu`, `keras_softmax`, `keras_maxpool2d`, `keras_avgpool2d`, `tf_add`,\n`tf_multiply`, `tf_matmul`, `tf_concat`, `tf_reshape`, `tf_transpose`, `tf_reduce`\n\n> Keras 卷积用 channels-last `(B,H,W,C)`,与 torch 的 channels-first 不同。\n\n---\n\n## 8. 构图与执行\n\n1. 从节点库点击/拖入节点(或画布右键搜索添加)。\n2. 拖端口连线;点击节点在右侧检查器改参数。\n3. **Run → Inference** 执行整图,结果显示在节点卡片和检查器预览里。\n\n执行后端:校验(端口/必需输入/环检测)→ Kahn 拓扑排序 → 逐节点 `execute`。\n`TensorValue` 是框架无关的中间表示(纯 Python 数据),torch/TF 节点在 `execute` 内\n惰性 import 并做转换。\n\n---\n\n## 9. 数据输入与预览节点\n\n- **输入**:`text_loader`(读文本)、`json_loader`(读 JSON)、`image_loader`\n (用 PIL 读图片,输出 H×W×3 image_tensor)、`csv_loader`、`mnist_loader` 等。\n- **预览**:\n - `text_preview`:把任意值转成文本。\n - `json_preview`:把张量/值序列化成缩进 JSON。\n - `image_preview`:把图像张量转成 **base64 PNG 缩略图**,在检查器里直接显示\n `<img>`。\n\n`TensorValue` 支持 `kind`:`scalar` / `tensor` / `image_tensor` / `text` / `json` /\n`model`。文本/JSON 不做数值 shape 推断。\n\n---\n\n## 10. Wrangle 代码节点\n\n`wrangle` 节点用于解决内置节点覆盖不到的逻辑。参数 `code` 是任意 Python:\n\n```python\n# 可用变量:x / a / b(已连接输入张量,未连接为 None)、torch、F、nn\nresult = x * 2 + 1\n# 或:\n# result = torch.softmax(x, dim=-1)\n# result = F.gelu(x)\n```\n\n`execute` 把输入转成 torch 张量后 `exec(code)`,取 `result`(或 `y`)作为输出。\n导出时会把代码内联进 `forward`。\n\n---\n\n## 11. 训练与实时 Loss 曲线\n\n**可训练图** = 数据加载节点(如 `dataloader`/`mnist_loader`)+ 损失节点\n(`cross_entropy_loss` 等),且不含 `graph_input`。\n\n- **Run → Train 20/100 steps** 或 Loss 面板按钮开始训练。\n- 后端用 codegen 生成 `nn.Module` → `AdamW` 优化器逐步 `backward()`/`step()`,\n 通过 **SSE(`/api/train/stream`)逐步回传 loss**,前端 SVG 曲线实时增长。\n\n注意:执行器本身仍用 `no_grad()` 逐次重建层;真正的训练走「导出 → 执行」这条路径。\n\n---\n\n## 12. 导出代码\n\n- **File → Export .py (PyTorch)**:生成干净的 `torch.nn.Module`。\n - 可学习层进 `__init__`,内联算子进 `forward`。\n - `graph_input` → `forward` 参数;`graph_output` → `return`。\n - `import`/`graph_reference` 递归内联为嵌套 `nn.Module` 类。\n - 数据加载节点生成真实可运行代码(torchvision + 随机回退)。\n - 自包含训练图附带 `train()` + `__main__` 训练循环。\n- **File → Export .py (Keras/TF)**:对 TF/Keras 节点图生成 `tf.keras.Model`\n (层进 `__init__`,算子进 `call`)。\n\n---\n\n## 13. .riko 文件格式\n\n`.riko` 是 JSON,保存完整工作流:\n\n```json\n{\n "version": "0.1",\n "metadata": { "name": "classifier", "description": "...",\n "inputs": [{"name": "input", "data_kind": "tensor"}],\n "outputs": [{"name": "output", "data_kind": "tensor"}] },\n "nodes": [\n {"id": "a", "type_name": "constant", "label": "Constant", "category": "Inputs",\n "position": [100, 200], "parameters": {"value": 2.0}, "inputs": [], "outputs": []}\n ],\n "edges": [\n {"id": "e1", "source_node": "a", "source_port": "value",\n "target_node": "b", "target_port": "left"}\n ],\n "settings": {}\n}\n```\n\n- Save 下载 `workflow.riko`;Load 接受 `.riko`/`.json`。\n- 文件管理器另提供 `POST /api/files/save` 存到 `workflows/`。\n\n---\n\n## 14. API 端点\n\n| 方法 | 路径 | 说明 |\n|------|------|------|\n| GET | `/api/health` | 健康检查 |\n| GET | `/api/nodes` | 全部节点定义(含已启用插件) |\n| POST | `/api/execute` | 执行 GraphDocument JSON,返回各节点端口输出 |\n| POST | `/api/export_python` | 生成 `torch.nn.Module` 代码 |\n| POST | `/api/export_keras` | 生成 `tf.keras.Model` 代码 |\n| POST | `/api/export_binary` | 将图文档编码为 `.ric` 二进制(base64 下载) |\n| GET | `/api/files` | 列出 `.riko` 文件及其 import 依赖 |\n| GET | `/api/files/content?path=` | 读取 `.riko` 内容 |\n| POST | `/api/files/save` | 保存 `.riko` 到 `workflows/` |\n| GET | `/api/plugins` | 列出插件(enabled/disabled + 注册节点) |\n| POST | `/api/plugins/toggle` | 启用/禁用插件(`{name, enabled}`) |\n| POST | `/api/plugins/upload` | 从 Python 源码安装插件(`{name, code}`) |\n| GET | `/api/project/tree` | 返回工作目录的递归目录树(隐藏 `.riko` 缓存) |\n| POST | `/api/project/set_root` | 导入工作文件夹(`{path}`),创建 `.riko` 缓存并持久化 |\n| POST | `/api/project/create` | 在工作目录新建空 `.riko`(`{name, dir?}`) |\n| POST | `/api/project/mkdir` | 在工作目录新建文件夹(`{name, dir?}`) |\n| POST | `/api/project/rename` | 重命名文件/文件夹(`{path, newName}`) |\n| POST | `/api/project/delete` | 删除文件/空文件夹(`{path}`) |\n| POST | `/api/project/code` | 返回该 `.riko` 导出的 PyTorch 代码(`{path}`) |\n| GET | `/api/project/open?path=` | 读取工作目录内 `.riko`/`.ric` 内容 |\n| POST | `/api/train` | 训练并一次性返回 loss 列表 |\n| POST | `/api/train/stream` | 训练并流式(NDJSON)返回逐步 loss |\n\n---\n\n## 15. 后端架构\n\n```\nReact UI (@xyflow/react) ──HTTP──▶ FastAPI\n │ │\n └─ 图文档 JSON ──▶ Runtime(registry + executor + codegen + trainer + subgraph)\n │\n Tensor IR(core/tensor.py TensorValue + document.py)\n │\n Backend(device/converter torch ↔ TensorValue;\n tf_converter tf ↔ TensorValue)\n```\n\n分层与依赖方向:\n\n- `core`(纯 Python,绝不 import torch/TF)\n- `runtime`(注册表 / 执行器 / codegen / codegen_tf / trainer / subgraph)\n- `nodes`(math 纯 Python;torch_ops、tf_ops 惰性 import)\n- `server`(FastAPI)\n- `backend`(torch/TF 转换,均 optional,缺依赖时报清晰错误)\n\n---\n\n## 16. 开发与测试\n\n```bash\n# Python 测试(97 tests,stdlib unittest)\n.venv/bin/python -m unittest discover -s tests -t .\n\n# 前端测试(8 tests,vitest)\nnpm test\n\n# 前端类型检查 + 构建\nnpm run build\n\n# 验证节点数(内置 194;加上插件后 199+)\n.venv/bin/python -c "import src.nodes; from src.runtime.registry import default_registry; print(len(default_registry().list()))"\n# 应打印 194(未加载插件时)\n```\n\n### 新增一个节点\n\n```python\n# src/nodes/.../my_node.py\nfrom ..base import BaseNode, NodeInput, NodeOutput, Parameter\nfrom ...runtime.registry import register\n\n@register("my_node")\nclass MyNode(BaseNode):\n type_name = "my_node"\n label = "My Node"\n category = "Utility"\n inputs = [NodeInput("x", data_kind="tensor", required=True)]\n outputs = [NodeOutput("result", data_kind="tensor")]\n parameters = [Parameter("scale", default=1.0, dtype="float")]\n\n def execute(self, inputs, params, context):\n ...\n return {"result": ...}\n```\n\n然后在对应 `__init__.py` 里 `from . import my_node`,并在 codegen 里加导出分支。\n\n---\n\n## 17. 已知限制与后续方向\n\n1. **执行器级训练未原生实现**:训练走「codegen → exec」路径;原生优化器节点待加。\n2. **子图固定 input/output 端口**:`graph_reference`/`import` 目前只支持单输入单输出。\n3. **Keras 卷积布局**:channels-last,与 torch 不同,混用时需 transpose。\n4. **TF 未随包安装**:`requirements.txt` 中注释了 `tensorflow>=2.10`,启用需自行安装。\n5. **无 undo/redo、无键盘快捷键、无画布内搜索**(右键菜单有搜索)。\n6. **`Toolbar.tsx` 与 `.doc_backups/` 遗留**:可安全删除。\n\n后续方向:原生训练循环、优化器/梯度裁剪节点、多端口子图、模型动物园(ResNet/VGG/BERT\n预制 `.riko`)、JAX 后端、分组卷积/谱分解(svd/qr/eig)等更多 torch API 覆盖。\n', Vn = '# Entropia Riko User Guide\n\n> Version: v0.1 · Nodes: 191 · Backends: PyTorch (default) + TensorFlow/Keras (optional)\n> This is the complete user manual: interface, nodes, graph building, training, export and development.\n\n---\n\n## Table of Contents\n\n1. [Overview](#1-overview)\n2. [Quick Start](#2-quick-start)\n3. [Interface](#3-interface)\n4. [Modular Panels (Blender-style)](#4-modular-panels-blender-style)\n5. [Welcome Screen](#5-welcome-screen)\n6. [File Manager & Import](#6-file-manager--import)\n7. [Node System (191 nodes)](#7-node-system-191-nodes)\n8. [Building & Running Graphs](#8-building--running-graphs)\n9. [Data Input & Preview Nodes](#9-data-input--preview-nodes)\n10. [Wrangle Code Node](#10-wrangle-code-node)\n11. [Training & Real-time Loss Curve](#11-training--real-time-loss-curve)\n12. [Exporting Code](#12-exporting-code)\n13. [.riko File Format](#13-tns-file-format)\n14. [API Endpoints](#14-api-endpoints)\n15. [Backend Architecture](#15-backend-architecture)\n16. [Development & Testing](#16-development--testing)\n17. [Known Limitations](#17-known-limitations)\n\n---\n\n## 1. Overview\n\nEntropia Riko is a **standalone node-graph editor** for building and running\nPyTorch (and optionally TensorFlow/Keras) tensor workflows, with a ComfyUI-style\ncanvas and a Material-inspired UI. It is **not** a Houdini plugin.\n\nKey features:\n\n- **191 nodes**: math, tensor ops, neural activations/layers, attention,\n normalization, reductions, shape ops, creation, losses, data loading, model\n inference, subgraph references, Wrangle code, previews, and TF/Keras.\n- **Blender-style modular panels**: drag a corner to split/merge, switch any\n window\'s type, and resize everything simultaneously.\n- **Real-time training + loss curve**: self-contained graphs (data + loss)\n train live with a streaming SVG loss chart.\n- **Code export**: generate clean `torch.nn.Module` or `tf.keras.Model` scripts.\n- **Subgraph import**: a `.riko` graph can reference another like `import xx`.\n- **Data previews**: image thumbnails, text and JSON rendered in the inspector.\n\n---\n\n## 2. Quick Start\n\n### Prerequisites\n- Python 3.10+\n- Node.js 18+\n- PyTorch (CPU build is enough; CUDA/MPS optional)\n\n### Run (two terminals)\n\n```bash\ncd ~/Documents/torch-node/entropia-riko\n\n# Terminal 1: API server (start FIRST)\n.venv/bin/python -m uvicorn src.server.app:app --reload --port 8000\n\n# Terminal 2: frontend dev server\nnpm run dev # → http://localhost:5173\n```\n\nOpen **http://localhost:5173** (the `/api` proxy only works through 5173).\n\n> ⚠️ If you don\'t see 190+ nodes, the API server isn\'t running, or a stale\n> process started without `--reload` is still serving old code — kill and\n> restart it. After code changes, hard-refresh the browser with `Cmd+Shift+R`.\n\n---\n\n## 3. Interface\n\n```\n┌ Menu bar (File / Run / Data / Help, status badge) ────────────────┐\n├ Modular panel area (Blender-style area tree) ─────────────────────┤\n│ [Node Library] [Files] [Canvas (React Flow)] [Inspector] │\n│ [Status / Logs] [Loss Curve] │\n└────────────────────────────────────────────────────────────────────┘\n```\n\n- **Menu bar**: File (new / save / load / export PyTorch / export Keras),\n Run (inference / Train 20 / 100 steps), Data (quick-add dataset nodes),\n Help (welcome screen / about).\n- **Node Library**: grouped, collapsible, searchable; click to add a node.\n- **Canvas**: React Flow node graph; right-click opens a searchable node menu.\n- **Inspector**: parameters / inputs / outputs / preview of the selected node.\n- **Status**: Logs and Queue tabs.\n- **Loss Curve**: live training loss as an SVG line chart.\n\n---\n\n## 4. Modular Panels (Blender-style)\n\nEach panel has a **type dropdown** (top-right) to switch what it shows and a\n**✕** (close) button; the bottom-right corner is a **grip**.\n\n| Action | Gesture | Effect |\n|--------|---------|--------|\n| Switch type | click `type ▾` | Node Library / Files / Graph / Inspector / Status / Loss Curve / Documentation |\n| Split | drag grip **up/left** | a blue preview line follows the mouse (clipped to the panel); release splits at 50%, orientation = drag axis |\n| Merge | drag grip **down/right** | the sibling panel is **shaded + bold-bordered**; release merges it away, current expands |\n| Close current | click **✕** | current panel removed, neighbor absorbs the space |\n| Resize | drag a splitter | all splitters resize independently and simultaneously |\n\nDefault layout: `[Node Library / Files stacked] | [Graph | Inspector]` over\n`[Status | Loss Curve]`.\n\n---\n\n## 5. Welcome Screen\n\nA Blender-style splash opens on startup, with two columns:\n\n- **New File**: `General` (blank graph) + preset templates read from\n `examples/` (cnn / classifier / mlp / mnist_cnn / transformer / …).\n- **Recent Files**: the last opened `.riko` files (deduped, up to 10, stored in\n `localStorage`).\n\nClose with ✕ or by picking an item; reopen via **Help → Welcome Screen**.\n\n---\n\n## 6. File Manager & Import\n\nThe **Files** panel is a Blender-outliner-style tree of `.riko` files under\n`workflows/` and `examples/`. Each file expands to show its `import`\ndependencies (✓ resolved / ✗ missing).\n\n- **Open**: click a file name to load it.\n- **Import as node**: click `⤵` to add an `import` node referencing it.\n- **Save**: name it and click `+ Save` → `workflows/<name>.riko`.\n\n**Subgraph references**: a `.riko` can be referenced by `graph_reference`\n(by `file` path) or `import` (by module name, e.g. `mlp`, resolved under\n`workflows/`, `examples/`, `examples/models/`). The referenced graph must\ncontain `graph_input(name="input")` and `graph_output(name="output")`.\n\n---\n\n## 7. Node System (191 nodes)\n\n### Inputs\n`constant`\n\n### Math (pure Python)\n`add`, `multiply`\n\n### Math (torch)\n`torch_add`, `torch_multiply`, `sub`, `div`, `pow`, `matmul`, `mm`, `maximum`,\n`minimum`, `fmod`, `remainder`, `atan2`\n\n### Tensor (unary)\n`abs`, `exp`, `log`, `log2`, `log10`, `log1p`, `expm1`, `sqrt`, `erf`, `erfc`,\n`neg`, `sign`, `reciprocal`, `floor`, `ceil`, `round`, `square`, `cos`, `sin`\n\n### Neural (activations)\n`relu`, `leaky_relu`, `sigmoid`, `tanh`, `gelu`, `silu`, `selu`, `elu`, `mish`,\n`hardswish`, `hardsigmoid`, `hardtanh`, `softsign`, `tanhshrink`, `softplus`,\n`relu6`, `glu`, `softmax`, `log_softmax`, `log_sigmoid`\n\n### Attention\n`multihead_attention`, `sdpa` (scaled dot-product attention)\n\n### Neural (layers)\n`linear`, `conv2d`, `conv1d`, `conv_transpose2d`, `maxpool2d`, `avgpool2d`,\n`embedding`, `dropout`, `batchnorm1d`, `batchnorm2d`, `layernorm`, `groupnorm`,\n`rmsnorm`, `transformer_encoder`\n\n### Reduce\n`sum`, `mean`, `amax`, `amin`, `logsumexp`, `median`, `max_reduce`, `min_reduce`,\n`prod`, `std`, `var`, `norm`, `argmax`, `argmin`, `cumsum`, `topk`, `sort`\n\n### Shape\n`reshape`, `view`, `transpose`, `swapaxes`, `movedim`, `permute`, `flatten`,\n`squeeze`, `unsqueeze`, `concat`, `stack`, `flip`, `expand`, `expand_as`,\n`broadcast_to`, `tile`, `repeat`, `repeat_interleave`, `tril`, `triu`,\n`diagonal`, `narrow`, `roll`, `index_select`, `gather`, `interpolate`\n\n### Creation\n`zeros`, `ones`, `rand`, `randn`, `randint`, `randperm`, `empty`, `zeros_like`,\n`ones_like`, `randn_like`, `rand_like`, `eye`, `arange`, `linspace`, `full`,\n`positional_encoding`\n\n### Tensor ops\n`bmm`, `dot`, `outer`, `xlogy`, `cross`, `addmm`, `einsum`, `clamp`,\n`contiguous`, `clone`, `detach`, `masked_fill`, `where`\n\n### Loss\n`mse_loss`, `cross_entropy_loss`, `l1_loss`, `smooth_l1_loss`,\n`binary_cross_entropy`, `kl_div`, `nll_loss`, `hinge_embedding_loss`,\n`cosine_embedding_loss`\n\n### Device\n`to_device`\n\n### Model\n`model_loader` (load .pt), `inference` (run forward)\n\n### Subgraph\n`graph_input`, `graph_output`, `graph_reference` (by path), `import` (by module)\n\n### Data (dataset loaders)\n`mnist_loader`, `cifar10_loader`, `csv_loader`, `image_folder_loader`,\n`tensor_file_loader`, `dataloader`, `text_loader`, `json_loader`, `image_loader`\n\n### Wrangle & Preview\n`wrangle` (inline Python code), `text_preview`, `json_preview`, `image_preview`\n\n### TensorFlow / Keras (optional backend)\n`keras_dense`, `keras_conv2d`, `keras_flatten`, `keras_embedding`,\n`keras_layernorm`, `keras_dropout`, `keras_relu`, `keras_sigmoid`, `keras_tanh`,\n`keras_gelu`, `keras_softmax`, `keras_maxpool2d`, `keras_avgpool2d`, `tf_add`,\n`tf_multiply`, `tf_matmul`, `tf_concat`, `tf_reshape`, `tf_transpose`, `tf_reduce`\n\n> Keras convolutions use channels-last `(B,H,W,C)`, unlike torch\'s channels-first.\n\n---\n\n## 8. Building & Running Graphs\n\n1. Add nodes from the library (or right-click the canvas to search).\n2. Drag between ports to connect; click a node to edit parameters in the inspector.\n3. **Run → Inference** executes the whole graph; results appear on node cards\n and in the inspector preview.\n\nExecution backend: validation (ports / required inputs / cycle detection) →\nKahn topological sort → per-node `execute`. `TensorValue` is a framework-neutral\nIR (pure-Python data); torch/TF nodes lazily import and convert inside `execute`.\n\n---\n\n## 9. Data Input & Preview Nodes\n\n- **Inputs**: `text_loader` (read text), `json_loader` (parse JSON),\n `image_loader` (PIL → H×W×3 image_tensor), `csv_loader`, `mnist_loader`, …\n- **Previews**:\n - `text_preview`: convert any value to text.\n - `json_preview`: serialize a tensor/value to indented JSON.\n - `image_preview`: render an image tensor as a **base64 PNG thumbnail** shown\n in the inspector.\n\n`TensorValue` supports `kind`: `scalar` / `tensor` / `image_tensor` / `text` /\n`json` / `model`. Text/JSON payloads skip numeric shape inference.\n\n---\n\n## 10. Wrangle Code Node\n\nThe `wrangle` node covers whatever built-in nodes can\'t. Its `code` parameter\nis arbitrary Python:\n\n```python\n# available: x / a / b (connected input tensors, None if unconnected), torch, F, nn\nresult = x * 2 + 1\n# result = torch.softmax(x, dim=-1)\n# result = F.gelu(x)\n```\n\n`execute` converts inputs to torch tensors, runs `exec(code)`, and returns\n`result` (or `y`). Export inlines the code into `forward`.\n\n---\n\n## 11. Training & Real-time Loss Curve\n\nA **trainable graph** = a data loader node (e.g. `dataloader`/`mnist_loader`) +\na loss node (e.g. `cross_entropy_loss`), with no `graph_input`.\n\n- **Run → Train 20/100 steps** (or the Loss panel buttons) starts training.\n- The backend generates an `nn.Module` via codegen, runs `AdamW` steps with\n `backward()`/`step()`, and streams each loss over **SSE\n (`/api/train/stream`)**; the frontend SVG curve grows in real time.\n\nNote: the executor itself still uses `no_grad()` and rebuilds layers per call;\nreal training goes through the "export → exec" path.\n\n---\n\n## 12. Exporting Code\n\n- **File → Export .py (PyTorch)**: emits a clean `torch.nn.Module`.\n - learnable layers → `__init__`, inline ops → `forward`.\n - `graph_input` → `forward` parameter; `graph_output` → `return`.\n - `import`/`graph_reference` are inlined recursively as nested modules.\n - data loaders emit real runnable code (torchvision + random fallback).\n - self-contained training graphs get a `train()` + `__main__` loop.\n- **File → Export .py (Keras/TF)**: emits a subclassed `tf.keras.Model`\n (layers → `__init__`, ops → `call`).\n\n---\n\n## 13. .riko File Format\n\n`.riko` is JSON storing a complete workflow:\n\n```json\n{\n "version": "0.1",\n "metadata": { "name": "classifier", "description": "...",\n "inputs": [{"name": "input", "data_kind": "tensor"}],\n "outputs": [{"name": "output", "data_kind": "tensor"}] },\n "nodes": [\n {"id": "a", "type_name": "constant", "label": "Constant", "category": "Inputs",\n "position": [100, 200], "parameters": {"value": 2.0}, "inputs": [], "outputs": []}\n ],\n "edges": [\n {"id": "e1", "source_node": "a", "source_port": "value",\n "target_node": "b", "target_port": "left"}\n ],\n "settings": {}\n}\n```\n\n- Save downloads `workflow.riko`; Load accepts `.riko`/`.json`.\n- The file manager additionally saves to `workflows/` via `POST /api/files/save`.\n\n---\n\n## 14. API Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| GET | `/api/health` | health check |\n| GET | `/api/nodes` | all 191 node definitions |\n| POST | `/api/execute` | execute a GraphDocument JSON, return per-node outputs |\n| POST | `/api/export_python` | generate `torch.nn.Module` code |\n| POST | `/api/export_keras` | generate `tf.keras.Model` code |\n| GET | `/api/files` | list `.riko` files + their import dependencies |\n| GET | `/api/files/content?path=` | read a `.riko` file |\n| POST | `/api/files/save` | save a `.riko` to `workflows/` |\n| POST | `/api/train` | train and return the loss list |\n| POST | `/api/train/stream` | train and stream per-step losses (NDJSON) |\n\n---\n\n## 15. Backend Architecture\n\n```\nReact UI (@xyflow/react) ──HTTP──▶ FastAPI\n │ │\n └─ graph JSON ──▶ Runtime (registry + executor + codegen + trainer + subgraph)\n │\n Tensor IR (core/tensor.py TensorValue + document.py)\n │\n Backend (device/converter torch ↔ TensorValue;\n tf_converter tf ↔ TensorValue)\n```\n\nLayers & dependency direction:\n\n- `core` (pure Python — never imports torch/TF)\n- `runtime` (registry / executor / codegen / codegen_tf / trainer / subgraph)\n- `nodes` (math pure-Python; torch_ops, tf_ops lazy-import)\n- `server` (FastAPI)\n- `backend` (torch/TF converters, both optional with clear errors)\n\n---\n\n## 16. Development & Testing\n\n```bash\n# Python tests (89 tests, stdlib unittest)\n.venv/bin/python -m unittest discover -s tests -t .\n\n# Frontend tests (8 tests, vitest)\nnpm test\n\n# Frontend type-check + build\nnpm run build\n\n# Verify node count\n.venv/bin/python -c "import src.nodes; from src.runtime.registry import default_registry; print(len(default_registry().list()))"\n# should print 191\n```\n\n### Adding a node\n\n```python\n# src/nodes/.../my_node.py\nfrom ..base import BaseNode, NodeInput, NodeOutput, Parameter\nfrom ...runtime.registry import register\n\n@register("my_node")\nclass MyNode(BaseNode):\n type_name = "my_node"\n label = "My Node"\n category = "Utility"\n inputs = [NodeInput("x", data_kind="tensor", required=True)]\n outputs = [NodeOutput("result", data_kind="tensor")]\n parameters = [Parameter("scale", default=1.0, dtype="float")]\n\n def execute(self, inputs, params, context):\n ...\n return {"result": ...}\n```\n\nThen `from . import my_node` in the package `__init__.py`, and add an export\nbranch in codegen.\n\n---\n\n## 17. Known Limitations\n\n1. **No native executor-level training**: training uses the codegen→exec path;\n an in-executor optimizer node is still pending.\n2. **Fixed subgraph input/output ports**: `graph_reference`/`import` support a\n single input and a single output only.\n3. **Keras convolution layout**: channels-last, differs from torch.\n4. **TF not bundled**: `requirements.txt` comments out `tensorflow>=2.10`;\n install it yourself to enable TF nodes.\n5. **No undo/redo, no keyboard shortcuts, no in-canvas search** (right-click menu\n has search).\n6. **`Toolbar.tsx` and `.doc_backups/` are legacy**: safe to delete.\n\nFuture: native training loop, optimizer/gradient-clipping nodes, multi-port\nsubgraphs, a model zoo (ResNet/VGG/BERT presets), a JAX backend, and more torch\nAPI coverage (grouped conv, spectral decompositions svd/qr/eig, …).\n';
2358
+ function z(n) {
2359
+ return n.split(/(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g).map((t, a) => {
2360
+ if (t.startsWith("`") && t.endsWith("`") && t.length > 2)
2361
+ return /* @__PURE__ */ s("code", { children: t.slice(1, -1) }, a);
2362
+ if (t.startsWith("**") && t.endsWith("**") && t.length > 4)
2363
+ return /* @__PURE__ */ s("strong", { children: t.slice(2, -2) }, a);
2364
+ const o = t.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
2365
+ return o ? /* @__PURE__ */ s("a", { href: o[2], target: "_blank", rel: "noreferrer", children: o[1] }, a) : /* @__PURE__ */ s($e, { children: t }, a);
2366
+ });
2367
+ }
2368
+ function Oe(n) {
2369
+ return /^#{1,4}\s/.test(n);
2370
+ }
2371
+ function me(n) {
2372
+ return /^\s*[-*]\s/.test(n) || /^\s*\d+\.\s/.test(n);
2373
+ }
2374
+ function qn(n) {
2375
+ return /^\s*\|[\s:|-]+\|\s*$/.test(n);
2376
+ }
2377
+ function ae(n) {
2378
+ const e = n.replace(/\r\n/g, `
2379
+ `).split(`
2380
+ `), t = [];
2381
+ let a = 0;
2382
+ const o = (r, i) => {
2383
+ t.push(/* @__PURE__ */ s($e, { children: r }, i));
2384
+ };
2385
+ for (; a < e.length; ) {
2386
+ const r = e[a];
2387
+ if (r.trim() === "") {
2388
+ a++;
2389
+ continue;
2390
+ }
2391
+ if (r.trim().startsWith("```")) {
2392
+ const d = [];
2393
+ for (a++; a < e.length && !e[a].trim().startsWith("```"); )
2394
+ d.push(e[a]), a++;
2395
+ a++, o(/* @__PURE__ */ s("pre", { children: /* @__PURE__ */ s("code", { children: d.join(`
2396
+ `) }) }), a);
2397
+ continue;
2398
+ }
2399
+ if (Oe(r)) {
2400
+ const d = r.match(/^(#+)/)[1].length, h = r.replace(/^#+\s*/, "").trim(), c = a;
2401
+ o(d === 1 ? /* @__PURE__ */ s("h1", { children: z(h) }) : d === 2 ? /* @__PURE__ */ s("h2", { children: z(h) }) : d === 3 ? /* @__PURE__ */ s("h3", { children: z(h) }) : /* @__PURE__ */ s("h4", { children: z(h) }), c), a++;
2402
+ continue;
2403
+ }
2404
+ if (/^\s*-{3,}\s*$/.test(r)) {
2405
+ o(/* @__PURE__ */ s("hr", {}), a), a++;
2406
+ continue;
2407
+ }
2408
+ if (r.startsWith(">")) {
2409
+ const d = [];
2410
+ for (; a < e.length && e[a].startsWith(">"); )
2411
+ d.push(e[a].replace(/^>\s?/, "")), a++;
2412
+ o(/* @__PURE__ */ s("blockquote", { children: ae(d.join(`
2413
+ `)) }), a);
2414
+ continue;
2415
+ }
2416
+ if (r.trim().startsWith("|") && e[a + 1] && qn(e[a + 1])) {
2417
+ const d = r.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
2418
+ a += 2;
2419
+ const h = [];
2420
+ for (; a < e.length && e[a].trim().startsWith("|"); )
2421
+ h.push(e[a].trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim())), a++;
2422
+ o(
2423
+ /* @__PURE__ */ p("table", { children: [
2424
+ /* @__PURE__ */ s("thead", { children: /* @__PURE__ */ s("tr", { children: d.map((c, m) => /* @__PURE__ */ s("th", { children: z(c) }, m)) }) }),
2425
+ /* @__PURE__ */ s("tbody", { children: h.map((c, m) => /* @__PURE__ */ s("tr", { children: c.map((u, y) => /* @__PURE__ */ s("td", { children: z(u) }, y)) }, m)) })
2426
+ ] }),
2427
+ a
2428
+ );
2429
+ continue;
2430
+ }
2431
+ if (me(r)) {
2432
+ const d = /^\s*\d+\.\s/.test(r), h = [];
2433
+ for (; a < e.length && me(e[a]); )
2434
+ h.push(e[a].replace(/^\s*(?:[-*]|\d+\.)\s*/, "")), a++;
2435
+ const c = a;
2436
+ o(
2437
+ d ? /* @__PURE__ */ s("ol", { children: h.map((m, u) => /* @__PURE__ */ s("li", { children: z(m) }, u)) }) : /* @__PURE__ */ s("ul", { children: h.map((m, u) => /* @__PURE__ */ s("li", { children: z(m) }, u)) }),
2438
+ c
2439
+ );
2440
+ continue;
2441
+ }
2442
+ const i = [];
2443
+ for (; a < e.length && e[a].trim() !== "" && !Oe(e[a]) && !e[a].trim().startsWith("```") && !e[a].trim().startsWith("|") && !e[a].startsWith(">") && !me(e[a]) && !/^\s*-{3,}\s*$/.test(e[a]); )
2444
+ i.push(e[a]), a++;
2445
+ i.length && o(/* @__PURE__ */ s("p", { children: z(i.join(" ")) }), a);
2446
+ }
2447
+ return t;
2448
+ }
2449
+ const Je = "entropia_riko_doc_lang";
2450
+ function Yn(n) {
2451
+ const e = n.split(`
2452
+ `);
2453
+ let t = "";
2454
+ const a = [];
2455
+ let o = null;
2456
+ const r = [];
2457
+ for (const i of e) {
2458
+ if (i.startsWith("# ")) {
2459
+ t = i.slice(2).trim();
2460
+ continue;
2461
+ }
2462
+ if (i.startsWith("## ")) {
2463
+ o && a.push(o), o = { id: i.slice(3).trim(), title: i.slice(3).trim(), body: "" };
2464
+ continue;
2465
+ }
2466
+ o ? o.body += i + `
2467
+ ` : i.trim() !== "" && r.push(i);
2468
+ }
2469
+ return o && a.push(o), { title: t, intro: r.join(`
2470
+ `), sections: a };
2471
+ }
2472
+ function Xn() {
2473
+ try {
2474
+ return localStorage.getItem(Je) === "zh" ? "zh" : "en";
2475
+ } catch {
2476
+ return "en";
2477
+ }
2478
+ }
2479
+ function Qn() {
2480
+ var y;
2481
+ const [n, e] = F(Xn), t = X(
2482
+ () => Yn(n === "zh" ? zn : Vn),
2483
+ [n]
2484
+ ), [a, o] = F(""), [r, i] = F(((y = t.sections[0]) == null ? void 0 : y.id) ?? "");
2485
+ B(() => {
2486
+ try {
2487
+ localStorage.setItem(Je, n);
2488
+ } catch {
2489
+ }
2490
+ }, [n]), B(() => {
2491
+ i(
2492
+ (l) => {
2493
+ var v;
2494
+ return t.sections.some((f) => f.id === l) ? l : ((v = t.sections[0]) == null ? void 0 : v.id) ?? "";
2495
+ }
2496
+ );
2497
+ }, [t]);
2498
+ const d = X(() => {
2499
+ const l = a.trim().toLowerCase();
2500
+ return l ? t.sections.filter(
2501
+ (v) => v.title.toLowerCase().includes(l) || v.body.toLowerCase().includes(l)
2502
+ ) : t.sections;
2503
+ }, [t.sections, a]), h = t.sections.findIndex((l) => l.id === r), c = t.sections[h] ?? t.sections[0], m = h > 0 ? t.sections[h - 1] : null, u = h >= 0 && h < t.sections.length - 1 ? t.sections[h + 1] : null;
2504
+ return /* @__PURE__ */ p("div", { className: "docs-panel", children: [
2505
+ /* @__PURE__ */ p("div", { className: "docs-sidebar", children: [
2506
+ /* @__PURE__ */ s("div", { className: "docs-search", children: /* @__PURE__ */ s(
2507
+ "input",
2508
+ {
2509
+ placeholder: n === "zh" ? "搜索文档…" : "Search docs…",
2510
+ value: a,
2511
+ onChange: (l) => o(l.target.value)
2512
+ }
2513
+ ) }),
2514
+ /* @__PURE__ */ p("div", { className: "docs-nav", children: [
2515
+ d.map((l) => /* @__PURE__ */ s(
2516
+ "div",
2517
+ {
2518
+ className: `docs-nav-item ${l.id === r ? "active" : ""}`,
2519
+ onClick: () => i(l.id),
2520
+ children: l.title
2521
+ },
2522
+ l.id
2523
+ )),
2524
+ d.length === 0 && /* @__PURE__ */ s("div", { className: "docs-nav-empty", children: n === "zh" ? "无匹配" : "No matches" })
2525
+ ] })
2526
+ ] }),
2527
+ /* @__PURE__ */ s("div", { className: "docs-content", children: /* @__PURE__ */ p("div", { className: "docs-content-inner", children: [
2528
+ /* @__PURE__ */ p("div", { className: "docs-langbar", children: [
2529
+ /* @__PURE__ */ s("div", { className: "docs-pager-spacer" }),
2530
+ /* @__PURE__ */ p("div", { className: "docs-lang-switch", children: [
2531
+ /* @__PURE__ */ s(
2532
+ "button",
2533
+ {
2534
+ className: `panel-btn ${n === "en" ? "active" : ""}`,
2535
+ onClick: () => e("en"),
2536
+ children: "EN"
2537
+ }
2538
+ ),
2539
+ /* @__PURE__ */ s(
2540
+ "button",
2541
+ {
2542
+ className: `panel-btn ${n === "zh" ? "active" : ""}`,
2543
+ onClick: () => e("zh"),
2544
+ children: "中文"
2545
+ }
2546
+ )
2547
+ ] })
2548
+ ] }),
2549
+ a ? /* @__PURE__ */ s("h1", { children: n === "zh" ? "搜索结果" : "Search Results" }) : /* @__PURE__ */ p(H, { children: [
2550
+ /* @__PURE__ */ s("h1", { children: t.title }),
2551
+ /* @__PURE__ */ s("div", { className: "docs-intro", children: ae(t.intro) }),
2552
+ /* @__PURE__ */ s("hr", {})
2553
+ ] }),
2554
+ a ? d.map((l) => /* @__PURE__ */ p("div", { className: "docs-result", children: [
2555
+ /* @__PURE__ */ s("h2", { onClick: () => {
2556
+ o(""), i(l.id);
2557
+ }, children: l.title }),
2558
+ ae(l.body.slice(0, 400))
2559
+ ] }, l.id)) : c && /* @__PURE__ */ p(H, { children: [
2560
+ /* @__PURE__ */ s("h1", { children: c.title }),
2561
+ ae(c.body)
2562
+ ] }),
2563
+ !a && /* @__PURE__ */ p("div", { className: "docs-pager", children: [
2564
+ m && /* @__PURE__ */ p("button", { className: "btn btn-sm", onClick: () => i(m.id), children: [
2565
+ "← ",
2566
+ m.title
2567
+ ] }),
2568
+ /* @__PURE__ */ s("div", { className: "docs-pager-spacer" }),
2569
+ u && /* @__PURE__ */ p("button", { className: "btn btn-sm", onClick: () => i(u.id), children: [
2570
+ u.title,
2571
+ " →"
2572
+ ] })
2573
+ ] })
2574
+ ] }) })
2575
+ ] });
2576
+ }
2577
+ function ze() {
2578
+ const [n, e] = F([]), [t, a] = F(!1), o = M(null), r = g((c) => c.pushToast), i = async () => {
2579
+ try {
2580
+ const m = await (await fetch("/api/plugins")).json();
2581
+ e(Array.isArray(m.plugins) ? m.plugins : []);
2582
+ } catch {
2583
+ e([]);
2584
+ }
2585
+ };
2586
+ B(() => {
2587
+ i();
2588
+ }, []);
2589
+ const d = async (c, m) => {
2590
+ a(!0);
2591
+ try {
2592
+ const y = await (await fetch("/api/plugins/toggle", {
2593
+ method: "POST",
2594
+ headers: { "Content-Type": "application/json" },
2595
+ body: JSON.stringify({ name: c.dir ?? c.name, enabled: m })
2596
+ })).json();
2597
+ y.status === "success" ? (e(Array.isArray(y.plugins) ? y.plugins : []), g.getState().loadNodeDefs(), r("success", `${c.name} ${m ? "enabled" : "disabled"}`)) : r("error", y.error ?? "toggle failed");
2598
+ } catch (u) {
2599
+ r("error", u instanceof Error ? u.message : String(u));
2600
+ } finally {
2601
+ a(!1);
2602
+ }
2603
+ };
2604
+ return /* @__PURE__ */ p("div", { className: "plugin-panel", children: [
2605
+ /* @__PURE__ */ p("div", { className: "panel-head", children: [
2606
+ /* @__PURE__ */ s("span", { className: "panel-head-title", children: "Plugins" }),
2607
+ /* @__PURE__ */ s("span", { className: "spacer" }),
2608
+ /* @__PURE__ */ s(
2609
+ "button",
2610
+ {
2611
+ className: "panel-btn",
2612
+ title: "Load plugin from a .py file",
2613
+ disabled: t,
2614
+ onClick: () => {
2615
+ var c;
2616
+ return (c = o.current) == null ? void 0 : c.click();
2617
+ },
2618
+ children: "+"
2619
+ }
2620
+ ),
2621
+ /* @__PURE__ */ s("button", { className: "panel-btn", title: "Refresh", onClick: i, children: "↻" }),
2622
+ /* @__PURE__ */ s(
2623
+ "input",
2624
+ {
2625
+ ref: o,
2626
+ type: "file",
2627
+ accept: ".py,text/x-python",
2628
+ style: { display: "none" },
2629
+ onChange: (c) => {
2630
+ var y;
2631
+ const m = (y = c.target.files) == null ? void 0 : y[0];
2632
+ if (c.target.value = "", !m) return;
2633
+ const u = new FileReader();
2634
+ u.onload = async () => {
2635
+ const l = String(u.result ?? ""), v = m.name.replace(/\.py$/i, "") || "plugin";
2636
+ a(!0);
2637
+ try {
2638
+ const C = await (await fetch("/api/plugins/upload", {
2639
+ method: "POST",
2640
+ headers: { "Content-Type": "application/json" },
2641
+ body: JSON.stringify({ name: v, code: l })
2642
+ })).json();
2643
+ C.status === "success" ? (e(Array.isArray(C.plugins) ? C.plugins : []), g.getState().loadNodeDefs(), r("success", `Loaded plugin ${v}`)) : r("error", C.error ?? "upload failed");
2644
+ } catch (f) {
2645
+ r("error", f instanceof Error ? f.message : String(f));
2646
+ } finally {
2647
+ a(!1);
2648
+ }
2649
+ }, u.readAsText(m);
2650
+ }
2651
+ }
2652
+ )
2653
+ ] }),
2654
+ /* @__PURE__ */ p("div", { className: "plugin-list", children: [
2655
+ n.length === 0 && /* @__PURE__ */ s("div", { className: "prefs-empty", children: "No plugins installed." }),
2656
+ n.map((c) => /* @__PURE__ */ p("div", { className: "plugin-item", children: [
2657
+ /* @__PURE__ */ p("div", { className: "plugin-item-main", children: [
2658
+ /* @__PURE__ */ p("span", { className: "plugin-item-name", children: [
2659
+ c.name,
2660
+ " ",
2661
+ c.version ? `v${c.version}` : ""
2662
+ ] }),
2663
+ /* @__PURE__ */ s("span", { className: `prefs-plugin-status ${c.status}`, children: c.status }),
2664
+ c.description && /* @__PURE__ */ s("span", { className: "plugin-item-desc", children: c.description }),
2665
+ c.error && /* @__PURE__ */ s("span", { className: "prefs-plugin-error", children: c.error })
2666
+ ] }),
2667
+ /* @__PURE__ */ p("label", { className: "plugin-toggle", title: c.enabled ? "Disable" : "Enable", children: [
2668
+ /* @__PURE__ */ s(
2669
+ "input",
2670
+ {
2671
+ type: "checkbox",
2672
+ checked: !!c.enabled,
2673
+ disabled: t || c.status === "error",
2674
+ onChange: (m) => d(c, m.target.checked)
2675
+ }
2676
+ ),
2677
+ /* @__PURE__ */ s("span", { className: "plugin-toggle-track" })
2678
+ ] })
2679
+ ] }, c.name))
2680
+ ] })
2681
+ ] });
2682
+ }
2683
+ function Zn() {
2684
+ const n = g((w) => w.pushToast), e = g((w) => w.openProjectFile), t = g((w) => w.importModule), a = g((w) => w.projectVersion), [o, r] = F([]), [i, d] = F({}), [h, c] = F(null), [m, u] = F(null), y = async () => {
2685
+ try {
2686
+ const x = await (await fetch("/api/project/tree")).json();
2687
+ r(Array.isArray(x.tree) ? x.tree : []);
2688
+ } catch {
2689
+ r([]);
2690
+ }
2691
+ };
2692
+ B(() => {
2693
+ y();
2694
+ }, [a]);
2695
+ const l = async (w, x) => (await fetch(w, {
2696
+ method: "POST",
2697
+ headers: { "Content-Type": "application/json" },
2698
+ body: JSON.stringify(x)
2699
+ })).json(), v = async (w) => {
2700
+ const x = window.prompt("New file name", "untitled.riko");
2701
+ if (!x) return;
2702
+ const k = await l("/api/project/create", { name: x, dir: w });
2703
+ k.status === "success" ? (await y(), e(k.path)) : n("error", k.error ?? "create failed");
2704
+ }, f = async (w) => {
2705
+ const x = window.prompt("New folder name");
2706
+ if (!x) return;
2707
+ const k = await l("/api/project/mkdir", { name: x, dir: w });
2708
+ k.status === "success" ? (d((L) => ({ ...L, [w]: !0 })), await y()) : n("error", k.error ?? "mkdir failed");
2709
+ }, C = async (w) => {
2710
+ const x = window.prompt("Rename", w.name);
2711
+ if (!x) return;
2712
+ const k = await l("/api/project/rename", { path: w.path, newName: x });
2713
+ k.status === "success" ? await y() : n("error", k.error ?? "rename failed");
2714
+ }, b = async (w) => {
2715
+ if (!window.confirm(`Delete ${w.path}?`)) return;
2716
+ const x = await l("/api/project/delete", { path: w.path });
2717
+ x.status === "success" ? (await y(), n("success", `Deleted ${w.name}`)) : n("error", x.error ?? "delete failed");
2718
+ }, P = async (w) => {
2719
+ const x = await l("/api/project/code", { path: w.path });
2720
+ x.status === "success" ? (g.getState().setCodeBuffer(x.code ?? ""), n("success", "Code → Code Editor panel")) : n("error", x.error ?? "code generation failed");
2721
+ }, _ = async (w, x) => {
2722
+ const k = await l("/api/project/move", { path: w, targetDir: x });
2723
+ k.status === "success" ? (await y(), n("success", `Moved ${w} → ${x || "(root)"}`)) : n("error", k.error ?? "move failed");
2724
+ }, $ = () => {
2725
+ const w = (h == null ? void 0 : h.target) ?? null;
2726
+ return w && w.type === "file" ? [
2727
+ { id: "open", label: "Open", onSelect: () => e(w.path) },
2728
+ { id: "code", label: "Preview PyTorch Code", description: "→ Code Editor panel", onSelect: () => P(w) },
2729
+ { id: "import", label: "Import as Node", description: "reference this graph", onSelect: () => t(w.name.replace(/\.(riko|ric)$/i, ""), { x: 260, y: 150 }) },
2730
+ { id: "sep1", label: "", separator: !0 },
2731
+ { id: "rename", label: "Rename", onSelect: () => C(w) },
2732
+ { id: "delete", label: "Delete", danger: !0, onSelect: () => b(w) }
2733
+ ] : w && w.type === "dir" ? [
2734
+ { id: "newfile", label: "New File", onSelect: () => v(w.path) },
2735
+ { id: "newfolder", label: "New Folder", onSelect: () => f(w.path) },
2736
+ { id: "sep1", label: "", separator: !0 },
2737
+ { id: "rename", label: "Rename", onSelect: () => C(w) },
2738
+ { id: "delete", label: "Delete", danger: !0, onSelect: () => b(w) }
2739
+ ] : [
2740
+ { id: "newfile", label: "New File", onSelect: () => v("") },
2741
+ { id: "newfolder", label: "New Folder", onSelect: () => f("") },
2742
+ { id: "sep1", label: "", separator: !0 },
2743
+ { id: "refresh", label: "Refresh", onSelect: y }
2744
+ ];
2745
+ }, A = (w, x) => {
2746
+ if (w.type === "dir") {
2747
+ const k = !!i[w.path];
2748
+ return /* @__PURE__ */ p("div", { children: [
2749
+ /* @__PURE__ */ p(
2750
+ "div",
2751
+ {
2752
+ className: "proj-row",
2753
+ style: { paddingLeft: 8 + x * 12 },
2754
+ draggable: !0,
2755
+ onDragStart: (L) => {
2756
+ u(w.path), L.dataTransfer.effectAllowed = "move";
2757
+ },
2758
+ onDragOver: (L) => {
2759
+ L.preventDefault(), L.dataTransfer.dropEffect = "move";
2760
+ },
2761
+ onDrop: (L) => {
2762
+ L.preventDefault(), L.stopPropagation(), m && m !== w.path && _(m, w.path), u(null);
2763
+ },
2764
+ onClick: () => d((L) => ({ ...L, [w.path]: !k })),
2765
+ onContextMenu: (L) => {
2766
+ L.preventDefault(), L.stopPropagation(), c({ x: L.clientX, y: L.clientY, target: w });
2767
+ },
2768
+ children: [
2769
+ /* @__PURE__ */ s("span", { className: "proj-caret", children: k ? "▾" : "▸" }),
2770
+ /* @__PURE__ */ s("span", { className: "proj-icon", children: "▦" }),
2771
+ /* @__PURE__ */ s("span", { className: "proj-name", children: w.name })
2772
+ ]
2773
+ }
2774
+ ),
2775
+ k && (w.children ?? []).map((L) => A(L, x + 1))
2776
+ ] }, w.path);
2777
+ }
2778
+ return /* @__PURE__ */ p(
2779
+ "div",
2780
+ {
2781
+ className: "proj-row proj-file",
2782
+ style: { paddingLeft: 8 + x * 12 },
2783
+ draggable: !0,
2784
+ onDragStart: (k) => {
2785
+ u(w.path), k.dataTransfer.effectAllowed = "move";
2786
+ },
2787
+ onClick: () => e(w.path),
2788
+ onContextMenu: (k) => {
2789
+ k.preventDefault(), k.stopPropagation(), c({ x: k.clientX, y: k.clientY, target: w });
2790
+ },
2791
+ children: [
2792
+ /* @__PURE__ */ s("span", { className: "proj-caret", children: "·" }),
2793
+ /* @__PURE__ */ s("span", { className: "proj-icon", children: "▤" }),
2794
+ /* @__PURE__ */ s("span", { className: "proj-name", children: w.name })
2795
+ ]
2796
+ },
2797
+ w.path
2798
+ );
2799
+ };
2800
+ return /* @__PURE__ */ p("div", { className: "project-panel", children: [
2801
+ /* @__PURE__ */ p("div", { className: "panel-head", children: [
2802
+ /* @__PURE__ */ s("span", { className: "panel-head-title", children: "New File" }),
2803
+ /* @__PURE__ */ s("span", { className: "spacer" }),
2804
+ /* @__PURE__ */ s("button", { className: "panel-btn", title: "Refresh", onClick: y, children: "↻" })
2805
+ ] }),
2806
+ /* @__PURE__ */ p(
2807
+ "div",
2808
+ {
2809
+ className: "proj-tree",
2810
+ onDragOver: (w) => w.preventDefault(),
2811
+ onDrop: (w) => {
2812
+ w.preventDefault(), m && _(m, ""), u(null);
2813
+ },
2814
+ onContextMenu: (w) => {
2815
+ w.preventDefault(), c({ x: w.clientX, y: w.clientY, target: null });
2816
+ },
2817
+ children: [
2818
+ o.map((w) => A(w, 0)),
2819
+ o.length === 0 && /* @__PURE__ */ s("div", { className: "welcome-empty", children: "Right-click to create a file." })
2820
+ ]
2821
+ }
2822
+ ),
2823
+ h && /* @__PURE__ */ s(oe, { entries: $(), x: h.x, y: h.y, onClose: () => c(null) })
2824
+ ] });
2825
+ }
2826
+ function et() {
2827
+ const n = g((b) => b.codeBuffer), e = g((b) => b.setCodeBuffer), t = g((b) => b.generatePythonCode), a = g((b) => b.pushToast), o = M(null), r = M(null), [i, d] = F(null), h = () => {
2828
+ var b;
2829
+ return (b = o.current) == null ? void 0 : b.focus();
2830
+ }, c = (b) => {
2831
+ h(), document.execCommand(b);
2832
+ }, m = async () => {
2833
+ try {
2834
+ await navigator.clipboard.writeText(n), a("success", "Copied");
2835
+ } catch {
2836
+ c("copy");
2837
+ }
2838
+ }, u = () => {
2839
+ var b;
2840
+ h(), (b = o.current) == null || b.select();
2841
+ }, y = () => {
2842
+ const b = new Blob([n], { type: "text/x-python" }), P = URL.createObjectURL(b), _ = document.createElement("a");
2843
+ _.href = P, _.download = "code.py", _.click(), URL.revokeObjectURL(P);
2844
+ }, l = () => e(""), v = (b) => {
2845
+ var $;
2846
+ const P = ($ = b.target.files) == null ? void 0 : $[0];
2847
+ if (b.target.value = "", !P) return;
2848
+ const _ = new FileReader();
2849
+ _.onload = () => e(String(_.result ?? "")), _.readAsText(P);
2850
+ }, f = [
2851
+ {
2852
+ label: "File",
2853
+ items: [
2854
+ { label: "New", run: l },
2855
+ { label: "Open…", run: () => {
2856
+ var b;
2857
+ return (b = r.current) == null ? void 0 : b.click();
2858
+ } },
2859
+ { label: "Generate from Graph", run: t },
2860
+ { label: "Save .py", run: y }
2861
+ ]
2862
+ },
2863
+ {
2864
+ label: "Edit",
2865
+ items: [
2866
+ { label: "Undo", run: () => c("undo") },
2867
+ { label: "Redo", run: () => c("redo") },
2868
+ { label: "Cut", run: () => c("cut") },
2869
+ { label: "Copy", run: m },
2870
+ { label: "Paste", run: () => c("paste") },
2871
+ { label: "Select All", run: u },
2872
+ { label: "Clear", run: l }
2873
+ ]
2874
+ }
2875
+ ], C = [
2876
+ { icon: "+", title: "New", run: l },
2877
+ { icon: "📂", title: "Open", run: () => {
2878
+ var b;
2879
+ return (b = r.current) == null ? void 0 : b.click();
2880
+ } },
2881
+ { icon: "💾", title: "Save .py", run: y },
2882
+ { icon: "↶", title: "Undo", run: () => c("undo") },
2883
+ { icon: "↷", title: "Redo", run: () => c("redo") },
2884
+ { icon: "✂", title: "Cut", run: () => c("cut") },
2885
+ { icon: "⧉", title: "Copy", run: m },
2886
+ { icon: "📋", title: "Paste", run: () => c("paste") },
2887
+ { icon: "▦", title: "Select All", run: u }
2888
+ ];
2889
+ return /* @__PURE__ */ p("div", { className: "code-editor", children: [
2890
+ /* @__PURE__ */ s("div", { className: "code-menubar", children: f.map((b) => /* @__PURE__ */ p(
2891
+ "div",
2892
+ {
2893
+ className: `code-menu-item ${i === b.label ? "active" : ""}`,
2894
+ onClick: () => d(i === b.label ? null : b.label),
2895
+ children: [
2896
+ b.label,
2897
+ i === b.label && /* @__PURE__ */ s("div", { className: "code-menu-dropdown", onClick: (P) => P.stopPropagation(), children: b.items.map((P, _) => /* @__PURE__ */ s(
2898
+ "div",
2899
+ {
2900
+ className: "code-menu-entry",
2901
+ onClick: () => {
2902
+ P.run(), d(null);
2903
+ },
2904
+ children: P.label
2905
+ },
2906
+ _
2907
+ )) })
2908
+ ]
2909
+ },
2910
+ b.label
2911
+ )) }),
2912
+ /* @__PURE__ */ s("div", { className: "code-toolbar", children: C.map((b, P) => /* @__PURE__ */ s("button", { className: "panel-btn", title: b.title, onClick: b.run, children: b.icon }, P)) }),
2913
+ /* @__PURE__ */ s(
2914
+ "textarea",
2915
+ {
2916
+ ref: o,
2917
+ className: "code-editor-area",
2918
+ value: n,
2919
+ onChange: (b) => e(b.target.value),
2920
+ placeholder: "// Right-click a .riko file → Preview PyTorch Code, or File → Generate from Graph.",
2921
+ spellCheck: !1
2922
+ }
2923
+ ),
2924
+ /* @__PURE__ */ s(
2925
+ "input",
2926
+ {
2927
+ ref: r,
2928
+ type: "file",
2929
+ accept: ".py,.txt,.riko,.ric,text/plain,text/x-python",
2930
+ style: { display: "none" },
2931
+ onChange: v
2932
+ }
2933
+ )
2934
+ ] });
2935
+ }
2936
+ function nt() {
2937
+ const n = M(null), e = M(!1), t = g((u) => u.addNode), a = g((u) => u.updateParam), o = g((u) => u.pushToast);
2938
+ B(() => {
2939
+ const u = n.current;
2940
+ if (!u) return;
2941
+ const y = u.getContext("2d");
2942
+ y && (y.fillStyle = "#fff", y.fillRect(0, 0, u.width, u.height), y.lineWidth = 12, y.lineCap = "round", y.lineJoin = "round", y.strokeStyle = "#000");
2943
+ }, []);
2944
+ const r = (u) => {
2945
+ const l = n.current.getBoundingClientRect();
2946
+ return { x: u.clientX - l.left, y: u.clientY - l.top };
2947
+ }, i = (u) => {
2948
+ e.current = !0;
2949
+ const l = n.current.getContext("2d"), { x: v, y: f } = r(u);
2950
+ l.beginPath(), l.moveTo(v, f), l.lineTo(v, f), l.stroke();
2951
+ }, d = (u) => {
2952
+ if (!e.current) return;
2953
+ const l = n.current.getContext("2d"), { x: v, y: f } = r(u);
2954
+ l.lineTo(v, f), l.stroke();
2955
+ }, h = () => {
2956
+ e.current = !1;
2957
+ };
2958
+ return /* @__PURE__ */ p("div", { className: "handpad", children: [
2959
+ /* @__PURE__ */ p("div", { className: "panel-head", children: [
2960
+ /* @__PURE__ */ s("span", { className: "panel-head-title", children: "Handwriting Pad" }),
2961
+ /* @__PURE__ */ s("span", { className: "spacer" }),
2962
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: () => {
2963
+ const u = n.current, y = u.getContext("2d");
2964
+ y.fillStyle = "#fff", y.fillRect(0, 0, u.width, u.height);
2965
+ }, children: "Clear" }),
2966
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: () => {
2967
+ const u = n.current, y = document.createElement("canvas");
2968
+ y.width = 28, y.height = 28;
2969
+ const l = y.getContext("2d");
2970
+ l.drawImage(u, 0, 0, 28, 28);
2971
+ const v = l.getImageData(0, 0, 28, 28).data, f = [];
2972
+ for (let _ = 0; _ < 28 * 28; _++) {
2973
+ const $ = 1 - (v[_ * 4] + v[_ * 4 + 1] + v[_ * 4 + 2]) / 765;
2974
+ f.push(Math.round($ * 1e3) / 1e3);
2975
+ }
2976
+ const C = [];
2977
+ for (let _ = 0; _ < 28; _++) C.push(f.slice(_ * 28, _ * 28 + 28));
2978
+ const b = [[C]];
2979
+ t("constant", { x: 260, y: 150 });
2980
+ const P = g.getState().selectedNodeId;
2981
+ P && (a(P, "value", b), o("success", "Digit → constant node (1×28×28)"));
2982
+ }, children: "Send → Node" })
2983
+ ] }),
2984
+ /* @__PURE__ */ s("div", { className: "handpad-canvas", children: /* @__PURE__ */ s(
2985
+ "canvas",
2986
+ {
2987
+ ref: n,
2988
+ width: 280,
2989
+ height: 280,
2990
+ onMouseDown: i,
2991
+ onMouseMove: d,
2992
+ onMouseUp: h,
2993
+ onMouseLeave: h
2994
+ }
2995
+ ) }),
2996
+ /* @__PURE__ */ p("div", { className: "handpad-hint", children: [
2997
+ "Draw a digit → Send creates a ",
2998
+ /* @__PURE__ */ s("code", { children: "constant" }),
2999
+ " node (1×28×28) to feed the MNIST example."
3000
+ ] })
3001
+ ] });
3002
+ }
3003
+ const _e = [
3004
+ { value: "canvas", label: "Graph", category: "Editor" },
3005
+ { value: "inspector", label: "Inspector", category: "Editor" },
3006
+ { value: "status", label: "Status / Logs", category: "Editor" },
3007
+ { value: "loss", label: "Loss Curve", category: "Editor" },
3008
+ { value: "nodes", label: "Scene", category: "Data" },
3009
+ { value: "files", label: "Asset Library", category: "Data" },
3010
+ { value: "project", label: "New File", category: "Data" },
3011
+ { value: "code", label: "Code Editor", category: "Tools" },
3012
+ { value: "pad", label: "Handwriting Pad", category: "Tools" },
3013
+ { value: "docs", label: "Documentation", category: "Tools" },
3014
+ { value: "plugins", label: "Plugins", category: "Tools" },
3015
+ { value: "shader", label: "Node Editor", category: "Editor" },
3016
+ { value: "timeline", label: "Timeline", category: "Editor" }
3017
+ ], tt = ["Editor", "Data", "Tools"];
3018
+ let Ve = new Set(_e.map((n) => n.value));
3019
+ function Nt(n) {
3020
+ Ve = new Set(n);
3021
+ }
3022
+ function st(n) {
3023
+ var e;
3024
+ return ((e = _e.find((t) => t.value === n)) == null ? void 0 : e.label) ?? n;
3025
+ }
3026
+ const qe = {
3027
+ nodes: () => /* @__PURE__ */ s(En, {}),
3028
+ files: () => /* @__PURE__ */ s(On, {}),
3029
+ project: () => /* @__PURE__ */ s(Zn, {}),
3030
+ canvas: () => /* @__PURE__ */ s(Mn, {}),
3031
+ inspector: () => /* @__PURE__ */ s(Bn, {}),
3032
+ status: () => /* @__PURE__ */ s(Kn, {}),
3033
+ loss: () => /* @__PURE__ */ s(Jn, {}),
3034
+ docs: () => /* @__PURE__ */ s(Qn, {}),
3035
+ plugins: () => /* @__PURE__ */ s(ze, {}),
3036
+ code: () => /* @__PURE__ */ s(et, {}),
3037
+ pad: () => /* @__PURE__ */ s(nt, {}),
3038
+ shader: () => null,
3039
+ timeline: () => null
3040
+ };
3041
+ function kt(n, e) {
3042
+ qe[n] = e;
3043
+ }
3044
+ function at(n) {
3045
+ const e = qe[n];
3046
+ return e ? e() : null;
3047
+ }
3048
+ function ot({
3049
+ type: n,
3050
+ onType: e
3051
+ }) {
3052
+ const [t, a] = F(!1), [o, r] = F(null), i = M(null);
3053
+ return /* @__PURE__ */ p("div", { className: "type-dropdown", children: [
3054
+ /* @__PURE__ */ p("button", { ref: i, className: "panel-btn", title: "Switch window type", onClick: () => {
3055
+ if (!t && i.current) {
3056
+ const h = i.current.getBoundingClientRect();
3057
+ r({ top: h.bottom + 4, left: h.right - 300 });
3058
+ }
3059
+ a((h) => !h);
3060
+ }, children: [
3061
+ st(n),
3062
+ " ▾"
3063
+ ] }),
3064
+ t && De(
3065
+ /* @__PURE__ */ p(H, { children: [
3066
+ /* @__PURE__ */ s("div", { className: "type-menu-overlay", onClick: () => a(!1) }),
3067
+ /* @__PURE__ */ s("div", { className: "type-menu", style: o ? { position: "fixed", top: o.top, left: o.left } : void 0, children: tt.map((h) => /* @__PURE__ */ p("div", { className: "type-menu-col", children: [
3068
+ /* @__PURE__ */ s("div", { className: "type-menu-cat", children: h }),
3069
+ _e.filter((c) => c.category === h && Ve.has(c.value)).map((c) => /* @__PURE__ */ s(
3070
+ "div",
3071
+ {
3072
+ className: `type-menu-item ${c.value === n ? "active" : ""}`,
3073
+ onClick: () => {
3074
+ e(c.value), a(!1);
3075
+ },
3076
+ children: c.label
3077
+ },
3078
+ c.value
3079
+ ))
3080
+ ] }, h)) })
3081
+ ] }),
3082
+ document.body
3083
+ )
3084
+ ] });
3085
+ }
3086
+ function je(n, e) {
3087
+ return Math.abs(n) < 14 && Math.abs(e) < 14 ? null : Math.abs(n) > Math.abs(e) ? n < 0 ? "split-row" : "merge" : e < 0 ? "split-column" : "merge";
3088
+ }
3089
+ function rt({
3090
+ onPreview: n,
3091
+ onCommit: e
3092
+ }) {
3093
+ const t = M(null);
3094
+ return /* @__PURE__ */ s(
3095
+ "div",
3096
+ {
3097
+ className: "corner-grip",
3098
+ title: "拖左上:分离面板 · 拖右下:合并面板",
3099
+ onMouseDown: (o) => {
3100
+ o.preventDefault(), o.stopPropagation();
3101
+ const r = o.currentTarget.parentElement, i = (r == null ? void 0 : r.getBoundingClientRect()) ?? null;
3102
+ t.current = { x: o.clientX, y: o.clientY };
3103
+ const d = (c) => {
3104
+ if (!t.current) return;
3105
+ const m = c.clientX - t.current.x, u = c.clientY - t.current.y, y = je(m, u);
3106
+ n(
3107
+ y ? { mode: y, x: c.clientX - ((i == null ? void 0 : i.left) ?? 0), y: c.clientY - ((i == null ? void 0 : i.top) ?? 0) } : null
3108
+ );
3109
+ }, h = (c) => {
3110
+ if (!t.current) return;
3111
+ const m = c.clientX - t.current.x, u = c.clientY - t.current.y;
3112
+ t.current = null, document.removeEventListener("mousemove", d), document.removeEventListener("mouseup", h), document.body.style.cursor = "";
3113
+ const y = je(m, u);
3114
+ n(null), y && e(y);
3115
+ };
3116
+ document.body.style.cursor = "crosshair", document.addEventListener("mousemove", d), document.addEventListener("mouseup", h);
3117
+ }
3118
+ }
3119
+ );
3120
+ }
3121
+ function it({
3122
+ id: n,
3123
+ type: e,
3124
+ onType: t,
3125
+ onSplitRow: a,
3126
+ onSplitColumn: o,
3127
+ onMerge: r,
3128
+ onClose: i,
3129
+ canMerge: d,
3130
+ mergeTarget: h,
3131
+ preview: c,
3132
+ onPreview: m
3133
+ }) {
3134
+ return /* @__PURE__ */ p("div", { className: `panel-slot ${h ? "merge-target" : ""}`, children: [
3135
+ /* @__PURE__ */ p("div", { className: "panel-bar", children: [
3136
+ /* @__PURE__ */ s("div", { className: "panel-bar-spacer" }),
3137
+ /* @__PURE__ */ s(ot, { type: e, onType: t }),
3138
+ d && /* @__PURE__ */ s("button", { className: "panel-btn", title: "Close / merge panel away", onClick: i, children: "✕" })
3139
+ ] }),
3140
+ /* @__PURE__ */ s("div", { className: "panel-body", children: at(e) }),
3141
+ c && c.mode === "split-row" && /* @__PURE__ */ p(H, { children: [
3142
+ /* @__PURE__ */ s("div", { className: "split-preview", style: { left: 0, top: 0, bottom: 0, width: Math.max(0, c.x - 4) } }),
3143
+ /* @__PURE__ */ s("div", { className: "split-preview", style: { left: c.x + 4, top: 0, bottom: 0, right: 0 } })
3144
+ ] }),
3145
+ c && c.mode === "split-column" && /* @__PURE__ */ p(H, { children: [
3146
+ /* @__PURE__ */ s("div", { className: "split-preview", style: { left: 0, right: 0, top: 0, height: Math.max(0, c.y - 4) } }),
3147
+ /* @__PURE__ */ s("div", { className: "split-preview", style: { left: 0, right: 0, top: c.y + 4, bottom: 0 } })
3148
+ ] }),
3149
+ /* @__PURE__ */ s(
3150
+ rt,
3151
+ {
3152
+ onPreview: m,
3153
+ onCommit: (u) => u === "split-row" ? a() : u === "split-column" ? o() : r()
3154
+ }
3155
+ )
3156
+ ] });
3157
+ }
3158
+ function lt({ onClose: n }) {
3159
+ const e = g((l) => l.recentFiles), t = g((l) => l.openFile), a = g((l) => l.newBlank), o = g((l) => l.load), r = g((l) => l.pushToast), [i, d] = F([]), h = M(null);
3160
+ B(() => {
3161
+ fetch("/api/files").then((l) => l.json()).then((l) => {
3162
+ Array.isArray(l.files) && d(
3163
+ l.files.filter((v) => v.path.startsWith("examples")).sort((v, f) => v.name.localeCompare(f.name))
3164
+ );
3165
+ }).catch(() => {
3166
+ });
3167
+ }, []);
3168
+ const c = (l) => {
3169
+ var v;
3170
+ return ((v = l.split("/").pop()) == null ? void 0 : v.replace(/\.riko$/, "")) ?? l;
3171
+ }, m = (l) => {
3172
+ l(), n();
3173
+ }, u = () => {
3174
+ const l = e[0];
3175
+ l ? m(() => t(l)) : r("error", "No previous session to recover");
3176
+ }, y = (l) => {
3177
+ var f;
3178
+ const v = (f = l.target.files) == null ? void 0 : f[0];
3179
+ l.target.value = "", v && (o(v), n());
3180
+ };
3181
+ return /* @__PURE__ */ s("div", { className: "welcome-overlay", children: /* @__PURE__ */ p("div", { className: "welcome", children: [
3182
+ /* @__PURE__ */ p("div", { className: "welcome-hero", children: [
3183
+ /* @__PURE__ */ s(
3184
+ de,
3185
+ {
3186
+ className: "welcome-logo",
3187
+ title: "Logo slot — replace public/brand/logo.png (bitmap) or logo.svg (vector)"
3188
+ }
3189
+ ),
3190
+ /* @__PURE__ */ p("span", { className: "welcome-version", children: [
3191
+ "v",
3192
+ te
3193
+ ] })
3194
+ ] }),
3195
+ /* @__PURE__ */ p("div", { className: "welcome-header", children: [
3196
+ /* @__PURE__ */ s("span", { className: "welcome-title", children: "Entropia Riko" }),
3197
+ /* @__PURE__ */ s("button", { className: "panel-btn", title: "Close", onClick: n, children: "✕" })
3198
+ ] }),
3199
+ /* @__PURE__ */ p("div", { className: "welcome-body", children: [
3200
+ /* @__PURE__ */ p("div", { className: "welcome-col", children: [
3201
+ /* @__PURE__ */ s("div", { className: "welcome-col-title", children: "New File" }),
3202
+ /* @__PURE__ */ p("button", { className: "welcome-item welcome-blank", onClick: () => m(a), children: [
3203
+ /* @__PURE__ */ s("span", { className: "welcome-item-icon", children: "⬚" }),
3204
+ /* @__PURE__ */ s("span", { children: "General" }),
3205
+ /* @__PURE__ */ s("span", { className: "welcome-item-hint", children: "blank graph" })
3206
+ ] }),
3207
+ /* @__PURE__ */ s("div", { className: "welcome-section-label", children: "Presets" }),
3208
+ /* @__PURE__ */ p("div", { className: "welcome-scroll", children: [
3209
+ i.length === 0 && /* @__PURE__ */ s("div", { className: "welcome-empty", children: "No presets (start the API server)" }),
3210
+ i.map((l) => /* @__PURE__ */ p("div", { className: "welcome-item", onClick: () => m(() => t(l.path)), children: [
3211
+ /* @__PURE__ */ s("span", { className: "welcome-item-icon", children: "▦" }),
3212
+ /* @__PURE__ */ s("span", { children: l.name }),
3213
+ /* @__PURE__ */ s("span", { className: "welcome-item-hint", children: l.imports.length ? `imports ${l.imports.length}` : "" })
3214
+ ] }, l.path))
3215
+ ] })
3216
+ ] }),
3217
+ /* @__PURE__ */ p("div", { className: "welcome-col", children: [
3218
+ /* @__PURE__ */ s("div", { className: "welcome-col-title", children: "Recent Files" }),
3219
+ /* @__PURE__ */ s("div", { className: "welcome-scroll", children: e.length === 0 ? /* @__PURE__ */ s("div", { className: "welcome-empty", children: "No recent files yet" }) : e.map((l) => /* @__PURE__ */ p("div", { className: "welcome-item", onClick: () => m(() => t(l)), children: [
3220
+ /* @__PURE__ */ s("span", { className: "welcome-item-icon", children: "▤" }),
3221
+ /* @__PURE__ */ s("span", { children: c(l) }),
3222
+ /* @__PURE__ */ s("span", { className: "welcome-item-hint", children: l })
3223
+ ] }, l)) })
3224
+ ] })
3225
+ ] }),
3226
+ /* @__PURE__ */ p("div", { className: "welcome-footer", children: [
3227
+ /* @__PURE__ */ s("button", { className: "welcome-footer-btn", onClick: u, children: "↩ Recover Last Session" }),
3228
+ /* @__PURE__ */ s("button", { className: "welcome-footer-btn", onClick: () => {
3229
+ var l;
3230
+ return (l = h.current) == null ? void 0 : l.click();
3231
+ }, children: "📂 Open" }),
3232
+ /* @__PURE__ */ s(
3233
+ "input",
3234
+ {
3235
+ ref: h,
3236
+ type: "file",
3237
+ accept: ".riko,.ric,.json,application/json,application/octet-stream",
3238
+ style: { display: "none" },
3239
+ onChange: y
3240
+ }
3241
+ )
3242
+ ] })
3243
+ ] }) });
3244
+ }
3245
+ const ct = [
3246
+ { id: "appearance", label: "Appearance", icon: "◧" },
3247
+ { id: "plugins", label: "Plugins", icon: "⬡" },
3248
+ { id: "about", label: "About", icon: "ⓘ" }
3249
+ ];
3250
+ function dt({ onClose: n }) {
3251
+ const e = q((m) => m.theme), t = q((m) => m.setTheme), a = q((m) => m.backgroundImage), o = q((m) => m.setBackgroundImage), [r, i] = F("appearance"), [d, h] = F(a);
3252
+ B(() => {
3253
+ h(a);
3254
+ }, [a]);
3255
+ const c = [
3256
+ { value: "light", label: "Light", hint: "Always use the light palette" },
3257
+ { value: "dark", label: "Dark", hint: "Always use the dark palette" },
3258
+ { value: "system", label: "System", hint: "Follow the OS color scheme" },
3259
+ { value: "glass", label: "Liquid Glass", hint: "Apple-style translucent glass" }
3260
+ ];
3261
+ return /* @__PURE__ */ s(ce, { title: "Preferences", onClose: n, width: 720, zIndex: 1001, children: /* @__PURE__ */ p("div", { className: "prefs-layout", children: [
3262
+ /* @__PURE__ */ s("nav", { className: "prefs-sidebar", children: ct.map((m) => /* @__PURE__ */ p(
3263
+ "button",
3264
+ {
3265
+ className: `prefs-nav ${r === m.id ? "active" : ""}`,
3266
+ onClick: () => i(m.id),
3267
+ children: [
3268
+ /* @__PURE__ */ s("span", { className: "prefs-nav-icon", children: m.icon }),
3269
+ m.label
3270
+ ]
3271
+ },
3272
+ m.id
3273
+ )) }),
3274
+ /* @__PURE__ */ p("div", { className: "prefs-content", children: [
3275
+ r === "appearance" && /* @__PURE__ */ p(H, { children: [
3276
+ /* @__PURE__ */ p("section", { className: "prefs-section", children: [
3277
+ /* @__PURE__ */ s("h3", { children: "Theme" }),
3278
+ /* @__PURE__ */ s("p", { className: "prefs-desc", children: "Choose how the interface looks." }),
3279
+ /* @__PURE__ */ s("div", { className: "prefs-theme-list", children: c.map((m) => /* @__PURE__ */ p(
3280
+ "label",
3281
+ {
3282
+ className: `prefs-theme ${e === m.value ? "active" : ""}`,
3283
+ children: [
3284
+ /* @__PURE__ */ s(
3285
+ "input",
3286
+ {
3287
+ type: "radio",
3288
+ name: "theme",
3289
+ checked: e === m.value,
3290
+ onChange: () => t(m.value)
3291
+ }
3292
+ ),
3293
+ /* @__PURE__ */ s("span", { className: "prefs-theme-swatch", "data-theme": m.value }),
3294
+ /* @__PURE__ */ s("span", { className: "prefs-theme-label", children: m.label }),
3295
+ /* @__PURE__ */ s("span", { className: "prefs-theme-hint", children: m.hint })
3296
+ ]
3297
+ },
3298
+ m.value
3299
+ )) })
3300
+ ] }),
3301
+ /* @__PURE__ */ p("section", { className: "prefs-section", children: [
3302
+ /* @__PURE__ */ s("h3", { children: "Background image" }),
3303
+ /* @__PURE__ */ s("p", { className: "prefs-desc", children: "A cover image behind the editor. URL is saved locally." }),
3304
+ /* @__PURE__ */ p("div", { className: "prefs-row", children: [
3305
+ /* @__PURE__ */ s(
3306
+ "input",
3307
+ {
3308
+ className: "prefs-input",
3309
+ placeholder: "Image URL (https://…)",
3310
+ value: d,
3311
+ onChange: (m) => h(m.target.value),
3312
+ onKeyDown: (m) => {
3313
+ m.key === "Enter" && o(d.trim());
3314
+ }
3315
+ }
3316
+ ),
3317
+ /* @__PURE__ */ s("button", { className: "btn btn-sm", onClick: () => o(d.trim()), children: "Apply" }),
3318
+ /* @__PURE__ */ s(
3319
+ "button",
3320
+ {
3321
+ className: "btn btn-sm",
3322
+ onClick: () => {
3323
+ h(""), o("");
3324
+ },
3325
+ children: "Clear"
3326
+ }
3327
+ )
3328
+ ] })
3329
+ ] })
3330
+ ] }),
3331
+ r === "plugins" && /* @__PURE__ */ s("section", { className: "prefs-section", style: { marginBottom: 0 }, children: /* @__PURE__ */ s(ze, {}) }),
3332
+ r === "about" && /* @__PURE__ */ s("section", { className: "prefs-section", children: /* @__PURE__ */ p("div", { className: "prefs-about", children: [
3333
+ /* @__PURE__ */ s(de, { className: "prefs-about-logo" }),
3334
+ /* @__PURE__ */ s("h3", { children: "Entropia Riko" }),
3335
+ /* @__PURE__ */ p("div", { className: "prefs-about-version", children: [
3336
+ "Version ",
3337
+ te
3338
+ ] }),
3339
+ /* @__PURE__ */ s("p", { className: "prefs-desc", children: "A professional node-graph deep-learning editor. Build models visually, run inference, train with live loss curves, and export to clean PyTorch or TensorFlow/Keras code." }),
3340
+ /* @__PURE__ */ p("div", { className: "prefs-about-meta", children: [
3341
+ /* @__PURE__ */ s("span", { children: "Backends: PyTorch · TensorFlow/Keras" }),
3342
+ /* @__PURE__ */ s("span", { children: "Brand assets: public/brand/" })
3343
+ ] })
3344
+ ] }) })
3345
+ ] })
3346
+ ] }) });
3347
+ }
3348
+ function pt({ onClose: n }) {
3349
+ return /* @__PURE__ */ p(ce, { title: "About Entropia Riko", onClose: n, width: 440, children: [
3350
+ /* @__PURE__ */ p("div", { className: "about-hero", children: [
3351
+ /* @__PURE__ */ s(de, { className: "about-logo" }),
3352
+ /* @__PURE__ */ s("h2", { children: "Entropia Riko" }),
3353
+ /* @__PURE__ */ p("div", { className: "about-version", children: [
3354
+ "Version ",
3355
+ te
3356
+ ] })
3357
+ ] }),
3358
+ /* @__PURE__ */ p("div", { className: "about-body", children: [
3359
+ /* @__PURE__ */ s("p", { className: "prefs-desc", children: "A professional node-graph deep-learning editor. Build models visually, run inference, train with a live loss curve, and export to clean PyTorch or TensorFlow/Keras code." }),
3360
+ /* @__PURE__ */ s("table", { className: "about-meta", children: /* @__PURE__ */ p("tbody", { children: [
3361
+ /* @__PURE__ */ p("tr", { children: [
3362
+ /* @__PURE__ */ s("td", { children: "Backends" }),
3363
+ /* @__PURE__ */ s("td", { children: "PyTorch · TensorFlow / Keras" })
3364
+ ] }),
3365
+ /* @__PURE__ */ p("tr", { children: [
3366
+ /* @__PURE__ */ s("td", { children: "File formats" }),
3367
+ /* @__PURE__ */ s("td", { children: ".riko (JSON) · .ric (binary zlib)" })
3368
+ ] }),
3369
+ /* @__PURE__ */ p("tr", { children: [
3370
+ /* @__PURE__ */ s("td", { children: "Plugins" }),
3371
+ /* @__PURE__ */ s("td", { children: "plugins/*/plugin.json (Python node modules)" })
3372
+ ] }),
3373
+ /* @__PURE__ */ p("tr", { children: [
3374
+ /* @__PURE__ */ s("td", { children: "License" }),
3375
+ /* @__PURE__ */ s("td", { children: "MIT" })
3376
+ ] })
3377
+ ] }) }),
3378
+ /* @__PURE__ */ p("div", { className: "about-assets", children: [
3379
+ /* @__PURE__ */ s("span", { children: "Brand assets (replace in place):" }),
3380
+ /* @__PURE__ */ s("code", { children: "public/brand/logo.svg" }),
3381
+ /* @__PURE__ */ s("code", { children: "public/brand/hero.jpg" })
3382
+ ] })
3383
+ ] })
3384
+ ] });
3385
+ }
3386
+ function ut({ onClose: n }) {
3387
+ const [e, t] = F(""), [a, o] = F(!1), r = g((d) => d.pushToast), i = async () => {
3388
+ const d = e.trim();
3389
+ if (d) {
3390
+ o(!0);
3391
+ try {
3392
+ const c = await (await fetch("/api/project/set_root", {
3393
+ method: "POST",
3394
+ headers: { "Content-Type": "application/json" },
3395
+ body: JSON.stringify({ path: d })
3396
+ })).json();
3397
+ c.status === "success" ? (g.getState().bumpProject(), r("success", `Working folder: ${c.root}`), n()) : r("error", c.error ?? "import failed");
3398
+ } catch (h) {
3399
+ r("error", h instanceof Error ? h.message : String(h));
3400
+ } finally {
3401
+ o(!1);
3402
+ }
3403
+ }
3404
+ };
3405
+ return /* @__PURE__ */ s(ce, { title: "Import Working Folder", onClose: n, width: 480, children: /* @__PURE__ */ p("div", { className: "import-folder-body", children: [
3406
+ /* @__PURE__ */ p("p", { className: "prefs-desc", children: [
3407
+ "Point the file manager at a local folder. A ",
3408
+ /* @__PURE__ */ s("code", { children: ".riko" }),
3409
+ " cache folder is created inside it for the tool's own state."
3410
+ ] }),
3411
+ /* @__PURE__ */ s(
3412
+ "input",
3413
+ {
3414
+ autoFocus: !0,
3415
+ className: "prefs-input",
3416
+ placeholder: "Absolute path, e.g. /Users/me/project",
3417
+ value: e,
3418
+ onChange: (d) => t(d.target.value),
3419
+ onKeyDown: (d) => {
3420
+ d.key === "Enter" && i(), d.key === "Escape" && n();
3421
+ }
3422
+ }
3423
+ ),
3424
+ /* @__PURE__ */ p("div", { className: "prefs-row", style: { marginTop: 12 }, children: [
3425
+ /* @__PURE__ */ s("button", { className: "btn btn-primary", disabled: a || !e.trim(), onClick: i, children: "Import" }),
3426
+ /* @__PURE__ */ s("button", { className: "btn", onClick: n, children: "Cancel" })
3427
+ ] })
3428
+ ] }) });
3429
+ }
3430
+ function mt() {
3431
+ const n = g((t) => t.toasts), e = g((t) => t.dismissToast);
3432
+ return /* @__PURE__ */ s("div", { className: "toast-stack", children: n.map((t) => /* @__PURE__ */ p("div", { className: `toast ${t.kind}`, children: [
3433
+ /* @__PURE__ */ s("span", { className: "toast-msg", children: t.message }),
3434
+ /* @__PURE__ */ s("button", { className: "toast-close", title: "Dismiss", onClick: () => e(t.id), children: "✕" })
3435
+ ] }, t.id)) });
3436
+ }
3437
+ let ht = 0;
3438
+ const he = () => `ws_${++ht}`, Re = [
3439
+ { preset: "layout", graph: "examples/models/mnist.riko" },
3440
+ { preset: "train", graph: "examples/models/mnist_cnn.riko" },
3441
+ { preset: "eval", graph: "examples/models/mnist_infer.riko" },
3442
+ { preset: "config", graph: "examples/models/mnist.riko" },
3443
+ { preset: "utils", graph: "examples/models/mnist.riko" },
3444
+ { preset: "models", graph: "examples/models/mnist.riko" },
3445
+ { preset: "datasets", graph: "examples/models/mnist.riko" }
3446
+ ];
3447
+ function wt() {
3448
+ var T;
3449
+ const n = g((N) => N.loadNodeDefs), e = g((N) => N.openFile), t = g((N) => N.welcomeOpen), a = g((N) => N.setWelcomeOpen), o = g((N) => N.prefsOpen), r = g((N) => N.setPrefsOpen), i = g((N) => N.aboutOpen), d = g((N) => N.setAboutOpen), h = g((N) => N.importFolderOpen), c = g((N) => N.setImportFolderOpen);
3450
+ B(() => {
3451
+ (async () => (await n(), await e(Re[0].graph)))();
3452
+ }, [n, e]);
3453
+ const [m, u] = F(
3454
+ () => Re.map((N) => {
3455
+ const E = ge.find((S) => S.id === N.preset);
3456
+ return { id: he(), name: E.label, root: E.build(), graph: N.graph };
3457
+ })
3458
+ ), [y, l] = F(null), [v, f] = F(null), C = y ?? ((T = m[0]) == null ? void 0 : T.id) ?? "", P = (m.find((N) => N.id === C) ?? m[0]).root, _ = ke(P) > 1, $ = (N) => {
3459
+ u((E) => E.map((S) => S.id === C ? { ...S, root: N(S.root) } : S));
3460
+ }, A = (N) => {
3461
+ l(N);
3462
+ const E = m.find((S) => S.id === N);
3463
+ E != null && E.graph && !g.getState().dirty && e(E.graph);
3464
+ }, w = (N) => {
3465
+ const E = ge.find((D) => D.id === N);
3466
+ if (!E) return;
3467
+ const S = { id: he(), name: E.label, root: E.build() };
3468
+ u((D) => [...D, S]), l(S.id);
3469
+ }, x = (N) => {
3470
+ u((E) => {
3471
+ if (E.length <= 1) return E;
3472
+ const S = E.filter((D) => D.id !== N);
3473
+ return N === C && l(S[0].id), S;
3474
+ });
3475
+ }, k = (N, E) => {
3476
+ u((S) => S.map((D) => D.id === N ? { ...D, name: E } : D));
3477
+ }, L = (N) => {
3478
+ const E = m.find((U) => U.id === N);
3479
+ if (!E) return;
3480
+ const S = {
3481
+ id: he(),
3482
+ name: `${E.name} copy`,
3483
+ root: JSON.parse(JSON.stringify(E.root))
3484
+ }, D = m.findIndex((U) => U.id === N), W = [...m];
3485
+ W.splice(D + 1, 0, S), u(W), l(S.id);
3486
+ }, j = (N, E) => {
3487
+ u((S) => {
3488
+ const D = S.findIndex((Z) => Z.id === N), W = D + E;
3489
+ if (D < 0 || W < 0 || W >= S.length) return S;
3490
+ const U = [...S], [pe] = U.splice(D, 1);
3491
+ return U.splice(W, 0, pe), U;
3492
+ });
3493
+ }, R = (N, E) => {
3494
+ u((S) => {
3495
+ const D = S.findIndex((Z) => Z.id === N), W = S.findIndex((Z) => Z.id === E);
3496
+ if (D < 0 || W < 0 || D === W) return S;
3497
+ const U = [...S], [pe] = U.splice(D, 1);
3498
+ return U.splice(W, 0, pe), U;
3499
+ });
3500
+ }, K = (v == null ? void 0 : v.mode) === "merge" ? ve(P, v.leafId) : null, V = (N) => {
3501
+ if (N.kind === "leaf") {
3502
+ const E = v && v.leafId === N.id && v.mode !== "merge" ? v : null;
3503
+ return /* @__PURE__ */ s(
3504
+ it,
3505
+ {
3506
+ id: N.id,
3507
+ type: N.type,
3508
+ canMerge: _,
3509
+ mergeTarget: N.id === K,
3510
+ preview: E,
3511
+ onType: (S) => $((D) => Ne(D, N.id, S)),
3512
+ onSplitRow: () => $((S) => re(S, N.id, "row")),
3513
+ onSplitColumn: () => $((S) => re(S, N.id, "column")),
3514
+ onMerge: () => $((S) => be(S, N.id)),
3515
+ onClose: () => $((S) => ye(S, N.id)),
3516
+ onPreview: (S) => f(S ? { ...S, leafId: N.id } : null)
3517
+ },
3518
+ N.id
3519
+ );
3520
+ }
3521
+ return N.direction === "row" ? /* @__PURE__ */ p(
3522
+ "div",
3523
+ {
3524
+ className: N.id === K ? "merge-target" : void 0,
3525
+ style: { display: "flex", flex: "1 1 0", minWidth: 0, minHeight: 0 },
3526
+ children: [
3527
+ /* @__PURE__ */ s("div", { style: { flex: N.ratio, minWidth: 0, minHeight: 0, display: "flex" }, children: V(N.first) }),
3528
+ /* @__PURE__ */ s(
3529
+ Pn,
3530
+ {
3531
+ onDrag: (E, S) => $((D) => ie(D, N.id, E / S))
3532
+ }
3533
+ ),
3534
+ /* @__PURE__ */ s("div", { style: { flex: 1 - N.ratio, minWidth: 0, minHeight: 0, display: "flex" }, children: V(N.second) })
3535
+ ]
3536
+ },
3537
+ N.id
3538
+ ) : /* @__PURE__ */ p(
3539
+ "div",
3540
+ {
3541
+ className: N.id === K ? "merge-target" : void 0,
3542
+ style: { display: "flex", flexDirection: "column", flex: "1 1 0", minWidth: 0, minHeight: 0 },
3543
+ children: [
3544
+ /* @__PURE__ */ s("div", { style: { flex: N.ratio, minWidth: 0, minHeight: 0, display: "flex" }, children: V(N.first) }),
3545
+ /* @__PURE__ */ s(
3546
+ Fn,
3547
+ {
3548
+ onDrag: (E, S) => $((D) => ie(D, N.id, E / S))
3549
+ }
3550
+ ),
3551
+ /* @__PURE__ */ s("div", { style: { flex: 1 - N.ratio, minWidth: 0, minHeight: 0, display: "flex" }, children: V(N.second) })
3552
+ ]
3553
+ },
3554
+ N.id
3555
+ );
3556
+ };
3557
+ return /* @__PURE__ */ p("div", { className: "app", children: [
3558
+ /* @__PURE__ */ s(Tn, {}),
3559
+ /* @__PURE__ */ s(
3560
+ Cn,
3561
+ {
3562
+ workspaces: m,
3563
+ activeWorkspaceId: C,
3564
+ onSwitchWorkspace: A,
3565
+ onAddWorkspace: w,
3566
+ onRemoveWorkspace: x,
3567
+ onRenameWorkspace: k,
3568
+ onDuplicateWorkspace: L,
3569
+ onMoveWorkspace: j,
3570
+ onReorderWorkspace: R
3571
+ }
3572
+ ),
3573
+ /* @__PURE__ */ s("div", { className: "app-body", children: V(P) }),
3574
+ t && /* @__PURE__ */ s(lt, { onClose: () => a(!1) }),
3575
+ o && /* @__PURE__ */ s(dt, { onClose: () => r(!1) }),
3576
+ i && /* @__PURE__ */ s(pt, { onClose: () => d(!1) }),
3577
+ h && /* @__PURE__ */ s(ut, { onClose: () => c(!1) }),
3578
+ /* @__PURE__ */ s(Sn, {}),
3579
+ /* @__PURE__ */ s(mt, {})
3580
+ ] });
3581
+ }
3582
+ function _t() {
3583
+ const n = g((i) => i.status), e = g((i) => i.run), t = g((i) => i.save), a = g((i) => i.load), o = g((i) => i.exportPython), r = M(null);
3584
+ return /* @__PURE__ */ p("div", { className: "toolbar", children: [
3585
+ /* @__PURE__ */ s("h1", { children: "Entropia Riko" }),
3586
+ /* @__PURE__ */ s(
3587
+ "button",
3588
+ {
3589
+ className: "btn btn-primary",
3590
+ onClick: e,
3591
+ disabled: n === "running",
3592
+ children: n === "running" ? "Running…" : "Inference"
3593
+ }
3594
+ ),
3595
+ /* @__PURE__ */ s(
3596
+ "button",
3597
+ {
3598
+ className: "btn",
3599
+ onClick: () => {
3600
+ g.getState().log("训练模式:单步执行..."), e();
3601
+ },
3602
+ disabled: n === "running",
3603
+ style: { borderColor: "var(--color-success)", color: "var(--color-success)" },
3604
+ children: "Train"
3605
+ }
3606
+ ),
3607
+ /* @__PURE__ */ s("button", { className: "btn", onClick: o, disabled: n === "running", children: "Export .py" }),
3608
+ /* @__PURE__ */ s(
3609
+ "button",
3610
+ {
3611
+ className: "btn",
3612
+ onClick: () => g.getState().addNode("mnist_loader", { x: 300, y: 200 }),
3613
+ disabled: n === "running",
3614
+ children: "Load Data"
3615
+ }
3616
+ ),
3617
+ /* @__PURE__ */ s("button", { className: "btn", onClick: t, disabled: n === "running", children: "Save" }),
3618
+ /* @__PURE__ */ s(
3619
+ "button",
3620
+ {
3621
+ className: "btn",
3622
+ onClick: () => {
3623
+ var i;
3624
+ return (i = r.current) == null ? void 0 : i.click();
3625
+ },
3626
+ disabled: n === "running",
3627
+ children: "Load"
3628
+ }
3629
+ ),
3630
+ /* @__PURE__ */ s(
3631
+ "input",
3632
+ {
3633
+ ref: r,
3634
+ type: "file",
3635
+ accept: ".json,application/json",
3636
+ style: { display: "none" },
3637
+ onChange: (i) => {
3638
+ var h;
3639
+ const d = (h = i.target.files) == null ? void 0 : h[0];
3640
+ d && a(d), i.target.value = "";
3641
+ }
3642
+ }
3643
+ ),
3644
+ /* @__PURE__ */ s("div", { className: "spacer" }),
3645
+ /* @__PURE__ */ s("span", { className: `status-badge ${n}`, children: n })
3646
+ ] });
3647
+ }
3648
+ function Ye(n, e = []) {
3649
+ for (const t of n)
3650
+ t.type === "file" ? e.push(t.path) : t.children && Ye(t.children, e);
3651
+ return e;
3652
+ }
3653
+ function xt({ onClose: n }) {
3654
+ const e = g((b) => b.pushToast), t = g((b) => b.bumpProject), [a, o] = F("~"), [r, i] = F([]), [d, h] = F(""), [c, m] = F([]), [u, y] = F(""), l = async (b) => {
3655
+ const _ = await (await fetch(`/api/fs/list?path=${encodeURIComponent(b)}`)).json();
3656
+ _.status === "success" ? (o(_.path), h(_.parent), i(_.entries ?? [])) : e("error", _.error ?? "list failed");
3657
+ }, v = async () => {
3658
+ try {
3659
+ const P = await (await fetch("/api/project/tree")).json(), _ = Ye(Array.isArray(P.tree) ? P.tree : []);
3660
+ m(_), _.length && !u && y(_[0]);
3661
+ } catch {
3662
+ }
3663
+ };
3664
+ B(() => {
3665
+ l("~"), v();
3666
+ }, []);
3667
+ const f = async (b) => {
3668
+ const _ = await (await fetch("/api/fs/import", {
3669
+ method: "POST",
3670
+ headers: { "Content-Type": "application/json" },
3671
+ body: JSON.stringify({ src: b.path })
3672
+ })).json();
3673
+ _.status === "success" ? (e("success", `Imported ${b.name}`), t(), v()) : e("error", _.error ?? "import failed");
3674
+ }, C = async () => {
3675
+ if (!u) return;
3676
+ const P = await (await fetch("/api/fs/export", {
3677
+ method: "POST",
3678
+ headers: { "Content-Type": "application/json" },
3679
+ body: JSON.stringify({ src: u, dest: a })
3680
+ })).json();
3681
+ P.status === "success" ? e("success", `Exported to ${P.path}`) : e("error", P.error ?? "export failed");
3682
+ };
3683
+ return /* @__PURE__ */ s("div", { className: "prefs-overlay", onClick: n, children: /* @__PURE__ */ p("div", { className: "explorer", onClick: (b) => b.stopPropagation(), children: [
3684
+ /* @__PURE__ */ p("div", { className: "prefs-header", children: [
3685
+ /* @__PURE__ */ s("span", { className: "prefs-title", children: "File Explorer" }),
3686
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: n, children: "✕" })
3687
+ ] }),
3688
+ /* @__PURE__ */ p("div", { className: "explorer-toolbar", children: [
3689
+ /* @__PURE__ */ s("button", { className: "panel-btn", title: "Up", onClick: () => l(d), children: "↑" }),
3690
+ /* @__PURE__ */ s(
3691
+ "input",
3692
+ {
3693
+ className: "prefs-input",
3694
+ value: a,
3695
+ onChange: (b) => o(b.target.value),
3696
+ onKeyDown: (b) => {
3697
+ b.key === "Enter" && l(a);
3698
+ }
3699
+ }
3700
+ ),
3701
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: () => l(a), children: "Go" })
3702
+ ] }),
3703
+ /* @__PURE__ */ p("div", { className: "explorer-body", children: [
3704
+ /* @__PURE__ */ p("div", { className: "explorer-list", children: [
3705
+ r.map((b) => /* @__PURE__ */ p(
3706
+ "div",
3707
+ {
3708
+ className: "explorer-row",
3709
+ onDoubleClick: () => b.type === "dir" && l(b.path),
3710
+ children: [
3711
+ /* @__PURE__ */ s("span", { className: "explorer-icon", children: b.type === "dir" ? "▦" : "▤" }),
3712
+ /* @__PURE__ */ s("span", { className: "explorer-name", children: b.name }),
3713
+ /* @__PURE__ */ s("span", { className: "explorer-size", children: b.type === "file" ? `${Math.round(b.size / 1024)} KB` : "" }),
3714
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: () => f(b), title: "Import into working folder", children: "⇥ Import" })
3715
+ ]
3716
+ },
3717
+ b.path
3718
+ )),
3719
+ r.length === 0 && /* @__PURE__ */ s("div", { className: "welcome-empty", children: "Empty folder." })
3720
+ ] }),
3721
+ /* @__PURE__ */ p("div", { className: "explorer-export", children: [
3722
+ /* @__PURE__ */ s("div", { className: "prefs-desc", style: { margin: 0 }, children: "Export a working-folder file to the current folder:" }),
3723
+ /* @__PURE__ */ s("select", { className: "prefs-input", value: u, onChange: (b) => y(b.target.value), children: c.map((b) => /* @__PURE__ */ s("option", { value: b, children: b }, b)) }),
3724
+ /* @__PURE__ */ s("button", { className: "btn", disabled: !u, onClick: C, children: "Export here" })
3725
+ ] })
3726
+ ] })
3727
+ ] }) });
3728
+ }
3729
+ function St({
3730
+ path: n,
3731
+ name: e,
3732
+ onClose: t
3733
+ }) {
3734
+ const [a, o] = F(""), [r, i] = F(null), d = g((c) => c.pushToast);
3735
+ return B(() => {
3736
+ fetch("/api/project/code", {
3737
+ method: "POST",
3738
+ headers: { "Content-Type": "application/json" },
3739
+ body: JSON.stringify({ path: n })
3740
+ }).then((c) => c.json()).then((c) => {
3741
+ c.status === "success" ? o(c.code ?? "") : i(c.error ?? "failed to generate code");
3742
+ }).catch((c) => i(c instanceof Error ? c.message : String(c)));
3743
+ }, [n]), /* @__PURE__ */ s("div", { className: "prefs-overlay", onClick: t, children: /* @__PURE__ */ p("div", { className: "code-preview", onClick: (c) => c.stopPropagation(), children: [
3744
+ /* @__PURE__ */ p("div", { className: "code-preview-head", children: [
3745
+ /* @__PURE__ */ p("span", { className: "code-preview-title", children: [
3746
+ "PyTorch · ",
3747
+ e
3748
+ ] }),
3749
+ /* @__PURE__ */ s("span", { className: "spacer" }),
3750
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: async () => {
3751
+ try {
3752
+ await navigator.clipboard.writeText(a), d("success", "Copied PyTorch code");
3753
+ } catch {
3754
+ d("error", "Copy failed");
3755
+ }
3756
+ }, children: "Copy" }),
3757
+ /* @__PURE__ */ s("button", { className: "panel-btn", onClick: t, children: "✕" })
3758
+ ] }),
3759
+ /* @__PURE__ */ s("pre", { className: "code-preview-body", children: r ? `// ${r}` : a || "// generating…" })
3760
+ ] }) });
3761
+ }
3762
+ export {
3763
+ te as APP_VERSION,
3764
+ pt as AboutPanel,
3765
+ wt as App,
3766
+ de as BrandLogo,
3767
+ et as CodeEditor,
3768
+ St as CodePreview,
3769
+ Ln as ContextMenu,
3770
+ Qn as DocsPanel,
3771
+ xt as FileExplorer,
3772
+ On as FileManager,
3773
+ Sn as FilePickerHost,
3774
+ ce as FloatingWindow,
3775
+ Mn as GraphCanvas,
3776
+ Fn as HSplitter,
3777
+ nt as HandwritingPad,
3778
+ ut as ImportFolderPanel,
3779
+ Jn as LossPanel,
3780
+ Cn as MenuBar,
3781
+ $n as NodeCard,
3782
+ En as NodeLibrary,
3783
+ _e as PANEL_TYPES,
3784
+ it as PanelSlot,
3785
+ ze as PluginPanel,
3786
+ oe as PopupMenu,
3787
+ dt as PreferencesPanel,
3788
+ Zn as ProjectPanel,
3789
+ Bn as SideInspector,
3790
+ Kn as StatusPanel,
3791
+ Tn as Titlebar,
3792
+ mt as ToastStack,
3793
+ _t as Toolbar,
3794
+ Pn as VSplitter,
3795
+ ge as WORKSPACE_PRESETS,
3796
+ lt as WelcomePanel,
3797
+ _n as WorkspaceTabs,
3798
+ Be as applyBackground,
3799
+ We as applyTheme,
3800
+ ye as closeLeaf,
3801
+ ke as countLeaves,
3802
+ fn as defaultLayout,
3803
+ I as leaf,
3804
+ un as loadTheme,
3805
+ be as mergeLeaf,
3806
+ Q as openFilePicker,
3807
+ st as panelLabel,
3808
+ kt as registerPanelContent,
3809
+ at as renderPanelContent,
3810
+ ie as resizeSplit,
3811
+ Ne as setLeafType,
3812
+ Nt as setPanelTypeVisibility,
3813
+ ve as siblingNodeId,
3814
+ O as split,
3815
+ re as splitLeaf,
3816
+ g as useGraphStore
3817
+ };