entropia-template-ui 0.2.2 → 0.2.3

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