noteconnection 1.6.8 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/LICENSE +674 -21
  2. package/README.md +258 -64
  3. package/dist/src/agent_workspace.contract.parity.test.js +475 -0
  4. package/dist/src/agent_workspace.frontend.test.js +9989 -0
  5. package/dist/src/agent_workspace.locale.contract.test.js +95 -0
  6. package/dist/src/agent_workspace.runtime.behavior.test.js +5072 -0
  7. package/dist/src/copy.assets.contract.test.js +130 -0
  8. package/dist/src/core/PathBridge.js +28 -3
  9. package/dist/src/export/WorkspaceExportBundle.js +1130 -0
  10. package/dist/src/export/WorkspaceExportBundle.test.js +1480 -0
  11. package/dist/src/export/index.js +18 -0
  12. package/dist/src/export/types.js +2 -0
  13. package/dist/src/fixrisk.issue.verifier.contract.test.js +3 -1
  14. package/dist/src/foundation.ann.runtime.contract.test.js +81 -0
  15. package/dist/src/foundation.release.evidence.contract.test.js +406 -0
  16. package/dist/src/foundation.sqlite.runtime.contract.test.js +82 -0
  17. package/dist/src/frontend/README.md +258 -64
  18. package/dist/src/frontend/agent_workspace.js +5353 -0
  19. package/dist/src/frontend/agent_workspace_runtime.js +4434 -0
  20. package/dist/src/frontend/analysis.js +31 -4
  21. package/dist/src/frontend/app.js +3597 -141
  22. package/dist/src/frontend/focus_mode_interactions.js +33 -0
  23. package/dist/src/frontend/godot_future_path_renderer.js +540 -0
  24. package/dist/src/frontend/godot_tree_interactions.js +178 -0
  25. package/dist/src/frontend/graph_state.mjs +105 -0
  26. package/dist/src/frontend/hosted_future_path_runtime.js +157 -0
  27. package/dist/src/frontend/i18n.mjs +186 -0
  28. package/dist/src/frontend/index.html +641 -266
  29. package/dist/src/frontend/layout_gpu.js +12 -4
  30. package/dist/src/frontend/locales/en.json +721 -5
  31. package/dist/src/frontend/locales/zh.json +721 -5
  32. package/dist/src/frontend/main.mjs +60 -0
  33. package/dist/src/frontend/markdown_runtime.js +827 -0
  34. package/dist/src/frontend/notemd.css +49 -0
  35. package/dist/src/frontend/notemd.html +7 -1
  36. package/dist/src/frontend/notemd.js +64 -0
  37. package/dist/src/frontend/path.html +107 -0
  38. package/dist/src/frontend/path_app.js +2189 -150
  39. package/dist/src/frontend/path_layout.mjs +143 -0
  40. package/dist/src/frontend/path_mermaid_utils.mjs +108 -0
  41. package/dist/src/frontend/path_modules_bridge.js +486 -0
  42. package/dist/src/frontend/path_state.mjs +118 -0
  43. package/dist/src/frontend/path_styles.css +146 -0
  44. package/dist/src/frontend/path_worker_bridge.mjs +85 -0
  45. package/dist/src/frontend/reader.js +522 -27
  46. package/dist/src/frontend/runtime_bridge.js +67 -54
  47. package/dist/src/frontend/runtime_bridge.mjs +279 -0
  48. package/dist/src/frontend/settings.js +130 -12
  49. package/dist/src/frontend/simulationWorker.js +241 -6
  50. package/dist/src/frontend/source_manager.js +190 -21
  51. package/dist/src/frontend/styles.css +2853 -72
  52. package/dist/src/frontend/workbench_state.mjs +101 -0
  53. package/dist/src/frontend/workspace_panes.js +10168 -0
  54. package/dist/src/frontend.locale.contract.test.js +62 -0
  55. package/dist/src/godot.sidecar.bootstrap.contract.test.js +244 -0
  56. package/dist/src/indexing/IndexLifecycle.js +195 -0
  57. package/dist/src/indexing/IndexLifecycle.test.js +49 -0
  58. package/dist/src/indexing/SegmentBuilder.js +64 -0
  59. package/dist/src/indexing/UnitBuilder.js +48 -0
  60. package/dist/src/indexing/types.js +2 -0
  61. package/dist/src/knowledge.api.contract.test.js +170 -0
  62. package/dist/src/learning/KnowledgeLearningPlatform.js +10386 -0
  63. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +327 -0
  64. package/dist/src/learning/KnowledgeLearningPlatform.program-f.test.js +99 -0
  65. package/dist/src/learning/KnowledgeLearningPlatform.test.js +2971 -0
  66. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +2974 -0
  67. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +3928 -0
  68. package/dist/src/learning/answerReleaseReview.js +4319 -0
  69. package/dist/src/learning/answerReleaseReview.test.js +2888 -0
  70. package/dist/src/learning/api.js +2 -0
  71. package/dist/src/learning/conversationComposer.js +1480 -0
  72. package/dist/src/learning/conversationComposer.test.js +1817 -0
  73. package/dist/src/learning/domains/ConversationManager.js +53 -0
  74. package/dist/src/learning/domains/KnowledgeIngestor.js +238 -0
  75. package/dist/src/learning/domains/KnowledgeQuerier.js +187 -0
  76. package/dist/src/learning/domains/MasteryEngine.js +387 -0
  77. package/dist/src/learning/domains/MemoryPolicyManager.js +408 -0
  78. package/dist/src/learning/domains/QualityEvaluator.js +307 -0
  79. package/dist/src/learning/domains/TutorRouter.js +313 -0
  80. package/dist/src/learning/domains/index.js +33 -0
  81. package/dist/src/learning/domains/types.js +7 -0
  82. package/dist/src/learning/errors.js +29 -0
  83. package/dist/src/learning/evidenceContextAssembler.js +1176 -0
  84. package/dist/src/learning/evidenceContextAssembler.test.js +6332 -0
  85. package/dist/src/learning/graphContextAssembler.js +870 -0
  86. package/dist/src/learning/graphContextAssembler.test.js +1033 -0
  87. package/dist/src/learning/index.js +28 -0
  88. package/dist/src/learning/queryBackend.js +1898 -0
  89. package/dist/src/learning/queryBackend.test.js +955 -0
  90. package/dist/src/learning/ragContextPack.js +257 -0
  91. package/dist/src/learning/ragContextPack.test.js +160 -0
  92. package/dist/src/learning/ragPublicText.js +38 -0
  93. package/dist/src/learning/ragSufficiencyJudge.js +161 -0
  94. package/dist/src/learning/ragSufficiencyJudge.test.js +177 -0
  95. package/dist/src/learning/ragSufficiencyProviderJudge.js +227 -0
  96. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +156 -0
  97. package/dist/src/learning/requestNormalization.js +198 -0
  98. package/dist/src/learning/runtimeCapability.js +4677 -0
  99. package/dist/src/learning/runtimeCapability.test.js +3635 -0
  100. package/dist/src/learning/store.js +1240 -0
  101. package/dist/src/learning/store.test.js +1126 -0
  102. package/dist/src/learning/tutorAdapter.js +2 -0
  103. package/dist/src/learning/types.js +2 -0
  104. package/dist/src/learning/vectorAccelerationAdapter.js +942 -0
  105. package/dist/src/learning/vectorAccelerationAdapter.test.js +382 -0
  106. package/dist/src/lfs.asset.policy.contract.test.js +153 -0
  107. package/dist/src/license.policy.contract.test.js +66 -0
  108. package/dist/src/memory/MemoryGovernance.js +74 -0
  109. package/dist/src/memory/MemoryGovernance.test.js +46 -0
  110. package/dist/src/memory/types.js +2 -0
  111. package/dist/src/mermaid.frontend.guard.contract.test.js +77 -0
  112. package/dist/src/middleware/auth.js +17 -0
  113. package/dist/src/middleware/body-parser.js +45 -0
  114. package/dist/src/middleware/cors.js +44 -0
  115. package/dist/src/middleware/index.js +21 -0
  116. package/dist/src/middleware/request-trace.js +96 -0
  117. package/dist/src/notemd/AppConfigToml.js +6 -4
  118. package/dist/src/notemd/MermaidProcessor.js +400 -50
  119. package/dist/src/notemd/NotemdService.js +498 -13
  120. package/dist/src/notemd/PromptManager.js +15 -0
  121. package/dist/src/notemd/cli/commands.js +357 -0
  122. package/dist/src/notemd/cli/dispatcher.js +225 -0
  123. package/dist/src/notemd/cli/index.js +169 -0
  124. package/dist/src/notemd/cli/parser.js +68 -0
  125. package/dist/src/notemd/cli/types.js +2 -0
  126. package/dist/src/notemd/constants.js +43 -0
  127. package/dist/src/notemd/diagram/diagramGenerationService.js +78 -0
  128. package/dist/src/notemd/diagram/diagramSpec.js +79 -0
  129. package/dist/src/notemd/diagram/diagramSpecResponseParser.js +131 -0
  130. package/dist/src/notemd/diagram/intent.js +95 -0
  131. package/dist/src/notemd/diagram/planner.js +71 -0
  132. package/dist/src/notemd/diagram/prompts/diagramSpecPrompt.js +42 -0
  133. package/dist/src/notemd/diagram/types.js +18 -0
  134. package/dist/src/notemd/index.js +26 -0
  135. package/dist/src/notemd/operations/capabilityManifest.js +23 -0
  136. package/dist/src/notemd/operations/cliContracts.js +17 -0
  137. package/dist/src/notemd/operations/configProfileCommands.js +85 -0
  138. package/dist/src/notemd/operations/registry.contract.test.js +95 -0
  139. package/dist/src/notemd/operations/registry.js +991 -0
  140. package/dist/src/notemd/operations/types.js +2 -0
  141. package/dist/src/notemd/providerDiagnostics.js +220 -0
  142. package/dist/src/notemd/providerProfiles.js +42 -0
  143. package/dist/src/notemd/providerTemplates.js +231 -0
  144. package/dist/src/notemd/search/DuckDuckGoProvider.js +39 -0
  145. package/dist/src/notemd/search/SearchManager.js +13 -0
  146. package/dist/src/notemd/search/SearchProvider.js +2 -0
  147. package/dist/src/notemd/search/TavilyProvider.js +44 -0
  148. package/dist/src/notemd.agent.manifest.test.js +85 -0
  149. package/dist/src/notemd.api.contract.test.js +14 -1
  150. package/dist/src/notemd.app_config_toml.test.js +2 -1
  151. package/dist/src/notemd.batch.workflow.test.js +117 -0
  152. package/dist/src/notemd.cli.e2e.test.js +136 -0
  153. package/dist/src/notemd.core.test.js +51 -0
  154. package/dist/src/notemd.diagram.pipeline.test.js +233 -0
  155. package/dist/src/notemd.providerTemplates.test.js +34 -0
  156. package/dist/src/notemd.server.integration.test.js +143 -35
  157. package/dist/src/notemd.workflow.pipeline.test.js +162 -0
  158. package/dist/src/pathbridge.handshake.contract.test.js +16 -2
  159. package/dist/src/pathmode.background.contract.test.js +69 -0
  160. package/dist/src/pathmode.settings.api.contract.test.js +9 -0
  161. package/dist/src/pkg.sidecar.contract.test.js +9 -3
  162. package/dist/src/platform/ExportProfile.js +58 -0
  163. package/dist/src/platform/PlatformCapabilities.js +45 -0
  164. package/dist/src/platform/PlatformCapabilities.test.js +30 -0
  165. package/dist/src/platform/RenderMaterializer.js +33 -0
  166. package/dist/src/platform/RenderMaterializer.test.js +32 -0
  167. package/dist/src/query_backend.external_http.integration.test.js +410 -0
  168. package/dist/src/reader_renderer.js +404 -3
  169. package/dist/src/reader_renderer.test.js +87 -0
  170. package/dist/src/release.godot.mirror.contract.test.js +73 -0
  171. package/dist/src/resources/ResourceRegistry.js +223 -0
  172. package/dist/src/resources/ResourceRegistry.test.js +61 -0
  173. package/dist/src/resources/types.js +2 -0
  174. package/dist/src/routes/agentWorkspaceDiagnostics.js +173 -0
  175. package/dist/src/routes/data.js +267 -0
  176. package/dist/src/routes/diagnostics.js +51 -0
  177. package/dist/src/routes/index.js +23 -0
  178. package/dist/src/routes/knowledge.js +968 -0
  179. package/dist/src/routes/markdown.js +287 -0
  180. package/dist/src/routes/notemd.js +565 -0
  181. package/dist/src/routes/registry.contract.test.js +130 -0
  182. package/dist/src/routes/render.js +285 -0
  183. package/dist/src/routes/runtimeRunbookRouteOps.js +149 -0
  184. package/dist/src/routes/runtimeRunbookRouteOps.test.js +194 -0
  185. package/dist/src/routes/settings.js +6 -0
  186. package/dist/src/routes/staticFiles.js +94 -0
  187. package/dist/src/routes/types.js +2 -0
  188. package/dist/src/runtime.transport.adapter.contract.test.js +81 -0
  189. package/dist/src/server.js +11090 -1549
  190. package/dist/src/server.migration.test.js +193 -21
  191. package/dist/src/server.port.fallback.contract.test.js +63 -0
  192. package/dist/src/session/SessionStateStore.js +81 -0
  193. package/dist/src/session/SessionStateStore.test.js +58 -0
  194. package/dist/src/session/types.js +2 -0
  195. package/dist/src/settings.runtime.contract.test.js +50 -0
  196. package/dist/src/shared/types.contract.test.js +107 -0
  197. package/dist/src/shared/types.js +22 -0
  198. package/dist/src/sidecar.replacement.boundary.contract.test.js +128 -0
  199. package/dist/src/sidecar.supply.readiness.contract.test.js +144 -0
  200. package/dist/src/source_manager.loadflow.test.js +46 -0
  201. package/dist/src/startup.layout.snapshot.contract.test.js +57 -0
  202. package/dist/src/tauri.frontend.build.contract.test.js +60 -0
  203. package/dist/src/tauri.sidecar.cleanup.contract.test.js +21 -0
  204. package/dist/src/utils/RuntimePaths.js +4 -13
  205. package/dist/src/utils/platform.js +153 -0
  206. package/dist/src/workflows/WorkflowArtifactStore.js +96 -0
  207. package/dist/src/workflows/WorkflowArtifactStore.test.js +80 -0
  208. package/dist/src/workflows/types.js +2 -0
  209. package/dist/src/workspace/WorkspaceRegistry.js +122 -0
  210. package/dist/src/workspace/WorkspaceRegistry.test.js +29 -0
  211. package/dist/src/workspace/types.js +2 -0
  212. package/package.json +61 -10
@@ -1,8 +1,25 @@
1
1
  /**
2
2
  * Path Mode Application Controller
3
3
  * Handles interaction, rendering, and worker communication.
4
+ *
5
+ * Phase 4 P5: Canonical utility implementations now live in
6
+ * path_mermaid_utils.mjs and path_state.mjs, exposed via
7
+ * window.pathModules (loaded by path_modules_bridge.js).
8
+ * The _methodName functions below delegate to window.pathModules
9
+ * when available, falling back to inline implementations.
4
10
  */
5
11
 
