entropia-template-ui 0.1.0

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