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.
- package/LICENSE +674 -21
- package/README.md +258 -64
- package/dist/src/agent_workspace.contract.parity.test.js +475 -0
- package/dist/src/agent_workspace.frontend.test.js +9989 -0
- package/dist/src/agent_workspace.locale.contract.test.js +95 -0
- package/dist/src/agent_workspace.runtime.behavior.test.js +5072 -0
- package/dist/src/copy.assets.contract.test.js +130 -0
- package/dist/src/core/PathBridge.js +28 -3
- package/dist/src/export/WorkspaceExportBundle.js +1130 -0
- package/dist/src/export/WorkspaceExportBundle.test.js +1480 -0
- package/dist/src/export/index.js +18 -0
- package/dist/src/export/types.js +2 -0
- package/dist/src/fixrisk.issue.verifier.contract.test.js +3 -1
- package/dist/src/foundation.ann.runtime.contract.test.js +81 -0
- package/dist/src/foundation.release.evidence.contract.test.js +406 -0
- package/dist/src/foundation.sqlite.runtime.contract.test.js +82 -0
- package/dist/src/frontend/README.md +258 -64
- package/dist/src/frontend/agent_workspace.js +5353 -0
- package/dist/src/frontend/agent_workspace_runtime.js +4434 -0
- package/dist/src/frontend/analysis.js +31 -4
- package/dist/src/frontend/app.js +3597 -141
- package/dist/src/frontend/focus_mode_interactions.js +33 -0
- package/dist/src/frontend/godot_future_path_renderer.js +540 -0
- package/dist/src/frontend/godot_tree_interactions.js +178 -0
- package/dist/src/frontend/graph_state.mjs +105 -0
- package/dist/src/frontend/hosted_future_path_runtime.js +157 -0
- package/dist/src/frontend/i18n.mjs +186 -0
- package/dist/src/frontend/index.html +641 -266
- package/dist/src/frontend/layout_gpu.js +12 -4
- package/dist/src/frontend/locales/en.json +721 -5
- package/dist/src/frontend/locales/zh.json +721 -5
- package/dist/src/frontend/main.mjs +60 -0
- package/dist/src/frontend/markdown_runtime.js +827 -0
- package/dist/src/frontend/notemd.css +49 -0
- package/dist/src/frontend/notemd.html +7 -1
- package/dist/src/frontend/notemd.js +64 -0
- package/dist/src/frontend/path.html +107 -0
- package/dist/src/frontend/path_app.js +2189 -150
- package/dist/src/frontend/path_layout.mjs +143 -0
- package/dist/src/frontend/path_mermaid_utils.mjs +108 -0
- package/dist/src/frontend/path_modules_bridge.js +486 -0
- package/dist/src/frontend/path_state.mjs +118 -0
- package/dist/src/frontend/path_styles.css +146 -0
- package/dist/src/frontend/path_worker_bridge.mjs +85 -0
- package/dist/src/frontend/reader.js +522 -27
- package/dist/src/frontend/runtime_bridge.js +67 -54
- package/dist/src/frontend/runtime_bridge.mjs +279 -0
- package/dist/src/frontend/settings.js +130 -12
- package/dist/src/frontend/simulationWorker.js +241 -6
- package/dist/src/frontend/source_manager.js +190 -21
- package/dist/src/frontend/styles.css +2853 -72
- package/dist/src/frontend/workbench_state.mjs +101 -0
- package/dist/src/frontend/workspace_panes.js +10168 -0
- package/dist/src/frontend.locale.contract.test.js +62 -0
- package/dist/src/godot.sidecar.bootstrap.contract.test.js +244 -0
- package/dist/src/indexing/IndexLifecycle.js +195 -0
- package/dist/src/indexing/IndexLifecycle.test.js +49 -0
- package/dist/src/indexing/SegmentBuilder.js +64 -0
- package/dist/src/indexing/UnitBuilder.js +48 -0
- package/dist/src/indexing/types.js +2 -0
- package/dist/src/knowledge.api.contract.test.js +170 -0
- package/dist/src/learning/KnowledgeLearningPlatform.js +10386 -0
- package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +327 -0
- package/dist/src/learning/KnowledgeLearningPlatform.program-f.test.js +99 -0
- package/dist/src/learning/KnowledgeLearningPlatform.test.js +2971 -0
- package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +2974 -0
- package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +3928 -0
- package/dist/src/learning/answerReleaseReview.js +4319 -0
- package/dist/src/learning/answerReleaseReview.test.js +2888 -0
- package/dist/src/learning/api.js +2 -0
- package/dist/src/learning/conversationComposer.js +1480 -0
- package/dist/src/learning/conversationComposer.test.js +1817 -0
- package/dist/src/learning/domains/ConversationManager.js +53 -0
- package/dist/src/learning/domains/KnowledgeIngestor.js +238 -0
- package/dist/src/learning/domains/KnowledgeQuerier.js +187 -0
- package/dist/src/learning/domains/MasteryEngine.js +387 -0
- package/dist/src/learning/domains/MemoryPolicyManager.js +408 -0
- package/dist/src/learning/domains/QualityEvaluator.js +307 -0
- package/dist/src/learning/domains/TutorRouter.js +313 -0
- package/dist/src/learning/domains/index.js +33 -0
- package/dist/src/learning/domains/types.js +7 -0
- package/dist/src/learning/errors.js +29 -0
- package/dist/src/learning/evidenceContextAssembler.js +1176 -0
- package/dist/src/learning/evidenceContextAssembler.test.js +6332 -0
- package/dist/src/learning/graphContextAssembler.js +870 -0
- package/dist/src/learning/graphContextAssembler.test.js +1033 -0
- package/dist/src/learning/index.js +28 -0
- package/dist/src/learning/queryBackend.js +1898 -0
- package/dist/src/learning/queryBackend.test.js +955 -0
- package/dist/src/learning/ragContextPack.js +257 -0
- package/dist/src/learning/ragContextPack.test.js +160 -0
- package/dist/src/learning/ragPublicText.js +38 -0
- package/dist/src/learning/ragSufficiencyJudge.js +161 -0
- package/dist/src/learning/ragSufficiencyJudge.test.js +177 -0
- package/dist/src/learning/ragSufficiencyProviderJudge.js +227 -0
- package/dist/src/learning/ragSufficiencyProviderJudge.test.js +156 -0
- package/dist/src/learning/requestNormalization.js +198 -0
- package/dist/src/learning/runtimeCapability.js +4677 -0
- package/dist/src/learning/runtimeCapability.test.js +3635 -0
- package/dist/src/learning/store.js +1240 -0
- package/dist/src/learning/store.test.js +1126 -0
- package/dist/src/learning/tutorAdapter.js +2 -0
- package/dist/src/learning/types.js +2 -0
- package/dist/src/learning/vectorAccelerationAdapter.js +942 -0
- package/dist/src/learning/vectorAccelerationAdapter.test.js +382 -0
- package/dist/src/lfs.asset.policy.contract.test.js +153 -0
- package/dist/src/license.policy.contract.test.js +66 -0
- package/dist/src/memory/MemoryGovernance.js +74 -0
- package/dist/src/memory/MemoryGovernance.test.js +46 -0
- package/dist/src/memory/types.js +2 -0
- package/dist/src/mermaid.frontend.guard.contract.test.js +77 -0
- package/dist/src/middleware/auth.js +17 -0
- package/dist/src/middleware/body-parser.js +45 -0
- package/dist/src/middleware/cors.js +44 -0
- package/dist/src/middleware/index.js +21 -0
- package/dist/src/middleware/request-trace.js +96 -0
- package/dist/src/notemd/AppConfigToml.js +6 -4
- package/dist/src/notemd/MermaidProcessor.js +400 -50
- package/dist/src/notemd/NotemdService.js +498 -13
- package/dist/src/notemd/PromptManager.js +15 -0
- package/dist/src/notemd/cli/commands.js +357 -0
- package/dist/src/notemd/cli/dispatcher.js +225 -0
- package/dist/src/notemd/cli/index.js +169 -0
- package/dist/src/notemd/cli/parser.js +68 -0
- package/dist/src/notemd/cli/types.js +2 -0
- package/dist/src/notemd/constants.js +43 -0
- package/dist/src/notemd/diagram/diagramGenerationService.js +78 -0
- package/dist/src/notemd/diagram/diagramSpec.js +79 -0
- package/dist/src/notemd/diagram/diagramSpecResponseParser.js +131 -0
- package/dist/src/notemd/diagram/intent.js +95 -0
- package/dist/src/notemd/diagram/planner.js +71 -0
- package/dist/src/notemd/diagram/prompts/diagramSpecPrompt.js +42 -0
- package/dist/src/notemd/diagram/types.js +18 -0
- package/dist/src/notemd/index.js +26 -0
- package/dist/src/notemd/operations/capabilityManifest.js +23 -0
- package/dist/src/notemd/operations/cliContracts.js +17 -0
- package/dist/src/notemd/operations/configProfileCommands.js +85 -0
- package/dist/src/notemd/operations/registry.contract.test.js +95 -0
- package/dist/src/notemd/operations/registry.js +991 -0
- package/dist/src/notemd/operations/types.js +2 -0
- package/dist/src/notemd/providerDiagnostics.js +220 -0
- package/dist/src/notemd/providerProfiles.js +42 -0
- package/dist/src/notemd/providerTemplates.js +231 -0
- package/dist/src/notemd/search/DuckDuckGoProvider.js +39 -0
- package/dist/src/notemd/search/SearchManager.js +13 -0
- package/dist/src/notemd/search/SearchProvider.js +2 -0
- package/dist/src/notemd/search/TavilyProvider.js +44 -0
- package/dist/src/notemd.agent.manifest.test.js +85 -0
- package/dist/src/notemd.api.contract.test.js +14 -1
- package/dist/src/notemd.app_config_toml.test.js +2 -1
- package/dist/src/notemd.batch.workflow.test.js +117 -0
- package/dist/src/notemd.cli.e2e.test.js +136 -0
- package/dist/src/notemd.core.test.js +51 -0
- package/dist/src/notemd.diagram.pipeline.test.js +233 -0
- package/dist/src/notemd.providerTemplates.test.js +34 -0
- package/dist/src/notemd.server.integration.test.js +143 -35
- package/dist/src/notemd.workflow.pipeline.test.js +162 -0
- package/dist/src/pathbridge.handshake.contract.test.js +16 -2
- package/dist/src/pathmode.background.contract.test.js +69 -0
- package/dist/src/pathmode.settings.api.contract.test.js +9 -0
- package/dist/src/pkg.sidecar.contract.test.js +9 -3
- package/dist/src/platform/ExportProfile.js +58 -0
- package/dist/src/platform/PlatformCapabilities.js +45 -0
- package/dist/src/platform/PlatformCapabilities.test.js +30 -0
- package/dist/src/platform/RenderMaterializer.js +33 -0
- package/dist/src/platform/RenderMaterializer.test.js +32 -0
- package/dist/src/query_backend.external_http.integration.test.js +410 -0
- package/dist/src/reader_renderer.js +404 -3
- package/dist/src/reader_renderer.test.js +87 -0
- package/dist/src/release.godot.mirror.contract.test.js +73 -0
- package/dist/src/resources/ResourceRegistry.js +223 -0
- package/dist/src/resources/ResourceRegistry.test.js +61 -0
- package/dist/src/resources/types.js +2 -0
- package/dist/src/routes/agentWorkspaceDiagnostics.js +173 -0
- package/dist/src/routes/data.js +267 -0
- package/dist/src/routes/diagnostics.js +51 -0
- package/dist/src/routes/index.js +23 -0
- package/dist/src/routes/knowledge.js +968 -0
- package/dist/src/routes/markdown.js +287 -0
- package/dist/src/routes/notemd.js +565 -0
- package/dist/src/routes/registry.contract.test.js +130 -0
- package/dist/src/routes/render.js +285 -0
- package/dist/src/routes/runtimeRunbookRouteOps.js +149 -0
- package/dist/src/routes/runtimeRunbookRouteOps.test.js +194 -0
- package/dist/src/routes/settings.js +6 -0
- package/dist/src/routes/staticFiles.js +94 -0
- package/dist/src/routes/types.js +2 -0
- package/dist/src/runtime.transport.adapter.contract.test.js +81 -0
- package/dist/src/server.js +11090 -1549
- package/dist/src/server.migration.test.js +193 -21
- package/dist/src/server.port.fallback.contract.test.js +63 -0
- package/dist/src/session/SessionStateStore.js +81 -0
- package/dist/src/session/SessionStateStore.test.js +58 -0
- package/dist/src/session/types.js +2 -0
- package/dist/src/settings.runtime.contract.test.js +50 -0
- package/dist/src/shared/types.contract.test.js +107 -0
- package/dist/src/shared/types.js +22 -0
- package/dist/src/sidecar.replacement.boundary.contract.test.js +128 -0
- package/dist/src/sidecar.supply.readiness.contract.test.js +144 -0
- package/dist/src/source_manager.loadflow.test.js +46 -0
- package/dist/src/startup.layout.snapshot.contract.test.js +57 -0
- package/dist/src/tauri.frontend.build.contract.test.js +60 -0
- package/dist/src/tauri.sidecar.cleanup.contract.test.js +21 -0
- package/dist/src/utils/RuntimePaths.js +4 -13
- package/dist/src/utils/platform.js +153 -0
- package/dist/src/workflows/WorkflowArtifactStore.js +96 -0
- package/dist/src/workflows/WorkflowArtifactStore.test.js +80 -0
- package/dist/src/workflows/types.js +2 -0
- package/dist/src/workspace/WorkspaceRegistry.js +122 -0
- package/dist/src/workspace/WorkspaceRegistry.test.js +29 -0
- package/dist/src/workspace/types.js +2 -0
- 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
|
-
|
|
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
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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(
|
|
1318
|
-
const rasterHeight = Math.max(1, Math.round(
|
|
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, '&')
|
|
2679
|
+
.replace(/</g, '<')
|
|
2680
|
+
.replace(/>/g, '>')
|
|
2681
|
+
.replace(/"/g, '"')
|
|
2682
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
1485
|
-
if (
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
const
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
|
|
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
|
-
|