12
+ (function () {
13
+ 'use strict';
14
+ var _pm = window.pathModules;
15
+ if (_pm && _pm.utils) {
16
+ // Pre-bind canonical implementations before pathApp definition.
17
+ // These will be used by the methods defined below.
18
+ window._pathUtils = _pm.utils;
19
+ window._pathState = _pm.state;
20
+ }
21
+ })();
22
+
6
23
  window.pathApp = {
7
24
  canvas: null,
8
25
  ctx: null,
@@ -36,8 +53,32 @@ window.pathApp = {
36
53
  },
37
54
  bridgeLanguageListenerRegistered: false,
38
55
  bridgeMermaidRenderQueue: Promise.resolve(),
56
+ pendingWindowVisibility: null,
39
57
  semanticA11yLastSummaryKey: '',
40
58
  semanticA11yLastAnnouncementAt: 0,
59
+ semanticA11yDeferredRefreshTimer: null,
60
+ learningWorkbench: {
61
+ userId: 'path_user_default',
62
+ loading: false,
63
+ lastError: '',
64
+ lastUpdatedAt: '',
65
+ sessionPlan: null,
66
+ qualitySnapshot: null,
67
+ qualityGateEvaluation: null,
68
+ qualityBaselineSnapshot: null,
69
+ misconceptions: null,
70
+ runtimeState: null,
71
+ tutorFeedback: null,
72
+ sessionExecution: null,
73
+ sessionHistory: null,
74
+ sessionHistoryQuery: {
75
+ limit: 8,
76
+ offset: 0,
77
+ executionKind: 'all',
78
+ fromDate: '',
79
+ toDate: '',
80
+ },
81
+ },
41
82
 
42
83
  // Animation State
43
84
  animationId: null,
@@ -101,6 +142,7 @@ window.pathApp = {
101
142
  this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend'));
102
143
  this._ensureLanguageSyncListener();
103
144
  this.syncLanguageWithBridge();
145
+ this._flushPendingWindowVisibility('socket-open');
104
146
  };
105
147
  this.ws.onmessage = (e) => {
106
148
  try {
@@ -254,6 +296,7 @@ window.pathApp = {
254
296
  this._sendBridgeMessage('identify', this._getBridgeIdentifyPayload('frontend'));
255
297
  this._ensureLanguageSyncListener();
256
298
  this.syncLanguageWithBridge();
299
+ this._flushPendingWindowVisibility('socket-reuse');
257
300
  }
258
301
  },
259
302
 
@@ -320,6 +363,31 @@ window.pathApp = {
320
363
  return true;
321
364
  },
322
365
 
366
+ _isDetachedPreviewWithoutRuntimeBootstrap: function() {
367
+ if (typeof window === 'undefined' || window.__TAURI__) {
368
+ return false;
369
+ }
370
+ const hasDetachedPreviewScript = Array.from(document.scripts || []).some((script) =>
371
+ /__tauri_cli/.test(String(script && (script.text || script.textContent) || ''))
372
+ );
373
+ if (!hasDetachedPreviewScript) {
374
+ return false;
375
+ }
376
+ if (!window.NoteConnectionRuntime || typeof window.NoteConnectionRuntime.getBaseUrl !== 'function') {
377
+ return true;
378
+ }
379
+ const currentOrigin = String((window.location && window.location.origin) || '').trim().replace(/\/+$/, '');
380
+ const baseUrl = String(window.NoteConnectionRuntime.getBaseUrl() || '').trim();
381
+ if (!currentOrigin || !baseUrl) {
382
+ return true;
383
+ }
384
+ try {
385
+ return currentOrigin !== new URL(baseUrl).origin;
386
+ } catch (_error) {
387
+ return true;
388
+ }
389
+ },
390
+
323
391
  _getBridgeWsUrl: function() {
324
392
  if (typeof window !== 'undefined' && window.NoteConnectionRuntime && typeof window.NoteConnectionRuntime.getBridgeWsUrl === 'function') {
325
393
  return window.NoteConnectionRuntime.getBridgeWsUrl('frontend');
@@ -497,6 +565,68 @@ window.pathApp = {
497
565
  return true;
498
566
  },
499
567
 
568
+ _waitForBridgeSocketOpen: async function(timeoutMs = 2500) {
569
+ const budget = Math.max(0, Number(timeoutMs) || 0);
570
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
571
+ return true;
572
+ }
573
+ if (budget === 0) {
574
+ return false;
575
+ }
576
+ this._connectBridgeSocket();
577
+ const startedAt = Date.now();
578
+ while (Date.now() - startedAt < budget) {
579
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
580
+ return true;
581
+ }
582
+ await new Promise((resolve) => setTimeout(resolve, 80));
583
+ this._connectBridgeSocket();
584
+ }
585
+ return this.ws && this.ws.readyState === WebSocket.OPEN;
586
+ },
587
+
588
+ _flushPendingWindowVisibility: function(reason = 'flush') {
589
+ if (typeof this.pendingWindowVisibility !== 'boolean') {
590
+ return false;
591
+ }
592
+ const visible = this.pendingWindowVisibility;
593
+ const sent = this._sendBridgeMessage('setWindowVisible', { visible });
594
+ if (sent) {
595
+ console.log(`[PathApp] Flushed pending setWindowVisible(${visible}) via ${reason}.`);
596
+ this.pendingWindowVisibility = null;
597
+ return true;
598
+ }
599
+ return false;
600
+ },
601
+
602
+ requestBridgeWindowVisibility: async function(visible, options = {}) {
603
+ const targetVisible = visible === true;
604
+ const waitMs = Math.max(0, Number(options.waitMs) || 0);
605
+ const reason = String(options.reason || 'manual');
606
+
607
+ const sentImmediately = this._sendBridgeMessage('setWindowVisible', { visible: targetVisible });
608
+ if (sentImmediately) {
609
+ console.log(`[PathApp] Sent setWindowVisible(${targetVisible}) immediately. reason=${reason}`);
610
+ this.pendingWindowVisibility = null;
611
+ return true;
612
+ }
613
+
614
+ this.pendingWindowVisibility = targetVisible;
615
+ this._connectBridgeSocket();
616
+ if (waitMs === 0) {
617
+ console.warn(`[PathApp] Deferred setWindowVisible(${targetVisible}) until socket opens. reason=${reason}`);
618
+ return false;
619
+ }
620
+
621
+ const ready = await this._waitForBridgeSocketOpen(waitMs);
622
+ if (!ready) {
623
+ console.warn(`[PathApp] Bridge socket not ready for setWindowVisible(${targetVisible}). reason=${reason}`);
624
+ return false;
625
+ }
626
+ const flushed = this._flushPendingWindowVisibility(`wait-${reason}`);
627
+ return flushed;
628
+ },
629
+
500
630
  _getBridgeMermaidConfig: function(theme = 'dark') {
501
631
  return {
502
632
  startOnLoad: false,
@@ -533,6 +663,313 @@ window.pathApp = {
533
663
  };
534
664
  },
535
665
 
666
+ _getReaderLikeMermaidConfig: function(theme = 'dark') {
667
+ return {
668
+ startOnLoad: false,
669
+ theme,
670
+ securityLevel: 'loose',
671
+ htmlLabels: true,
672
+ };
673
+ },
674
+
675
+ _getBridgeTextMeasureContext: function() {
676
+ if (!this._bridgeTextMeasureCanvas) {
677
+ this._bridgeTextMeasureCanvas = document.createElement('canvas');
678
+ }
679
+ if (!this._bridgeTextMeasureContext) {
680
+ this._bridgeTextMeasureContext = this._bridgeTextMeasureCanvas.getContext('2d');
681
+ }
682
+ return this._bridgeTextMeasureContext;
683
+ },
684
+
685
+ _measureBridgeCanvasTextWidth: function(text, fontDescriptor) {
686
+ const context = this._getBridgeTextMeasureContext();
687
+ if (!context) {
688
+ return this._estimateBridgeTextLineWidth(text, fontDescriptor.fontSize || 16);
689
+ }
690
+ context.font = [
691
+ fontDescriptor.fontStyle || 'normal',
692
+ fontDescriptor.fontWeight || '400',
693
+ `${fontDescriptor.fontSize || 16}px`,
694
+ fontDescriptor.fontFamily || '"trebuchet ms", verdana, arial, sans-serif',
695
+ ].join(' ');
696
+ return context.measureText(String(text || '')).width;
697
+ },
698
+
699
+ _extractBridgeForeignObjectSourceLines: function(foreignObject) {
700
+ const htmlRoot = foreignObject && (
701
+ foreignObject.querySelector('div, span, p')
702
+ || foreignObject.firstElementChild
703
+ || foreignObject
704
+ );
705
+ if (!htmlRoot) {
706
+ return { lines: [], fromRenderedLayout: false };
707
+ }
708
+
709
+ const collectNormalizedLines = (value) => String(value || '')
710
+ .split(/\r?\n/)
711
+ .map((line) => this._normalizeBridgeInlineText(line))
712
+ .filter(Boolean);
713
+
714
+ // Prefer the browser's actual rendered line layout so the bridge mirrors
715
+ // the reader's foreignObject wrapping instead of guessing it again.
716
+ const renderedText = typeof htmlRoot.innerText === 'string' ? htmlRoot.innerText : '';
717
+ const renderedLines = collectNormalizedLines(renderedText);
718
+ if (renderedLines.length > 0) {
719
+ return {
720
+ lines: renderedLines,
721
+ fromRenderedLayout: true,
722
+ };
723
+ }
724
+
725
+ const paragraphs = Array.from(htmlRoot.querySelectorAll('p'));
726
+ const htmlSegments = paragraphs.length > 0
727
+ ? paragraphs.map((paragraph) => String(paragraph.innerHTML || ''))
728
+ : [String(htmlRoot.innerHTML || htmlRoot.textContent || '')];
729
+ const scratch = document.createElement('div');
730
+ const lines = [];
731
+ htmlSegments.forEach((segment) => {
732
+ scratch.innerHTML = String(segment || '').replace(/<br\s*\/?>/gi, '\n');
733
+ collectNormalizedLines(scratch.innerText || scratch.textContent || '')
734
+ .forEach((line) => lines.push(line));
735
+ });
736
+ return {
737
+ lines,
738
+ fromRenderedLayout: false,
739
+ };
740
+ },
741
+
742
+ _wrapBridgeForeignObjectLines: function(lines, fontDescriptor, maxWidth, whiteSpaceMode) {
743
+ const safeLines = Array.isArray(lines) ? lines.filter((line) => String(line || '').trim()) : [];
744
+ if (safeLines.length === 0) {
745
+ return [];
746
+ }
747
+ if (String(whiteSpaceMode || '').toLowerCase() === 'nowrap') {
748
+ return safeLines;
749
+ }
750
+
751
+ const wrapped = [];
752
+ const hardMaxLines = 24;
753
+ const measure = (text) => this._measureBridgeCanvasTextWidth(text, fontDescriptor);
754
+
755
+ safeLines.forEach((line) => {
756
+ const normalized = this._normalizeBridgeInlineText(line);
757
+ if (!normalized) {
758
+ return;
759
+ }
760
+ if (measure(normalized) <= maxWidth) {
761
+ wrapped.push(normalized);
762
+ return;
763
+ }
764
+
765
+ const tokens = /\s/.test(normalized)
766
+ ? normalized.split(/\s+/).filter(Boolean)
767
+ : Array.from(normalized);
768
+ let current = '';
769
+ const pushCurrent = () => {
770
+ if (current) {
771
+ wrapped.push(current);
772
+ current = '';
773
+ }
774
+ };
775
+
776
+ for (const token of tokens) {
777
+ const candidate = !current
778
+ ? token
779
+ : (/\s/.test(normalized) ? `${current} ${token}` : `${current}${token}`);
780
+ if (!current || measure(candidate) <= maxWidth) {
781
+ current = candidate;
782
+ continue;
783
+ }
784
+ pushCurrent();
785
+ if (measure(token) <= maxWidth) {
786
+ current = token;
787
+ continue;
788
+ }
789
+ let fragment = '';
790
+ for (const char of Array.from(token)) {
791
+ const fragmentCandidate = fragment + char;
792
+ if (!fragment || measure(fragmentCandidate) <= maxWidth) {
793
+ fragment = fragmentCandidate;
794
+ continue;
795
+ }
796
+ wrapped.push(fragment);
797
+ fragment = char;
798
+ if (wrapped.length >= hardMaxLines) {
799
+ break;
800
+ }
801
+ }
802
+ if (fragment && wrapped.length < hardMaxLines) {
803
+ current = fragment;
804
+ }
805
+ if (wrapped.length >= hardMaxLines) {
806
+ break;
807
+ }
808
+ }
809
+ pushCurrent();
810
+ });
811
+
812
+ return wrapped.slice(0, hardMaxLines);
813
+ },
814
+
815
+ _convertBridgeForeignObjectToSvgText: function(svgElement, foreignObject) {
816
+ if (!foreignObject || !foreignObject.parentNode) {
817
+ return;
818
+ }
819
+
820
+ const extracted = this._extractBridgeForeignObjectSourceLines(foreignObject);
821
+ if (!extracted || extracted.lines.length === 0) {
822
+ foreignObject.remove();
823
+ return;
824
+ }
825
+
826
+ const styleNode = foreignObject.querySelector('.nodeLabel, .edgeLabel, span, div, p') || foreignObject.firstElementChild || foreignObject;
827
+ const computedStyle = window.getComputedStyle(styleNode);
828
+ const fontSize = Number.parseFloat(computedStyle.fontSize || '') || 16;
829
+ const fontFamily = computedStyle.fontFamily || '"trebuchet ms", verdana, arial, sans-serif';
830
+ const fontWeight = computedStyle.fontWeight || '400';
831
+ const fontStyle = computedStyle.fontStyle || 'normal';
832
+ const lineHeight = Number.parseFloat(computedStyle.lineHeight || '') || (fontSize * 1.5);
833
+ const textAlign = String(computedStyle.textAlign || '').trim().toLowerCase();
834
+ const whiteSpace = String(computedStyle.whiteSpace || '').trim().toLowerCase();
835
+ const fill = computedStyle.color || '#ccc';
836
+
837
+ const x = this._parseBridgeNumericAttribute(foreignObject, 'x', 0);
838
+ const y = this._parseBridgeNumericAttribute(foreignObject, 'y', 0);
839
+ const width = this._parseBridgeNumericAttribute(foreignObject, 'width', 0);
840
+ const height = this._parseBridgeNumericAttribute(foreignObject, 'height', 0);
841
+ const horizontalPadding = 6;
842
+ const verticalPadding = 4;
843
+ const usableWidth = Math.max(12, width - horizontalPadding * 2);
844
+ const fontDescriptor = { fontSize, fontFamily, fontWeight, fontStyle };
845
+ const wrappedLines = extracted.fromRenderedLayout
846
+ ? extracted.lines.slice(0, 24)
847
+ : this._wrapBridgeForeignObjectLines(extracted.lines, fontDescriptor, usableWidth, whiteSpace);
848
+ if (wrappedLines.length === 0) {
849
+ foreignObject.remove();
850
+ return;
851
+ }
852
+
853
+ const totalHeight = (wrappedLines.length - 1) * lineHeight + fontSize;
854
+ const startY = y + Math.max(verticalPadding + fontSize * 0.85, (height - totalHeight) * 0.5 + fontSize * 0.85);
855
+ let anchor = 'middle';
856
+ let textX = x + (width * 0.5);
857
+ if (textAlign === 'left' || textAlign === 'start') {
858
+ anchor = 'start';
859
+ textX = x + horizontalPadding;
860
+ } else if (textAlign === 'right' || textAlign === 'end') {
861
+ anchor = 'end';
862
+ textX = x + width - horizontalPadding;
863
+ }
864
+
865
+ const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
866
+ textElement.setAttribute('fill', fill);
867
+ textElement.setAttribute('font-size', `${fontSize}`);
868
+ textElement.setAttribute('font-family', fontFamily);
869
+ textElement.setAttribute('font-weight', fontWeight);
870
+ textElement.setAttribute('font-style', fontStyle);
871
+ textElement.setAttribute('text-anchor', anchor);
872
+ textElement.setAttribute('dominant-baseline', 'alphabetic');
873
+
874
+ wrappedLines.forEach((line, index) => {
875
+ const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
876
+ tspan.textContent = line;
877
+ tspan.setAttribute('x', `${textX}`);
878
+ if (index === 0) {
879
+ tspan.setAttribute('y', `${startY}`);
880
+ } else {
881
+ tspan.setAttribute('dy', `${lineHeight}`);
882
+ }
883
+ textElement.appendChild(tspan);
884
+ });
885
+
886
+ foreignObject.parentNode.insertBefore(textElement, foreignObject);
887
+ foreignObject.remove();
888
+ },
889
+
890
+ _convertBridgeForeignObjectsToSvgText: function(svgElement) {
891
+ const foreignObjects = Array.from(svgElement.querySelectorAll('foreignObject'));
892
+ foreignObjects.forEach((foreignObject) => this._convertBridgeForeignObjectToSvgText(svgElement, foreignObject));
893
+ },
894
+
895
+ _renderParityMermaidSvg: async function(sourceText, options = {}) {
896
+ if (!window.mermaid) {
897
+ throw new Error('Mermaid runtime is unavailable in the frontend renderer.');
898
+ }
899
+
900
+ const source = this._normalizeBridgeMermaidDefinition(sourceText || '');
901
+ if (!source) {
902
+ throw new Error('Mermaid render request is missing source.');
903
+ }
904
+
905
+ const theme = String(options.theme || 'dark') === 'default' ? 'default' : 'dark';
906
+ window.mermaid.initialize(this._getReaderLikeMermaidConfig(theme));
907
+
908
+ const requestedWidth = Number.isFinite(Number(options.maxWidth)) && Number(options.maxWidth) > 0
909
+ ? Math.floor(Number(options.maxWidth))
910
+ : 620;
911
+ const requestedHeight = Number.isFinite(Number(options.maxHeight)) && Number(options.maxHeight) > 0
912
+ ? Math.floor(Number(options.maxHeight))
913
+ : 860;
914
+ const hostWidth = Math.max(240, requestedWidth);
915
+ const host = document.createElement('div');
916
+ host.className = 'mermaid-render-host-offscreen';
917
+ host.style.position = 'fixed';
918
+ host.style.left = '-20000px';
919
+ host.style.top = '0';
920
+ host.style.width = String(hostWidth) + 'px';
921
+ host.style.minWidth = String(hostWidth) + 'px';
922
+ host.style.height = 'auto';
923
+ host.style.overflow = 'visible';
924
+ host.style.opacity = '0';
925
+ host.style.pointerEvents = 'none';
926
+ host.style.background = 'transparent';
927
+ host.style.fontFamily = '"Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Segoe UI", sans-serif';
928
+ document.body.appendChild(host);
929
+
930
+ try {
931
+ const renderId = 'parity-mermaid-' + Date.now() + '-' + Math.random().toString(16).slice(2);
932
+ const result = await window.mermaid.render(renderId, source, host);
933
+ if (result && typeof result.svg === 'string' && this._isBridgeMermaidErrorSvgMarkup(result.svg)) {
934
+ throw new Error('Frontend Mermaid parity renderer returned an error SVG instead of a diagram.');
935
+ }
936
+ let svgElement = host.querySelector('svg');
937
+ if (!svgElement) {
938
+ const parser = new DOMParser();
939
+ const documentSvg = parser.parseFromString(result.svg, 'image/svg+xml');
940
+ const parsedSvg = documentSvg.querySelector('svg');
941
+ if (!parsedSvg) {
942
+ throw new Error('Frontend Mermaid parity renderer did not produce an SVG root.');
943
+ }
944
+ host.replaceChildren(parsedSvg);
945
+ svgElement = host.querySelector('svg');
946
+ }
947
+ if (!svgElement) {
948
+ throw new Error('Frontend Mermaid parity renderer did not produce an SVG root.');
949
+ }
950
+
951
+ svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
952
+ svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
953
+ this._convertBridgeForeignObjectsToSvgText(svgElement);
954
+ const naturalSize = this._tightenBridgeMermaidSvgBounds(svgElement);
955
+ const clampedSize = this._clampBridgeMermaidSize(naturalSize.width, naturalSize.height, requestedWidth, requestedHeight);
956
+ svgElement.setAttribute('width', String(clampedSize.width));
957
+ svgElement.setAttribute('height', String(clampedSize.height));
958
+ svgElement.style.maxWidth = '100%';
959
+ svgElement.style.height = 'auto';
960
+ svgElement.style.background = 'transparent';
961
+
962
+ return {
963
+ source,
964
+ svg: this._serializeBridgeMermaidSvg(svgElement),
965
+ width: Math.max(1, Math.round(clampedSize.width)),
966
+ height: Math.max(1, Math.round(clampedSize.height))
967
+ };
968
+ } finally {
969
+ host.remove();
970
+ }
971
+ },
972
+
536
973
  _upsertBridgeMermaidOverrideStyle: function(svgElement) {
537
974
  const styleId = 'noteconnection-mermaid-overrides';
538
975
  let styleNode = svgElement.querySelector('#' + styleId);
@@ -660,15 +1097,40 @@ window.pathApp = {
660
1097
  },
661
1098
 
662
1099
  _normalizeBridgeInlineText: function(text) {
1100
+ if (window._pathUtils) return window._pathUtils.normalizeBridgeInlineText(text);
663
1101
  return String(text || '').replace(/\s+/g, ' ').trim();
664
1102
  },
665
1103
 
1104
+ _normalizeBridgeMermaidDefinition: function(source) {
1105
+ if (window._pathUtils && typeof window._pathUtils.normalizeBridgeMermaidDefinition === 'function') {
1106
+ return window._pathUtils.normalizeBridgeMermaidDefinition(source);
1107
+ }
1108
+ return String(source || '').trim();
1109
+ },
1110
+
1111
+ _isBridgeMermaidErrorSvgMarkup: function(markup) {
1112
+ const text = String(markup || '').toLowerCase();
1113
+ if (!text) {
1114
+ return false;
1115
+ }
1116
+ return (
1117
+ text.includes('syntax error in text') ||
1118
+ text.includes('lexical error on line') ||
1119
+ text.includes('parse error on line') ||
1120
+ text.includes('mermaid version') ||
1121
+ text.includes('class="error-icon"') ||
1122
+ text.includes('id="error-icon"')
1123
+ );
1124
+ },
1125
+
666
1126
  _parseBridgeNumericAttribute: function(element, name, fallback = 0) {
1127
+ if (window._pathUtils) return window._pathUtils.parseBridgeNumericAttribute(element, name, fallback);
667
1128
  const numeric = Number.parseFloat(String(element?.getAttribute?.(name) || ''));
668
1129
  return Number.isFinite(numeric) ? numeric : fallback;
669
1130
  },
670
1131
 
671
1132
  _extractBridgeInlineStyleValue: function(styleValue, propertyName) {
1133
+ if (window._pathUtils) return window._pathUtils.extractBridgeInlineStyleValue(styleValue, propertyName);
672
1134
  if (!styleValue) {
673
1135
  return null;
674
1136
  }
@@ -678,6 +1140,7 @@ window.pathApp = {
678
1140
  },
679
1141
 
680
1142
  _resolveBridgeTextProperty: function(element, propertyName) {
1143
+ if (window._pathUtils) return window._pathUtils.resolveBridgeTextProperty(element, propertyName);
681
1144
  let current = element;
682
1145
  while (current) {
683
1146
  const attributeValue = current.getAttribute && current.getAttribute(propertyName);
@@ -694,6 +1157,7 @@ window.pathApp = {
694
1157
  },
695
1158
 
696
1159
  _parseBridgeCssLength: function(lengthValue, baseFontSize) {
1160
+ if (window._pathUtils) return window._pathUtils.parseBridgeCssLength(lengthValue, baseFontSize);
697
1161
  if (!lengthValue) {
698
1162
  return 0;
699
1163
  }
@@ -715,12 +1179,14 @@ window.pathApp = {
715
1179
  },
716
1180
 
717
1181
  _resolveBridgeSvgFontSize: function(element) {
1182
+ if (window._pathUtils) return window._pathUtils.resolveBridgeSvgFontSize(element);
718
1183
  const resolvedValue = this._resolveBridgeTextProperty(element, 'font-size');
719
1184
  const parsed = this._parseBridgeCssLength(resolvedValue, 16);
720
1185
  return parsed > 0 ? parsed : 16;
721
1186
  },
722
1187
 
723
1188
  _resolveBridgeSvgLineHeight: function(element, fontSize) {
1189
+ if (window._pathUtils) return window._pathUtils.resolveBridgeSvgLineHeight(element, fontSize);
724
1190
  const resolvedValue = this._resolveBridgeTextProperty(element, 'line-height');
725
1191
  const parsed = this._parseBridgeCssLength(resolvedValue, fontSize);
726
1192
  return parsed > 0 ? parsed : Math.max(fontSize * 1.18, fontSize + 4);
@@ -731,6 +1197,7 @@ window.pathApp = {
731
1197
  },
732
1198
 
733
1199
  _estimateBridgeGlyphWidthUnits: function(char) {
1200
+ if (window._pathUtils) return window._pathUtils.estimateBridgeGlyphWidthUnits(char);
734
1201
  if (!char) {
735
1202
  return 0;
736
1203
  }
@@ -762,6 +1229,7 @@ window.pathApp = {
762
1229
  },
763
1230
 
764
1231
  _estimateBridgeTextLineWidth: function(text, fontSize) {
1232
+ if (window._pathUtils) return window._pathUtils.estimateBridgeTextLineWidth(text, fontSize);
765
1233
  let units = 0;
766
1234
  for (const char of Array.from(String(text || ''))) {
767
1235
  units += this._estimateBridgeGlyphWidthUnits(char);
@@ -770,6 +1238,7 @@ window.pathApp = {
770
1238
  },
771
1239
 
772
1240
  _splitBridgeTokenForWrap: function(token, fontSize, maxLineWidth) {
1241
+ if (window._pathUtils) return window._pathUtils.splitBridgeTokenForWrap(token, fontSize, maxLineWidth);
773
1242
  const wrapped = [];
774
1243
  let segment = '';
775
1244
  for (const char of Array.from(token)) {
@@ -788,6 +1257,7 @@ window.pathApp = {
788
1257
  },
789
1258
 
790
1259
  _wrapBridgeMeasurementLine: function(line, fontSize, maxLineWidth) {
1260
+ if (window._pathUtils) return window._pathUtils.wrapBridgeMeasurementLine(line, fontSize, maxLineWidth);
791
1261
  const normalizedLine = this._normalizeBridgeInlineText(line);
792
1262
  if (!normalizedLine) {
793
1263
  return [];
@@ -1243,79 +1713,38 @@ window.pathApp = {
1243
1713
  },
1244
1714
 
1245
1715
  _renderMermaidForBridge: async function(payload) {
1246
- if (!window.mermaid) {
1247
- throw new Error('Mermaid runtime is unavailable in the frontend renderer.');
1248
- }
1249
-
1250
1716
  const requestId = String(payload?.requestId || '').trim();
1251
- const source = String(payload?.source || '').trim();
1252
- if (!requestId || !source) {
1717
+ if (!requestId) {
1253
1718
  throw new Error('Mermaid render request is missing a request id or source.');
1254
1719
  }
1255
1720
 
1256
1721
  const includeStages = payload?.includeStages === true;
1257
1722
  const includeSvg = includeStages || payload?.includeSvg === true;
1258
1723
  const stageSnapshots = [];
1259
- const theme = String(payload?.theme || 'dark') === 'default' ? 'default' : 'dark';
1260
- window.mermaid.initialize(this._getBridgeMermaidConfig(theme));
1261
-
1262
- const requestedWidth = Number.isFinite(Number(payload?.maxWidth)) && Number(payload.maxWidth) > 0 ? Math.floor(Number(payload.maxWidth)) : 1600;
1263
- const hostWidth = Math.max(480, requestedWidth);
1264
- const host = document.createElement('div');
1265
- host.style.position = 'fixed';
1266
- host.style.left = '-20000px';
1267
- host.style.top = '0';
1268
- host.style.width = String(hostWidth) + 'px';
1269
- host.style.minWidth = String(hostWidth) + 'px';
1270
- host.style.height = 'auto';
1271
- host.style.overflow = 'visible';
1272
- host.style.opacity = '0';
1273
- host.style.pointerEvents = 'none';
1274
- host.style.background = 'transparent';
1275
- host.style.fontFamily = '"Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Segoe UI", sans-serif';
1276
- document.body.appendChild(host);
1277
1724
 
1278
1725
  let objectUrl = null;
1279
1726
  try {
1280
- const renderId = 'bridge-mermaid-' + Date.now() + '-' + Math.random().toString(16).slice(2);
1281
- const result = await window.mermaid.render(renderId, source, host);
1282
- let svgElement = host.querySelector('svg');
1283
- if (!svgElement) {
1284
- const parser = new DOMParser();
1285
- const documentSvg = parser.parseFromString(result.svg, 'image/svg+xml');
1286
- const parsedSvg = documentSvg.querySelector('svg');
1287
- if (!parsedSvg) {
1288
- throw new Error('Frontend Mermaid renderer did not produce an SVG root.');
1289
- }
1290
- host.replaceChildren(parsedSvg);
1291
- svgElement = host.querySelector('svg');
1292
- }
1727
+ const parity = await this._renderParityMermaidSvg(payload?.source || '', {
1728
+ theme: payload?.theme || 'dark',
1729
+ maxWidth: payload?.maxWidth,
1730
+ maxHeight: payload?.maxHeight
1731
+ });
1732
+ const parser = new DOMParser();
1733
+ const documentSvg = parser.parseFromString(parity.svg, 'image/svg+xml');
1734
+ const svgElement = documentSvg.querySelector('svg');
1293
1735
  if (!svgElement) {
1294
- throw new Error('Frontend Mermaid renderer did not produce an SVG root.');
1736
+ throw new Error('Frontend Mermaid parity renderer did not produce an SVG root.');
1295
1737
  }
1296
-
1297
- svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
1298
- svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
1299
1738
  if (includeStages) {
1300
1739
  stageSnapshots.push(this._captureBridgeMermaidStage('raw', svgElement));
1301
- }
1302
-
1303
- this._normalizeBridgeMermaidSvg(svgElement);
1304
- if (includeStages) {
1305
1740
  stageSnapshots.push(this._captureBridgeMermaidStage('visual_normalized', svgElement));
1306
- }
1307
-
1308
- this._fitBridgeMermaidLabelShapes(svgElement);
1309
- if (includeStages) {
1310
1741
  stageSnapshots.push(this._captureBridgeMermaidStage('labels_fitted', svgElement));
1311
1742
  }
1312
1743
 
1313
- const naturalSize = this._tightenBridgeMermaidSvgBounds(svgElement);
1314
- const clampedSize = this._clampBridgeMermaidSize(naturalSize.width, naturalSize.height, payload?.maxWidth, payload?.maxHeight);
1315
1744
  const requestedRenderScale = Number.isFinite(Number(payload?.renderScale)) && Number(payload.renderScale) > 0 ? Number(payload.renderScale) : 1;
1316
1745
  const rasterScale = Math.min(4, Math.max(1, requestedRenderScale));
1317
- const rasterWidth = Math.max(1, Math.round(clampedSize.width * rasterScale));
1318
- const rasterHeight = Math.max(1, Math.round(clampedSize.height * rasterScale));
1746
+ const rasterWidth = Math.max(1, Math.round(parity.width * rasterScale));
1747
+ const rasterHeight = Math.max(1, Math.round(parity.height * rasterScale));
1319
1748
  svgElement.setAttribute('width', String(rasterWidth));
1320
1749
  svgElement.setAttribute('height', String(rasterHeight));
1321
1750
  svgElement.style.maxWidth = String(rasterWidth) + 'px';
@@ -1366,7 +1795,6 @@ window.pathApp = {
1366
1795
  if (objectUrl) {
1367
1796
  URL.revokeObjectURL(objectUrl);
1368
1797
  }
1369
- host.remove();
1370
1798
  }
1371
1799
  },
1372
1800
 
@@ -1470,109 +1898,1531 @@ window.pathApp = {
1470
1898
  };
1471
1899
  },
1472
1900
 
1473
- _sendBridgeStatus: function(level, code, message, details = {}, terminal = false) {
1474
- return this._sendBridgeMessage('pathStatus', {
1475
- level,
1476
- code,
1477
- message,
1478
- details,
1479
- terminal,
1480
- timestamp: Date.now()
1481
- });
1901
+ _sendBridgeStatus: function(level, code, message, details = {}, terminal = false) {
1902
+ return this._sendBridgeMessage('pathStatus', {
1903
+ level,
1904
+ code,
1905
+ message,
1906
+ details,
1907
+ terminal,
1908
+ timestamp: Date.now()
1909
+ });
1910
+ },
1911
+
1912
+ _sanitizeTargetIds: function(candidateIds) {
1913
+ if (!Array.isArray(candidateIds)) {
1914
+ return [];
1915
+ }
1916
+ const uniqueIds = [];
1917
+ const seen = new Set();
1918
+ candidateIds.forEach((id) => {
1919
+ if (typeof id !== 'string') return;
1920
+ const normalized = id.trim();
1921
+ if (!normalized || seen.has(normalized)) return;
1922
+ seen.add(normalized);
1923
+ uniqueIds.push(normalized);
1924
+ });
1925
+ return uniqueIds;
1926
+ },
1927
+
1928
+ _getDefaultTargetIds: function(limit = 1) {
1929
+ const maxCount = Math.max(0, Number.isFinite(limit) ? Math.floor(limit) : 0);
1930
+ if (maxCount <= 0) return [];
1931
+ const result = [];
1932
+ const seen = new Set();
1933
+ const appendCandidate = (rawId) => {
1934
+ if (typeof rawId !== 'string') return;
1935
+ const normalized = rawId.trim();
1936
+ if (!normalized || seen.has(normalized)) return;
1937
+ seen.add(normalized);
1938
+ result.push(normalized);
1939
+ };
1940
+
1941
+ const sourceData = this._getSourceGraphData();
1942
+ if (sourceData && Array.isArray(sourceData.nodes)) {
1943
+ sourceData.nodes.forEach((node) => appendCandidate(node?.id));
1944
+ }
1945
+ if (result.length < maxCount && Array.isArray(this.nodes)) {
1946
+ this.nodes.forEach((node) => appendCandidate(node?.id));
1947
+ }
1948
+
1949
+ return result.slice(0, maxCount);
1950
+ },
1951
+
1952
+ _isTauriMode: function() {
1953
+ const hasTauriGlobal = typeof window !== 'undefined' && !!window.__TAURI__;
1954
+ const runtimeCaps = (typeof window !== 'undefined' && window.__NC_RUNTIME_CAPS) ? window.__NC_RUNTIME_CAPS : null;
1955
+ const hasDesktopRuntimeCaps = !!(runtimeCaps && runtimeCaps.supports_sidecar === true);
1956
+ const userAgent = typeof navigator !== 'undefined' ? String(navigator.userAgent || '') : '';
1957
+ return hasTauriGlobal || hasDesktopRuntimeCaps || userAgent.includes('Tauri');
1958
+ },
1959
+
1960
+ _getModeValue: function() {
1961
+ if (this._isTauriMode()) {
1962
+ return this.runtimeConfig.mode || (this.currentTargetId ? 'diffusion' : 'domain');
1963
+ }
1964
+ return document.getElementById('learning-mode')?.value || 'domain';
1965
+ },
1966
+
1967
+ _getStrategyValue: function() {
1968
+ if (this._isTauriMode()) {
1969
+ return this.runtimeConfig.strategy || 'foundational';
1970
+ }
1971
+ return document.getElementById('strategy')?.value || 'foundational';
1972
+ },
1973
+
1974
+ _getLayoutValue: function() {
1975
+ if (this._isTauriMode()) {
1976
+ // Layout is backend-defaulted to Track(Focus) in Tauri flow.
1977
+ return 'orbital';
1978
+ }
1979
+ return document.getElementById('layout-style')?.value || 'orbital';
1980
+ },
1981
+
1982
+ _getRetainHistoryEnabled: function() {
1983
+ if (this._isTauriMode()) {
1984
+ return this.runtimeConfig.retainHistory !== false;
1985
+ }
1986
+ return document.getElementById('set-retain-history')?.checked ?? true;
1987
+ },
1988
+
1989
+ _toggleHistorySidebar: function() {
1990
+ const sidebar = document.getElementById('learning-history-sidebar');
1991
+ if (!sidebar) return;
1992
+ sidebar.style.zIndex = '3000';
1993
+ if (sidebar.style.display === 'none' || sidebar.style.display === '') {
1994
+ sidebar.style.display = 'flex';
1995
+ sidebar.offsetHeight;
1996
+ setTimeout(() => {
1997
+ sidebar.style.transform = 'translateX(0)';
1998
+ }, 10);
1999
+ } else {
2000
+ sidebar.style.transform = 'translateX(100%)';
2001
+ setTimeout(() => {
2002
+ sidebar.style.display = 'none';
2003
+ }, 300);
2004
+ }
2005
+ },
2006
+
2007
+ _normalizeLearningWorkbenchUserId: function(rawValue) {
2008
+ const candidate = String(rawValue || '')
2009
+ .trim()
2010
+ .toLowerCase()
2011
+ .replace(/[^\p{L}\p{N}_-]+/gu, '_')
2012
+ .replace(/_+/g, '_');
2013
+ if (!candidate) {
2014
+ return 'path_user_default';
2015
+ }
2016
+ return candidate.slice(0, 64);
2017
+ },
2018
+
2019
+ _getLearningQualityBaselineStorageKey: function(userId) {
2020
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2021
+ return `nc_learning_quality_baseline::${normalizedUserId}`;
2022
+ },
2023
+
2024
+ _normalizeLearningQualitySnapshotShape: function(snapshot) {
2025
+ if (!snapshot || typeof snapshot !== 'object') {
2026
+ return null;
2027
+ }
2028
+ const requiredKeys = [
2029
+ 'retestPassRatePct',
2030
+ 'misconceptionRecurrenceRatePct',
2031
+ 'evidenceBackedSuggestionRatioPct',
2032
+ 'averagePathMasteryGainPct',
2033
+ 'randomPathMasteryGainPct',
2034
+ ];
2035
+ const hasRequiredKeys = requiredKeys.every((key) => Number.isFinite(Number(snapshot[key])));
2036
+ if (!hasRequiredKeys) {
2037
+ return null;
2038
+ }
2039
+ const clamp = (value, minValue, maxValue, digits = 4) => {
2040
+ const numericValue = Number(value || 0);
2041
+ const bounded = Math.min(maxValue, Math.max(minValue, numericValue));
2042
+ return Number(bounded.toFixed(digits));
2043
+ };
2044
+ return {
2045
+ retestPassRatePct: clamp(snapshot.retestPassRatePct, 0, 100),
2046
+ misconceptionRecurrenceRatePct: clamp(snapshot.misconceptionRecurrenceRatePct, 0, 100),
2047
+ evidenceBackedSuggestionRatioPct: clamp(snapshot.evidenceBackedSuggestionRatioPct, 0, 100),
2048
+ averagePathMasteryGainPct: clamp(snapshot.averagePathMasteryGainPct, 0, 100),
2049
+ randomPathMasteryGainPct: clamp(snapshot.randomPathMasteryGainPct, 0, 100),
2050
+ historyWindowDays: Math.max(1, Math.min(180, Math.floor(Number(snapshot.historyWindowDays || 14)))),
2051
+ historyWindowRecords: Math.max(0, Math.floor(Number(snapshot.historyWindowRecords || 0))),
2052
+ historyWindowAverageMasteryDelta: clamp(snapshot.historyWindowAverageMasteryDelta, -1, 1, 6),
2053
+ historyWindowRetestPositiveDeltaRatePct: clamp(snapshot.historyWindowRetestPositiveDeltaRatePct, 0, 100),
2054
+ queryP95Ms: clamp(snapshot.queryP95Ms, 0, 60000),
2055
+ };
2056
+ },
2057
+
2058
+ _loadLearningQualityBaseline: function(userId) {
2059
+ if (typeof localStorage === 'undefined') {
2060
+ return null;
2061
+ }
2062
+ try {
2063
+ const storageKey = this._getLearningQualityBaselineStorageKey(userId);
2064
+ const raw = localStorage.getItem(storageKey);
2065
+ if (!raw) {
2066
+ return null;
2067
+ }
2068
+ const parsed = JSON.parse(raw);
2069
+ if (!parsed || typeof parsed !== 'object') {
2070
+ return null;
2071
+ }
2072
+ return this._normalizeLearningQualitySnapshotShape(parsed.snapshot || null);
2073
+ } catch (error) {
2074
+ console.warn('[PathApp] Failed to load quality baseline:', error);
2075
+ return null;
2076
+ }
2077
+ },
2078
+
2079
+ _fetchLearningQualityBaselineFromServer: async function(userId) {
2080
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2081
+ try {
2082
+ const result = await this._requestLearningApiGet('/api/knowledge/quality/baseline', {
2083
+ userId: normalizedUserId,
2084
+ });
2085
+ if (!result || result.found !== true) {
2086
+ return null;
2087
+ }
2088
+ return this._normalizeLearningQualitySnapshotShape(result.snapshot || null);
2089
+ } catch (error) {
2090
+ console.warn('[PathApp] Failed to fetch quality baseline from server:', error);
2091
+ return null;
2092
+ }
2093
+ },
2094
+
2095
+ _saveLearningQualityBaseline: function(userId, snapshot) {
2096
+ if (typeof localStorage === 'undefined') {
2097
+ return false;
2098
+ }
2099
+ const normalizedSnapshot = this._normalizeLearningQualitySnapshotShape(snapshot);
2100
+ if (!normalizedSnapshot) {
2101
+ return false;
2102
+ }
2103
+ try {
2104
+ const storageKey = this._getLearningQualityBaselineStorageKey(userId);
2105
+ localStorage.setItem(storageKey, JSON.stringify({
2106
+ userId: this._normalizeLearningWorkbenchUserId(userId),
2107
+ storedAt: new Date().toISOString(),
2108
+ snapshot: normalizedSnapshot,
2109
+ }));
2110
+ return true;
2111
+ } catch (error) {
2112
+ console.warn('[PathApp] Failed to save quality baseline:', error);
2113
+ return false;
2114
+ }
2115
+ },
2116
+
2117
+ _saveLearningQualityBaselineToServer: async function(userId, snapshot) {
2118
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2119
+ const normalizedSnapshot = this._normalizeLearningQualitySnapshotShape(snapshot);
2120
+ if (!normalizedSnapshot) {
2121
+ return false;
2122
+ }
2123
+ try {
2124
+ const result = await this._requestLearningApi('/api/knowledge/quality/baseline', {
2125
+ userId: normalizedUserId,
2126
+ snapshot: normalizedSnapshot,
2127
+ });
2128
+ return Boolean(result && result.found === true);
2129
+ } catch (error) {
2130
+ console.warn('[PathApp] Failed to persist quality baseline to server:', error);
2131
+ return false;
2132
+ }
2133
+ },
2134
+
2135
+ _syncLearningQualityBaselineFromServer: async function(userId) {
2136
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2137
+ const serverSnapshot = await this._fetchLearningQualityBaselineFromServer(normalizedUserId);
2138
+ if (!serverSnapshot) {
2139
+ this.learningWorkbench.qualityBaselineSnapshot = this._loadLearningQualityBaseline(normalizedUserId);
2140
+ return this.learningWorkbench.qualityBaselineSnapshot;
2141
+ }
2142
+ this.learningWorkbench.qualityBaselineSnapshot = serverSnapshot;
2143
+ this._saveLearningQualityBaseline(normalizedUserId, serverSnapshot);
2144
+ return serverSnapshot;
2145
+ },
2146
+
2147
+ evaluateLearningWorkbenchQualityGates: async function() {
2148
+ if (this.learningWorkbench.loading) {
2149
+ return;
2150
+ }
2151
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2152
+ const currentSnapshot = this.learningWorkbench.qualitySnapshot?.snapshot || null;
2153
+ const baselineSnapshot = await this._syncLearningQualityBaselineFromServer(userId);
2154
+ if (!currentSnapshot) {
2155
+ this._setLearningWorkbenchStatus('No current quality snapshot available. Refresh first.', true);
2156
+ return;
2157
+ }
2158
+ if (!baselineSnapshot) {
2159
+ this._setLearningWorkbenchStatus('No quality baseline found. Set baseline first.', true);
2160
+ return;
2161
+ }
2162
+ this.learningWorkbench.loading = true;
2163
+ this.learningWorkbench.qualityBaselineSnapshot = baselineSnapshot;
2164
+ this._setLearningWorkbenchStatus('Evaluating learning quality gates...');
2165
+ this._renderLearningWorkbenchState();
2166
+ try {
2167
+ let evaluation = null;
2168
+ let usedLegacyFallback = false;
2169
+ try {
2170
+ const evaluationEnvelope = await this._requestLearningApi('/api/knowledge/quality/baseline/evaluate', {
2171
+ userId,
2172
+ current: currentSnapshot,
2173
+ sampledAt: this.learningWorkbench.qualitySnapshot?.sampledAt || undefined,
2174
+ historyWindowDays: this.learningWorkbench.qualitySnapshot?.snapshot?.historyWindowDays || undefined,
2175
+ });
2176
+ evaluation = evaluationEnvelope && typeof evaluationEnvelope === 'object'
2177
+ ? evaluationEnvelope.evaluation
2178
+ : null;
2179
+ } catch (_baselineEvaluateError) {
2180
+ evaluation = await this._requestLearningApi('/api/knowledge/quality/evaluate', {
2181
+ baseline: baselineSnapshot,
2182
+ current: currentSnapshot,
2183
+ });
2184
+ usedLegacyFallback = true;
2185
+ }
2186
+ this.learningWorkbench.qualityGateEvaluation = evaluation || null;
2187
+ this._setLearningWorkbenchStatus(
2188
+ `Quality gate evaluation finished: ${evaluation?.overallPassed === true ? 'PASS' : 'FAIL'}`
2189
+ + `${usedLegacyFallback ? ' (legacy fallback route).' : '.'}`
2190
+ );
2191
+ } catch (error) {
2192
+ const message = String(error?.message || error || 'Unknown quality gate evaluation error');
2193
+ this.learningWorkbench.lastError = message;
2194
+ this._setLearningWorkbenchStatus(`Quality gate evaluation failed: ${message}`, true);
2195
+ } finally {
2196
+ this.learningWorkbench.loading = false;
2197
+ this._renderLearningWorkbenchState();
2198
+ }
2199
+ },
2200
+
2201
+ setLearningWorkbenchQualityBaselineFromCurrent: async function() {
2202
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2203
+ const snapshot = this.learningWorkbench.qualitySnapshot?.snapshot || null;
2204
+ if (!snapshot) {
2205
+ this._setLearningWorkbenchStatus('No quality snapshot available. Refresh first.', true);
2206
+ return;
2207
+ }
2208
+ this.learningWorkbench.loading = true;
2209
+ this._renderLearningWorkbenchState();
2210
+ const persistedServer = await this._saveLearningQualityBaselineToServer(userId, snapshot);
2211
+ const persistedLocal = this._saveLearningQualityBaseline(userId, snapshot);
2212
+ if (!persistedServer && !persistedLocal) {
2213
+ this.learningWorkbench.loading = false;
2214
+ this._renderLearningWorkbenchState();
2215
+ this._setLearningWorkbenchStatus('Failed to persist quality baseline snapshot.', true);
2216
+ return;
2217
+ }
2218
+ this.learningWorkbench.qualityBaselineSnapshot = await this._syncLearningQualityBaselineFromServer(userId);
2219
+ this.learningWorkbench.qualityGateEvaluation = null;
2220
+ this.learningWorkbench.loading = false;
2221
+ const storageHint = persistedServer
2222
+ ? 'server + local cache'
2223
+ : 'local cache';
2224
+ this._setLearningWorkbenchStatus(`Quality baseline updated from current snapshot (${storageHint}).`);
2225
+ this._renderLearningWorkbenchState();
2226
+ },
2227
+
2228
+ _normalizeLearningHistoryExecutionKind: function(rawValue) {
2229
+ const value = String(rawValue || 'all').trim().toLowerCase();
2230
+ if (value === 'session' || value === 'retest' || value === 'custom') {
2231
+ return value;
2232
+ }
2233
+ return 'all';
2234
+ },
2235
+
2236
+ _normalizeLearningHistoryDateValue: function(rawValue) {
2237
+ const value = String(rawValue || '').trim();
2238
+ if (!value) {
2239
+ return '';
2240
+ }
2241
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
2242
+ return value;
2243
+ }
2244
+ const parsed = new Date(value);
2245
+ if (Number.isNaN(parsed.getTime())) {
2246
+ return '';
2247
+ }
2248
+ return parsed.toISOString().slice(0, 10);
2249
+ },
2250
+
2251
+ _getLearningWorkbenchSessionHistoryQuery: function() {
2252
+ const existing = this.learningWorkbench.sessionHistoryQuery && typeof this.learningWorkbench.sessionHistoryQuery === 'object'
2253
+ ? this.learningWorkbench.sessionHistoryQuery
2254
+ : {};
2255
+ const limit = Math.max(1, Math.min(20, Math.floor(Number(existing.limit) || 8)));
2256
+ const offset = Math.max(0, Math.floor(Number(existing.offset) || 0));
2257
+ const executionKind = this._normalizeLearningHistoryExecutionKind(existing.executionKind);
2258
+ const fromDate = this._normalizeLearningHistoryDateValue(existing.fromDate);
2259
+ const toDate = this._normalizeLearningHistoryDateValue(existing.toDate);
2260
+ const normalized = {
2261
+ limit,
2262
+ offset,
2263
+ executionKind,
2264
+ fromDate,
2265
+ toDate,
2266
+ };
2267
+ this.learningWorkbench.sessionHistoryQuery = normalized;
2268
+ return normalized;
2269
+ },
2270
+
2271
+ _buildLearningWorkbenchSessionHistoryRequest: function(userId) {
2272
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2273
+ const fromExecutedAt = query.fromDate
2274
+ ? `${query.fromDate}T00:00:00.000Z`
2275
+ : undefined;
2276
+ const toExecutedAt = query.toDate
2277
+ ? `${query.toDate}T23:59:59.999Z`
2278
+ : undefined;
2279
+ return {
2280
+ userId,
2281
+ limit: query.limit,
2282
+ offset: query.offset,
2283
+ executionKinds: query.executionKind === 'all' ? undefined : [query.executionKind],
2284
+ fromExecutedAt,
2285
+ toExecutedAt,
2286
+ };
2287
+ },
2288
+
2289
+ _setLearningWorkbenchHistoryExecutionKind: function(rawValue, options = {}) {
2290
+ const nextKind = this._normalizeLearningHistoryExecutionKind(rawValue);
2291
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2292
+ const changed = query.executionKind !== nextKind;
2293
+ this.learningWorkbench.sessionHistoryQuery = {
2294
+ ...query,
2295
+ executionKind: nextKind,
2296
+ offset: 0,
2297
+ };
2298
+ this._persistLearningWorkbenchPreferences();
2299
+ if (changed && options.refresh !== false) {
2300
+ void this.refreshLearningWorkbench({ force: false });
2301
+ return;
2302
+ }
2303
+ this._renderLearningWorkbenchState();
2304
+ },
2305
+
2306
+ _applyLearningWorkbenchHistoryDateRange: function(rawFromDate, rawToDate, options = {}) {
2307
+ const fromDate = this._normalizeLearningHistoryDateValue(rawFromDate);
2308
+ const toDate = this._normalizeLearningHistoryDateValue(rawToDate);
2309
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2310
+ const changed = query.fromDate !== fromDate || query.toDate !== toDate;
2311
+ this.learningWorkbench.sessionHistoryQuery = {
2312
+ ...query,
2313
+ fromDate,
2314
+ toDate,
2315
+ offset: 0,
2316
+ };
2317
+ this._persistLearningWorkbenchPreferences();
2318
+ if (changed && options.refresh !== false) {
2319
+ void this.refreshLearningWorkbench({ force: false });
2320
+ return;
2321
+ }
2322
+ this._renderLearningWorkbenchState();
2323
+ },
2324
+
2325
+ _changeLearningWorkbenchHistoryPage: function(direction) {
2326
+ const step = Math.sign(Number(direction) || 0);
2327
+ if (step === 0) {
2328
+ return;
2329
+ }
2330
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2331
+ const page = this.learningWorkbench.sessionHistory && this.learningWorkbench.sessionHistory.page
2332
+ ? this.learningWorkbench.sessionHistory.page
2333
+ : null;
2334
+ if (step > 0 && page && page.hasMore !== true) {
2335
+ return;
2336
+ }
2337
+ const nextOffset = step > 0
2338
+ ? query.offset + query.limit
2339
+ : Math.max(0, query.offset - query.limit);
2340
+ if (nextOffset === query.offset) {
2341
+ return;
2342
+ }
2343
+ this.learningWorkbench.sessionHistoryQuery = {
2344
+ ...query,
2345
+ offset: nextOffset,
2346
+ };
2347
+ this._persistLearningWorkbenchPreferences();
2348
+ void this.refreshLearningWorkbench({ force: false });
2349
+ },
2350
+
2351
+ _restoreLearningWorkbenchPreferences: function() {
2352
+ try {
2353
+ const stored = localStorage.getItem('nc_learning_workbench_prefs');
2354
+ if (!stored) {
2355
+ return;
2356
+ }
2357
+ const parsed = JSON.parse(stored);
2358
+ if (parsed && typeof parsed === 'object' && typeof parsed.userId === 'string') {
2359
+ this.learningWorkbench.userId = this._normalizeLearningWorkbenchUserId(parsed.userId);
2360
+ }
2361
+ if (parsed && typeof parsed === 'object') {
2362
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2363
+ this.learningWorkbench.sessionHistoryQuery = {
2364
+ ...query,
2365
+ executionKind: this._normalizeLearningHistoryExecutionKind(parsed.historyExecutionKind),
2366
+ fromDate: this._normalizeLearningHistoryDateValue(parsed.historyFromDate),
2367
+ toDate: this._normalizeLearningHistoryDateValue(parsed.historyToDate),
2368
+ offset: 0,
2369
+ };
2370
+ }
2371
+ this.learningWorkbench.qualityBaselineSnapshot = this._loadLearningQualityBaseline(this.learningWorkbench.userId);
2372
+ this.learningWorkbench.qualityGateEvaluation = null;
2373
+ } catch (error) {
2374
+ console.warn('[PathApp] Failed to restore learning workbench prefs:', error);
2375
+ }
2376
+ },
2377
+
2378
+ _persistLearningWorkbenchPreferences: function() {
2379
+ try {
2380
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
2381
+ const payload = {
2382
+ userId: this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId),
2383
+ historyExecutionKind: historyQuery.executionKind,
2384
+ historyFromDate: historyQuery.fromDate,
2385
+ historyToDate: historyQuery.toDate,
2386
+ };
2387
+ localStorage.setItem('nc_learning_workbench_prefs', JSON.stringify(payload));
2388
+ } catch (error) {
2389
+ console.warn('[PathApp] Failed to persist learning workbench prefs:', error);
2390
+ }
2391
+ },
2392
+
2393
+ _toggleLearningWorkbenchSidebar: function() {
2394
+ const sidebar = document.getElementById('learning-workbench-sidebar');
2395
+ if (!sidebar) return;
2396
+ sidebar.style.zIndex = '3001';
2397
+ if (sidebar.style.display === 'none' || sidebar.style.display === '') {
2398
+ sidebar.style.display = 'flex';
2399
+ sidebar.offsetHeight;
2400
+ setTimeout(() => {
2401
+ sidebar.style.transform = 'translateX(0)';
2402
+ }, 10);
2403
+ void this.refreshLearningWorkbench({ force: false });
2404
+ return;
2405
+ }
2406
+ this._closeLearningWorkbenchSidebar();
2407
+ },
2408
+
2409
+ _closeLearningWorkbenchSidebar: function() {
2410
+ const sidebar = document.getElementById('learning-workbench-sidebar');
2411
+ if (!sidebar) return;
2412
+ sidebar.style.transform = 'translateX(-100%)';
2413
+ setTimeout(() => {
2414
+ sidebar.style.display = 'none';
2415
+ }, 300);
2416
+ },
2417
+
2418
+ _setLearningWorkbenchStatus: function(message, isError = false) {
2419
+ const statusEl = document.getElementById('learning-workbench-status');
2420
+ if (!statusEl) return;
2421
+ statusEl.textContent = String(message || '');
2422
+ statusEl.classList.toggle('is-error', !!isError);
2423
+ statusEl.classList.toggle('is-loading', this.learningWorkbench.loading === true);
2424
+ },
2425
+
2426
+ _requestLearningApi: async function(endpoint, payload = {}) {
2427
+ const response = await fetch(endpoint, {
2428
+ method: 'POST',
2429
+ headers: {
2430
+ 'Content-Type': 'application/json',
2431
+ },
2432
+ body: JSON.stringify(payload || {}),
2433
+ });
2434
+
2435
+ let body = null;
2436
+ try {
2437
+ body = await response.json();
2438
+ } catch (_error) {
2439
+ body = null;
2440
+ }
2441
+
2442
+ if (!response.ok) {
2443
+ const message = body && body.error
2444
+ ? body.error
2445
+ : `Request failed (${response.status})`;
2446
+ throw new Error(String(message));
2447
+ }
2448
+ if (!body || body.success !== true) {
2449
+ const message = body && body.error
2450
+ ? body.error
2451
+ : `API ${endpoint} returned an unexpected response`;
2452
+ throw new Error(String(message));
2453
+ }
2454
+ return body.result;
2455
+ },
2456
+
2457
+ _requestLearningApiGet: async function(endpoint, params = {}) {
2458
+ const query = new URLSearchParams();
2459
+ Object.keys(params || {}).forEach((key) => {
2460
+ const value = params[key];
2461
+ if (value === undefined || value === null || value === '') {
2462
+ return;
2463
+ }
2464
+ query.set(key, String(value));
2465
+ });
2466
+ const url = query.toString()
2467
+ ? `${endpoint}?${query.toString()}`
2468
+ : endpoint;
2469
+ const response = await fetch(url, {
2470
+ method: 'GET',
2471
+ });
2472
+
2473
+ let body = null;
2474
+ try {
2475
+ body = await response.json();
2476
+ } catch (_error) {
2477
+ body = null;
2478
+ }
2479
+
2480
+ if (!response.ok) {
2481
+ const message = body && body.error
2482
+ ? body.error
2483
+ : `Request failed (${response.status})`;
2484
+ throw new Error(String(message));
2485
+ }
2486
+ if (!body || body.success !== true) {
2487
+ const message = body && body.error
2488
+ ? body.error
2489
+ : `API ${url} returned an unexpected response`;
2490
+ throw new Error(String(message));
2491
+ }
2492
+ return body.result;
2493
+ },
2494
+
2495
+ _getFocusAtomIdsForLearning: function() {
2496
+ const ids = [];
2497
+ if (typeof this.currentTargetId === 'string' && this.currentTargetId.trim()) {
2498
+ ids.push(this.currentTargetId.trim());
2499
+ }
2500
+ if (Array.isArray(this.currentTargetIds)) {
2501
+ this.currentTargetIds.forEach((id) => {
2502
+ if (typeof id === 'string' && id.trim()) {
2503
+ ids.push(id.trim());
2504
+ }
2505
+ });
2506
+ }
2507
+ if (typeof this.centralNodeId === 'string' && this.centralNodeId.trim()) {
2508
+ ids.push(this.centralNodeId.trim());
2509
+ }
2510
+ return Array.from(new Set(ids));
2511
+ },
2512
+
2513
+ _getCurrentFocusNode: function() {
2514
+ const sourceData = this._getSourceGraphData();
2515
+ if (!sourceData || !Array.isArray(sourceData.nodes)) {
2516
+ return null;
2517
+ }
2518
+ const preferredIds = this._getFocusAtomIdsForLearning();
2519
+ for (const candidateId of preferredIds) {
2520
+ const matched = sourceData.nodes.find((node) => node && node.id === candidateId);
2521
+ if (matched) {
2522
+ return matched;
2523
+ }
2524
+ }
2525
+ if (typeof this.centralNodeId === 'string' && this.centralNodeId.trim()) {
2526
+ const matched = sourceData.nodes.find((node) => node && node.id === this.centralNodeId);
2527
+ if (matched) {
2528
+ return matched;
2529
+ }
2530
+ }
2531
+ return this.nodes && this.nodes.length > 0 ? this.nodes[0] : null;
2532
+ },
2533
+
2534
+ _buildLearningDocumentFromNode: function(node) {
2535
+ if (!node || typeof node !== 'object') {
2536
+ return null;
2537
+ }
2538
+ const nodeId = typeof node.id === 'string' ? node.id.trim() : '';
2539
+ if (!nodeId) {
2540
+ return null;
2541
+ }
2542
+ const normalizedDocId = `path_${nodeId.toLowerCase().replace(/[^\p{L}\p{N}_-]+/gu, '_').slice(0, 96)}`;
2543
+ const sourcePathRaw = node?.metadata?.filepath
2544
+ || node?.filepath
2545
+ || `Knowledge_Base/path_mode/${normalizedDocId}.md`;
2546
+ const sourcePath = String(sourcePathRaw || '').replace(/\\/g, '/');
2547
+ const title = typeof node.label === 'string' && node.label.trim()
2548
+ ? node.label.trim()
2549
+ : nodeId;
2550
+ const contentRaw = typeof node.content === 'string'
2551
+ ? node.content
2552
+ : (typeof node.summary === 'string' ? node.summary : '');
2553
+ const content = String(contentRaw || '').trim();
2554
+ const finalContent = content.length > 0
2555
+ ? `# ${title}\n\n${content}`
2556
+ : `# ${title}\n\nNode ${nodeId} was captured from Path Mode for learning orchestration.`;
2557
+ return {
2558
+ documentId: normalizedDocId,
2559
+ sourcePath,
2560
+ language: 'en',
2561
+ content: finalContent,
2562
+ };
2563
+ },
2564
+
2565
+ ingestFocusNodeForLearningWorkbench: async function() {
2566
+ const focusNode = this._getCurrentFocusNode();
2567
+ if (!focusNode) {
2568
+ this._setLearningWorkbenchStatus('No focus node available to ingest.', true);
2569
+ return;
2570
+ }
2571
+
2572
+ const document = this._buildLearningDocumentFromNode(focusNode);
2573
+ if (!document) {
2574
+ this._setLearningWorkbenchStatus('Failed to build ingest document from focus node.', true);
2575
+ return;
2576
+ }
2577
+
2578
+ this.learningWorkbench.loading = true;
2579
+ this.learningWorkbench.lastError = '';
2580
+ this._setLearningWorkbenchStatus('Ingesting focus node into learning graph...');
2581
+ try {
2582
+ const result = await this._requestLearningApi('/api/knowledge/ingest', {
2583
+ incremental: true,
2584
+ relationRecomputeMode: 'incremental',
2585
+ documents: [document],
2586
+ });
2587
+ const changedDocs = Number(result?.summary?.changedDocuments || 0);
2588
+ this._setLearningWorkbenchStatus(`Ingest succeeded. Changed documents: ${changedDocs}.`);
2589
+ await this.refreshLearningWorkbench({ force: true });
2590
+ } catch (error) {
2591
+ const message = String(error?.message || error || 'Unknown ingest error');
2592
+ this.learningWorkbench.lastError = message;
2593
+ this._setLearningWorkbenchStatus(`Ingest failed: ${message}`, true);
2594
+ } finally {
2595
+ this.learningWorkbench.loading = false;
2596
+ this._renderLearningWorkbenchState();
2597
+ }
2598
+ },
2599
+
2600
+ refreshLearningWorkbench: async function(options = {}) {
2601
+ if (this.learningWorkbench.loading) {
2602
+ return;
2603
+ }
2604
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2605
+ this.learningWorkbench.userId = userId;
2606
+ this.learningWorkbench.qualityBaselineSnapshot = await this._syncLearningQualityBaselineFromServer(userId);
2607
+ this.learningWorkbench.qualityGateEvaluation = null;
2608
+ this._persistLearningWorkbenchPreferences();
2609
+ this.learningWorkbench.loading = true;
2610
+ this.learningWorkbench.lastError = '';
2611
+ this._setLearningWorkbenchStatus('Refreshing learning workbench...');
2612
+ this._renderLearningWorkbenchState();
2613
+
2614
+ const focusAtomIds = this._getFocusAtomIdsForLearning();
2615
+ const includeDivergence = true;
2616
+ const includeRetrain = true;
2617
+ const sessionHistoryRequest = this._buildLearningWorkbenchSessionHistoryRequest(userId);
2618
+ try {
2619
+ const [sessionPlan, qualitySnapshot, misconceptions, runtimeState, sessionHistory] = await Promise.all([
2620
+ this._requestLearningApi('/api/knowledge/session/plan', {
2621
+ userId,
2622
+ focusAtomIds,
2623
+ maxActions: 14,
2624
+ includeDivergence,
2625
+ includeRetrain,
2626
+ }),
2627
+ this._requestLearningApi('/api/knowledge/quality/snapshot', {
2628
+ userId,
2629
+ }),
2630
+ this._requestLearningApi('/api/knowledge/mastery/misconceptions', {
2631
+ userId,
2632
+ atomIds: focusAtomIds,
2633
+ topK: 6,
2634
+ }),
2635
+ fetch('/api/knowledge/state', { method: 'GET' })
2636
+ .then((response) => response.json())
2637
+ .catch(() => null),
2638
+ this._requestLearningApi('/api/knowledge/session/history', sessionHistoryRequest),
2639
+ ]);
2640
+ this.learningWorkbench.sessionPlan = sessionPlan || null;
2641
+ this.learningWorkbench.qualitySnapshot = qualitySnapshot || null;
2642
+ this.learningWorkbench.misconceptions = misconceptions || null;
2643
+ this.learningWorkbench.runtimeState = runtimeState && runtimeState.success === true
2644
+ ? runtimeState
2645
+ : null;
2646
+ this.learningWorkbench.sessionHistory = sessionHistory || null;
2647
+ this.learningWorkbench.lastUpdatedAt = new Date().toISOString();
2648
+ const currentSnapshot = qualitySnapshot && qualitySnapshot.snapshot
2649
+ ? this._normalizeLearningQualitySnapshotShape(qualitySnapshot.snapshot)
2650
+ : null;
2651
+ if (currentSnapshot && this.learningWorkbench.qualityBaselineSnapshot) {
2652
+ try {
2653
+ const qualityGateEvaluation = await this._requestLearningApi('/api/knowledge/quality/evaluate', {
2654
+ baseline: this.learningWorkbench.qualityBaselineSnapshot,
2655
+ current: currentSnapshot,
2656
+ });
2657
+ this.learningWorkbench.qualityGateEvaluation = qualityGateEvaluation || null;
2658
+ } catch (qualityGateError) {
2659
+ console.warn('[PathApp] Quality gate evaluation during refresh failed:', qualityGateError);
2660
+ this.learningWorkbench.qualityGateEvaluation = null;
2661
+ }
2662
+ }
2663
+
2664
+ const actionCount = Number(sessionPlan?.summary?.totalActions || sessionPlan?.actions?.length || 0);
2665
+ this._setLearningWorkbenchStatus(`Learning workbench updated. Planned actions: ${actionCount}.`);
2666
+ } catch (error) {
2667
+ const message = String(error?.message || error || 'Unknown refresh error');
2668
+ this.learningWorkbench.lastError = message;
2669
+ this._setLearningWorkbenchStatus(`Refresh failed: ${message}`, true);
2670
+ } finally {
2671
+ this.learningWorkbench.loading = false;
2672
+ this._renderLearningWorkbenchState();
2673
+ }
2674
+ },
2675
+
2676
+ _escapeHtml: function(value) {
2677
+ return String(value || '')
2678
+ .replace(/&/g, '&amp;')
2679
+ .replace(/</g, '&lt;')
2680
+ .replace(/>/g, '&gt;')
2681
+ .replace(/"/g, '&quot;')
2682
+ .replace(/'/g, '&#39;');
2683
+ },
2684
+
2685
+ _collectOptionalAnswerForLearningAction: function(actionKind, atomId) {
2686
+ const normalized = String(actionKind || '').trim().toLowerCase();
2687
+ if (!['quiz', 'review', 'explain'].includes(normalized)) {
2688
+ return '';
2689
+ }
2690
+ if (typeof window === 'undefined' || typeof window.prompt !== 'function') {
2691
+ return '';
2692
+ }
2693
+ const promptMessage = `Optional: enter learner answer for ${actionKind} on ${atomId}.\nLeave empty to skip mastery diagnostics.`;
2694
+ const answer = window.prompt(promptMessage, '');
2695
+ if (typeof answer !== 'string') {
2696
+ return '';
2697
+ }
2698
+ return answer.trim();
2699
+ },
2700
+
2701
+ _collectOptionalAnswersForSessionActions: function(actions = []) {
2702
+ if (typeof window === 'undefined' || typeof window.prompt !== 'function') {
2703
+ return {};
2704
+ }
2705
+ if (!Array.isArray(actions) || actions.length === 0) {
2706
+ return {};
2707
+ }
2708
+ const confirmation = window.prompt(
2709
+ 'Provide learner answers for this session run? Type "yes" to enable, leave empty to skip.',
2710
+ ''
2711
+ );
2712
+ if (!confirmation || !/^y(es)?$/i.test(String(confirmation).trim())) {
2713
+ return {};
2714
+ }
2715
+ const answerMap = {};
2716
+ const promptableActions = actions
2717
+ .filter((action) => ['quiz', 'review', 'explain'].includes(String(action?.kind || '').trim().toLowerCase()))
2718
+ .slice(0, 6);
2719
+ promptableActions.forEach((action) => {
2720
+ const actionId = String(action?.id || '').trim();
2721
+ if (!actionId) {
2722
+ return;
2723
+ }
2724
+ const atomId = String(action?.atomId || 'unknown_atom').trim();
2725
+ const actionKind = String(action?.kind || 'action').trim();
2726
+ const promptText = `Optional answer for ${actionKind} on ${atomId} (leave empty to skip):`;
2727
+ const answer = window.prompt(promptText, '');
2728
+ if (typeof answer !== 'string') {
2729
+ return;
2730
+ }
2731
+ const normalized = answer.trim();
2732
+ if (normalized.length > 0) {
2733
+ answerMap[actionId] = normalized;
2734
+ }
2735
+ });
2736
+ return answerMap;
2737
+ },
2738
+
2739
+ _appendLearningWorkbenchSessionRecord: function(record) {
2740
+ if (!record || typeof record !== 'object') {
2741
+ return;
2742
+ }
2743
+ const recordId = String(record.id || '').trim();
2744
+ if (!recordId) {
2745
+ return;
2746
+ }
2747
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
2748
+ if (historyQuery.offset > 0) {
2749
+ return;
2750
+ }
2751
+ const normalizedRecordKind = this._normalizeLearningHistoryExecutionKind(record.executionKind);
2752
+ const recordExecutionKind = normalizedRecordKind === 'all' ? 'session' : normalizedRecordKind;
2753
+ if (historyQuery.executionKind !== 'all' && historyQuery.executionKind !== recordExecutionKind) {
2754
+ return;
2755
+ }
2756
+ const base = this.learningWorkbench.sessionHistory && typeof this.learningWorkbench.sessionHistory === 'object'
2757
+ ? this.learningWorkbench.sessionHistory
2758
+ : {
2759
+ userId: this.learningWorkbench.userId,
2760
+ generatedAt: new Date().toISOString(),
2761
+ records: [],
2762
+ page: {
2763
+ limit: historyQuery.limit,
2764
+ offset: 0,
2765
+ returnedRecords: 0,
2766
+ totalFilteredRecords: 0,
2767
+ hasMore: false,
2768
+ nextOffset: null,
2769
+ },
2770
+ summary: {
2771
+ totalRecords: 0,
2772
+ totalExecutedActions: 0,
2773
+ totalUpdatedMasteryCount: 0,
2774
+ averageMasteryDelta: 0,
2775
+ averageTutorConfidence: 0,
2776
+ executionKindBreakdown: [],
2777
+ },
2778
+ };
2779
+ const existingRecords = Array.isArray(base.records) ? base.records.slice() : [];
2780
+ const deduped = [record, ...existingRecords.filter((item) => String(item?.id || '').trim() !== recordId)]
2781
+ .slice(0, historyQuery.limit);
2782
+ const executionKindBreakdown = ['session', 'retest', 'custom'].map((executionKind) => {
2783
+ const kindRecords = deduped.filter((item) => {
2784
+ const normalizedKind = this._normalizeLearningHistoryExecutionKind(item?.executionKind);
2785
+ const resolvedKind = normalizedKind === 'all' ? 'session' : normalizedKind;
2786
+ return resolvedKind === executionKind;
2787
+ });
2788
+ const totalExecutedActions = kindRecords.reduce((sum, item) => sum + Number(item?.executedCount || 0), 0);
2789
+ const averageMasteryDelta = kindRecords.length > 0
2790
+ ? kindRecords.reduce((sum, item) => sum + Number(item?.averageMasteryDelta || 0), 0) / kindRecords.length
2791
+ : 0;
2792
+ return {
2793
+ executionKind,
2794
+ recordCount: kindRecords.length,
2795
+ totalExecutedActions,
2796
+ averageMasteryDelta,
2797
+ };
2798
+ });
2799
+ this.learningWorkbench.sessionHistory = {
2800
+ ...base,
2801
+ generatedAt: new Date().toISOString(),
2802
+ records: deduped,
2803
+ page: {
2804
+ limit: historyQuery.limit,
2805
+ offset: 0,
2806
+ returnedRecords: deduped.length,
2807
+ totalFilteredRecords: deduped.length,
2808
+ hasMore: false,
2809
+ nextOffset: null,
2810
+ },
2811
+ summary: {
2812
+ ...(base.summary || {}),
2813
+ totalRecords: deduped.length,
2814
+ totalExecutedActions: deduped.reduce((sum, item) => sum + Number(item?.executedCount || 0), 0),
2815
+ totalUpdatedMasteryCount: deduped.reduce((sum, item) => sum + Number(item?.updatedMasteryCount || 0), 0),
2816
+ averageMasteryDelta: deduped.length > 0
2817
+ ? deduped.reduce((sum, item) => sum + Number(item?.averageMasteryDelta || 0), 0) / deduped.length
2818
+ : 0,
2819
+ averageTutorConfidence: deduped.length > 0
2820
+ ? deduped.reduce((sum, item) => sum + Number(item?.averageTutorConfidence || 0), 0) / deduped.length
2821
+ : 0,
2822
+ executionKindBreakdown,
2823
+ },
2824
+ };
2825
+ },
2826
+
2827
+ executeLearningWorkbenchAction: async function(params = {}) {
2828
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2829
+ const atomId = String(params.atomId || '').trim();
2830
+ const actionKind = String(params.actionKind || '').trim();
2831
+ const source = String(params.source || '').trim();
2832
+ if (!atomId || !actionKind) {
2833
+ this._setLearningWorkbenchStatus('Invalid action payload for tutor execution.', true);
2834
+ return;
2835
+ }
2836
+ const answer = this._collectOptionalAnswerForLearningAction(actionKind, atomId);
2837
+ const shouldAnalyzeAnswer = answer.length > 0;
2838
+
2839
+ this.learningWorkbench.loading = true;
2840
+ this._setLearningWorkbenchStatus(`Running session action (${actionKind}) for ${atomId}...`);
2841
+ this._renderLearningWorkbenchState();
2842
+ try {
2843
+ const result = await this._requestLearningApi('/api/knowledge/session/action', {
2844
+ userId,
2845
+ action: {
2846
+ atomId,
2847
+ kind: actionKind,
2848
+ source: source || 'session_plan',
2849
+ prompt: `Learning workbench action: ${actionKind} from ${source || 'session_plan'}`,
2850
+ answer: shouldAnalyzeAnswer ? answer : undefined,
2851
+ },
2852
+ autoAnalyzeAnswer: shouldAnalyzeAnswer,
2853
+ autoUpdateMasteryFromAnswer: shouldAnalyzeAnswer,
2854
+ persistMemory: true,
2855
+ memoryLayer: 'session',
2856
+ });
2857
+ const tutorResult = result && result.tutor ? result.tutor : null;
2858
+ this.learningWorkbench.tutorFeedback = {
2859
+ atomId,
2860
+ actionKind,
2861
+ source,
2862
+ result,
2863
+ tutorResult,
2864
+ receivedAt: new Date().toISOString(),
2865
+ };
2866
+ const tutorActionKind = result?.trace?.tutorActionKind || 'unknown';
2867
+ const persistedMemory = result?.trace?.persistedMemory === true;
2868
+ const updatedMastery = result?.trace?.updatedMastery === true;
2869
+ const effectiveOutcome = result?.trace?.effectiveOutcome || 'n/a';
2870
+ const masterySource = result?.trace?.masterySource || 'none';
2871
+ this._setLearningWorkbenchStatus(
2872
+ `Session action finished (${tutorActionKind}); memory ${persistedMemory ? 'persisted' : 'skipped'}; mastery ${updatedMastery ? `updated (${effectiveOutcome}, ${masterySource})` : 'unchanged'}.`
2873
+ );
2874
+ } catch (error) {
2875
+ const message = String(error?.message || error || 'Unknown tutor execution error');
2876
+ this.learningWorkbench.lastError = message;
2877
+ this._setLearningWorkbenchStatus(`Tutor action failed: ${message}`, true);
2878
+ } finally {
2879
+ this.learningWorkbench.loading = false;
2880
+ this._renderLearningWorkbenchState();
2881
+ }
1482
2882
  },
1483
2883
 
1484
- _sanitizeTargetIds: function(candidateIds) {
1485
- if (!Array.isArray(candidateIds)) {
1486
- return [];
2884
+ runLearningWorkbenchSession: async function(options = {}) {
2885
+ if (this.learningWorkbench.loading) {
2886
+ return;
2887
+ }
2888
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2889
+ const sessionPlan = this.learningWorkbench.sessionPlan;
2890
+ if (!sessionPlan || !Array.isArray(sessionPlan.actions) || sessionPlan.actions.length === 0) {
2891
+ this._setLearningWorkbenchStatus('No session plan actions to execute. Refresh first.', true);
2892
+ return;
2893
+ }
2894
+ let actionLimit = Number(options.actionLimit);
2895
+ if (!Number.isFinite(actionLimit) || actionLimit <= 0) {
2896
+ if (typeof window !== 'undefined' && typeof window.prompt === 'function') {
2897
+ const suggestedLimit = Math.min(3, sessionPlan.actions.length);
2898
+ const raw = window.prompt(
2899
+ `Run how many planned actions? (1-${Math.min(20, sessionPlan.actions.length)})`,
2900
+ String(suggestedLimit)
2901
+ );
2902
+ if (raw === null) {
2903
+ this._setLearningWorkbenchStatus('Session execution canceled.');
2904
+ return;
2905
+ }
2906
+ actionLimit = Number(raw);
2907
+ } else {
2908
+ actionLimit = Math.min(3, sessionPlan.actions.length);
2909
+ }
2910
+ }
2911
+ actionLimit = Math.max(1, Math.min(20, Math.floor(Number(actionLimit) || 1), sessionPlan.actions.length));
2912
+ const selectedActions = sessionPlan.actions.slice(0, actionLimit);
2913
+ const answersByActionId = this._collectOptionalAnswersForSessionActions(selectedActions);
2914
+ const answerCount = Object.keys(answersByActionId).length;
2915
+ const autoAnalyzeAnswer = answerCount > 0;
2916
+
2917
+ this.learningWorkbench.loading = true;
2918
+ this._setLearningWorkbenchStatus(`Running session plan execution for top ${actionLimit} actions...`);
2919
+ this._renderLearningWorkbenchState();
2920
+ try {
2921
+ const result = await this._requestLearningApi('/api/knowledge/session/execute', {
2922
+ userId,
2923
+ executionKind: 'session',
2924
+ sessionPlan,
2925
+ actionLimit,
2926
+ answersByActionId,
2927
+ autoAnalyzeAnswer,
2928
+ autoUpdateMasteryFromAnswer: autoAnalyzeAnswer,
2929
+ persistMemory: true,
2930
+ memoryLayer: 'session',
2931
+ });
2932
+ this.learningWorkbench.sessionExecution = {
2933
+ ...result,
2934
+ receivedAt: new Date().toISOString(),
2935
+ };
2936
+ this._appendLearningWorkbenchSessionRecord(result?.record || null);
2937
+ const firstExecuted = Array.isArray(result?.items)
2938
+ ? result.items.find((item) => item && item.status === 'executed' && item.result)
2939
+ : null;
2940
+ if (firstExecuted && firstExecuted.result) {
2941
+ this.learningWorkbench.tutorFeedback = {
2942
+ atomId: firstExecuted.action?.atomId || '',
2943
+ actionKind: firstExecuted.action?.kind || '',
2944
+ source: firstExecuted.action?.source || 'session_plan',
2945
+ result: firstExecuted.result,
2946
+ tutorResult: firstExecuted.result?.tutor || null,
2947
+ receivedAt: new Date().toISOString(),
2948
+ };
2949
+ }
2950
+ const summary = result?.summary || {};
2951
+ const retestCount = Number(result?.retestPlan?.summary?.totalActions || 0);
2952
+ this._setLearningWorkbenchStatus(
2953
+ `Session execution finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery updates ${Number(summary.updatedMasteryCount || 0)}, analyzed answers ${Number(summary.analyzedAnswerCount || 0)} (input=${answerCount}), retest actions ${retestCount}.`
2954
+ );
2955
+ } catch (error) {
2956
+ const message = String(error?.message || error || 'Unknown session execution error');
2957
+ this.learningWorkbench.lastError = message;
2958
+ this._setLearningWorkbenchStatus(`Session execution failed: ${message}`, true);
2959
+ } finally {
2960
+ this.learningWorkbench.loading = false;
2961
+ this._renderLearningWorkbenchState();
1487
2962
  }
1488
- const uniqueIds = [];
1489
- const seen = new Set();
1490
- candidateIds.forEach((id) => {
1491
- if (typeof id !== 'string') return;
1492
- const normalized = id.trim();
1493
- if (!normalized || seen.has(normalized)) return;
1494
- seen.add(normalized);
1495
- uniqueIds.push(normalized);
1496
- });
1497
- return uniqueIds;
1498
2963
  },
1499
2964
 
1500
- _getDefaultTargetIds: function(limit = 1) {
1501
- const maxCount = Math.max(0, Number.isFinite(limit) ? Math.floor(limit) : 0);
1502
- if (maxCount <= 0) return [];
1503
- const result = [];
1504
- const seen = new Set();
1505
- const appendCandidate = (rawId) => {
1506
- if (typeof rawId !== 'string') return;
1507
- const normalized = rawId.trim();
1508
- if (!normalized || seen.has(normalized)) return;
1509
- seen.add(normalized);
1510
- result.push(normalized);
2965
+ runLearningWorkbenchRetestSession: async function() {
2966
+ if (this.learningWorkbench.loading) {
2967
+ return;
2968
+ }
2969
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2970
+ const existingExecution = this.learningWorkbench.sessionExecution || null;
2971
+ const retestActions = Array.isArray(existingExecution?.retestPlan?.actions)
2972
+ ? existingExecution.retestPlan.actions
2973
+ : [];
2974
+ if (retestActions.length === 0) {
2975
+ this._setLearningWorkbenchStatus('No immediate retest actions available. Run a session first.', true);
2976
+ return;
2977
+ }
2978
+ const answersByActionId = this._collectOptionalAnswersForSessionActions(retestActions);
2979
+ const answerCount = Object.keys(answersByActionId).length;
2980
+ const autoAnalyzeAnswer = answerCount > 0;
2981
+ const generatedAt = new Date().toISOString();
2982
+ const totalEstimatedMinutes = retestActions.reduce(
2983
+ (sum, action) => sum + Number(action?.estimatedMinutes || 0),
2984
+ 0
2985
+ );
2986
+ const retestSessionPlan = {
2987
+ userId,
2988
+ generatedAt,
2989
+ actions: retestActions,
2990
+ signals: {
2991
+ misconceptions: [],
2992
+ dueRetrainAtoms: Array.from(new Set(retestActions.map((action) => String(action.atomId || '').trim()))),
2993
+ masteryPathTargets: [],
2994
+ divergenceTargets: [],
2995
+ },
2996
+ summary: {
2997
+ totalActions: retestActions.length,
2998
+ totalEstimatedMinutes,
2999
+ evidenceCoverageRatio: 1,
3000
+ },
1511
3001
  };
1512
3002
 
1513
- const sourceData = this._getSourceGraphData();
1514
- if (sourceData && Array.isArray(sourceData.nodes)) {
1515
- sourceData.nodes.forEach((node) => appendCandidate(node?.id));
3003
+ this.learningWorkbench.loading = true;
3004
+ this._setLearningWorkbenchStatus(`Running immediate retest for ${retestActions.length} actions...`);
3005
+ this._renderLearningWorkbenchState();
3006
+ try {
3007
+ const result = await this._requestLearningApi('/api/knowledge/session/execute', {
3008
+ userId,
3009
+ executionKind: 'retest',
3010
+ sessionPlan: retestSessionPlan,
3011
+ actionLimit: retestActions.length,
3012
+ answersByActionId,
3013
+ autoAnalyzeAnswer,
3014
+ autoUpdateMasteryFromAnswer: autoAnalyzeAnswer,
3015
+ persistMemory: true,
3016
+ memoryLayer: 'session',
3017
+ includeRetestPlan: false,
3018
+ });
3019
+ this.learningWorkbench.sessionExecution = {
3020
+ ...result,
3021
+ receivedAt: new Date().toISOString(),
3022
+ };
3023
+ this._appendLearningWorkbenchSessionRecord(result?.record || null);
3024
+ const summary = result?.summary || {};
3025
+ this._setLearningWorkbenchStatus(
3026
+ `Retest execution finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery delta ${Number(summary.averageMasteryDelta || 0).toFixed(3)}.`
3027
+ );
3028
+ } catch (error) {
3029
+ const message = String(error?.message || error || 'Unknown retest execution error');
3030
+ this.learningWorkbench.lastError = message;
3031
+ this._setLearningWorkbenchStatus(`Retest execution failed: ${message}`, true);
3032
+ } finally {
3033
+ this.learningWorkbench.loading = false;
3034
+ this._renderLearningWorkbenchState();
1516
3035
  }
1517
- if (result.length < maxCount && Array.isArray(this.nodes)) {
1518
- this.nodes.forEach((node) => appendCandidate(node?.id));
3036
+ },
3037
+
3038
+ runLearningWorkbenchHistoryRetest: async function(recordId) {
3039
+ if (this.learningWorkbench.loading) {
3040
+ return;
3041
+ }
3042
+ const normalizedRecordId = String(recordId || '').trim();
3043
+ if (!normalizedRecordId) {
3044
+ this._setLearningWorkbenchStatus('Invalid history record id for replay.', true);
3045
+ return;
3046
+ }
3047
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
3048
+ const records = Array.isArray(this.learningWorkbench.sessionHistory?.records)
3049
+ ? this.learningWorkbench.sessionHistory.records
3050
+ : [];
3051
+ const targetRecord = records.find((item) => String(item?.id || '').trim() === normalizedRecordId) || null;
3052
+ if (!targetRecord) {
3053
+ this._setLearningWorkbenchStatus('Target history record was not found in current page.', true);
3054
+ return;
3055
+ }
3056
+ const focusAtomIds = Array.from(
3057
+ new Set(
3058
+ (Array.isArray(targetRecord.focusAtomIds) ? targetRecord.focusAtomIds : [])
3059
+ .map((atomId) => String(atomId || '').trim())
3060
+ .filter((atomId) => atomId.length > 0)
3061
+ )
3062
+ );
3063
+ if (focusAtomIds.length === 0) {
3064
+ this._setLearningWorkbenchStatus('Selected history record has no focus atoms for replay.', true);
3065
+ return;
1519
3066
  }
1520
3067
 
1521
- return result.slice(0, maxCount);
3068
+ this.learningWorkbench.loading = true;
3069
+ this._setLearningWorkbenchStatus(`Replaying retest from history record ${normalizedRecordId}...`);
3070
+ this._renderLearningWorkbenchState();
3071
+ try {
3072
+ const sessionPlan = await this._requestLearningApi('/api/knowledge/session/plan', {
3073
+ userId,
3074
+ focusAtomIds,
3075
+ maxActions: Math.min(14, Math.max(4, focusAtomIds.length * 2)),
3076
+ includeDivergence: false,
3077
+ includeRetrain: true,
3078
+ });
3079
+ if (!sessionPlan || !Array.isArray(sessionPlan.actions) || sessionPlan.actions.length === 0) {
3080
+ throw new Error('Replay retest failed because no runnable actions were generated.');
3081
+ }
3082
+ const actionLimit = Math.max(1, Math.min(8, focusAtomIds.length, sessionPlan.actions.length));
3083
+ const selectedActions = sessionPlan.actions.slice(0, actionLimit);
3084
+ const answersByActionId = this._collectOptionalAnswersForSessionActions(selectedActions);
3085
+ const answerCount = Object.keys(answersByActionId).length;
3086
+ const autoAnalyzeAnswer = answerCount > 0;
3087
+ const result = await this._requestLearningApi('/api/knowledge/session/execute', {
3088
+ userId,
3089
+ executionKind: 'retest',
3090
+ sessionPlan,
3091
+ actionLimit,
3092
+ answersByActionId,
3093
+ autoAnalyzeAnswer,
3094
+ autoUpdateMasteryFromAnswer: autoAnalyzeAnswer,
3095
+ persistMemory: true,
3096
+ memoryLayer: 'session',
3097
+ includeRetestPlan: false,
3098
+ });
3099
+ this.learningWorkbench.sessionExecution = {
3100
+ ...result,
3101
+ receivedAt: new Date().toISOString(),
3102
+ };
3103
+ this._appendLearningWorkbenchSessionRecord(result?.record || null);
3104
+ const summary = result?.summary || {};
3105
+ this._setLearningWorkbenchStatus(
3106
+ `History replay finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery delta ${Number(summary.averageMasteryDelta || 0).toFixed(3)}.`
3107
+ );
3108
+ } catch (error) {
3109
+ const message = String(error?.message || error || 'Unknown history replay error');
3110
+ this.learningWorkbench.lastError = message;
3111
+ this._setLearningWorkbenchStatus(`History replay failed: ${message}`, true);
3112
+ } finally {
3113
+ this.learningWorkbench.loading = false;
3114
+ this._renderLearningWorkbenchState();
3115
+ }
1522
3116
  },
1523
3117
 
1524
- _isTauriMode: function() {
1525
- const hasTauriGlobal = typeof window !== 'undefined' && !!window.__TAURI__;
1526
- const runtimeCaps = (typeof window !== 'undefined' && window.__NC_RUNTIME_CAPS) ? window.__NC_RUNTIME_CAPS : null;
1527
- const hasDesktopRuntimeCaps = !!(runtimeCaps && runtimeCaps.supports_sidecar === true);
1528
- const userAgent = typeof navigator !== 'undefined' ? String(navigator.userAgent || '') : '';
1529
- return hasTauriGlobal || hasDesktopRuntimeCaps || userAgent.includes('Tauri');
1530
- },
3118
+ _renderLearningWorkbenchState: function() {
3119
+ const userIdInput = document.getElementById('learning-user-id');
3120
+ if (userIdInput && userIdInput.value !== this.learningWorkbench.userId) {
3121
+ userIdInput.value = this.learningWorkbench.userId;
3122
+ }
3123
+
3124
+ const qualityEl = document.getElementById('learning-quality-summary');
3125
+ const qualityGatesEl = document.getElementById('learning-quality-gates');
3126
+ const setQualityBaselineBtn = document.getElementById('btn-set-quality-baseline');
3127
+ const evaluateQualityGatesBtn = document.getElementById('btn-evaluate-quality-gates');
3128
+ const misconceptionEl = document.getElementById('learning-misconception-list');
3129
+ const actionsEl = document.getElementById('learning-session-actions');
3130
+ const runtimeEl = document.getElementById('learning-runtime-summary');
3131
+ const updatedEl = document.getElementById('learning-workbench-updated-at');
3132
+ const tutorFeedbackEl = document.getElementById('learning-tutor-feedback');
3133
+ const sessionExecutionEl = document.getElementById('learning-session-execution');
3134
+ const sessionHistoryEl = document.getElementById('learning-session-history');
3135
+ const sessionHistorySummaryEl = document.getElementById('learning-session-history-summary');
3136
+ const sessionHistoryFilterEl = document.getElementById('learning-history-kind-filter');
3137
+ const sessionHistoryFromDateEl = document.getElementById('learning-history-from-date');
3138
+ const sessionHistoryToDateEl = document.getElementById('learning-history-to-date');
3139
+ const sessionHistoryApplyRangeBtn = document.getElementById('btn-learning-history-apply-range');
3140
+ const sessionHistoryClearRangeBtn = document.getElementById('btn-learning-history-clear-range');
3141
+ const sessionHistoryPrevBtn = document.getElementById('btn-learning-history-prev');
3142
+ const sessionHistoryNextBtn = document.getElementById('btn-learning-history-next');
3143
+ const sessionHistoryPageEl = document.getElementById('learning-history-page');
3144
+
3145
+ if (updatedEl) {
3146
+ updatedEl.textContent = this.learningWorkbench.lastUpdatedAt
3147
+ ? `Last updated: ${new Date(this.learningWorkbench.lastUpdatedAt).toLocaleString()}`
3148
+ : 'Last updated: -';
3149
+ }
3150
+
3151
+ if (qualityEl) {
3152
+ const snapshot = this.learningWorkbench.qualitySnapshot?.snapshot || null;
3153
+ const baseline = this.learningWorkbench.qualityBaselineSnapshot || null;
3154
+ if (setQualityBaselineBtn) {
3155
+ setQualityBaselineBtn.disabled = this.learningWorkbench.loading === true || !snapshot;
3156
+ }
3157
+ if (evaluateQualityGatesBtn) {
3158
+ evaluateQualityGatesBtn.disabled = this.learningWorkbench.loading === true || !snapshot || !baseline;
3159
+ }
3160
+ if (!snapshot) {
3161
+ qualityEl.innerHTML = '<li class="muted">No quality snapshot yet.</li>';
3162
+ } else {
3163
+ qualityEl.innerHTML = [
3164
+ `<li>Retest pass rate: <strong>${Number(snapshot.retestPassRatePct || 0).toFixed(2)}%</strong></li>`,
3165
+ `<li>Misconception recurrence: <strong>${Number(snapshot.misconceptionRecurrenceRatePct || 0).toFixed(2)}%</strong></li>`,
3166
+ `<li>Evidence-backed suggestions: <strong>${Number(snapshot.evidenceBackedSuggestionRatioPct || 0).toFixed(2)}%</strong></li>`,
3167
+ `<li>Path gain vs random: <strong>${Number(snapshot.averagePathMasteryGainPct || 0).toFixed(2)}% / ${Number(snapshot.randomPathMasteryGainPct || 0).toFixed(2)}%</strong></li>`,
3168
+ `<li>History window: <strong>${Math.max(1, Math.floor(Number(snapshot.historyWindowDays || 14)))}d, records ${Math.max(0, Math.floor(Number(snapshot.historyWindowRecords || 0)))}</strong></li>`,
3169
+ `<li>History mastery delta avg: <strong>${Number(snapshot.historyWindowAverageMasteryDelta || 0).toFixed(3)}</strong></li>`,
3170
+ `<li>History retest positive delta rate: <strong>${Number(snapshot.historyWindowRetestPositiveDeltaRatePct || 0).toFixed(2)}%</strong></li>`,
3171
+ `<li>Query p95: <strong>${Number(snapshot.queryP95Ms || 0).toFixed(2)} ms</strong></li>`,
3172
+ `<li>Baseline status: <strong>${baseline ? 'configured' : 'missing'}</strong></li>`,
3173
+ ].join('');
3174
+ }
3175
+ }
1531
3176
 
1532
- _getModeValue: function() {
1533
- if (this._isTauriMode()) {
1534
- return this.runtimeConfig.mode || (this.currentTargetId ? 'diffusion' : 'domain');
3177
+ if (qualityGatesEl) {
3178
+ const evaluation = this.learningWorkbench.qualityGateEvaluation || null;
3179
+ const baseline = this.learningWorkbench.qualityBaselineSnapshot || null;
3180
+ if (!baseline) {
3181
+ qualityGatesEl.textContent = 'No quality baseline yet. Use "Set Baseline" after refreshing quality snapshot.';
3182
+ } else if (!evaluation) {
3183
+ qualityGatesEl.textContent = 'No quality gate evaluation yet.';
3184
+ } else {
3185
+ const gateLines = Array.isArray(evaluation.gates)
3186
+ ? evaluation.gates.map((gate) => {
3187
+ const status = gate.passed === true ? 'PASS' : 'FAIL';
3188
+ const comparator = String(gate.comparator || '>=');
3189
+ const observedValue = Number(gate.observedValue || 0).toFixed(gate.unit === 'ms' ? 2 : 4);
3190
+ const threshold = Number(gate.threshold || 0).toFixed(gate.unit === 'ms' ? 2 : 4);
3191
+ return `- [${status}] ${gate.gateId}: ${observedValue} ${comparator} ${threshold} ${gate.unit}`;
3192
+ })
3193
+ : [];
3194
+ const deltas = evaluation.deltas || {};
3195
+ qualityGatesEl.textContent = [
3196
+ `Evaluated at: ${evaluation.evaluatedAt || '-'}`,
3197
+ `Overall: ${evaluation.overallPassed === true ? 'PASS' : 'FAIL'}`,
3198
+ `Delta retest pass-rate uplift: ${Number(deltas.retestPassRateUpliftPct || 0).toFixed(4)}`,
3199
+ `Delta misconception reduction: ${Number(deltas.misconceptionRecurrenceReductionPct || 0).toFixed(4)}`,
3200
+ `Delta path effectiveness lift: ${Number(deltas.pathEffectivenessLiftPct || 0).toFixed(4)}`,
3201
+ `Delta history mastery uplift: ${Number(deltas.historyWindowAverageMasteryDeltaUplift || 0).toFixed(6)}`,
3202
+ 'Gates:',
3203
+ ...(gateLines.length > 0 ? gateLines : ['- n/a']),
3204
+ ].join('\n');
3205
+ }
1535
3206
  }
1536
- return document.getElementById('learning-mode')?.value || 'domain';
1537
- },
1538
3207
 
1539
- _getStrategyValue: function() {
1540
- if (this._isTauriMode()) {
1541
- return this.runtimeConfig.strategy || 'foundational';
3208
+ if (misconceptionEl) {
3209
+ const items = this.learningWorkbench.misconceptions?.items || [];
3210
+ if (!Array.isArray(items) || items.length === 0) {
3211
+ misconceptionEl.innerHTML = '<li class="muted">No recurring misconceptions detected.</li>';
3212
+ } else {
3213
+ misconceptionEl.innerHTML = items.map((item) => {
3214
+ const tag = String(item.errorTag || 'unknown');
3215
+ const count = Number(item.count || 0);
3216
+ const severity = Number(item.severityScore || 0).toFixed(3);
3217
+ return `<li><span class="chip">${tag}</span> count=${count}, severity=${severity}</li>`;
3218
+ }).join('');
3219
+ }
1542
3220
  }
1543
- return document.getElementById('strategy')?.value || 'foundational';
1544
- },
1545
3221
 
1546
- _getLayoutValue: function() {
1547
- if (this._isTauriMode()) {
1548
- // Layout is backend-defaulted to Track(Focus) in Tauri flow.
1549
- return 'orbital';
3222
+ if (actionsEl) {
3223
+ const actions = this.learningWorkbench.sessionPlan?.actions || [];
3224
+ if (!Array.isArray(actions) || actions.length === 0) {
3225
+ actionsEl.innerHTML = '<li class="muted">No session actions yet.</li>';
3226
+ } else {
3227
+ actionsEl.innerHTML = actions.slice(0, 10).map((action) => {
3228
+ const source = String(action.source || 'unknown');
3229
+ const kind = String(action.kind || 'action');
3230
+ const atomId = String(action.atomId || '');
3231
+ const minutes = Number(action.estimatedMinutes || 0);
3232
+ const safeKind = this._escapeHtml(kind);
3233
+ const safeSource = this._escapeHtml(source);
3234
+ const safeAtomId = this._escapeHtml(atomId);
3235
+ const encodedKind = encodeURIComponent(kind);
3236
+ const encodedAtomId = encodeURIComponent(atomId);
3237
+ const encodedSource = encodeURIComponent(source);
3238
+ return `
3239
+ <li>
3240
+ <div><strong>${safeKind}</strong> <span class="chip">${safeSource}</span> <code>${safeAtomId}</code> (${minutes}m)</div>
3241
+ <button
3242
+ class="btn-small workbench-run-action"
3243
+ data-action-kind="${encodedKind}"
3244
+ data-atom-id="${encodedAtomId}"
3245
+ data-action-source="${encodedSource}"
3246
+ type="button"
3247
+ >
3248
+ Ask Tutor
3249
+ </button>
3250
+ </li>
3251
+ `;
3252
+ }).join('');
3253
+ }
1550
3254
  }
1551
- return document.getElementById('layout-style')?.value || 'orbital';
1552
- },
1553
3255
 
1554
- _getRetainHistoryEnabled: function() {
1555
- if (this._isTauriMode()) {
1556
- return this.runtimeConfig.retainHistory !== false;
3256
+ if (tutorFeedbackEl) {
3257
+ const tutorFeedback = this.learningWorkbench.tutorFeedback;
3258
+ if (!tutorFeedback || !tutorFeedback.result) {
3259
+ tutorFeedbackEl.textContent = 'No tutor feedback yet.';
3260
+ } else {
3261
+ const trace = tutorFeedback.result?.trace || {};
3262
+ const tutorResult = tutorFeedback.tutorResult || tutorFeedback.result?.tutor || {};
3263
+ const message = String(tutorResult.message || '').trim();
3264
+ const safeMessage = message.length > 0 ? message : 'Tutor returned an empty message.';
3265
+ const answerAnalysis = tutorFeedback.result?.answerAnalysis || null;
3266
+ const answerAnalysisMessage = String(answerAnalysis?.message || '').trim();
3267
+ const safeAnswerAnalysis = answerAnalysisMessage.length > 0
3268
+ ? answerAnalysisMessage
3269
+ : 'No answer analysis was generated.';
3270
+ tutorFeedbackEl.textContent = [
3271
+ `Action: ${tutorFeedback.actionKind} (${trace.tutorActionKind || 'unknown'})`,
3272
+ `Atom: ${tutorFeedback.atomId}`,
3273
+ `Memory persisted: ${trace.persistedMemory === true ? 'yes' : 'no'}`,
3274
+ `Mastery updated: ${trace.updatedMastery === true ? 'yes' : 'no'} (${trace.masterySource || 'none'})`,
3275
+ `Outcome/Error: ${trace.effectiveOutcome || 'n/a'} / ${trace.effectiveErrorTag || 'n/a'}`,
3276
+ safeMessage,
3277
+ '',
3278
+ 'Answer Analysis:',
3279
+ safeAnswerAnalysis,
3280
+ ].join('\n');
3281
+ }
1557
3282
  }
1558
- return document.getElementById('set-retain-history')?.checked ?? true;
1559
- },
1560
3283
 
1561
- _toggleHistorySidebar: function() {
1562
- const sidebar = document.getElementById('learning-history-sidebar');
1563
- if (!sidebar) return;
1564
- sidebar.style.zIndex = '3000';
1565
- if (sidebar.style.display === 'none' || sidebar.style.display === '') {
1566
- sidebar.style.display = 'flex';
1567
- sidebar.offsetHeight;
1568
- setTimeout(() => {
1569
- sidebar.style.transform = 'translateX(0)';
1570
- }, 10);
1571
- } else {
1572
- sidebar.style.transform = 'translateX(100%)';
1573
- setTimeout(() => {
1574
- sidebar.style.display = 'none';
1575
- }, 300);
3284
+ if (sessionExecutionEl) {
3285
+ const execution = this.learningWorkbench.sessionExecution;
3286
+ if (!execution || !execution.summary) {
3287
+ sessionExecutionEl.textContent = 'No session execution yet.';
3288
+ } else {
3289
+ const summary = execution.summary || {};
3290
+ const masteryDelta = execution.masteryDelta || null;
3291
+ const retestPlan = execution.retestPlan || null;
3292
+ const topDeltaLines = Array.isArray(masteryDelta?.items)
3293
+ ? masteryDelta.items.slice(0, 3).map((item) => {
3294
+ const before = Number(item.beforeMastery || 0).toFixed(3);
3295
+ const after = Number(item.afterMastery || 0).toFixed(3);
3296
+ const delta = Number(item.deltaMastery || 0);
3297
+ const signedDelta = `${delta >= 0 ? '+' : ''}${delta.toFixed(3)}`;
3298
+ const atomId = String(item.atomId || 'unknown_atom');
3299
+ return `- ${atomId}: ${before} -> ${after} (${signedDelta})`;
3300
+ })
3301
+ : [];
3302
+ const retestPreviewLines = Array.isArray(retestPlan?.actions)
3303
+ ? retestPlan.actions.slice(0, 3).map((action) => {
3304
+ const atomId = String(action.atomId || 'unknown_atom');
3305
+ const kind = String(action.kind || 'action');
3306
+ const source = String(action.source || 'retrain_plan');
3307
+ return `- ${kind} @ ${atomId} (${source})`;
3308
+ })
3309
+ : [];
3310
+ sessionExecutionEl.textContent = [
3311
+ `Executed at: ${execution.executedAt || '-'}`,
3312
+ `Planned/Attempted/Executed: ${Number(summary.plannedActions || 0)}/${Number(summary.attemptedActions || 0)}/${Number(summary.executedCount || 0)}`,
3313
+ `Skipped/Failed: ${Number(summary.skippedCount || 0)}/${Number(summary.failedCount || 0)}`,
3314
+ `Mastery updates (inferred/explicit): ${Number(summary.updatedMasteryCount || 0)} (${Number(summary.inferredMasteryCount || 0)}/${Number(summary.explicitMasteryCount || 0)})`,
3315
+ `Answer analyzed: ${Number(summary.analyzedAnswerCount || 0)}, memory persisted: ${Number(summary.memoryPersistedCount || 0)}`,
3316
+ `Avg tutor confidence: ${Number(summary.averageTutorConfidence || 0).toFixed(3)}`,
3317
+ `Mastery avg before/after/delta: ${Number(summary.averageMasteryBefore || 0).toFixed(3)} / ${Number(summary.averageMasteryAfter || 0).toFixed(3)} / ${Number(summary.averageMasteryDelta || 0).toFixed(3)}`,
3318
+ `Mastery movement improved/regressed/flat: ${Number(summary.improvedAtomCount || 0)}/${Number(summary.regressedAtomCount || 0)}/${Number(summary.unchangedAtomCount || 0)}`,
3319
+ `Stopped early: ${summary.stoppedEarly === true ? 'yes' : 'no'}`,
3320
+ ...(masteryDelta
3321
+ ? [
3322
+ `Compared atoms: ${Number(masteryDelta.comparedAtoms || 0)}, avg delta: ${Number(masteryDelta.averageDelta || 0).toFixed(3)}`,
3323
+ 'Top mastery delta atoms:',
3324
+ ...(topDeltaLines.length > 0 ? topDeltaLines : ['- n/a']),
3325
+ ]
3326
+ : []),
3327
+ ...(retestPlan
3328
+ ? [
3329
+ `Immediate retest actions: ${Number(retestPlan.summary?.totalActions || 0)}`,
3330
+ 'Retest preview:',
3331
+ ...(retestPreviewLines.length > 0 ? retestPreviewLines : ['- n/a']),
3332
+ ]
3333
+ : []),
3334
+ ].join('\n');
3335
+ }
3336
+ }
3337
+
3338
+ if (sessionHistoryEl) {
3339
+ const records = this.learningWorkbench.sessionHistory?.records || [];
3340
+ const summary = this.learningWorkbench.sessionHistory?.summary || null;
3341
+ const page = this.learningWorkbench.sessionHistory?.page || null;
3342
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
3343
+ if (sessionHistoryFilterEl) {
3344
+ sessionHistoryFilterEl.value = historyQuery.executionKind;
3345
+ }
3346
+ if (sessionHistoryFromDateEl) {
3347
+ sessionHistoryFromDateEl.value = historyQuery.fromDate;
3348
+ }
3349
+ if (sessionHistoryToDateEl) {
3350
+ sessionHistoryToDateEl.value = historyQuery.toDate;
3351
+ }
3352
+ if (sessionHistoryApplyRangeBtn) {
3353
+ sessionHistoryApplyRangeBtn.disabled = this.learningWorkbench.loading === true;
3354
+ }
3355
+ if (sessionHistoryClearRangeBtn) {
3356
+ const hasDateRange = historyQuery.fromDate.length > 0 || historyQuery.toDate.length > 0;
3357
+ sessionHistoryClearRangeBtn.disabled = this.learningWorkbench.loading === true || !hasDateRange;
3358
+ }
3359
+ if (sessionHistoryPrevBtn) {
3360
+ sessionHistoryPrevBtn.disabled = this.learningWorkbench.loading === true || historyQuery.offset <= 0;
3361
+ }
3362
+ if (sessionHistoryNextBtn) {
3363
+ const canGoNext = page ? page.hasMore === true : false;
3364
+ sessionHistoryNextBtn.disabled = this.learningWorkbench.loading === true || !canGoNext;
3365
+ }
3366
+ if (sessionHistoryPageEl) {
3367
+ const totalFilteredRecords = Math.max(0, Number(page?.totalFilteredRecords || 0));
3368
+ const totalPages = Math.max(1, Math.ceil(totalFilteredRecords / Math.max(1, historyQuery.limit)));
3369
+ const pageIndex = Math.floor(historyQuery.offset / Math.max(1, historyQuery.limit)) + 1;
3370
+ sessionHistoryPageEl.textContent = `Page ${Math.min(pageIndex, totalPages)}/${totalPages}`;
3371
+ }
3372
+ if (sessionHistorySummaryEl) {
3373
+ const totalRecords = Number(summary?.totalRecords || 0);
3374
+ const totalExecuted = Number(summary?.totalExecutedActions || 0);
3375
+ const totalUpdated = Number(summary?.totalUpdatedMasteryCount || 0);
3376
+ const avgDelta = Number(summary?.averageMasteryDelta || 0).toFixed(3);
3377
+ const avgConfidence = Number(summary?.averageTutorConfidence || 0).toFixed(3);
3378
+ const rangeSummary = historyQuery.fromDate || historyQuery.toDate
3379
+ ? `, range=${historyQuery.fromDate || '-'}..${historyQuery.toDate || '-'}`
3380
+ : '';
3381
+ sessionHistorySummaryEl.textContent = `History summary: records=${totalRecords}, executed=${totalExecuted}, updated=${totalUpdated}, avgDelta=${avgDelta}, avgConfidence=${avgConfidence}${rangeSummary}`;
3382
+ }
3383
+ if (!Array.isArray(records) || records.length === 0) {
3384
+ sessionHistoryEl.innerHTML = '<li class="muted">No session history yet.</li>';
3385
+ } else {
3386
+ sessionHistoryEl.innerHTML = records.slice(0, Math.max(1, historyQuery.limit)).map((record) => {
3387
+ const executedAt = new Date(record.executedAt || Date.now()).toLocaleString();
3388
+ const kind = this._escapeHtml(String(record.executionKind || 'session'));
3389
+ const executedCount = Number(record.executedCount || 0);
3390
+ const attempted = Number(record.attemptedActions || 0);
3391
+ const delta = Number(record.averageMasteryDelta || 0);
3392
+ const signedDelta = `${delta >= 0 ? '+' : ''}${delta.toFixed(3)}`;
3393
+ const recordId = String(record.id || '').trim();
3394
+ const encodedRecordId = encodeURIComponent(recordId);
3395
+ const focusAtomIds = Array.isArray(record.focusAtomIds) ? record.focusAtomIds : [];
3396
+ const focusAtomCount = focusAtomIds.length;
3397
+ const focusPreview = focusAtomIds
3398
+ .slice(0, 3)
3399
+ .map((atomId) => this._escapeHtml(String(atomId || '')))
3400
+ .filter((atomId) => atomId.length > 0)
3401
+ .join(', ');
3402
+ const replayDisabledAttr = focusAtomCount > 0 ? '' : ' disabled';
3403
+ return `
3404
+ <li>
3405
+ <div><span class="chip">${kind}</span> ${executedAt} - ${executedCount}/${attempted} - mastery delta ${signedDelta}</div>
3406
+ <div class="muted">Focus atoms: ${focusAtomCount}${focusPreview ? ` (${focusPreview})` : ''}</div>
3407
+ <button class="btn-small workbench-run-history-retest" data-history-record-id="${encodedRecordId}" type="button"${replayDisabledAttr}>Replay Retest</button>
3408
+ </li>
3409
+ `;
3410
+ }).join('');
3411
+ }
3412
+ }
3413
+
3414
+ if (runtimeEl) {
3415
+ const runtimeState = this.learningWorkbench.runtimeState?.state || null;
3416
+ if (!runtimeState) {
3417
+ runtimeEl.textContent = 'Runtime: unavailable';
3418
+ } else {
3419
+ const sessionTelemetry = runtimeState.sessionActionTelemetry || null;
3420
+ const historyCount = Number(runtimeState.sessionExecutionHistoryRecords || 0);
3421
+ const sessionSummary = sessionTelemetry
3422
+ ? `, sessionActions=${Number(sessionTelemetry.executionCount || 0)}, inferred=${Number(sessionTelemetry.inferredMasteryUpdateCount || 0)}, explicit=${Number(sessionTelemetry.explicitMasteryUpdateCount || 0)}, history=${historyCount}`
3423
+ : `, history=${historyCount}`;
3424
+ runtimeEl.textContent = `Runtime: docs=${runtimeState.documents}, atoms=${runtimeState.activeAtoms}, relations=${runtimeState.activeRelationEdges}, ingestP95=${Number(runtimeState.ingestTelemetry?.ingestP95Ms || 0).toFixed(2)}ms${sessionSummary}`;
3425
+ }
1576
3426
  }
1577
3427
  },
1578
3428
 
@@ -1643,6 +3493,7 @@ window.pathApp = {
1643
3493
  const pathContainer = document.getElementById('path-container');
1644
3494
  const graphWrapper = document.getElementById('graph-wrapper');
1645
3495
  const sidebar = document.getElementById('learning-history-sidebar');
3496
+ const workbenchSidebar = document.getElementById('learning-workbench-sidebar');
1646
3497
 
1647
3498
  if (pathContainer) pathContainer.style.display = 'none';
1648
3499
  if (graphWrapper) graphWrapper.style.display = 'block';
@@ -1650,21 +3501,21 @@ window.pathApp = {
1650
3501
  sidebar.style.transform = 'translateX(100%)';
1651
3502
  sidebar.style.display = 'none';
1652
3503
  }
3504
+ if (workbenchSidebar) {
3505
+ workbenchSidebar.style.transform = 'translateX(-100%)';
3506
+ workbenchSidebar.style.display = 'none';
3507
+ }
1653
3508
  const multiWindowOptions = this._resolveMultiWindowOptions();
1654
3509
 
1655
3510
  // Single-window toggle: hide Godot, show Tauri.
1656
3511
  // 单窗口切换:隐藏 Godot,显示 Tauri。
1657
3512
  if (window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === 'function') {
1658
3513
  // 1. Hide Godot window via PathBridge WebSocket.
1659
- if (
1660
- multiWindowOptions.singleWindowMode &&
1661
- this.ws &&
1662
- this.ws.readyState === WebSocket.OPEN
1663
- ) {
1664
- this.ws.send(JSON.stringify({
1665
- type: 'setWindowVisible',
1666
- payload: { visible: false }
1667
- }));
3514
+ if (multiWindowOptions.singleWindowMode) {
3515
+ void this.requestBridgeWindowVisibility(false, {
3516
+ waitMs: 1200,
3517
+ reason: 'exit-pathmode'
3518
+ });
1668
3519
  }
1669
3520
  // 2. Restore Tauri window via Rust IPC.
1670
3521
  if (multiWindowOptions.restoreTauriWhenPathmodeExits) {
@@ -2176,6 +4027,8 @@ window.pathApp = {
2176
4027
  const tauriMode = this._isTauriMode();
2177
4028
  const toolbar = document.getElementById('path-toolbar');
2178
4029
 
4030
+ this._restoreLearningWorkbenchPreferences();
4031
+
2179
4032
  if (tauriMode && toolbar) {
2180
4033
  // In Tauri, controls are migrated to Godot. Keep canvas view only.
2181
4034
  toolbar.style.display = 'none';
@@ -2205,6 +4058,24 @@ window.pathApp = {
2205
4058
  const markBtn = document.getElementById('btn-mark-complete');
2206
4059
  const historyBtn = document.getElementById('btn-toggle-history');
2207
4060
  const closeHistoryBtn = document.getElementById('btn-close-history');
4061
+ const workbenchToggleBtn = document.getElementById('btn-toggle-learning-workbench');
4062
+ const workbenchCloseBtn = document.getElementById('btn-close-learning-workbench');
4063
+ const workbenchRefreshBtn = document.getElementById('btn-refresh-learning-workbench');
4064
+ const workbenchSetQualityBaselineBtn = document.getElementById('btn-set-quality-baseline');
4065
+ const workbenchEvaluateQualityGatesBtn = document.getElementById('btn-evaluate-quality-gates');
4066
+ const workbenchIngestBtn = document.getElementById('btn-ingest-focus-node');
4067
+ const workbenchRunSessionBtn = document.getElementById('btn-run-learning-session');
4068
+ const workbenchRunRetestBtn = document.getElementById('btn-run-retest-session');
4069
+ const workbenchUserIdInput = document.getElementById('learning-user-id');
4070
+ const workbenchActionsList = document.getElementById('learning-session-actions');
4071
+ const workbenchHistoryList = document.getElementById('learning-session-history');
4072
+ const workbenchHistoryKindFilter = document.getElementById('learning-history-kind-filter');
4073
+ const workbenchHistoryFromDate = document.getElementById('learning-history-from-date');
4074
+ const workbenchHistoryToDate = document.getElementById('learning-history-to-date');
4075
+ const workbenchHistoryApplyRangeBtn = document.getElementById('btn-learning-history-apply-range');
4076
+ const workbenchHistoryClearRangeBtn = document.getElementById('btn-learning-history-clear-range');
4077
+ const workbenchHistoryPrevBtn = document.getElementById('btn-learning-history-prev');
4078
+ const workbenchHistoryNextBtn = document.getElementById('btn-learning-history-next');
2208
4079
 
2209
4080
  if (!tauriMode) {
2210
4081
  if (learningModeEl) {
@@ -2225,6 +4096,11 @@ window.pathApp = {
2225
4096
  if (layoutEl) layoutEl.addEventListener('change', () => this.triggerUpdate());
2226
4097
  if (markBtn) markBtn.addEventListener('click', () => this.markComplete());
2227
4098
  if (historyBtn) historyBtn.addEventListener('click', () => this._toggleHistorySidebar());
4099
+ if (workbenchToggleBtn) {
4100
+ workbenchToggleBtn.addEventListener('click', () => {
4101
+ this._toggleLearningWorkbenchSidebar();
4102
+ });
4103
+ }
2228
4104
  }
2229
4105
 
2230
4106
  if (closeHistoryBtn) {
@@ -2238,6 +4114,150 @@ window.pathApp = {
2238
4114
  });
2239
4115
  }
2240
4116
 
4117
+ if (workbenchCloseBtn) {
4118
+ workbenchCloseBtn.addEventListener('click', () => this._closeLearningWorkbenchSidebar());
4119
+ }
4120
+
4121
+ if (workbenchRefreshBtn) {
4122
+ workbenchRefreshBtn.addEventListener('click', () => {
4123
+ void this.refreshLearningWorkbench({ force: true });
4124
+ });
4125
+ }
4126
+
4127
+ if (workbenchSetQualityBaselineBtn) {
4128
+ workbenchSetQualityBaselineBtn.addEventListener('click', () => {
4129
+ void this.setLearningWorkbenchQualityBaselineFromCurrent();
4130
+ });
4131
+ }
4132
+
4133
+ if (workbenchEvaluateQualityGatesBtn) {
4134
+ workbenchEvaluateQualityGatesBtn.addEventListener('click', () => {
4135
+ void this.evaluateLearningWorkbenchQualityGates();
4136
+ });
4137
+ }
4138
+
4139
+ if (workbenchIngestBtn) {
4140
+ workbenchIngestBtn.addEventListener('click', () => {
4141
+ void this.ingestFocusNodeForLearningWorkbench();
4142
+ });
4143
+ }
4144
+
4145
+ if (workbenchRunSessionBtn) {
4146
+ workbenchRunSessionBtn.addEventListener('click', () => {
4147
+ void this.runLearningWorkbenchSession();
4148
+ });
4149
+ }
4150
+
4151
+ if (workbenchRunRetestBtn) {
4152
+ workbenchRunRetestBtn.addEventListener('click', () => {
4153
+ void this.runLearningWorkbenchRetestSession();
4154
+ });
4155
+ }
4156
+
4157
+ if (workbenchUserIdInput) {
4158
+ workbenchUserIdInput.value = this.learningWorkbench.userId || 'path_user_default';
4159
+ workbenchUserIdInput.addEventListener('change', (event) => {
4160
+ const candidate = typeof event?.target?.value === 'string'
4161
+ ? event.target.value
4162
+ : '';
4163
+ this.learningWorkbench.userId = this._normalizeLearningWorkbenchUserId(candidate);
4164
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
4165
+ this.learningWorkbench.sessionHistoryQuery = {
4166
+ ...historyQuery,
4167
+ offset: 0,
4168
+ };
4169
+ this.learningWorkbench.qualityBaselineSnapshot = this._loadLearningQualityBaseline(this.learningWorkbench.userId);
4170
+ this.learningWorkbench.qualityGateEvaluation = null;
4171
+ workbenchUserIdInput.value = this.learningWorkbench.userId;
4172
+ this._persistLearningWorkbenchPreferences();
4173
+ this._renderLearningWorkbenchState();
4174
+ void this._syncLearningQualityBaselineFromServer(this.learningWorkbench.userId)
4175
+ .then(() => {
4176
+ this._renderLearningWorkbenchState();
4177
+ });
4178
+ });
4179
+ }
4180
+
4181
+ if (workbenchHistoryKindFilter) {
4182
+ workbenchHistoryKindFilter.value = this._getLearningWorkbenchSessionHistoryQuery().executionKind;
4183
+ workbenchHistoryKindFilter.addEventListener('change', (event) => {
4184
+ const nextValue = typeof event?.target?.value === 'string'
4185
+ ? event.target.value
4186
+ : 'all';
4187
+ this._setLearningWorkbenchHistoryExecutionKind(nextValue, { refresh: true });
4188
+ });
4189
+ }
4190
+
4191
+ if (workbenchHistoryFromDate) {
4192
+ workbenchHistoryFromDate.value = this._getLearningWorkbenchSessionHistoryQuery().fromDate;
4193
+ }
4194
+
4195
+ if (workbenchHistoryToDate) {
4196
+ workbenchHistoryToDate.value = this._getLearningWorkbenchSessionHistoryQuery().toDate;
4197
+ }
4198
+
4199
+ if (workbenchHistoryApplyRangeBtn) {
4200
+ workbenchHistoryApplyRangeBtn.addEventListener('click', () => {
4201
+ const fromDate = workbenchHistoryFromDate && typeof workbenchHistoryFromDate.value === 'string'
4202
+ ? workbenchHistoryFromDate.value
4203
+ : '';
4204
+ const toDate = workbenchHistoryToDate && typeof workbenchHistoryToDate.value === 'string'
4205
+ ? workbenchHistoryToDate.value
4206
+ : '';
4207
+ this._applyLearningWorkbenchHistoryDateRange(fromDate, toDate, { refresh: true });
4208
+ });
4209
+ }
4210
+
4211
+ if (workbenchHistoryClearRangeBtn) {
4212
+ workbenchHistoryClearRangeBtn.addEventListener('click', () => {
4213
+ this._applyLearningWorkbenchHistoryDateRange('', '', { refresh: true });
4214
+ });
4215
+ }
4216
+
4217
+ if (workbenchHistoryPrevBtn) {
4218
+ workbenchHistoryPrevBtn.addEventListener('click', () => {
4219
+ this._changeLearningWorkbenchHistoryPage(-1);
4220
+ });
4221
+ }
4222
+
4223
+ if (workbenchHistoryNextBtn) {
4224
+ workbenchHistoryNextBtn.addEventListener('click', () => {
4225
+ this._changeLearningWorkbenchHistoryPage(1);
4226
+ });
4227
+ }
4228
+
4229
+ if (workbenchActionsList) {
4230
+ workbenchActionsList.addEventListener('click', (event) => {
4231
+ const runBtn = event.target && typeof event.target.closest === 'function'
4232
+ ? event.target.closest('.workbench-run-action')
4233
+ : null;
4234
+ if (!runBtn) {
4235
+ return;
4236
+ }
4237
+ const actionKind = decodeURIComponent(runBtn.getAttribute('data-action-kind') || '');
4238
+ const atomId = decodeURIComponent(runBtn.getAttribute('data-atom-id') || '');
4239
+ const source = decodeURIComponent(runBtn.getAttribute('data-action-source') || '');
4240
+ void this.executeLearningWorkbenchAction({
4241
+ actionKind,
4242
+ atomId,
4243
+ source,
4244
+ });
4245
+ });
4246
+ }
4247
+
4248
+ if (workbenchHistoryList) {
4249
+ workbenchHistoryList.addEventListener('click', (event) => {
4250
+ const replayBtn = event.target && typeof event.target.closest === 'function'
4251
+ ? event.target.closest('.workbench-run-history-retest')
4252
+ : null;
4253
+ if (!replayBtn) {
4254
+ return;
4255
+ }
4256
+ const recordId = decodeURIComponent(replayBtn.getAttribute('data-history-record-id') || '');
4257
+ void this.runLearningWorkbenchHistoryRetest(recordId);
4258
+ });
4259
+ }
4260
+
2241
4261
  // Add Target Display UI if missing
2242
4262
  if (!tauriMode && !document.getElementById('target-display') && toolbar && learningModeEl) {
2243
4263
  const targetDiv = document.createElement('div');
@@ -2273,6 +4293,7 @@ window.pathApp = {
2273
4293
 
2274
4294
  this._ensurePathSemanticA11y();
2275
4295
  this._refreshPathSemanticA11y('Path mode initialized');
4296
+ this._renderLearningWorkbenchState();
2276
4297
  },
2277
4298
 
2278
4299
  _ensurePathSemanticA11y: function() {
@@ -2392,6 +4413,14 @@ window.pathApp = {
2392
4413
 
2393
4414
  const now = Date.now();
2394
4415
  if ((now - this.semanticA11yLastAnnouncementAt) < 250) {
4416
+ if (this.semanticA11yDeferredRefreshTimer) {
4417
+ clearTimeout(this.semanticA11yDeferredRefreshTimer);
4418
+ }
4419
+ const delayMs = Math.max(25, 260 - (now - this.semanticA11yLastAnnouncementAt));
4420
+ this.semanticA11yDeferredRefreshTimer = setTimeout(() => {
4421
+ this.semanticA11yDeferredRefreshTimer = null;
4422
+ this._refreshPathSemanticA11y(reason);
4423
+ }, delayMs);
2395
4424
  return;
2396
4425
  }
2397
4426
 
@@ -2871,6 +4900,10 @@ window.pathApp = {
2871
4900
  }
2872
4901
  this.saveHistory();
2873
4902
  this.updateHistorySidebar();
4903
+ const workbenchSidebar = document.getElementById('learning-workbench-sidebar');
4904
+ if (workbenchSidebar && workbenchSidebar.style.display && workbenchSidebar.style.display !== 'none') {
4905
+ void this.refreshLearningWorkbench({ force: false });
4906
+ }
2874
4907
 
2875
4908
  const next = this.nodes.find(n => !this.completedNodes.has(n.id) && n.id !== node.id);
2876
4909
  if (next) setTimeout(() => this.switchCentral(next.id), 500);
@@ -3158,6 +5191,11 @@ window.pathApp = {
3158
5191
  const bridge = (typeof window !== 'undefined') ? window.NoteConnectionRuntime : null;
3159
5192
  const waitForRuntime = this._isTauriMode() && bridge && typeof bridge.whenReady === 'function';
3160
5193
 
5194
+ if (this._isDetachedPreviewWithoutRuntimeBootstrap() && !forceDesktop) {
5195
+ console.log('[PathApp] Detached preview runtime detected; skipping early WebSocket bootstrap.');
5196
+ return;
5197
+ }
5198
+
3161
5199
  if (this._isTauriMode() && !forceDesktop) {
3162
5200
  // In normal Tauri flow, avoid idle early bridge sockets unless the desktop shell explicitly asks for one.
3163
5201
  return;
@@ -3285,7 +5323,12 @@ window.pathApp = {
3285
5323
  // This runs as soon as path_app.js is loaded, before init() is called.
3286
5324
  (function() {
3287
5325
  const isTauri = !!(window.pathApp && typeof window.pathApp._isTauriMode === 'function' && window.pathApp._isTauriMode());
3288
- if (isTauri) {
5326
+ const isDetachedPreview = !!(
5327
+ window.pathApp &&
5328
+ typeof window.pathApp._isDetachedPreviewWithoutRuntimeBootstrap === 'function' &&
5329
+ window.pathApp._isDetachedPreviewWithoutRuntimeBootstrap()
5330
+ );
5331
+ if (isTauri || isDetachedPreview) {
3289
5332
  // Bridge-first Tauri: only connect when Path Mode is explicitly initialized.
3290
5333
  return;
3291
5334
  }
@@ -3297,7 +5340,3 @@ window.pathApp = {
3297
5340
  }
3298
5341
  }, 500);
3299
5342
  })();
3300
-
3301
-
3302
-
3303
-