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
package/dist/src/frontend/app.js
CHANGED
|
@@ -8,6 +8,1559 @@ const graphSemanticA11yState = {
|
|
|
8
8
|
pendingTimer: null
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
function nowMs() {
|
|
12
|
+
return (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
13
|
+
? performance.now()
|
|
14
|
+
: Date.now();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readStartupPerfProfileOverride() {
|
|
18
|
+
if (typeof localStorage === 'undefined') {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const value = localStorage.getItem('nc.startupPerfProfile');
|
|
24
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
25
|
+
} catch (_err) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const mermaidErrorGuardState = {
|
|
31
|
+
observer: null,
|
|
32
|
+
toastHost: null,
|
|
33
|
+
seenAtBySignature: new Map(),
|
|
34
|
+
renderGuardInstalled: false,
|
|
35
|
+
periodicSweepHandle: null,
|
|
36
|
+
suppressedArtifacts: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function isMermaidErrorArtifactText(text) {
|
|
40
|
+
const normalized = String(text || '').toLowerCase();
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
normalized.includes('syntax error in text') ||
|
|
46
|
+
normalized.includes('lexical error on line') ||
|
|
47
|
+
normalized.includes('parse error on line') ||
|
|
48
|
+
normalized.includes('mermaid version') ||
|
|
49
|
+
normalized.includes('diagram syntax error')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeMermaidErrorArtifactText(text) {
|
|
54
|
+
return String(text || '')
|
|
55
|
+
.replace(/\s+/g, ' ')
|
|
56
|
+
.trim()
|
|
57
|
+
.slice(0, 240);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerMermaidSuppressedArtifact(detail, context = {}) {
|
|
61
|
+
const signature = normalizeMermaidErrorArtifactText(detail);
|
|
62
|
+
if (!signature) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const entry = {
|
|
66
|
+
signature,
|
|
67
|
+
detectedAt: new Date().toISOString(),
|
|
68
|
+
context,
|
|
69
|
+
};
|
|
70
|
+
mermaidErrorGuardState.suppressedArtifacts.push(entry);
|
|
71
|
+
if (mermaidErrorGuardState.suppressedArtifacts.length > 20) {
|
|
72
|
+
mermaidErrorGuardState.suppressedArtifacts.shift();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensureMermaidErrorToastHost() {
|
|
77
|
+
if (mermaidErrorGuardState.toastHost && mermaidErrorGuardState.toastHost.isConnected) {
|
|
78
|
+
return mermaidErrorGuardState.toastHost;
|
|
79
|
+
}
|
|
80
|
+
const host = document.createElement('div');
|
|
81
|
+
host.id = 'mermaid-error-toast-stack';
|
|
82
|
+
host.className = 'mermaid-error-toast-stack';
|
|
83
|
+
document.body.appendChild(host);
|
|
84
|
+
mermaidErrorGuardState.toastHost = host;
|
|
85
|
+
return host;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function emitMermaidErrorToast(detail) {
|
|
89
|
+
const signature = normalizeMermaidErrorArtifactText(detail);
|
|
90
|
+
if (!signature) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const lastSeenAt = mermaidErrorGuardState.seenAtBySignature.get(signature) || 0;
|
|
95
|
+
if ((now - lastSeenAt) < 8000) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
mermaidErrorGuardState.seenAtBySignature.set(signature, now);
|
|
99
|
+
|
|
100
|
+
const host = ensureMermaidErrorToastHost();
|
|
101
|
+
const toast = document.createElement('div');
|
|
102
|
+
toast.className = 'mermaid-error-toast';
|
|
103
|
+
|
|
104
|
+
const title = document.createElement('div');
|
|
105
|
+
title.className = 'mermaid-error-toast-title';
|
|
106
|
+
title.textContent = 'Mermaid diagram skipped';
|
|
107
|
+
toast.appendChild(title);
|
|
108
|
+
|
|
109
|
+
const summary = document.createElement('div');
|
|
110
|
+
summary.className = 'mermaid-error-toast-summary';
|
|
111
|
+
summary.textContent = 'A malformed Mermaid block was suppressed to keep the workspace readable.';
|
|
112
|
+
toast.appendChild(summary);
|
|
113
|
+
|
|
114
|
+
const detailLine = document.createElement('div');
|
|
115
|
+
detailLine.className = 'mermaid-error-toast-detail';
|
|
116
|
+
detailLine.textContent = signature;
|
|
117
|
+
toast.appendChild(detailLine);
|
|
118
|
+
|
|
119
|
+
host.appendChild(toast);
|
|
120
|
+
while (host.childElementCount > 3) {
|
|
121
|
+
host.removeChild(host.firstElementChild);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
window.setTimeout(() => {
|
|
125
|
+
if (toast.parentNode) {
|
|
126
|
+
toast.parentNode.removeChild(toast);
|
|
127
|
+
}
|
|
128
|
+
}, 6500);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createInlineMermaidErrorNotice(detail) {
|
|
132
|
+
const wrapper = document.createElement('div');
|
|
133
|
+
wrapper.className = 'mermaid-inline-guard';
|
|
134
|
+
|
|
135
|
+
const title = document.createElement('div');
|
|
136
|
+
title.className = 'mermaid-inline-guard-title';
|
|
137
|
+
title.textContent = 'Mermaid diagram unavailable';
|
|
138
|
+
wrapper.appendChild(title);
|
|
139
|
+
|
|
140
|
+
const summary = document.createElement('div');
|
|
141
|
+
summary.className = 'mermaid-inline-guard-summary';
|
|
142
|
+
summary.textContent = 'Malformed Mermaid content was suppressed in this view.';
|
|
143
|
+
wrapper.appendChild(summary);
|
|
144
|
+
|
|
145
|
+
if (detail) {
|
|
146
|
+
const detailLine = document.createElement('div');
|
|
147
|
+
detailLine.className = 'mermaid-inline-guard-detail';
|
|
148
|
+
detailLine.textContent = normalizeMermaidErrorArtifactText(detail);
|
|
149
|
+
wrapper.appendChild(detailLine);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return wrapper;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveMermaidErrorArtifactHost(node) {
|
|
156
|
+
if (!node || typeof node.closest !== 'function') {
|
|
157
|
+
return node;
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
node.closest('.mermaid-render-host-offscreen') ||
|
|
161
|
+
node.closest('.mermaid-render-failed') ||
|
|
162
|
+
node.closest('.mermaid') ||
|
|
163
|
+
node.closest('.reader-block') ||
|
|
164
|
+
node.closest('svg') ||
|
|
165
|
+
node
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isProtectedMermaidSuppressionHost(host) {
|
|
170
|
+
if (!host || host.nodeType !== Node.ELEMENT_NODE) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return (
|
|
174
|
+
host === document.body ||
|
|
175
|
+
host === document.documentElement ||
|
|
176
|
+
host === document.head ||
|
|
177
|
+
host.id === 'graph-wrapper' ||
|
|
178
|
+
host.id === 'path-container' ||
|
|
179
|
+
host.id === 'reading-window' ||
|
|
180
|
+
host.id === 'reading-content-box' ||
|
|
181
|
+
host.id === 'reading-body'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function collectMermaidErrorArtifactCandidates(root) {
|
|
186
|
+
const hosts = [];
|
|
187
|
+
const hostSet = new Set();
|
|
188
|
+
const candidates = [];
|
|
189
|
+
|
|
190
|
+
if (!root) {
|
|
191
|
+
return hosts;
|
|
192
|
+
}
|
|
193
|
+
if (
|
|
194
|
+
root.nodeType === Node.ELEMENT_NODE &&
|
|
195
|
+
root !== document.body &&
|
|
196
|
+
root !== document.documentElement &&
|
|
197
|
+
root !== document.head
|
|
198
|
+
) {
|
|
199
|
+
candidates.push(root);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const queryRoot = root.nodeType === Node.ELEMENT_NODE ? root : document.body;
|
|
203
|
+
if (queryRoot && typeof queryRoot.querySelectorAll === 'function') {
|
|
204
|
+
queryRoot.querySelectorAll('svg, .mermaid, div, section, article, aside, img, foreignObject').forEach((node) => candidates.push(node));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
candidates.forEach((candidate) => {
|
|
208
|
+
if (!candidate || candidate.nodeType !== Node.ELEMENT_NODE) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const element = candidate;
|
|
212
|
+
const text = normalizeMermaidErrorArtifactText(
|
|
213
|
+
element.textContent ||
|
|
214
|
+
element.getAttribute?.('alt') ||
|
|
215
|
+
element.getAttribute?.('aria-label') ||
|
|
216
|
+
''
|
|
217
|
+
);
|
|
218
|
+
if (!isMermaidErrorArtifactText(text)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const host = resolveMermaidErrorArtifactHost(element);
|
|
222
|
+
if (!host || hostSet.has(host) || isProtectedMermaidSuppressionHost(host)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
hostSet.add(host);
|
|
226
|
+
hosts.push(host);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return hosts;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function suppressMermaidErrorArtifacts(root, context = {}) {
|
|
233
|
+
if (!root) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const candidates = collectMermaidErrorArtifactCandidates(root);
|
|
238
|
+
candidates.forEach((candidate) => {
|
|
239
|
+
if (!candidate || candidate.dataset?.mermaidErrorSuppressed === '1') {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const text = normalizeMermaidErrorArtifactText(candidate.textContent || '');
|
|
243
|
+
if (!isMermaidErrorArtifactText(text)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const host = resolveMermaidErrorArtifactHost(candidate);
|
|
248
|
+
if (!host || host.dataset?.mermaidErrorSuppressed === '1') {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (host.dataset) {
|
|
252
|
+
host.dataset.mermaidErrorSuppressed = '1';
|
|
253
|
+
}
|
|
254
|
+
registerMermaidSuppressedArtifact(text, {
|
|
255
|
+
hostTag: host.tagName || '',
|
|
256
|
+
hostId: host.id || '',
|
|
257
|
+
hostClass: host.className || '',
|
|
258
|
+
...context,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const shouldKeepInline = Boolean(
|
|
262
|
+
host.closest('#reading-window') ||
|
|
263
|
+
host.closest('#reading-body') ||
|
|
264
|
+
host.closest('.notemd-embed-shell')
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (shouldKeepInline) {
|
|
268
|
+
const inlineNotice = createInlineMermaidErrorNotice(text);
|
|
269
|
+
if (host.parentNode) {
|
|
270
|
+
host.parentNode.replaceChild(inlineNotice, host);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
emitMermaidErrorToast(text);
|
|
274
|
+
if (host.parentNode) {
|
|
275
|
+
host.parentNode.removeChild(host);
|
|
276
|
+
} else {
|
|
277
|
+
host.style.display = 'none';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function installMermaidRuntimeGuards() {
|
|
284
|
+
const mermaid = window.mermaid;
|
|
285
|
+
if (!mermaid || mermaidErrorGuardState.renderGuardInstalled === true || mermaid.__noteConnectionErrorGuardInstalled === true) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (typeof mermaid.render === 'function') {
|
|
290
|
+
const originalRender = mermaid.render.bind(mermaid);
|
|
291
|
+
mermaid.render = async function(...args) {
|
|
292
|
+
const result = await originalRender(...args);
|
|
293
|
+
if (result && typeof result.svg === 'string' && isMermaidErrorArtifactText(result.svg)) {
|
|
294
|
+
registerMermaidSuppressedArtifact(result.svg, {
|
|
295
|
+
source: 'mermaid.render',
|
|
296
|
+
});
|
|
297
|
+
throw new Error('Suppressed Mermaid error SVG before it could be mounted.');
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (typeof mermaid.run === 'function') {
|
|
304
|
+
const originalRun = mermaid.run.bind(mermaid);
|
|
305
|
+
mermaid.run = async function(options) {
|
|
306
|
+
const result = await originalRun(options);
|
|
307
|
+
try {
|
|
308
|
+
if (options && Array.isArray(options.nodes) && options.nodes.length > 0) {
|
|
309
|
+
options.nodes.forEach((node) => suppressMermaidErrorArtifacts(node, {
|
|
310
|
+
source: 'mermaid.run.nodes',
|
|
311
|
+
}));
|
|
312
|
+
} else {
|
|
313
|
+
suppressMermaidErrorArtifacts(document.body, {
|
|
314
|
+
source: 'mermaid.run.document',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
} catch (_error) {
|
|
318
|
+
// Guard should never break main rendering.
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
mermaid.__noteConnectionErrorGuardInstalled = true;
|
|
325
|
+
mermaidErrorGuardState.renderGuardInstalled = true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function exposeMermaidDebugInterface() {
|
|
329
|
+
const existing = (window.__NC_DEBUG__ && typeof window.__NC_DEBUG__ === 'object')
|
|
330
|
+
? window.__NC_DEBUG__
|
|
331
|
+
: {};
|
|
332
|
+
window.__NC_DEBUG__ = {
|
|
333
|
+
...existing,
|
|
334
|
+
scanMermaidErrorArtifacts: () => collectMermaidErrorArtifactCandidates(document.body).map((host) => ({
|
|
335
|
+
tag: host.tagName || '',
|
|
336
|
+
id: host.id || '',
|
|
337
|
+
className: host.className || '',
|
|
338
|
+
text: normalizeMermaidErrorArtifactText(host.textContent || ''),
|
|
339
|
+
})),
|
|
340
|
+
suppressMermaidErrorArtifactsNow: () => {
|
|
341
|
+
suppressMermaidErrorArtifacts(document.body, { source: 'manual-debug-scan' });
|
|
342
|
+
return mermaidErrorGuardState.suppressedArtifacts.slice();
|
|
343
|
+
},
|
|
344
|
+
getMermaidGuardState: () => ({
|
|
345
|
+
renderGuardInstalled: mermaidErrorGuardState.renderGuardInstalled,
|
|
346
|
+
suppressedArtifacts: mermaidErrorGuardState.suppressedArtifacts.slice(),
|
|
347
|
+
}),
|
|
348
|
+
captureRuntimeState: () => ({
|
|
349
|
+
url: window.location.href,
|
|
350
|
+
title: document.title,
|
|
351
|
+
mermaidGuardState: {
|
|
352
|
+
renderGuardInstalled: mermaidErrorGuardState.renderGuardInstalled,
|
|
353
|
+
suppressedArtifacts: mermaidErrorGuardState.suppressedArtifacts.slice(),
|
|
354
|
+
},
|
|
355
|
+
activeMermaidErrors: collectMermaidErrorArtifactCandidates(document.body).map((host) => ({
|
|
356
|
+
tag: host.tagName || '',
|
|
357
|
+
id: host.id || '',
|
|
358
|
+
className: host.className || '',
|
|
359
|
+
text: normalizeMermaidErrorArtifactText(host.textContent || ''),
|
|
360
|
+
})),
|
|
361
|
+
}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function installMermaidErrorArtifactObserver() {
|
|
366
|
+
installMermaidRuntimeGuards();
|
|
367
|
+
exposeMermaidDebugInterface();
|
|
368
|
+
|
|
369
|
+
if (mermaidErrorGuardState.observer || typeof MutationObserver === 'undefined') {
|
|
370
|
+
suppressMermaidErrorArtifacts(document.body, { source: 'observer-reuse' });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
mermaidErrorGuardState.observer = new MutationObserver((mutations) => {
|
|
375
|
+
mutations.forEach((mutation) => {
|
|
376
|
+
mutation.addedNodes.forEach((node) => {
|
|
377
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
suppressMermaidErrorArtifacts(node, { source: 'mutation-observer' });
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
mermaidErrorGuardState.observer.observe(document.body, {
|
|
386
|
+
childList: true,
|
|
387
|
+
subtree: true,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
suppressMermaidErrorArtifacts(document.body, { source: 'observer-install' });
|
|
391
|
+
window.setTimeout(() => {
|
|
392
|
+
installMermaidRuntimeGuards();
|
|
393
|
+
suppressMermaidErrorArtifacts(document.body, { source: 'delayed-sweep-1200' });
|
|
394
|
+
}, 1200);
|
|
395
|
+
window.setTimeout(() => {
|
|
396
|
+
installMermaidRuntimeGuards();
|
|
397
|
+
suppressMermaidErrorArtifacts(document.body, { source: 'delayed-sweep-3200' });
|
|
398
|
+
}, 3200);
|
|
399
|
+
if (mermaidErrorGuardState.periodicSweepHandle === null) {
|
|
400
|
+
mermaidErrorGuardState.periodicSweepHandle = window.setInterval(() => {
|
|
401
|
+
installMermaidRuntimeGuards();
|
|
402
|
+
suppressMermaidErrorArtifacts(document.body, { source: 'periodic-sweep' });
|
|
403
|
+
}, 750);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
installMermaidErrorArtifactObserver();
|
|
408
|
+
|
|
409
|
+
function resolveRuntimePlatform(runtimeCaps) {
|
|
410
|
+
const rawPlatform = runtimeCaps && typeof runtimeCaps.platform === 'string'
|
|
411
|
+
? runtimeCaps.platform.trim().toLowerCase()
|
|
412
|
+
: '';
|
|
413
|
+
|
|
414
|
+
if (rawPlatform.includes('android')) {
|
|
415
|
+
return 'android';
|
|
416
|
+
}
|
|
417
|
+
if (rawPlatform.includes('ios')) {
|
|
418
|
+
return 'ios';
|
|
419
|
+
}
|
|
420
|
+
if (rawPlatform.includes('windows') || rawPlatform === 'win32') {
|
|
421
|
+
return 'windows';
|
|
422
|
+
}
|
|
423
|
+
if (rawPlatform.includes('macos') || rawPlatform.includes('darwin') || rawPlatform === 'mac') {
|
|
424
|
+
return 'macos';
|
|
425
|
+
}
|
|
426
|
+
if (rawPlatform.includes('linux')) {
|
|
427
|
+
return 'linux';
|
|
428
|
+
}
|
|
429
|
+
if (rawPlatform.includes('web')) {
|
|
430
|
+
return 'web';
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string') {
|
|
434
|
+
const ua = navigator.userAgent;
|
|
435
|
+
if (/Android/i.test(ua)) return 'android';
|
|
436
|
+
if (/iPhone|iPad|iPod/i.test(ua)) return 'ios';
|
|
437
|
+
if (/Windows/i.test(ua)) return 'windows';
|
|
438
|
+
if (/Macintosh|Mac OS X/i.test(ua)) return 'macos';
|
|
439
|
+
if (/Linux/i.test(ua)) return 'linux';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (typeof window !== 'undefined' && window.Capacitor && typeof window.Capacitor.getPlatform === 'function') {
|
|
443
|
+
try {
|
|
444
|
+
const capPlatform = String(window.Capacitor.getPlatform() || '').toLowerCase();
|
|
445
|
+
if (capPlatform === 'android') return 'android';
|
|
446
|
+
if (capPlatform === 'ios') return 'ios';
|
|
447
|
+
if (capPlatform === 'web') return 'web';
|
|
448
|
+
} catch (_err) {
|
|
449
|
+
// Ignore and fall through.
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return 'unknown';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resolveStartupPerfProfile(runtimeCaps) {
|
|
457
|
+
const base = {
|
|
458
|
+
id: 'default',
|
|
459
|
+
pilotEnabled: false,
|
|
460
|
+
tickMaxFps: 0,
|
|
461
|
+
edgeGeometryDelayMs: 0,
|
|
462
|
+
edgeStartupWindowMs: 0,
|
|
463
|
+
edgeStartupSvgCap: 0,
|
|
464
|
+
edgeStage1TopK: 0,
|
|
465
|
+
stableAlphaThreshold: 0.05,
|
|
466
|
+
stableHoldTicks: 8,
|
|
467
|
+
stableTimeoutMs: 12000,
|
|
468
|
+
lowAlphaThreshold: 0.08,
|
|
469
|
+
lowAlphaTickMaxFps: 0,
|
|
470
|
+
deltaTickEnabled: false,
|
|
471
|
+
deltaEpsilonPx: 0.6,
|
|
472
|
+
deltaFullSyncEveryTicks: 3,
|
|
473
|
+
deltaLowAlphaEpsilonMultiplier: 1.35,
|
|
474
|
+
deltaLowAlphaFullSyncEveryTicks: 5,
|
|
475
|
+
startupOverlayEnabled: true,
|
|
476
|
+
overlaySafetyTimeoutMs: 30000,
|
|
477
|
+
overlayMinStars: 70,
|
|
478
|
+
overlayMaxStars: 180,
|
|
479
|
+
overlayStarDensity: 2400,
|
|
480
|
+
overlayDprCap: 2
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const profileCatalog = {
|
|
484
|
+
desktop_windows_pilot: {
|
|
485
|
+
...base,
|
|
486
|
+
id: 'desktop_windows_pilot',
|
|
487
|
+
pilotEnabled: true,
|
|
488
|
+
tickMaxFps: 26,
|
|
489
|
+
edgeGeometryDelayMs: 400,
|
|
490
|
+
edgeStartupWindowMs: 1500,
|
|
491
|
+
edgeStartupSvgCap: 18000,
|
|
492
|
+
edgeStage1TopK: 3500,
|
|
493
|
+
lowAlphaTickMaxFps: 12,
|
|
494
|
+
deltaTickEnabled: true,
|
|
495
|
+
deltaEpsilonPx: 0.75,
|
|
496
|
+
deltaFullSyncEveryTicks: 3,
|
|
497
|
+
deltaLowAlphaEpsilonMultiplier: 1.45,
|
|
498
|
+
deltaLowAlphaFullSyncEveryTicks: 5,
|
|
499
|
+
overlayMinStars: 90,
|
|
500
|
+
overlayMaxStars: 220,
|
|
501
|
+
overlayStarDensity: 2200
|
|
502
|
+
},
|
|
503
|
+
desktop_macos_pilot: {
|
|
504
|
+
...base,
|
|
505
|
+
id: 'desktop_macos_pilot',
|
|
506
|
+
pilotEnabled: true,
|
|
507
|
+
tickMaxFps: 24,
|
|
508
|
+
edgeGeometryDelayMs: 430,
|
|
509
|
+
edgeStartupWindowMs: 1700,
|
|
510
|
+
edgeStartupSvgCap: 15000,
|
|
511
|
+
edgeStage1TopK: 3000,
|
|
512
|
+
lowAlphaTickMaxFps: 11,
|
|
513
|
+
deltaTickEnabled: true,
|
|
514
|
+
deltaEpsilonPx: 0.75,
|
|
515
|
+
deltaFullSyncEveryTicks: 3,
|
|
516
|
+
deltaLowAlphaEpsilonMultiplier: 1.45,
|
|
517
|
+
deltaLowAlphaFullSyncEveryTicks: 5,
|
|
518
|
+
overlayMinStars: 84,
|
|
519
|
+
overlayMaxStars: 200,
|
|
520
|
+
overlayStarDensity: 2400
|
|
521
|
+
},
|
|
522
|
+
desktop_linux_pilot: {
|
|
523
|
+
...base,
|
|
524
|
+
id: 'desktop_linux_pilot',
|
|
525
|
+
pilotEnabled: true,
|
|
526
|
+
tickMaxFps: 24,
|
|
527
|
+
edgeGeometryDelayMs: 420,
|
|
528
|
+
edgeStartupWindowMs: 1600,
|
|
529
|
+
edgeStartupSvgCap: 16000,
|
|
530
|
+
edgeStage1TopK: 3200,
|
|
531
|
+
lowAlphaTickMaxFps: 11,
|
|
532
|
+
deltaTickEnabled: true,
|
|
533
|
+
deltaEpsilonPx: 0.75,
|
|
534
|
+
deltaFullSyncEveryTicks: 3,
|
|
535
|
+
deltaLowAlphaEpsilonMultiplier: 1.45,
|
|
536
|
+
deltaLowAlphaFullSyncEveryTicks: 5,
|
|
537
|
+
overlayMinStars: 86,
|
|
538
|
+
overlayMaxStars: 205,
|
|
539
|
+
overlayStarDensity: 2350
|
|
540
|
+
},
|
|
541
|
+
mobile_android_pilot: {
|
|
542
|
+
...base,
|
|
543
|
+
id: 'mobile_android_pilot',
|
|
544
|
+
pilotEnabled: true,
|
|
545
|
+
tickMaxFps: 18,
|
|
546
|
+
edgeGeometryDelayMs: 560,
|
|
547
|
+
edgeStartupWindowMs: 2200,
|
|
548
|
+
edgeStartupSvgCap: 7000,
|
|
549
|
+
edgeStage1TopK: 1500,
|
|
550
|
+
stableHoldTicks: 10,
|
|
551
|
+
stableTimeoutMs: 18000,
|
|
552
|
+
lowAlphaThreshold: 0.1,
|
|
553
|
+
lowAlphaTickMaxFps: 8,
|
|
554
|
+
deltaTickEnabled: true,
|
|
555
|
+
deltaEpsilonPx: 0.95,
|
|
556
|
+
deltaFullSyncEveryTicks: 4,
|
|
557
|
+
deltaLowAlphaEpsilonMultiplier: 1.6,
|
|
558
|
+
deltaLowAlphaFullSyncEveryTicks: 6,
|
|
559
|
+
overlaySafetyTimeoutMs: 36000,
|
|
560
|
+
overlayMinStars: 52,
|
|
561
|
+
overlayMaxStars: 120,
|
|
562
|
+
overlayStarDensity: 3600,
|
|
563
|
+
overlayDprCap: 1.6
|
|
564
|
+
},
|
|
565
|
+
mobile_ios_pilot: {
|
|
566
|
+
...base,
|
|
567
|
+
id: 'mobile_ios_pilot',
|
|
568
|
+
pilotEnabled: true,
|
|
569
|
+
tickMaxFps: 17,
|
|
570
|
+
edgeGeometryDelayMs: 600,
|
|
571
|
+
edgeStartupWindowMs: 2300,
|
|
572
|
+
edgeStartupSvgCap: 6200,
|
|
573
|
+
edgeStage1TopK: 1300,
|
|
574
|
+
stableHoldTicks: 10,
|
|
575
|
+
stableTimeoutMs: 19000,
|
|
576
|
+
lowAlphaThreshold: 0.1,
|
|
577
|
+
lowAlphaTickMaxFps: 8,
|
|
578
|
+
deltaTickEnabled: true,
|
|
579
|
+
deltaEpsilonPx: 0.95,
|
|
580
|
+
deltaFullSyncEveryTicks: 4,
|
|
581
|
+
deltaLowAlphaEpsilonMultiplier: 1.6,
|
|
582
|
+
deltaLowAlphaFullSyncEveryTicks: 6,
|
|
583
|
+
overlaySafetyTimeoutMs: 36000,
|
|
584
|
+
overlayMinStars: 48,
|
|
585
|
+
overlayMaxStars: 110,
|
|
586
|
+
overlayStarDensity: 3900,
|
|
587
|
+
overlayDprCap: 1.5
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const detectedPlatform = resolveRuntimePlatform(runtimeCaps);
|
|
592
|
+
const override = readStartupPerfProfileOverride();
|
|
593
|
+
if (override === 'off') {
|
|
594
|
+
return {
|
|
595
|
+
...base,
|
|
596
|
+
override: 'off',
|
|
597
|
+
detectedPlatform
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (override && Object.prototype.hasOwnProperty.call(profileCatalog, override)) {
|
|
602
|
+
return {
|
|
603
|
+
...profileCatalog[override],
|
|
604
|
+
override: 'forced',
|
|
605
|
+
detectedPlatform
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (override && override !== 'auto') {
|
|
610
|
+
console.warn(`[Startup Perf] Unknown override profile "${override}", fallback to auto-detected platform profile.`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
switch (detectedPlatform) {
|
|
614
|
+
case 'windows':
|
|
615
|
+
return { ...profileCatalog.desktop_windows_pilot, override: '', detectedPlatform };
|
|
616
|
+
case 'macos':
|
|
617
|
+
return { ...profileCatalog.desktop_macos_pilot, override: '', detectedPlatform };
|
|
618
|
+
case 'linux':
|
|
619
|
+
return { ...profileCatalog.desktop_linux_pilot, override: '', detectedPlatform };
|
|
620
|
+
case 'android':
|
|
621
|
+
return { ...profileCatalog.mobile_android_pilot, override: '', detectedPlatform };
|
|
622
|
+
case 'ios':
|
|
623
|
+
return { ...profileCatalog.mobile_ios_pilot, override: '', detectedPlatform };
|
|
624
|
+
default:
|
|
625
|
+
return { ...base, override: '', detectedPlatform };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const startupRuntimeCaps = (typeof window !== 'undefined' && window.__NC_RUNTIME_CAPS)
|
|
630
|
+
? window.__NC_RUNTIME_CAPS
|
|
631
|
+
: {};
|
|
632
|
+
const startupPerfProfile = resolveStartupPerfProfile(startupRuntimeCaps);
|
|
633
|
+
const startupPerfState = {
|
|
634
|
+
bootTs: nowMs(),
|
|
635
|
+
checkpoints: {},
|
|
636
|
+
t3Seen: false,
|
|
637
|
+
t4Seen: false,
|
|
638
|
+
t5Seen: false,
|
|
639
|
+
edgeDelayLogged: false,
|
|
640
|
+
edgeDelayReleasedLogged: false,
|
|
641
|
+
edgeCapLogged: false,
|
|
642
|
+
edgeCapReleasedLogged: false,
|
|
643
|
+
edgeStage1Logged: false,
|
|
644
|
+
edgeStage1ReleasedLogged: false,
|
|
645
|
+
tickModeSamples: {},
|
|
646
|
+
lastWorkerAlpha: null,
|
|
647
|
+
tickMessagesReceived: 0,
|
|
648
|
+
tickPayloadNodesTotal: 0,
|
|
649
|
+
tickPayloadNodesMax: 0,
|
|
650
|
+
tickFramesApplied: 0,
|
|
651
|
+
tickEmptyDeltaFramesSkipped: 0
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const STARTUP_LAYOUT_SNAPSHOT_DB_NAME = 'noteconnection-startup';
|
|
655
|
+
const STARTUP_LAYOUT_SNAPSHOT_STORE_NAME = 'layoutSnapshots';
|
|
656
|
+
const STARTUP_LAYOUT_SNAPSHOT_LS_PREFIX = 'nc.startupLayoutSnapshot.';
|
|
657
|
+
const STARTUP_LAYOUT_SNAPSHOT_VERSION = 1;
|
|
658
|
+
const STARTUP_LAYOUT_SNAPSHOT_MAX_RECORDS = 6;
|
|
659
|
+
const STARTUP_LAYOUT_SNAPSHOT_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 30;
|
|
660
|
+
|
|
661
|
+
const startupLayoutSnapshotState = {
|
|
662
|
+
fingerprint: '',
|
|
663
|
+
warmRestoreApplied: false,
|
|
664
|
+
pendingRecord: null,
|
|
665
|
+
restorePromise: null,
|
|
666
|
+
saveHandle: null,
|
|
667
|
+
lastSaveAtMs: 0,
|
|
668
|
+
sourceLayoutSummary: null,
|
|
669
|
+
sourceLayoutById: null
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
function hashFNV1a32(input) {
|
|
673
|
+
let hash = 0x811c9dc5;
|
|
674
|
+
const text = String(input || '');
|
|
675
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
676
|
+
hash ^= text.charCodeAt(i);
|
|
677
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
678
|
+
}
|
|
679
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function buildStartupGraphFingerprint(nodeList, linkList, graphPayload) {
|
|
683
|
+
const safeNodes = Array.isArray(nodeList) ? nodeList : [];
|
|
684
|
+
const safeLinks = Array.isArray(linkList) ? linkList : [];
|
|
685
|
+
const graphVersion = graphPayload && graphPayload.version !== undefined
|
|
686
|
+
? String(graphPayload.version)
|
|
687
|
+
: 'v0';
|
|
688
|
+
|
|
689
|
+
const nodeStep = Math.max(1, Math.floor(safeNodes.length / 256));
|
|
690
|
+
const nodeTokens = [];
|
|
691
|
+
for (let index = 0; index < safeNodes.length && nodeTokens.length < 256; index += nodeStep) {
|
|
692
|
+
const item = safeNodes[index];
|
|
693
|
+
nodeTokens.push(item && item.id !== undefined ? String(item.id) : `idx:${index}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const linkStep = Math.max(1, Math.floor(safeLinks.length / 256));
|
|
697
|
+
const linkTokens = [];
|
|
698
|
+
for (let index = 0; index < safeLinks.length && linkTokens.length < 256; index += linkStep) {
|
|
699
|
+
const item = safeLinks[index];
|
|
700
|
+
const sourceId = item && item.source && typeof item.source === 'object' ? item.source.id : item && item.source;
|
|
701
|
+
const targetId = item && item.target && typeof item.target === 'object' ? item.target.id : item && item.target;
|
|
702
|
+
linkTokens.push(`${sourceId}->${targetId}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const signature = [
|
|
706
|
+
`version=${graphVersion}`,
|
|
707
|
+
`nodes=${safeNodes.length}`,
|
|
708
|
+
`links=${safeLinks.length}`,
|
|
709
|
+
`nodeSample=${nodeTokens.join('|')}`,
|
|
710
|
+
`linkSample=${linkTokens.join('|')}`
|
|
711
|
+
].join('||');
|
|
712
|
+
|
|
713
|
+
return `nc-layout-v${STARTUP_LAYOUT_SNAPSHOT_VERSION}-${hashFNV1a32(signature)}`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function summarizeLayoutPositions(positionItems) {
|
|
717
|
+
const safeItems = Array.isArray(positionItems) ? positionItems : [];
|
|
718
|
+
const finitePositions = safeItems.filter((item) => (
|
|
719
|
+
item &&
|
|
720
|
+
Number.isFinite(Number(item.x)) &&
|
|
721
|
+
Number.isFinite(Number(item.y))
|
|
722
|
+
));
|
|
723
|
+
|
|
724
|
+
if (finitePositions.length === 0) {
|
|
725
|
+
return {
|
|
726
|
+
finiteCount: 0,
|
|
727
|
+
minX: 0,
|
|
728
|
+
maxX: 0,
|
|
729
|
+
minY: 0,
|
|
730
|
+
maxY: 0,
|
|
731
|
+
spanX: 0,
|
|
732
|
+
spanY: 0,
|
|
733
|
+
uniqueRatio: 0
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
let minX = Infinity;
|
|
738
|
+
let maxX = -Infinity;
|
|
739
|
+
let minY = Infinity;
|
|
740
|
+
let maxY = -Infinity;
|
|
741
|
+
const uniqueBuckets = new Set();
|
|
742
|
+
|
|
743
|
+
for (let index = 0; index < finitePositions.length; index += 1) {
|
|
744
|
+
const item = finitePositions[index];
|
|
745
|
+
const x = Number(item.x);
|
|
746
|
+
const y = Number(item.y);
|
|
747
|
+
if (x < minX) minX = x;
|
|
748
|
+
if (x > maxX) maxX = x;
|
|
749
|
+
if (y < minY) minY = y;
|
|
750
|
+
if (y > maxY) maxY = y;
|
|
751
|
+
uniqueBuckets.add(`${Math.round(x)}:${Math.round(y)}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
finiteCount: finitePositions.length,
|
|
756
|
+
minX,
|
|
757
|
+
maxX,
|
|
758
|
+
minY,
|
|
759
|
+
maxY,
|
|
760
|
+
spanX: maxX - minX,
|
|
761
|
+
spanY: maxY - minY,
|
|
762
|
+
uniqueRatio: uniqueBuckets.size / Math.max(1, finitePositions.length)
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function isDegenerateLayoutSummary(summary) {
|
|
767
|
+
return Boolean(
|
|
768
|
+
summary &&
|
|
769
|
+
summary.finiteCount > 0 &&
|
|
770
|
+
((summary.spanX < 48 && summary.spanY < 48) || summary.uniqueRatio < 0.12)
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function isSnapshotLayoutCollapsedVsSource(summary, sourceSummary) {
|
|
775
|
+
if (!summary || !sourceSummary || sourceSummary.finiteCount < 10) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const sourceWideX = sourceSummary.spanX >= 320;
|
|
780
|
+
const sourceWideY = sourceSummary.spanY >= 320;
|
|
781
|
+
if (!sourceWideX && !sourceWideY) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const spanXTooSmall = sourceWideX && summary.spanX < Math.max(72, sourceSummary.spanX * 0.08);
|
|
786
|
+
const spanYTooSmall = sourceWideY && summary.spanY < Math.max(72, sourceSummary.spanY * 0.08);
|
|
787
|
+
return spanXTooSmall && spanYTooSmall;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function buildSourceLayoutById(nodeList) {
|
|
791
|
+
const map = new Map();
|
|
792
|
+
const safeNodes = Array.isArray(nodeList) ? nodeList : [];
|
|
793
|
+
for (let index = 0; index < safeNodes.length; index += 1) {
|
|
794
|
+
const node = safeNodes[index];
|
|
795
|
+
if (!node || node.id === undefined || node.id === null) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const x = Number(node.x);
|
|
799
|
+
const y = Number(node.y);
|
|
800
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
map.set(node.id, { x, y });
|
|
804
|
+
}
|
|
805
|
+
return map;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function restoreSourceLayoutOrJitterNodes(nodeList, viewportWidth, viewportHeight, reason = '') {
|
|
809
|
+
const safeNodes = Array.isArray(nodeList) ? nodeList : [];
|
|
810
|
+
if (safeNodes.length === 0) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const currentSummary = summarizeLayoutPositions(safeNodes);
|
|
815
|
+
const sourceSummary = startupLayoutSnapshotState.sourceLayoutSummary;
|
|
816
|
+
const sourceLayoutById = startupLayoutSnapshotState.sourceLayoutById;
|
|
817
|
+
const shouldRestoreSource = Boolean(
|
|
818
|
+
sourceLayoutById instanceof Map &&
|
|
819
|
+
sourceLayoutById.size > 0 &&
|
|
820
|
+
isSnapshotLayoutCollapsedVsSource(currentSummary, sourceSummary)
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
if (!isDegenerateLayoutSummary(currentSummary) && !shouldRestoreSource) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (shouldRestoreSource) {
|
|
828
|
+
let restoredCount = 0;
|
|
829
|
+
for (let index = 0; index < safeNodes.length; index += 1) {
|
|
830
|
+
const node = safeNodes[index];
|
|
831
|
+
const sourcePos = sourceLayoutById.get(node.id);
|
|
832
|
+
if (!sourcePos) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
node.x = sourcePos.x;
|
|
836
|
+
node.y = sourcePos.y;
|
|
837
|
+
node.fx = null;
|
|
838
|
+
node.fy = null;
|
|
839
|
+
if (currentPositions && typeof currentPositions.set === 'function') {
|
|
840
|
+
currentPositions.set(node.id, { x: node.x, y: node.y });
|
|
841
|
+
}
|
|
842
|
+
restoredCount += 1;
|
|
843
|
+
}
|
|
844
|
+
console.warn('[Startup Warm Snapshot] Restored source layout because active positions collapsed relative to source.', {
|
|
845
|
+
reason,
|
|
846
|
+
restoredCount,
|
|
847
|
+
currentSpanX: Number(currentSummary.spanX.toFixed(2)),
|
|
848
|
+
currentSpanY: Number(currentSummary.spanY.toFixed(2)),
|
|
849
|
+
sourceSpanX: sourceSummary ? Number(sourceSummary.spanX.toFixed(2)) : null,
|
|
850
|
+
sourceSpanY: sourceSummary ? Number(sourceSummary.spanY.toFixed(2)) : null
|
|
851
|
+
});
|
|
852
|
+
return restoredCount > 0;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const centerX = Number.isFinite(Number(viewportWidth)) && Number(viewportWidth) > 0
|
|
856
|
+
? Number(viewportWidth) / 2
|
|
857
|
+
: 400;
|
|
858
|
+
const centerY = Number.isFinite(Number(viewportHeight)) && Number(viewportHeight) > 0
|
|
859
|
+
? Number(viewportHeight) / 2
|
|
860
|
+
: 300;
|
|
861
|
+
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
|
|
862
|
+
|
|
863
|
+
for (let index = 0; index < safeNodes.length; index += 1) {
|
|
864
|
+
const node = safeNodes[index];
|
|
865
|
+
const rankOffset = Number.isFinite(Number(node.rank)) ? Number(node.rank) * 4 : 0;
|
|
866
|
+
const radius = 28 + (Math.sqrt(index + 1) * 18) + rankOffset;
|
|
867
|
+
const angle = (index * goldenAngle) + (rankOffset * 0.017);
|
|
868
|
+
node.x = centerX + (Math.cos(angle) * radius);
|
|
869
|
+
node.y = centerY + (Math.sin(angle) * radius);
|
|
870
|
+
node.fx = null;
|
|
871
|
+
node.fy = null;
|
|
872
|
+
if (currentPositions && typeof currentPositions.set === 'function') {
|
|
873
|
+
currentPositions.set(node.id, { x: node.x, y: node.y });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
console.warn('[Startup Warm Snapshot] Re-seeded degenerate initial layout before simulation bootstrap.', {
|
|
878
|
+
reason,
|
|
879
|
+
nodeCount: safeNodes.length,
|
|
880
|
+
spanX: Number(currentSummary.spanX.toFixed(2)),
|
|
881
|
+
spanY: Number(currentSummary.spanY.toFixed(2)),
|
|
882
|
+
uniqueRatio: Number(currentSummary.uniqueRatio.toFixed(4))
|
|
883
|
+
});
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function openStartupLayoutSnapshotDb() {
|
|
888
|
+
return new Promise((resolve, reject) => {
|
|
889
|
+
if (typeof window === 'undefined' || !window.indexedDB) {
|
|
890
|
+
resolve(null);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let request;
|
|
895
|
+
try {
|
|
896
|
+
request = window.indexedDB.open(STARTUP_LAYOUT_SNAPSHOT_DB_NAME, STARTUP_LAYOUT_SNAPSHOT_VERSION);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
reject(error);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
request.onupgradeneeded = () => {
|
|
903
|
+
const db = request.result;
|
|
904
|
+
if (!db.objectStoreNames.contains(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME)) {
|
|
905
|
+
const store = db.createObjectStore(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME, { keyPath: 'fingerprint' });
|
|
906
|
+
store.createIndex('savedAt', 'savedAt');
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
request.onsuccess = () => resolve(request.result);
|
|
911
|
+
request.onerror = () => reject(request.error || new Error('indexedDB open failed'));
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function loadStartupLayoutSnapshotFromLocalStorage(fingerprint) {
|
|
916
|
+
if (!fingerprint || typeof localStorage === 'undefined') {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const key = `${STARTUP_LAYOUT_SNAPSHOT_LS_PREFIX}${fingerprint}`;
|
|
920
|
+
try {
|
|
921
|
+
const raw = localStorage.getItem(key);
|
|
922
|
+
if (!raw) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
const parsed = JSON.parse(raw);
|
|
926
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
927
|
+
} catch (_err) {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function saveStartupLayoutSnapshotToLocalStorage(record) {
|
|
933
|
+
if (!record || !record.fingerprint || typeof localStorage === 'undefined') {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
const key = `${STARTUP_LAYOUT_SNAPSHOT_LS_PREFIX}${record.fingerprint}`;
|
|
938
|
+
localStorage.setItem(key, JSON.stringify(record));
|
|
939
|
+
} catch (_err) {
|
|
940
|
+
// Ignore quota failures.
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function loadStartupLayoutSnapshotRecord(fingerprint) {
|
|
945
|
+
if (!fingerprint) {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const db = await openStartupLayoutSnapshotDb();
|
|
950
|
+
if (!db) {
|
|
951
|
+
return loadStartupLayoutSnapshotFromLocalStorage(fingerprint);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return new Promise((resolve, reject) => {
|
|
955
|
+
const tx = db.transaction(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME, 'readonly');
|
|
956
|
+
const store = tx.objectStore(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME);
|
|
957
|
+
const req = store.get(fingerprint);
|
|
958
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
959
|
+
req.onerror = () => reject(req.error || new Error('indexedDB read failed'));
|
|
960
|
+
tx.oncomplete = () => db.close();
|
|
961
|
+
tx.onerror = () => db.close();
|
|
962
|
+
tx.onabort = () => db.close();
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function persistStartupLayoutSnapshotRecord(record) {
|
|
967
|
+
if (!record || !record.fingerprint) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const db = await openStartupLayoutSnapshotDb();
|
|
972
|
+
if (!db) {
|
|
973
|
+
saveStartupLayoutSnapshotToLocalStorage(record);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
await new Promise((resolve, reject) => {
|
|
978
|
+
const tx = db.transaction(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME, 'readwrite');
|
|
979
|
+
const store = tx.objectStore(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME);
|
|
980
|
+
store.put(record);
|
|
981
|
+
tx.oncomplete = () => resolve();
|
|
982
|
+
tx.onerror = () => reject(tx.error || new Error('indexedDB write failed'));
|
|
983
|
+
tx.onabort = () => reject(tx.error || new Error('indexedDB write aborted'));
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
// Keep snapshot store bounded to avoid unbounded growth.
|
|
987
|
+
await new Promise((resolve, reject) => {
|
|
988
|
+
const tx = db.transaction(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME, 'readwrite');
|
|
989
|
+
const store = tx.objectStore(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME);
|
|
990
|
+
const req = store.getAll();
|
|
991
|
+
req.onsuccess = () => {
|
|
992
|
+
const now = Date.now();
|
|
993
|
+
const allRecords = Array.isArray(req.result) ? req.result : [];
|
|
994
|
+
allRecords.sort((a, b) => Number(b.savedAt || 0) - Number(a.savedAt || 0));
|
|
995
|
+
for (let i = 0; i < allRecords.length; i += 1) {
|
|
996
|
+
const item = allRecords[i];
|
|
997
|
+
const isExpired = Number.isFinite(item.savedAt) && (now - item.savedAt) > STARTUP_LAYOUT_SNAPSHOT_MAX_AGE_MS;
|
|
998
|
+
const overflow = i >= STARTUP_LAYOUT_SNAPSHOT_MAX_RECORDS;
|
|
999
|
+
if (isExpired || overflow) {
|
|
1000
|
+
store.delete(item.fingerprint);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
tx.oncomplete = () => resolve();
|
|
1005
|
+
tx.onerror = () => reject(tx.error || new Error('indexedDB cleanup failed'));
|
|
1006
|
+
tx.onabort = () => reject(tx.error || new Error('indexedDB cleanup aborted'));
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
db.close();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function deleteStartupLayoutSnapshotRecord(fingerprint) {
|
|
1013
|
+
if (!fingerprint) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (typeof localStorage !== 'undefined') {
|
|
1018
|
+
try {
|
|
1019
|
+
localStorage.removeItem(`${STARTUP_LAYOUT_SNAPSHOT_LS_PREFIX}${fingerprint}`);
|
|
1020
|
+
} catch (_err) {
|
|
1021
|
+
// Ignore localStorage cleanup failures.
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const db = await openStartupLayoutSnapshotDb();
|
|
1026
|
+
if (!db) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
await new Promise((resolve, reject) => {
|
|
1031
|
+
const tx = db.transaction(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME, 'readwrite');
|
|
1032
|
+
const store = tx.objectStore(STARTUP_LAYOUT_SNAPSHOT_STORE_NAME);
|
|
1033
|
+
store.delete(fingerprint);
|
|
1034
|
+
tx.oncomplete = () => resolve();
|
|
1035
|
+
tx.onerror = () => reject(tx.error || new Error('indexedDB delete failed'));
|
|
1036
|
+
tx.onabort = () => reject(tx.error || new Error('indexedDB delete aborted'));
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
db.close();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function collectStartupLayoutSnapshotRecord(reason = '') {
|
|
1043
|
+
if (!startupLayoutSnapshotState.fingerprint || !Array.isArray(nodes) || nodes.length === 0) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const positions = new Array(nodes.length);
|
|
1048
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1049
|
+
const n = nodes[index];
|
|
1050
|
+
positions[index] = {
|
|
1051
|
+
id: n.id,
|
|
1052
|
+
x: Number.isFinite(Number(n.x)) ? Number(n.x) : 0,
|
|
1053
|
+
y: Number.isFinite(Number(n.y)) ? Number(n.y) : 0,
|
|
1054
|
+
fx: null,
|
|
1055
|
+
fy: null
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
fingerprint: startupLayoutSnapshotState.fingerprint,
|
|
1061
|
+
savedAt: Date.now(),
|
|
1062
|
+
reason: String(reason || ''),
|
|
1063
|
+
profileId: startupPerfProfile.id,
|
|
1064
|
+
nodeCount: nodes.length,
|
|
1065
|
+
edgeCount: Array.isArray(links) ? links.length : 0,
|
|
1066
|
+
positions
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function validateStartupLayoutSnapshotRecord(record) {
|
|
1071
|
+
if (!record || !Array.isArray(record.positions) || record.positions.length === 0) {
|
|
1072
|
+
return { ok: false, reason: 'empty-positions' };
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (startupLayoutSnapshotState.fingerprint && record.fingerprint && record.fingerprint !== startupLayoutSnapshotState.fingerprint) {
|
|
1076
|
+
return { ok: false, reason: 'fingerprint-mismatch' };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
if (Number.isFinite(record.savedAt) && (now - record.savedAt) > STARTUP_LAYOUT_SNAPSHOT_MAX_AGE_MS) {
|
|
1081
|
+
return { ok: false, reason: 'snapshot-expired' };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const expectedNodeCount = Array.isArray(nodes) ? nodes.length : 0;
|
|
1085
|
+
const expectedEdgeCount = Array.isArray(links) ? links.length : 0;
|
|
1086
|
+
|
|
1087
|
+
if (Number.isFinite(record.nodeCount) && expectedNodeCount > 0 && record.nodeCount !== expectedNodeCount) {
|
|
1088
|
+
return { ok: false, reason: 'node-count-mismatch' };
|
|
1089
|
+
}
|
|
1090
|
+
if (Number.isFinite(record.edgeCount) && expectedEdgeCount > 0 && record.edgeCount !== expectedEdgeCount) {
|
|
1091
|
+
return { ok: false, reason: 'edge-count-mismatch' };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const positionCount = record.positions.length;
|
|
1095
|
+
const coverage = expectedNodeCount > 0 ? (positionCount / expectedNodeCount) : 0;
|
|
1096
|
+
if (expectedNodeCount > 0 && coverage < 0.9) {
|
|
1097
|
+
return { ok: false, reason: 'position-coverage-low', coverage: Number(coverage.toFixed(4)) };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const positionSummary = summarizeLayoutPositions(record.positions);
|
|
1101
|
+
if (expectedNodeCount >= 10 && positionSummary.finiteCount > 0) {
|
|
1102
|
+
if (isDegenerateLayoutSummary(positionSummary)) {
|
|
1103
|
+
return {
|
|
1104
|
+
ok: false,
|
|
1105
|
+
reason: 'degenerate-layout',
|
|
1106
|
+
purge: true,
|
|
1107
|
+
spanX: Number(positionSummary.spanX.toFixed(2)),
|
|
1108
|
+
spanY: Number(positionSummary.spanY.toFixed(2)),
|
|
1109
|
+
uniqueRatio: Number(positionSummary.uniqueRatio.toFixed(4)),
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (isSnapshotLayoutCollapsedVsSource(positionSummary, startupLayoutSnapshotState.sourceLayoutSummary)) {
|
|
1114
|
+
return {
|
|
1115
|
+
ok: false,
|
|
1116
|
+
reason: 'degenerate-layout-vs-source',
|
|
1117
|
+
purge: true,
|
|
1118
|
+
spanX: Number(positionSummary.spanX.toFixed(2)),
|
|
1119
|
+
spanY: Number(positionSummary.spanY.toFixed(2)),
|
|
1120
|
+
uniqueRatio: Number(positionSummary.uniqueRatio.toFixed(4)),
|
|
1121
|
+
sourceSpanX: startupLayoutSnapshotState.sourceLayoutSummary
|
|
1122
|
+
? Number(startupLayoutSnapshotState.sourceLayoutSummary.spanX.toFixed(2))
|
|
1123
|
+
: null,
|
|
1124
|
+
sourceSpanY: startupLayoutSnapshotState.sourceLayoutSummary
|
|
1125
|
+
? Number(startupLayoutSnapshotState.sourceLayoutSummary.spanY.toFixed(2))
|
|
1126
|
+
: null
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
ok: true,
|
|
1133
|
+
coverage: Number(coverage.toFixed(4)),
|
|
1134
|
+
positionCount,
|
|
1135
|
+
expectedNodeCount,
|
|
1136
|
+
expectedEdgeCount
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function applyStartupLayoutSnapshotRecord(record) {
|
|
1141
|
+
if (!record || !Array.isArray(record.positions) || record.positions.length === 0 || !nodeMap) {
|
|
1142
|
+
return { appliedCount: 0, total: 0 };
|
|
1143
|
+
}
|
|
1144
|
+
const layoutById = new Map(record.positions.map((item) => [item.id, item]));
|
|
1145
|
+
let appliedCount = 0;
|
|
1146
|
+
|
|
1147
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1148
|
+
const n = nodes[index];
|
|
1149
|
+
const saved = layoutById.get(n.id);
|
|
1150
|
+
if (!saved) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
n.x = Number.isFinite(saved.x) ? saved.x : n.x;
|
|
1154
|
+
n.y = Number.isFinite(saved.y) ? saved.y : n.y;
|
|
1155
|
+
n.fx = null;
|
|
1156
|
+
n.fy = null;
|
|
1157
|
+
if (currentPositions && typeof currentPositions.set === 'function') {
|
|
1158
|
+
currentPositions.set(n.id, { x: n.x, y: n.y });
|
|
1159
|
+
}
|
|
1160
|
+
appliedCount += 1;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return { appliedCount, total: nodes.length };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function maybeApplyStartupWarmSnapshot(trigger = '') {
|
|
1167
|
+
if (startupLayoutSnapshotState.warmRestoreApplied) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
const record = startupLayoutSnapshotState.pendingRecord;
|
|
1171
|
+
if (!record || !simulationWorker) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const validation = validateStartupLayoutSnapshotRecord(record);
|
|
1176
|
+
if (!validation.ok) {
|
|
1177
|
+
console.warn('[Startup Warm Snapshot] Skip applying invalid snapshot record.', {
|
|
1178
|
+
trigger,
|
|
1179
|
+
reason: validation.reason,
|
|
1180
|
+
fingerprint: startupLayoutSnapshotState.fingerprint,
|
|
1181
|
+
recordFingerprint: record.fingerprint || null,
|
|
1182
|
+
recordNodeCount: record.nodeCount || 0,
|
|
1183
|
+
recordEdgeCount: record.edgeCount || 0,
|
|
1184
|
+
positionCount: Array.isArray(record.positions) ? record.positions.length : 0,
|
|
1185
|
+
spanX: validation.spanX,
|
|
1186
|
+
spanY: validation.spanY,
|
|
1187
|
+
uniqueRatio: validation.uniqueRatio,
|
|
1188
|
+
});
|
|
1189
|
+
if (validation.purge === true && record.fingerprint) {
|
|
1190
|
+
deleteStartupLayoutSnapshotRecord(record.fingerprint).catch((error) => {
|
|
1191
|
+
console.warn('[Startup Warm Snapshot] Failed to purge invalid snapshot record:', error && error.message ? error.message : String(error));
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
startupLayoutSnapshotState.pendingRecord = null;
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const result = applyStartupLayoutSnapshotRecord(record);
|
|
1199
|
+
if (result.appliedCount <= 0) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
startupLayoutSnapshotState.warmRestoreApplied = true;
|
|
1203
|
+
|
|
1204
|
+
console.log('[Startup Warm Snapshot] Applied persisted layout snapshot.', {
|
|
1205
|
+
trigger,
|
|
1206
|
+
fingerprint: startupLayoutSnapshotState.fingerprint,
|
|
1207
|
+
appliedCount: result.appliedCount,
|
|
1208
|
+
totalNodes: result.total,
|
|
1209
|
+
savedAt: record.savedAt || null,
|
|
1210
|
+
coverage: validation.coverage
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
if (typeof ticked === 'function') {
|
|
1214
|
+
ticked();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const syncNodes = nodes.map((n) => ({
|
|
1218
|
+
id: n.id,
|
|
1219
|
+
x: Number.isFinite(Number(n.x)) ? Number(n.x) : 0,
|
|
1220
|
+
y: Number.isFinite(Number(n.y)) ? Number(n.y) : 0,
|
|
1221
|
+
fx: Number.isFinite(Number(n.fx)) ? Number(n.fx) : null,
|
|
1222
|
+
fy: Number.isFinite(Number(n.fy)) ? Number(n.fy) : null,
|
|
1223
|
+
rank: n.rank
|
|
1224
|
+
}));
|
|
1225
|
+
const syncLinks = physicsLinks.map((l) => ({
|
|
1226
|
+
source: l.source && typeof l.source === 'object' ? l.source.id : l.source,
|
|
1227
|
+
target: l.target && typeof l.target === 'object' ? l.target.id : l.target
|
|
1228
|
+
}));
|
|
1229
|
+
simulationWorker.postMessage({
|
|
1230
|
+
type: 'setNodes',
|
|
1231
|
+
payload: {
|
|
1232
|
+
nodes: syncNodes,
|
|
1233
|
+
links: syncLinks,
|
|
1234
|
+
restart: true
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
simulationWorker.postMessage({
|
|
1238
|
+
type: 'updateParams',
|
|
1239
|
+
payload: {
|
|
1240
|
+
alpha: 0.18,
|
|
1241
|
+
velocityDecay: 0.92,
|
|
1242
|
+
restart: true
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function scheduleStartupLayoutSnapshotPersist(reason = '', delayMs = 800) {
|
|
1250
|
+
if (startupPerfProfile.pilotEnabled !== true) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (!startupLayoutSnapshotState.fingerprint) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (startupLayoutSnapshotState.saveHandle !== null) {
|
|
1258
|
+
clearTimeout(startupLayoutSnapshotState.saveHandle);
|
|
1259
|
+
startupLayoutSnapshotState.saveHandle = null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
startupLayoutSnapshotState.saveHandle = setTimeout(async () => {
|
|
1263
|
+
startupLayoutSnapshotState.saveHandle = null;
|
|
1264
|
+
const record = collectStartupLayoutSnapshotRecord(reason);
|
|
1265
|
+
if (!record) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
await persistStartupLayoutSnapshotRecord(record);
|
|
1270
|
+
startupLayoutSnapshotState.lastSaveAtMs = record.savedAt;
|
|
1271
|
+
console.log('[Startup Warm Snapshot] Persisted layout snapshot.', {
|
|
1272
|
+
fingerprint: record.fingerprint,
|
|
1273
|
+
reason: record.reason,
|
|
1274
|
+
nodeCount: record.nodeCount
|
|
1275
|
+
});
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
console.warn('[Startup Warm Snapshot] Persist failed:', error && error.message ? error.message : String(error));
|
|
1278
|
+
}
|
|
1279
|
+
}, Math.max(0, Math.floor(delayMs)));
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
let startupWorldOverlayState = null;
|
|
1283
|
+
|
|
1284
|
+
function createStartupWorldOverlay() {
|
|
1285
|
+
if (startupPerfProfile.startupOverlayEnabled === false) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const wrapper = document.getElementById('graph-wrapper');
|
|
1290
|
+
if (!wrapper) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const existing = document.getElementById('startup-world-overlay');
|
|
1295
|
+
if (existing) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const overlay = document.createElement('div');
|
|
1300
|
+
overlay.id = 'startup-world-overlay';
|
|
1301
|
+
overlay.className = 'startup-world-overlay';
|
|
1302
|
+
overlay.innerHTML = `
|
|
1303
|
+
<div class="startup-world-card" role="status" aria-live="polite">
|
|
1304
|
+
<div class="startup-world-title">等待世界构建</div>
|
|
1305
|
+
<div class="startup-world-subtitle">星辰正在连接知识节点...</div>
|
|
1306
|
+
<canvas class="startup-world-canvas" aria-label="Startup Starfield"></canvas>
|
|
1307
|
+
<div class="startup-world-hint">点击星辰可点暗</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
`;
|
|
1310
|
+
wrapper.appendChild(overlay);
|
|
1311
|
+
|
|
1312
|
+
const card = overlay.querySelector('.startup-world-card');
|
|
1313
|
+
const canvas = overlay.querySelector('.startup-world-canvas');
|
|
1314
|
+
if (!card || !canvas) {
|
|
1315
|
+
overlay.remove();
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const ctx = canvas.getContext('2d');
|
|
1320
|
+
if (!ctx) {
|
|
1321
|
+
overlay.remove();
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const state = {
|
|
1326
|
+
overlay,
|
|
1327
|
+
card,
|
|
1328
|
+
canvas,
|
|
1329
|
+
ctx,
|
|
1330
|
+
stars: [],
|
|
1331
|
+
running: true,
|
|
1332
|
+
frameHandle: null,
|
|
1333
|
+
resizeObserver: null,
|
|
1334
|
+
resizeHandler: null,
|
|
1335
|
+
width: 0,
|
|
1336
|
+
height: 0,
|
|
1337
|
+
dpr: 1,
|
|
1338
|
+
leaveHandle: null,
|
|
1339
|
+
fallbackHandle: null,
|
|
1340
|
+
hidden: false,
|
|
1341
|
+
reducedMotion: false
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
if (typeof window.matchMedia === 'function') {
|
|
1345
|
+
try {
|
|
1346
|
+
state.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
1347
|
+
} catch (_err) {
|
|
1348
|
+
state.reducedMotion = false;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function buildStars() {
|
|
1353
|
+
const minStars = Number.isFinite(startupPerfProfile.overlayMinStars)
|
|
1354
|
+
? Math.max(20, Math.floor(startupPerfProfile.overlayMinStars))
|
|
1355
|
+
: 70;
|
|
1356
|
+
const maxStars = Number.isFinite(startupPerfProfile.overlayMaxStars)
|
|
1357
|
+
? Math.max(minStars, Math.floor(startupPerfProfile.overlayMaxStars))
|
|
1358
|
+
: 180;
|
|
1359
|
+
const densityDivisor = Number.isFinite(startupPerfProfile.overlayStarDensity)
|
|
1360
|
+
? Math.max(800, Math.floor(startupPerfProfile.overlayStarDensity))
|
|
1361
|
+
: 2400;
|
|
1362
|
+
let starCount = Math.min(maxStars, Math.max(minStars, Math.round((state.width * state.height) / densityDivisor)));
|
|
1363
|
+
if (state.reducedMotion) {
|
|
1364
|
+
starCount = Math.max(24, Math.round(starCount * 0.7));
|
|
1365
|
+
}
|
|
1366
|
+
state.stars = new Array(starCount);
|
|
1367
|
+
const twinkleScale = state.reducedMotion ? 0.45 : 1;
|
|
1368
|
+
for (let index = 0; index < starCount; index += 1) {
|
|
1369
|
+
state.stars[index] = {
|
|
1370
|
+
x: Math.random() * state.width,
|
|
1371
|
+
y: Math.random() * state.height,
|
|
1372
|
+
radius: 0.7 + Math.random() * 2.4,
|
|
1373
|
+
hue: 195 + Math.random() * 35,
|
|
1374
|
+
lightness: 75 + Math.random() * 20,
|
|
1375
|
+
baseAlpha: 0.3 + Math.random() * 0.55,
|
|
1376
|
+
twinkleSpeed: (0.8 + Math.random() * 2.4) * twinkleScale,
|
|
1377
|
+
phase: Math.random() * Math.PI * 2,
|
|
1378
|
+
dimTarget: 0,
|
|
1379
|
+
dimValue: 0
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function resizeCanvas() {
|
|
1385
|
+
const rect = state.canvas.getBoundingClientRect();
|
|
1386
|
+
state.width = Math.max(1, Math.floor(rect.width));
|
|
1387
|
+
state.height = Math.max(1, Math.floor(rect.height));
|
|
1388
|
+
const dprCap = Number.isFinite(startupPerfProfile.overlayDprCap)
|
|
1389
|
+
? Math.max(1, startupPerfProfile.overlayDprCap)
|
|
1390
|
+
: 2;
|
|
1391
|
+
state.dpr = Math.min(dprCap, Math.max(1, window.devicePixelRatio || 1));
|
|
1392
|
+
state.canvas.width = Math.floor(state.width * state.dpr);
|
|
1393
|
+
state.canvas.height = Math.floor(state.height * state.dpr);
|
|
1394
|
+
state.ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
|
|
1395
|
+
buildStars();
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function drawFrame(frameTimeMs) {
|
|
1399
|
+
if (!state.running) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const t = frameTimeMs * 0.001;
|
|
1404
|
+
const renderCtx = state.ctx;
|
|
1405
|
+
renderCtx.clearRect(0, 0, state.width, state.height);
|
|
1406
|
+
|
|
1407
|
+
const nebulaA = renderCtx.createRadialGradient(
|
|
1408
|
+
state.width * 0.28,
|
|
1409
|
+
state.height * 0.28,
|
|
1410
|
+
8,
|
|
1411
|
+
state.width * 0.28,
|
|
1412
|
+
state.height * 0.28,
|
|
1413
|
+
state.width * 0.85
|
|
1414
|
+
);
|
|
1415
|
+
nebulaA.addColorStop(0, 'rgba(108, 149, 255, 0.22)');
|
|
1416
|
+
nebulaA.addColorStop(1, 'rgba(108, 149, 255, 0)');
|
|
1417
|
+
renderCtx.fillStyle = nebulaA;
|
|
1418
|
+
renderCtx.fillRect(0, 0, state.width, state.height);
|
|
1419
|
+
|
|
1420
|
+
const nebulaB = renderCtx.createRadialGradient(
|
|
1421
|
+
state.width * 0.78,
|
|
1422
|
+
state.height * 0.72,
|
|
1423
|
+
12,
|
|
1424
|
+
state.width * 0.78,
|
|
1425
|
+
state.height * 0.72,
|
|
1426
|
+
state.width * 0.7
|
|
1427
|
+
);
|
|
1428
|
+
nebulaB.addColorStop(0, 'rgba(165, 126, 255, 0.18)');
|
|
1429
|
+
nebulaB.addColorStop(1, 'rgba(165, 126, 255, 0)');
|
|
1430
|
+
renderCtx.fillStyle = nebulaB;
|
|
1431
|
+
renderCtx.fillRect(0, 0, state.width, state.height);
|
|
1432
|
+
|
|
1433
|
+
for (let index = 0; index < state.stars.length; index += 1) {
|
|
1434
|
+
const star = state.stars[index];
|
|
1435
|
+
star.dimValue += (star.dimTarget - star.dimValue) * 0.12;
|
|
1436
|
+
|
|
1437
|
+
const twinkle = state.reducedMotion
|
|
1438
|
+
? 0.82
|
|
1439
|
+
: (0.55 + 0.45 * (0.5 + 0.5 * Math.sin(t * star.twinkleSpeed + star.phase)));
|
|
1440
|
+
const alpha = Math.max(0.02, star.baseAlpha * twinkle * (1 - 0.9 * star.dimValue));
|
|
1441
|
+
const radius = star.radius * (0.9 + twinkle * 0.3);
|
|
1442
|
+
|
|
1443
|
+
renderCtx.beginPath();
|
|
1444
|
+
renderCtx.arc(star.x, star.y, radius, 0, Math.PI * 2);
|
|
1445
|
+
renderCtx.fillStyle = `hsla(${star.hue}, 92%, ${star.lightness}%, ${alpha})`;
|
|
1446
|
+
renderCtx.shadowBlur = state.reducedMotion ? (5 + star.radius * 3) : (9 + star.radius * 6);
|
|
1447
|
+
renderCtx.shadowColor = `hsla(${star.hue}, 95%, ${Math.min(98, star.lightness + 5)}%, ${Math.min(0.85, alpha + 0.12)})`;
|
|
1448
|
+
renderCtx.fill();
|
|
1449
|
+
renderCtx.shadowBlur = 0;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
state.frameHandle = window.requestAnimationFrame(drawFrame);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function dimNearestStar(event) {
|
|
1456
|
+
const rect = state.canvas.getBoundingClientRect();
|
|
1457
|
+
const x = event.clientX - rect.left;
|
|
1458
|
+
const y = event.clientY - rect.top;
|
|
1459
|
+
|
|
1460
|
+
let nearestStar = null;
|
|
1461
|
+
let nearestDistSq = Infinity;
|
|
1462
|
+
for (let index = 0; index < state.stars.length; index += 1) {
|
|
1463
|
+
const star = state.stars[index];
|
|
1464
|
+
const dx = star.x - x;
|
|
1465
|
+
const dy = star.y - y;
|
|
1466
|
+
const distSq = dx * dx + dy * dy;
|
|
1467
|
+
const hitRadius = Math.max(11, star.radius * 5);
|
|
1468
|
+
if (distSq <= hitRadius * hitRadius && distSq < nearestDistSq) {
|
|
1469
|
+
nearestDistSq = distSq;
|
|
1470
|
+
nearestStar = star;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (nearestStar) {
|
|
1475
|
+
nearestStar.dimTarget = 1;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
state.canvas.addEventListener('pointerdown', dimNearestStar);
|
|
1480
|
+
|
|
1481
|
+
state.resizeHandler = resizeCanvas;
|
|
1482
|
+
if (typeof ResizeObserver === 'function') {
|
|
1483
|
+
state.resizeObserver = new ResizeObserver(() => resizeCanvas());
|
|
1484
|
+
state.resizeObserver.observe(state.card);
|
|
1485
|
+
} else {
|
|
1486
|
+
window.addEventListener('resize', state.resizeHandler);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
resizeCanvas();
|
|
1490
|
+
state.frameHandle = window.requestAnimationFrame(drawFrame);
|
|
1491
|
+
|
|
1492
|
+
return state;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function hideStartupWorldOverlay(reason = '') {
|
|
1496
|
+
if (!startupWorldOverlayState || startupWorldOverlayState.hidden) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const state = startupWorldOverlayState;
|
|
1501
|
+
state.hidden = true;
|
|
1502
|
+
state.running = false;
|
|
1503
|
+
|
|
1504
|
+
if (state.frameHandle !== null) {
|
|
1505
|
+
window.cancelAnimationFrame(state.frameHandle);
|
|
1506
|
+
state.frameHandle = null;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (state.resizeObserver) {
|
|
1510
|
+
state.resizeObserver.disconnect();
|
|
1511
|
+
state.resizeObserver = null;
|
|
1512
|
+
} else if (state.resizeHandler) {
|
|
1513
|
+
window.removeEventListener('resize', state.resizeHandler);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (state.fallbackHandle !== null) {
|
|
1517
|
+
window.clearTimeout(state.fallbackHandle);
|
|
1518
|
+
state.fallbackHandle = null;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
state.overlay.classList.add('is-leaving');
|
|
1522
|
+
if (reason) {
|
|
1523
|
+
console.log(`[Startup Overlay] Closing overlay: ${reason}`);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
state.leaveHandle = window.setTimeout(() => {
|
|
1527
|
+
if (state.overlay && state.overlay.parentNode) {
|
|
1528
|
+
state.overlay.parentNode.removeChild(state.overlay);
|
|
1529
|
+
}
|
|
1530
|
+
startupWorldOverlayState = null;
|
|
1531
|
+
}, 620);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function markStartupCheckpoint(label, details = null) {
|
|
1535
|
+
if (Object.prototype.hasOwnProperty.call(startupPerfState.checkpoints, label)) {
|
|
1536
|
+
return startupPerfState.checkpoints[label];
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const at = nowMs();
|
|
1540
|
+
const elapsedMs = at - startupPerfState.bootTs;
|
|
1541
|
+
startupPerfState.checkpoints[label] = at;
|
|
1542
|
+
|
|
1543
|
+
if (details && typeof details === 'object' && Object.keys(details).length > 0) {
|
|
1544
|
+
console.log(`[Startup Perf] ${label} +${elapsedMs.toFixed(2)}ms`, details);
|
|
1545
|
+
} else {
|
|
1546
|
+
console.log(`[Startup Perf] ${label} +${elapsedMs.toFixed(2)}ms`);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (typeof label === 'string' && label.startsWith('T5 ')) {
|
|
1550
|
+
hideStartupWorldOverlay('initial-layout-complete');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
return at;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
markStartupCheckpoint('T0 app_boot', {
|
|
1557
|
+
profile: startupPerfProfile.id,
|
|
1558
|
+
pilotEnabled: startupPerfProfile.pilotEnabled,
|
|
1559
|
+
platform: startupPerfProfile.detectedPlatform || 'unknown',
|
|
1560
|
+
runtimePlatformRaw: startupRuntimeCaps && startupRuntimeCaps.platform ? startupRuntimeCaps.platform : 'unknown',
|
|
1561
|
+
override: startupPerfProfile.override || 'none'
|
|
1562
|
+
});
|
|
1563
|
+
|
|
11
1564
|
// State for Cluster Filtering
|
|
12
1565
|
let activeClusterFilter = localStorage.getItem('activeClusterFilter') || 'all';
|
|
13
1566
|
// Clear it immediately so it doesn't persist unwantedly on manual refreshes?
|
|
@@ -62,6 +1615,31 @@ const runtimeGraphData =
|
|
|
62
1615
|
const graphDataExists = runtimeGraphData !== null;
|
|
63
1616
|
const sourceNodes = (graphDataExists && Array.isArray(runtimeGraphData.nodes)) ? runtimeGraphData.nodes : [];
|
|
64
1617
|
const sourceLinks = (graphDataExists && Array.isArray(runtimeGraphData.edges)) ? runtimeGraphData.edges : [];
|
|
1618
|
+
|
|
1619
|
+
if (graphDataExists && sourceNodes.length > 0) {
|
|
1620
|
+
startupWorldOverlayState = createStartupWorldOverlay();
|
|
1621
|
+
if (startupWorldOverlayState) {
|
|
1622
|
+
const overlaySafetyTimeoutMs = Number.isFinite(startupPerfProfile.overlaySafetyTimeoutMs)
|
|
1623
|
+
? Math.max(8000, Math.floor(startupPerfProfile.overlaySafetyTimeoutMs))
|
|
1624
|
+
: 30000;
|
|
1625
|
+
console.log('[Startup Overlay] Activated.', {
|
|
1626
|
+
profile: startupPerfProfile.id,
|
|
1627
|
+
platform: startupPerfProfile.detectedPlatform || 'unknown',
|
|
1628
|
+
overlaySafetyTimeoutMs
|
|
1629
|
+
});
|
|
1630
|
+
startupWorldOverlayState.fallbackHandle = window.setTimeout(() => {
|
|
1631
|
+
hideStartupWorldOverlay('safety-timeout');
|
|
1632
|
+
}, overlaySafetyTimeoutMs);
|
|
1633
|
+
} else {
|
|
1634
|
+
console.log('[Startup Overlay] Skipped by profile/runtime policy.', {
|
|
1635
|
+
profile: startupPerfProfile.id,
|
|
1636
|
+
platform: startupPerfProfile.detectedPlatform || 'unknown'
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
} else {
|
|
1640
|
+
console.log('[Startup Overlay] Skipped: no preloaded graph payload detected.');
|
|
1641
|
+
}
|
|
1642
|
+
|
|
65
1643
|
const graphPreprocessStartTs = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
66
1644
|
? performance.now()
|
|
67
1645
|
: Date.now();
|
|
@@ -131,6 +1709,50 @@ const graphPreprocessElapsedMs = ((typeof performance !== 'undefined' && typeof
|
|
|
131
1709
|
? performance.now()
|
|
132
1710
|
: Date.now()) - graphPreprocessStartTs;
|
|
133
1711
|
console.log(`[Init Perf] Graph preprocessing completed in ${graphPreprocessElapsedMs.toFixed(2)}ms (nodes=${nodes.length}, links=${links.length}).`);
|
|
1712
|
+
markStartupCheckpoint('T1 graph_preprocessed', {
|
|
1713
|
+
nodes: nodes.length,
|
|
1714
|
+
links: links.length,
|
|
1715
|
+
preprocessMs: Number(graphPreprocessElapsedMs.toFixed(2))
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
startupLayoutSnapshotState.sourceLayoutById = buildSourceLayoutById(nodes);
|
|
1719
|
+
startupLayoutSnapshotState.sourceLayoutSummary = summarizeLayoutPositions(nodes);
|
|
1720
|
+
if (startupLayoutSnapshotState.sourceLayoutSummary.finiteCount > 0) {
|
|
1721
|
+
console.log('[Startup Warm Snapshot] Source layout summary prepared.', {
|
|
1722
|
+
finiteCount: startupLayoutSnapshotState.sourceLayoutSummary.finiteCount,
|
|
1723
|
+
spanX: Number(startupLayoutSnapshotState.sourceLayoutSummary.spanX.toFixed(2)),
|
|
1724
|
+
spanY: Number(startupLayoutSnapshotState.sourceLayoutSummary.spanY.toFixed(2)),
|
|
1725
|
+
uniqueRatio: Number(startupLayoutSnapshotState.sourceLayoutSummary.uniqueRatio.toFixed(4))
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
startupLayoutSnapshotState.fingerprint = buildStartupGraphFingerprint(nodes, links, runtimeGraphData);
|
|
1730
|
+
console.log('[Startup Warm Snapshot] Fingerprint prepared.', {
|
|
1731
|
+
fingerprint: startupLayoutSnapshotState.fingerprint,
|
|
1732
|
+
nodes: nodes.length,
|
|
1733
|
+
links: links.length
|
|
1734
|
+
});
|
|
1735
|
+
startupLayoutSnapshotState.restorePromise = loadStartupLayoutSnapshotRecord(startupLayoutSnapshotState.fingerprint)
|
|
1736
|
+
.then((record) => {
|
|
1737
|
+
startupLayoutSnapshotState.pendingRecord = record;
|
|
1738
|
+
if (!record) {
|
|
1739
|
+
console.log('[Startup Warm Snapshot] No persisted snapshot matched current fingerprint.');
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (!Array.isArray(record.positions) || record.positions.length === 0) {
|
|
1743
|
+
console.log('[Startup Warm Snapshot] Matched snapshot is empty. Skip applying.');
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
console.log('[Startup Warm Snapshot] Snapshot matched and ready for apply.', {
|
|
1747
|
+
fingerprint: startupLayoutSnapshotState.fingerprint,
|
|
1748
|
+
savedAt: record.savedAt || null,
|
|
1749
|
+
positionCount: record.positions.length
|
|
1750
|
+
});
|
|
1751
|
+
maybeApplyStartupWarmSnapshot('restore-ready');
|
|
1752
|
+
})
|
|
1753
|
+
.catch((error) => {
|
|
1754
|
+
console.warn('[Startup Warm Snapshot] Restore load failed:', error && error.message ? error.message : String(error));
|
|
1755
|
+
});
|
|
134
1756
|
|
|
135
1757
|
// Optimization: Default to Canvas for large graphs (>3000 nodes) to save memory
|
|
136
1758
|
if (nodes.length > 3000) {
|
|
@@ -262,25 +1884,187 @@ const simulationWorker = new Worker("simulationWorker.js");
|
|
|
262
1884
|
|
|
263
1885
|
// Position buffer for rendering
|
|
264
1886
|
let currentPositions = new Map();
|
|
1887
|
+
const startupTickApplyQueue = {
|
|
1888
|
+
frameHandle: null,
|
|
1889
|
+
pendingById: new Map(),
|
|
1890
|
+
pendingTickMode: 'delta',
|
|
1891
|
+
pendingStable: false
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
function buildStartupTickModeSummary() {
|
|
1895
|
+
const fullCount = Number(startupPerfState.tickModeSamples.full || 0);
|
|
1896
|
+
const deltaCount = Number(startupPerfState.tickModeSamples.delta || 0);
|
|
1897
|
+
const totalTickCount = fullCount + deltaCount;
|
|
1898
|
+
return {
|
|
1899
|
+
tickMessages: startupPerfState.tickMessagesReceived,
|
|
1900
|
+
tickFramesApplied: startupPerfState.tickFramesApplied,
|
|
1901
|
+
fullTicks: fullCount,
|
|
1902
|
+
deltaTicks: deltaCount,
|
|
1903
|
+
deltaRatio: totalTickCount > 0 ? Number((deltaCount / totalTickCount).toFixed(4)) : 0,
|
|
1904
|
+
avgPayloadNodes: startupPerfState.tickMessagesReceived > 0
|
|
1905
|
+
? Number((startupPerfState.tickPayloadNodesTotal / startupPerfState.tickMessagesReceived).toFixed(2))
|
|
1906
|
+
: 0,
|
|
1907
|
+
maxPayloadNodes: startupPerfState.tickPayloadNodesMax,
|
|
1908
|
+
skippedEmptyDeltaFrames: startupPerfState.tickEmptyDeltaFramesSkipped
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function flushStartupTickApplyQueue() {
|
|
1913
|
+
startupTickApplyQueue.frameHandle = null;
|
|
1914
|
+
|
|
1915
|
+
if (focusNode) {
|
|
1916
|
+
startupTickApplyQueue.pendingById.clear();
|
|
1917
|
+
startupTickApplyQueue.pendingTickMode = 'delta';
|
|
1918
|
+
startupTickApplyQueue.pendingStable = false;
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
let appliedCount = 0;
|
|
1923
|
+
startupTickApplyQueue.pendingById.forEach((payloadNode, nodeId) => {
|
|
1924
|
+
if (!payloadNode) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const payloadIndex = Number(payloadNode.i);
|
|
1929
|
+
const hasValidIndex = Number.isInteger(payloadIndex) && payloadIndex >= 0 && payloadIndex < nodes.length;
|
|
1930
|
+
let originalNode = hasValidIndex ? nodes[payloadIndex] : null;
|
|
1931
|
+
if (!originalNode || originalNode.id !== nodeId) {
|
|
1932
|
+
originalNode = nodeMap.get(nodeId);
|
|
1933
|
+
}
|
|
1934
|
+
if (!originalNode) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const nextX = Number.isFinite(Number(payloadNode.x)) ? Number(payloadNode.x) : originalNode.x;
|
|
1939
|
+
const nextY = Number.isFinite(Number(payloadNode.y)) ? Number(payloadNode.y) : originalNode.y;
|
|
1940
|
+
originalNode.x = nextX;
|
|
1941
|
+
originalNode.y = nextY;
|
|
1942
|
+
currentPositions.set(nodeId, { x: nextX, y: nextY });
|
|
1943
|
+
appliedCount += 1;
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
const shouldRender = appliedCount > 0 || startupTickApplyQueue.pendingTickMode !== 'delta';
|
|
1947
|
+
if (shouldRender) {
|
|
1948
|
+
ticked();
|
|
1949
|
+
startupPerfState.tickFramesApplied += 1;
|
|
1950
|
+
} else {
|
|
1951
|
+
startupPerfState.tickEmptyDeltaFramesSkipped += 1;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
if (!startupPerfState.t4Seen && shouldRender) {
|
|
1955
|
+
startupPerfState.t4Seen = true;
|
|
1956
|
+
const rendererModeInput = document.querySelector('input[name="rendererMode"]:checked');
|
|
1957
|
+
const layoutModeInput = document.querySelector('input[name="layoutMode"]:checked');
|
|
1958
|
+
markStartupCheckpoint('T4 first_interactive_render', {
|
|
1959
|
+
renderer: rendererModeInput ? rendererModeInput.value : 'unknown',
|
|
1960
|
+
layout: layoutModeInput ? layoutModeInput.value : 'unknown'
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (startupTickApplyQueue.pendingStable === true && !startupPerfState.t5Seen) {
|
|
1965
|
+
startupPerfState.t5Seen = true;
|
|
1966
|
+
markStartupCheckpoint('T5 stable_layout', {
|
|
1967
|
+
alpha: Number.isFinite(startupPerfState.lastWorkerAlpha) ? Number(startupPerfState.lastWorkerAlpha.toFixed(4)) : null,
|
|
1968
|
+
source: 'worker_tick',
|
|
1969
|
+
tickSummary: buildStartupTickModeSummary()
|
|
1970
|
+
});
|
|
1971
|
+
scheduleStartupLayoutSnapshotPersist('startup-stable-worker-tick');
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
startupTickApplyQueue.pendingById.clear();
|
|
1975
|
+
startupTickApplyQueue.pendingTickMode = 'delta';
|
|
1976
|
+
startupTickApplyQueue.pendingStable = false;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function scheduleStartupTickApplyFlush() {
|
|
1980
|
+
if (startupTickApplyQueue.frameHandle !== null) {
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
|
1985
|
+
startupTickApplyQueue.frameHandle = window.requestAnimationFrame(() => {
|
|
1986
|
+
flushStartupTickApplyQueue();
|
|
1987
|
+
});
|
|
1988
|
+
} else {
|
|
1989
|
+
startupTickApplyQueue.frameHandle = setTimeout(() => {
|
|
1990
|
+
flushStartupTickApplyQueue();
|
|
1991
|
+
}, 16);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function queueStartupTickForApply(workerNodes, resolvedTickMode, isStartupStable) {
|
|
1996
|
+
if (resolvedTickMode === 'full') {
|
|
1997
|
+
startupTickApplyQueue.pendingById.clear();
|
|
1998
|
+
startupTickApplyQueue.pendingTickMode = 'full';
|
|
1999
|
+
} else if (startupTickApplyQueue.pendingTickMode !== 'full') {
|
|
2000
|
+
startupTickApplyQueue.pendingTickMode = 'delta';
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
for (let index = 0; index < workerNodes.length; index += 1) {
|
|
2004
|
+
const n = workerNodes[index];
|
|
2005
|
+
if (!n || n.id === undefined || n.id === null) {
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
startupTickApplyQueue.pendingById.set(n.id, n);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (isStartupStable === true) {
|
|
2012
|
+
startupTickApplyQueue.pendingStable = true;
|
|
2013
|
+
}
|
|
2014
|
+
scheduleStartupTickApplyFlush();
|
|
2015
|
+
}
|
|
265
2016
|
|
|
266
2017
|
simulationWorker.onmessage = function(event) {
|
|
267
|
-
const {
|
|
2018
|
+
const {
|
|
2019
|
+
type,
|
|
2020
|
+
nodes: workerNodes,
|
|
2021
|
+
alpha: workerAlpha,
|
|
2022
|
+
isStartupStable,
|
|
2023
|
+
isDelta,
|
|
2024
|
+
tickMode
|
|
2025
|
+
} = event.data;
|
|
268
2026
|
if (type === 'tick') {
|
|
2027
|
+
const safeWorkerNodes = Array.isArray(workerNodes) ? workerNodes : [];
|
|
2028
|
+
const resolvedTickMode = (typeof tickMode === 'string' && tickMode.length > 0)
|
|
2029
|
+
? tickMode
|
|
2030
|
+
: (isDelta === true ? 'delta' : 'full');
|
|
2031
|
+
|
|
2032
|
+
startupPerfState.tickMessagesReceived += 1;
|
|
2033
|
+
startupPerfState.tickPayloadNodesTotal += safeWorkerNodes.length;
|
|
2034
|
+
startupPerfState.tickPayloadNodesMax = Math.max(startupPerfState.tickPayloadNodesMax, safeWorkerNodes.length);
|
|
2035
|
+
|
|
2036
|
+
if (!Object.prototype.hasOwnProperty.call(startupPerfState.tickModeSamples, resolvedTickMode)) {
|
|
2037
|
+
startupPerfState.tickModeSamples[resolvedTickMode] = 0;
|
|
2038
|
+
}
|
|
2039
|
+
startupPerfState.tickModeSamples[resolvedTickMode] += 1;
|
|
2040
|
+
|
|
2041
|
+
if (!startupPerfState.t3Seen) {
|
|
2042
|
+
startupPerfState.t3Seen = true;
|
|
2043
|
+
markStartupCheckpoint('T3 first_tick_received', {
|
|
2044
|
+
alpha: Number.isFinite(workerAlpha) ? Number(workerAlpha.toFixed(4)) : null,
|
|
2045
|
+
tickMode: resolvedTickMode,
|
|
2046
|
+
payloadNodes: safeWorkerNodes.length
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
if (Number.isFinite(workerAlpha)) {
|
|
2051
|
+
startupPerfState.lastWorkerAlpha = workerAlpha;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
269
2054
|
// v0.9.80: Ignore worker ticks in Focus Mode to prevent position overwrite
|
|
270
2055
|
// In Focus Mode, positions are managed by the main thread's highlightManager
|
|
271
2056
|
if (focusNode) return;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
ticked();
|
|
2057
|
+
queueStartupTickForApply(safeWorkerNodes, resolvedTickMode, isStartupStable === true);
|
|
2058
|
+
} else if (type === 'startupStable') {
|
|
2059
|
+
if (!startupPerfState.t5Seen) {
|
|
2060
|
+
startupPerfState.t5Seen = true;
|
|
2061
|
+
markStartupCheckpoint('T5 stable_layout', {
|
|
2062
|
+
alpha: Number.isFinite(workerAlpha) ? Number(workerAlpha.toFixed(4)) : null,
|
|
2063
|
+
source: 'worker_signal',
|
|
2064
|
+
tickSummary: buildStartupTickModeSummary()
|
|
2065
|
+
});
|
|
2066
|
+
scheduleStartupLayoutSnapshotPersist('startup-stable-worker-signal');
|
|
2067
|
+
}
|
|
284
2068
|
}
|
|
285
2069
|
};
|
|
286
2070
|
|
|
@@ -339,6 +2123,7 @@ const simulation = {
|
|
|
339
2123
|
const workerPayloadBuildStartTs = (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
340
2124
|
? performance.now()
|
|
341
2125
|
: Date.now();
|
|
2126
|
+
restoreSourceLayoutOrJitterNodes(nodes, width, height, 'worker-init');
|
|
342
2127
|
const workerNodes = new Array(nodes.length);
|
|
343
2128
|
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += 1) {
|
|
344
2129
|
const node = nodes[nodeIndex];
|
|
@@ -366,6 +2151,32 @@ const workerPayloadBuildElapsedMs = ((typeof performance !== 'undefined' && type
|
|
|
366
2151
|
: Date.now()) - workerPayloadBuildStartTs;
|
|
367
2152
|
console.log(`[Init Perf] Worker payload prepared in ${workerPayloadBuildElapsedMs.toFixed(2)}ms (workerNodes=${workerNodes.length}, workerLinks=${workerLinks.length}).`);
|
|
368
2153
|
|
|
2154
|
+
const workerStartupProfile = {
|
|
2155
|
+
id: startupPerfProfile.id,
|
|
2156
|
+
pilotEnabled: startupPerfProfile.pilotEnabled === true,
|
|
2157
|
+
tickMaxFps: startupPerfProfile.tickMaxFps,
|
|
2158
|
+
lowAlphaTickMaxFps: startupPerfProfile.lowAlphaTickMaxFps,
|
|
2159
|
+
lowAlphaThreshold: startupPerfProfile.lowAlphaThreshold,
|
|
2160
|
+
stableAlphaThreshold: startupPerfProfile.stableAlphaThreshold,
|
|
2161
|
+
stableHoldTicks: startupPerfProfile.stableHoldTicks,
|
|
2162
|
+
stableTimeoutMs: startupPerfProfile.stableTimeoutMs,
|
|
2163
|
+
deltaEnabled: startupPerfProfile.deltaTickEnabled === true,
|
|
2164
|
+
deltaEpsilonPx: startupPerfProfile.deltaEpsilonPx,
|
|
2165
|
+
fullSyncEveryTicks: startupPerfProfile.deltaFullSyncEveryTicks,
|
|
2166
|
+
lowAlphaDeltaEpsilonMultiplier: startupPerfProfile.deltaLowAlphaEpsilonMultiplier,
|
|
2167
|
+
lowAlphaFullSyncEveryTicks: startupPerfProfile.deltaLowAlphaFullSyncEveryTicks
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
markStartupCheckpoint('T2 worker_init_sent', {
|
|
2171
|
+
workerNodes: workerNodes.length,
|
|
2172
|
+
workerLinks: workerLinks.length,
|
|
2173
|
+
profile: workerStartupProfile.id,
|
|
2174
|
+
tickMaxFps: workerStartupProfile.tickMaxFps,
|
|
2175
|
+
deltaEnabled: workerStartupProfile.deltaEnabled,
|
|
2176
|
+
deltaFullSyncEveryTicks: workerStartupProfile.fullSyncEveryTicks,
|
|
2177
|
+
deltaLowAlphaEpsilonMultiplier: workerStartupProfile.lowAlphaDeltaEpsilonMultiplier,
|
|
2178
|
+
deltaLowAlphaFullSyncEveryTicks: workerStartupProfile.lowAlphaFullSyncEveryTicks
|
|
2179
|
+
});
|
|
369
2180
|
simulationWorker.postMessage({
|
|
370
2181
|
type: 'init',
|
|
371
2182
|
payload: {
|
|
@@ -378,9 +2189,11 @@ simulationWorker.postMessage({
|
|
|
378
2189
|
distance: 100,
|
|
379
2190
|
velocityDecay: 0.2,
|
|
380
2191
|
gpuRendering: (window.settingsManager ? window.settingsManager.settings.performance.gpuRendering : true)
|
|
381
|
-
}
|
|
2192
|
+
},
|
|
2193
|
+
startupProfile: workerStartupProfile
|
|
382
2194
|
}
|
|
383
2195
|
});
|
|
2196
|
+
maybeApplyStartupWarmSnapshot('after-worker-init');
|
|
384
2197
|
|
|
385
2198
|
// v0.9.37: Two-stage Damping Strategy handled in worker or re-implemented here?
|
|
386
2199
|
// Re-implementing logic via messages
|
|
@@ -400,10 +2213,23 @@ setTimeout(() => {
|
|
|
400
2213
|
// Static Mode Enforcement
|
|
401
2214
|
if (nodes.length > 5000 || links.length > 200000) {
|
|
402
2215
|
console.log("[Simulation] Large graph detected. Freezing simulation after relaxation.");
|
|
403
|
-
|
|
2216
|
+
simulation.stop();
|
|
404
2217
|
}
|
|
405
2218
|
}, 2000);
|
|
406
2219
|
|
|
2220
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
2221
|
+
document.addEventListener('visibilitychange', () => {
|
|
2222
|
+
if (document.visibilityState === 'hidden') {
|
|
2223
|
+
scheduleStartupLayoutSnapshotPersist('visibility-hidden', 0);
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
2228
|
+
window.addEventListener('beforeunload', () => {
|
|
2229
|
+
scheduleStartupLayoutSnapshotPersist('beforeunload', 0);
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
407
2233
|
// Handle Resize
|
|
408
2234
|
const resizeObserver = new ResizeObserver(entries => {
|
|
409
2235
|
for (let entry of entries) {
|
|
@@ -465,6 +2291,68 @@ const link = g.append("g")
|
|
|
465
2291
|
.attr("class", "link")
|
|
466
2292
|
.attr("marker-end", "url(#arrow)");
|
|
467
2293
|
|
|
2294
|
+
function buildStartupStage1LinkSet(allNodes, allLinks, topK) {
|
|
2295
|
+
if (!Array.isArray(allLinks) || allLinks.length === 0) {
|
|
2296
|
+
return null;
|
|
2297
|
+
}
|
|
2298
|
+
const normalizedTopK = Math.max(0, Math.floor(Number(topK) || 0));
|
|
2299
|
+
if (normalizedTopK <= 0 || normalizedTopK >= allLinks.length) {
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const topNodeCount = Math.min(Math.max(normalizedTopK, 64), Math.max(64, Math.floor(allNodes.length * 0.08)));
|
|
2304
|
+
const sortedNodes = allNodes
|
|
2305
|
+
.map((n) => ({
|
|
2306
|
+
id: n.id,
|
|
2307
|
+
score: Number.isFinite(Number(n.centrality))
|
|
2308
|
+
? Number(n.centrality)
|
|
2309
|
+
: Number((n.inDegree || 0) + (n.outDegree || 0))
|
|
2310
|
+
}))
|
|
2311
|
+
.sort((a, b) => b.score - a.score)
|
|
2312
|
+
.slice(0, topNodeCount);
|
|
2313
|
+
const topNodeIds = new Set(sortedNodes.map((item) => item.id));
|
|
2314
|
+
|
|
2315
|
+
const selectedLinks = [];
|
|
2316
|
+
for (let index = 0; index < allLinks.length && selectedLinks.length < normalizedTopK; index += 1) {
|
|
2317
|
+
const edge = allLinks[index];
|
|
2318
|
+
const sourceId = edge && edge.source && typeof edge.source === 'object' ? edge.source.id : edge.source;
|
|
2319
|
+
const targetId = edge && edge.target && typeof edge.target === 'object' ? edge.target.id : edge.target;
|
|
2320
|
+
if (topNodeIds.has(sourceId) || topNodeIds.has(targetId)) {
|
|
2321
|
+
selectedLinks.push(edge);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
if (selectedLinks.length < normalizedTopK) {
|
|
2326
|
+
for (let index = 0; index < allLinks.length && selectedLinks.length < normalizedTopK; index += 1) {
|
|
2327
|
+
const edge = allLinks[index];
|
|
2328
|
+
if (!selectedLinks.includes(edge)) {
|
|
2329
|
+
selectedLinks.push(edge);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
return new Set(selectedLinks);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
const startupStage1TopK = Math.max(
|
|
2338
|
+
0,
|
|
2339
|
+
Math.floor(
|
|
2340
|
+
Number.isFinite(startupPerfProfile.edgeStage1TopK) && startupPerfProfile.edgeStage1TopK > 0
|
|
2341
|
+
? startupPerfProfile.edgeStage1TopK
|
|
2342
|
+
: startupPerfProfile.edgeStartupSvgCap
|
|
2343
|
+
)
|
|
2344
|
+
);
|
|
2345
|
+
const startupStage1LinkSet = buildStartupStage1LinkSet(nodes, links, startupStage1TopK);
|
|
2346
|
+
let startupSvgStage1LinkSelection = link;
|
|
2347
|
+
if (startupStage1LinkSet && startupStage1LinkSet.size > 0 && startupStage1LinkSet.size < links.length) {
|
|
2348
|
+
startupSvgStage1LinkSelection = link.filter((edgeDatum) => startupStage1LinkSet.has(edgeDatum));
|
|
2349
|
+
console.log('[Startup Perf] Startup SVG key-edge stage prepared.', {
|
|
2350
|
+
totalLinks: links.length,
|
|
2351
|
+
stage1Links: startupStage1LinkSet.size,
|
|
2352
|
+
stage1TopK: startupStage1TopK
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
468
2356
|
// Render Nodes
|
|
469
2357
|
const node = g.append("g")
|
|
470
2358
|
.attr("class", "nodes")
|
|
@@ -1225,11 +3113,23 @@ function handleDoubleClick(event, d) {
|
|
|
1225
3113
|
// v0.9.20 Enhancement: Auto-clear selection state when entering focus mode
|
|
1226
3114
|
// v0.9.20 增强:进入专注模式时自动清除选择状态
|
|
1227
3115
|
|
|
1228
|
-
|
|
3116
|
+
const focusInteractions = window.NoteConnectionFocusModeInteractions;
|
|
3117
|
+
const focusAction = focusInteractions && typeof focusInteractions.resolveDoubleClickAction === 'function'
|
|
3118
|
+
? focusInteractions.resolveDoubleClickAction({
|
|
3119
|
+
currentAnchorId: focusNode && focusNode.id,
|
|
3120
|
+
clickedNodeId: d && d.id,
|
|
3121
|
+
})
|
|
3122
|
+
: {
|
|
3123
|
+
action: focusNode && focusNode.id === d.id ? 'open-reader' : 'switch-focus',
|
|
3124
|
+
clickedNodeId: d && d.id,
|
|
3125
|
+
currentAnchorId: focusNode && focusNode.id,
|
|
3126
|
+
};
|
|
3127
|
+
|
|
3128
|
+
if (focusAction.action === 'open-reader') {
|
|
1229
3129
|
// Already focused on same node -> Open Reader
|
|
1230
3130
|
// 已经专注于同一节点 -> 打开阅读器
|
|
1231
3131
|
if (window.reader) window.reader.open(d);
|
|
1232
|
-
} else {
|
|
3132
|
+
} else if (focusAction.action === 'switch-focus') {
|
|
1233
3133
|
// Clear any existing selection/highlight state before entering focus mode
|
|
1234
3134
|
// 在进入专注模式前清除任何现有的选择/高亮状态
|
|
1235
3135
|
if (window.highlightManager) {
|
|
@@ -1611,6 +3511,55 @@ function checkSimulationState() {
|
|
|
1611
3511
|
}
|
|
1612
3512
|
}
|
|
1613
3513
|
|
|
3514
|
+
function startupElapsedMs() {
|
|
3515
|
+
return nowMs() - startupPerfState.bootTs;
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
function shouldBypassStartupEdgeOptimizations() {
|
|
3519
|
+
if (focusNode) {
|
|
3520
|
+
return true;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
const highlightState = window.highlightManager ? window.highlightManager.getState() : null;
|
|
3524
|
+
return Boolean(highlightState && highlightState.currentNode);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
function isStartupEdgeDelayActive() {
|
|
3528
|
+
if (startupPerfProfile.pilotEnabled !== true) {
|
|
3529
|
+
return false;
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
if (!Number.isFinite(startupPerfProfile.edgeGeometryDelayMs) || startupPerfProfile.edgeGeometryDelayMs <= 0) {
|
|
3533
|
+
return false;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
if (shouldBypassStartupEdgeOptimizations()) {
|
|
3537
|
+
return false;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
return startupElapsedMs() < startupPerfProfile.edgeGeometryDelayMs;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
function shouldUseStartupSvgStage1Edges() {
|
|
3544
|
+
if (startupPerfProfile.pilotEnabled !== true) {
|
|
3545
|
+
return false;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
if (!Number.isFinite(startupPerfProfile.edgeStartupWindowMs) || startupPerfProfile.edgeStartupWindowMs <= 0) {
|
|
3549
|
+
return false;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
if (startupSvgStage1LinkSelection === link) {
|
|
3553
|
+
return false;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
if (shouldBypassStartupEdgeOptimizations()) {
|
|
3557
|
+
return false;
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
return startupElapsedMs() < startupPerfProfile.edgeStartupWindowMs;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
1614
3563
|
// Simulation Tick
|
|
1615
3564
|
function ticked() {
|
|
1616
3565
|
const renderer = document.querySelector('input[name="rendererMode"]:checked').value;
|
|
@@ -1630,17 +3579,50 @@ function ticked() {
|
|
|
1630
3579
|
}
|
|
1631
3580
|
|
|
1632
3581
|
if (renderer === 'svg') {
|
|
1633
|
-
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const tx = d.target.x;
|
|
1639
|
-
const ty = d.target.y;
|
|
1640
|
-
return `M${sx},${sy} C${sx},${(sy + ty) / 2} ${tx},${(sy + ty) / 2} ${tx},${ty}`;
|
|
3582
|
+
const edgeDelayActive = isStartupEdgeDelayActive();
|
|
3583
|
+
if (edgeDelayActive && !startupPerfState.edgeDelayLogged) {
|
|
3584
|
+
startupPerfState.edgeDelayLogged = true;
|
|
3585
|
+
console.log('[Startup Perf] Startup SVG edge geometry delay active.', {
|
|
3586
|
+
edgeGeometryDelayMs: startupPerfProfile.edgeGeometryDelayMs
|
|
1641
3587
|
});
|
|
1642
|
-
}
|
|
1643
|
-
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
if (!edgeDelayActive) {
|
|
3591
|
+
if (startupPerfState.edgeDelayLogged && !startupPerfState.edgeDelayReleasedLogged) {
|
|
3592
|
+
startupPerfState.edgeDelayReleasedLogged = true;
|
|
3593
|
+
console.log('[Startup Perf] Startup SVG edge geometry delay released.', {
|
|
3594
|
+
elapsedMs: Number(startupElapsedMs().toFixed(2))
|
|
3595
|
+
});
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
const useStartupStage1Edges = shouldUseStartupSvgStage1Edges();
|
|
3599
|
+
if (useStartupStage1Edges && !startupPerfState.edgeStage1Logged) {
|
|
3600
|
+
startupPerfState.edgeStage1Logged = true;
|
|
3601
|
+
console.log('[Startup Perf] Startup SVG key-edge stage active.', {
|
|
3602
|
+
edgeStartupWindowMs: startupPerfProfile.edgeStartupWindowMs,
|
|
3603
|
+
edgeStage1TopK: startupStage1TopK
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
if (!useStartupStage1Edges && startupPerfState.edgeStage1Logged && !startupPerfState.edgeStage1ReleasedLogged) {
|
|
3608
|
+
startupPerfState.edgeStage1ReleasedLogged = true;
|
|
3609
|
+
console.log('[Startup Perf] Startup SVG key-edge stage released.', {
|
|
3610
|
+
elapsedMs: Number(startupElapsedMs().toFixed(2))
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
const activeLinkSelection = useStartupStage1Edges ? startupSvgStage1LinkSelection : link;
|
|
3615
|
+
if (layoutMode === 'dag') {
|
|
3616
|
+
activeLinkSelection.attr("d", d => {
|
|
3617
|
+
const sx = d.source.x;
|
|
3618
|
+
const sy = d.source.y;
|
|
3619
|
+
const tx = d.target.x;
|
|
3620
|
+
const ty = d.target.y;
|
|
3621
|
+
return `M${sx},${sy} C${sx},${(sy + ty) / 2} ${tx},${(sy + ty) / 2} ${tx},${ty}`;
|
|
3622
|
+
});
|
|
3623
|
+
} else {
|
|
3624
|
+
activeLinkSelection.attr("d", d => `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`);
|
|
3625
|
+
}
|
|
1644
3626
|
}
|
|
1645
3627
|
node.filter(d => !d.isCulled).attr("transform", d => `translate(${d.x},${d.y})`);
|
|
1646
3628
|
} else {
|
|
@@ -2368,26 +4350,578 @@ window.highlightNode = function(id) {
|
|
|
2368
4350
|
// 3. Show Popup
|
|
2369
4351
|
showNodePopup(id);
|
|
2370
4352
|
}
|
|
2371
|
-
};
|
|
4353
|
+
};
|
|
4354
|
+
|
|
4355
|
+
// Helper to expose focusOnNode for external modules
|
|
4356
|
+
// 为外部模块公开focusOnNode
|
|
4357
|
+
window.focusOnNode = function(id) {
|
|
4358
|
+
const d = nodes.find(n => n.id === id);
|
|
4359
|
+
if (d) {
|
|
4360
|
+
// Reuse double click logic or call enterFocusMode directly
|
|
4361
|
+
// Clear highlight first
|
|
4362
|
+
if (window.highlightManager) {
|
|
4363
|
+
window.highlightManager.unhighlight({ force: true });
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
// Hide popup
|
|
4367
|
+
const popup = document.getElementById('node-stats-popup');
|
|
4368
|
+
if (popup) popup.style.display = 'none';
|
|
4369
|
+
|
|
4370
|
+
// Enter Focus Mode
|
|
4371
|
+
enterFocusMode(d);
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
|
|
4375
|
+
function normalizeGraphViewText(value) {
|
|
4376
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
function normalizeGraphViewLookupKey(value) {
|
|
4380
|
+
return normalizeGraphViewText(value).toLowerCase();
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
function resolveGraphViewSourceBasename(sourcePath) {
|
|
4384
|
+
const normalized = String(sourcePath || '').replace(/\\/g, '/').trim();
|
|
4385
|
+
if (!normalized) {
|
|
4386
|
+
return '';
|
|
4387
|
+
}
|
|
4388
|
+
const fileName = normalized.split('/').filter(Boolean).pop() || normalized;
|
|
4389
|
+
return fileName.replace(/\.[^/.]+$/, '').trim();
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4392
|
+
function getGraphViewNodeLabel(node) {
|
|
4393
|
+
return normalizeGraphViewText(node && (node.label || node.title || node.name || node.id));
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
function getGraphViewLinkNodeId(endpoint) {
|
|
4397
|
+
if (endpoint && typeof endpoint === 'object') {
|
|
4398
|
+
return normalizeGraphViewText(endpoint.id || endpoint.nodeId || endpoint.key);
|
|
4399
|
+
}
|
|
4400
|
+
return normalizeGraphViewText(endpoint);
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
function collectGraphViewKnowledgePointCandidates(payload) {
|
|
4404
|
+
const candidates = [];
|
|
4405
|
+
const seen = new Set();
|
|
4406
|
+
const appendCandidate = (value) => {
|
|
4407
|
+
const normalized = normalizeGraphViewText(value);
|
|
4408
|
+
if (!normalized || seen.has(normalized)) {
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
seen.add(normalized);
|
|
4412
|
+
candidates.push(normalized);
|
|
4413
|
+
};
|
|
4414
|
+
appendCandidate(payload && payload.graphNodeId);
|
|
4415
|
+
appendCandidate(payload && payload.graphTargetId);
|
|
4416
|
+
appendCandidate(payload && payload.nodeId);
|
|
4417
|
+
appendCandidate(payload && payload.documentId);
|
|
4418
|
+
appendCandidate(payload && payload.title);
|
|
4419
|
+
appendCandidate(payload && payload.label);
|
|
4420
|
+
appendCandidate(payload && payload.sourceBasename);
|
|
4421
|
+
appendCandidate(resolveGraphViewSourceBasename(payload && payload.sourcePath));
|
|
4422
|
+
appendCandidate(payload && payload.atomId);
|
|
4423
|
+
if (Array.isArray(payload && payload.atomIds)) {
|
|
4424
|
+
payload.atomIds.forEach(appendCandidate);
|
|
4425
|
+
}
|
|
4426
|
+
if (Array.isArray(payload && payload.nodeIds)) {
|
|
4427
|
+
payload.nodeIds.forEach(appendCandidate);
|
|
4428
|
+
}
|
|
4429
|
+
if (Array.isArray(payload && payload.matchedSpans)) {
|
|
4430
|
+
payload.matchedSpans.forEach((span) => {
|
|
4431
|
+
appendCandidate(span && span.title);
|
|
4432
|
+
appendCandidate(span && span.atomId);
|
|
4433
|
+
appendCandidate(resolveGraphViewSourceBasename(span && span.sourcePath));
|
|
4434
|
+
});
|
|
4435
|
+
}
|
|
4436
|
+
return candidates;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
function resolveGraphViewNodeByIdOrLabel(candidate) {
|
|
4440
|
+
const normalized = normalizeGraphViewText(candidate);
|
|
4441
|
+
if (!normalized || !Array.isArray(nodes)) {
|
|
4442
|
+
return null;
|
|
4443
|
+
}
|
|
4444
|
+
const exactId = nodes.find((node) => normalizeGraphViewText(node && node.id) === normalized);
|
|
4445
|
+
if (exactId) {
|
|
4446
|
+
return exactId;
|
|
4447
|
+
}
|
|
4448
|
+
const lookupKey = normalizeGraphViewLookupKey(normalized);
|
|
4449
|
+
const exactLabelMatches = nodes.filter((node) => normalizeGraphViewLookupKey(getGraphViewNodeLabel(node)) === lookupKey);
|
|
4450
|
+
if (exactLabelMatches.length === 1) {
|
|
4451
|
+
return exactLabelMatches[0];
|
|
4452
|
+
}
|
|
4453
|
+
const sourceMatches = nodes.filter((node) => {
|
|
4454
|
+
const metadata = node && typeof node.metadata === 'object' ? node.metadata : {};
|
|
4455
|
+
return normalizeGraphViewLookupKey(resolveGraphViewSourceBasename(node && (node.sourcePath || node.filepath || metadata.filepath || metadata.sourcePath))) === lookupKey;
|
|
4456
|
+
});
|
|
4457
|
+
return sourceMatches.length === 1 ? sourceMatches[0] : null;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
function resolveGraphViewNodeByKnowledgePoint(payload) {
|
|
4461
|
+
const candidates = collectGraphViewKnowledgePointCandidates(payload);
|
|
4462
|
+
for (const candidate of candidates) {
|
|
4463
|
+
const node = resolveGraphViewNodeByIdOrLabel(candidate);
|
|
4464
|
+
if (node) {
|
|
4465
|
+
return node;
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
return null;
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function buildGraphViewFocusModeSnapshot(nodeId) {
|
|
4472
|
+
const focusD = resolveGraphViewNodeByIdOrLabel(nodeId);
|
|
4473
|
+
if (!focusD) {
|
|
4474
|
+
return null;
|
|
4475
|
+
}
|
|
4476
|
+
const incoming = [];
|
|
4477
|
+
const outgoing = [];
|
|
4478
|
+
const linkList = Array.isArray(links) ? links : [];
|
|
4479
|
+
linkList.forEach((linkItem) => {
|
|
4480
|
+
const sourceId = getGraphViewLinkNodeId(linkItem && linkItem.source);
|
|
4481
|
+
const targetId = getGraphViewLinkNodeId(linkItem && linkItem.target);
|
|
4482
|
+
if (sourceId === focusD.id) {
|
|
4483
|
+
const targetNode = resolveGraphViewNodeByIdOrLabel(targetId);
|
|
4484
|
+
if (targetNode) {
|
|
4485
|
+
outgoing.push(targetNode);
|
|
4486
|
+
}
|
|
4487
|
+
} else if (targetId === focusD.id) {
|
|
4488
|
+
const sourceNode = resolveGraphViewNodeByIdOrLabel(sourceId);
|
|
4489
|
+
if (sourceNode) {
|
|
4490
|
+
incoming.push(sourceNode);
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
});
|
|
4494
|
+
|
|
4495
|
+
const uniqueById = (items) => {
|
|
4496
|
+
const seen = new Set();
|
|
4497
|
+
const unique = [];
|
|
4498
|
+
items.forEach((item) => {
|
|
4499
|
+
const id = normalizeGraphViewText(item && item.id);
|
|
4500
|
+
if (!id || seen.has(id)) {
|
|
4501
|
+
return;
|
|
4502
|
+
}
|
|
4503
|
+
seen.add(id);
|
|
4504
|
+
unique.push(item);
|
|
4505
|
+
});
|
|
4506
|
+
return unique;
|
|
4507
|
+
};
|
|
4508
|
+
const incomingNodes = uniqueById(incoming).slice(0, 5);
|
|
4509
|
+
const outgoingNodes = uniqueById(outgoing).slice(0, 5);
|
|
4510
|
+
const snapshotNodes = [];
|
|
4511
|
+
const addSnapshotNode = (node, role, x, y) => {
|
|
4512
|
+
snapshotNodes.push({
|
|
4513
|
+
id: normalizeGraphViewText(node && node.id),
|
|
4514
|
+
label: getGraphViewNodeLabel(node),
|
|
4515
|
+
role,
|
|
4516
|
+
x,
|
|
4517
|
+
y,
|
|
4518
|
+
});
|
|
4519
|
+
};
|
|
4520
|
+
const placeLane = (nodeList, role, x) => {
|
|
4521
|
+
const step = 80 / (nodeList.length + 1);
|
|
4522
|
+
nodeList.forEach((node, index) => {
|
|
4523
|
+
addSnapshotNode(node, role, x, 10 + step * (index + 1));
|
|
4524
|
+
});
|
|
4525
|
+
};
|
|
4526
|
+
placeLane(incomingNodes, 'incoming', 22);
|
|
4527
|
+
addSnapshotNode(focusD, 'anchor', 50, 50);
|
|
4528
|
+
placeLane(outgoingNodes, 'outgoing', 78);
|
|
4529
|
+
|
|
4530
|
+
const visibleIds = new Set(snapshotNodes.map((node) => node.id));
|
|
4531
|
+
const snapshotEdges = linkList
|
|
4532
|
+
.map((linkItem) => {
|
|
4533
|
+
const sourceId = getGraphViewLinkNodeId(linkItem && linkItem.source);
|
|
4534
|
+
const targetId = getGraphViewLinkNodeId(linkItem && linkItem.target);
|
|
4535
|
+
if (!sourceId || !targetId || !visibleIds.has(sourceId) || !visibleIds.has(targetId)) {
|
|
4536
|
+
return null;
|
|
4537
|
+
}
|
|
4538
|
+
return {
|
|
4539
|
+
sourceId,
|
|
4540
|
+
targetId,
|
|
4541
|
+
relationKind: normalizeGraphViewText(linkItem && (linkItem.type || linkItem.relationKind || linkItem.kind)) || 'related',
|
|
4542
|
+
confidence: Number(linkItem && (linkItem.confidence || linkItem.weight)),
|
|
4543
|
+
};
|
|
4544
|
+
})
|
|
4545
|
+
.filter(Boolean);
|
|
4546
|
+
|
|
4547
|
+
return {
|
|
4548
|
+
anchorId: focusD.id,
|
|
4549
|
+
anchorLabel: getGraphViewNodeLabel(focusD),
|
|
4550
|
+
nodes: snapshotNodes,
|
|
4551
|
+
edges: snapshotEdges,
|
|
4552
|
+
};
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
function buildGraphViewFocusModeProjection(nodeId, options = {}) {
|
|
4556
|
+
const focusD = resolveGraphViewNodeByIdOrLabel(nodeId);
|
|
4557
|
+
if (!focusD) {
|
|
4558
|
+
return null;
|
|
4559
|
+
}
|
|
4560
|
+
const linkList = Array.isArray(links) ? links : [];
|
|
4561
|
+
const uniqueById = (items) => {
|
|
4562
|
+
const seen = new Set();
|
|
4563
|
+
const unique = [];
|
|
4564
|
+
items.forEach((item) => {
|
|
4565
|
+
const id = normalizeGraphViewText(item && item.id);
|
|
4566
|
+
if (!id || seen.has(id)) {
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
seen.add(id);
|
|
4570
|
+
unique.push(item);
|
|
4571
|
+
});
|
|
4572
|
+
return unique;
|
|
4573
|
+
};
|
|
4574
|
+
const getEndpointNode = (endpoint) => {
|
|
4575
|
+
if (endpoint && typeof endpoint === 'object') {
|
|
4576
|
+
const endpointId = normalizeGraphViewText(endpoint.id || endpoint.nodeId || endpoint.key);
|
|
4577
|
+
return endpointId ? resolveGraphViewNodeByIdOrLabel(endpointId) || endpoint : null;
|
|
4578
|
+
}
|
|
4579
|
+
return resolveGraphViewNodeByIdOrLabel(endpoint);
|
|
4580
|
+
};
|
|
4581
|
+
const getLinkWeight = (linkItem) => {
|
|
4582
|
+
const numericWeight = Number(linkItem && (linkItem.weight || linkItem.confidence));
|
|
4583
|
+
return Number.isFinite(numericWeight) ? numericWeight : 0.5;
|
|
4584
|
+
};
|
|
4585
|
+
const getRelationKind = (linkItem) => (
|
|
4586
|
+
normalizeGraphViewText(linkItem && (linkItem.type || linkItem.relationKind || linkItem.kind)) || 'related'
|
|
4587
|
+
);
|
|
4588
|
+
const outgoing = [];
|
|
4589
|
+
const incoming = [];
|
|
4590
|
+
const focusEdgeMap = new Map();
|
|
4591
|
+
linkList.forEach((linkItem) => {
|
|
4592
|
+
const sourceId = getGraphViewLinkNodeId(linkItem && linkItem.source);
|
|
4593
|
+
const targetId = getGraphViewLinkNodeId(linkItem && linkItem.target);
|
|
4594
|
+
if (!sourceId || !targetId) {
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
if (sourceId === focusD.id || targetId === focusD.id) {
|
|
4598
|
+
focusEdgeMap.set(`${sourceId}-${targetId}`, linkItem);
|
|
4599
|
+
}
|
|
4600
|
+
if (sourceId === focusD.id) {
|
|
4601
|
+
const targetNode = getEndpointNode(linkItem && linkItem.target);
|
|
4602
|
+
if (targetNode) {
|
|
4603
|
+
outgoing.push(targetNode);
|
|
4604
|
+
}
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
if (targetId === focusD.id) {
|
|
4608
|
+
const sourceNode = getEndpointNode(linkItem && linkItem.source);
|
|
4609
|
+
if (sourceNode) {
|
|
4610
|
+
incoming.push(sourceNode);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
4614
|
+
const scoreNode = (node) => {
|
|
4615
|
+
const key1 = `${focusD.id}-${node.id}`;
|
|
4616
|
+
const key2 = `${node.id}-${focusD.id}`;
|
|
4617
|
+
const edge = focusEdgeMap.get(key1) || focusEdgeMap.get(key2);
|
|
4618
|
+
const weight = getLinkWeight(edge);
|
|
4619
|
+
const degreeRatio = (Number(node && node.outDegree) || 0) / ((Number(node && node.inDegree) || 0) + 1);
|
|
4620
|
+
const normalizedDegreeRatio = Math.min(degreeRatio, 5) / 5;
|
|
4621
|
+
return (weight * 0.7) + (normalizedDegreeRatio * 0.3);
|
|
4622
|
+
};
|
|
4623
|
+
const sortByFocusScore = (left, right) => scoreNode(right) - scoreNode(left);
|
|
4624
|
+
const incomingNodes = uniqueById(incoming).sort(sortByFocusScore);
|
|
4625
|
+
const outgoingNodes = uniqueById(outgoing).sort(sortByFocusScore);
|
|
4626
|
+
const activeIdSet = new Set([focusD.id].concat(
|
|
4627
|
+
incomingNodes.map((node) => node.id),
|
|
4628
|
+
outgoingNodes.map((node) => node.id)
|
|
4629
|
+
));
|
|
4630
|
+
const associatedNodes = uniqueById(linkList
|
|
4631
|
+
.map((linkItem) => {
|
|
4632
|
+
const sourceId = getGraphViewLinkNodeId(linkItem && linkItem.source);
|
|
4633
|
+
const targetId = getGraphViewLinkNodeId(linkItem && linkItem.target);
|
|
4634
|
+
if ((sourceId !== focusD.id && targetId !== focusD.id) || getLinkWeight(linkItem) <= 0.6) {
|
|
4635
|
+
return null;
|
|
4636
|
+
}
|
|
4637
|
+
const otherId = sourceId === focusD.id ? targetId : sourceId;
|
|
4638
|
+
if (!otherId || activeIdSet.has(otherId)) {
|
|
4639
|
+
return null;
|
|
4640
|
+
}
|
|
4641
|
+
return getEndpointNode(sourceId === focusD.id ? linkItem.target : linkItem.source);
|
|
4642
|
+
})
|
|
4643
|
+
.filter(Boolean))
|
|
4644
|
+
.sort(sortByFocusScore);
|
|
4645
|
+
associatedNodes.forEach((node) => activeIdSet.add(node.id));
|
|
4646
|
+
|
|
4647
|
+
const currentWidth = Number.isFinite(Number(width)) ? Number(width) : 1200;
|
|
4648
|
+
const currentHeight = Number.isFinite(Number(height)) ? Number(height) : 800;
|
|
4649
|
+
const cx = Number.isFinite(Number(focusD.x)) ? Number(focusD.x) : currentWidth / 2;
|
|
4650
|
+
const cy = Number.isFinite(Number(focusD.y)) ? Number(focusD.y) : currentHeight / 2;
|
|
4651
|
+
const selectedLayoutType = normalizeGraphViewText(options.layoutType)
|
|
4652
|
+
|| (document.getElementById('focus-layout-select') ? document.getElementById('focus-layout-select').value : '')
|
|
4653
|
+
|| 'horizontal';
|
|
4654
|
+
const layoutType = selectedLayoutType === 'vertical' ? 'vertical' : 'horizontal';
|
|
4655
|
+
const defaultLayerGap = focusSpacingSettings && focusSpacingSettings[layoutType]
|
|
4656
|
+
? Number(focusSpacingSettings[layoutType].layer)
|
|
4657
|
+
: (layoutType === 'horizontal' ? 125 : 250);
|
|
4658
|
+
const defaultNodeGap = focusSpacingSettings && focusSpacingSettings[layoutType]
|
|
4659
|
+
? Number(focusSpacingSettings[layoutType].node)
|
|
4660
|
+
: (layoutType === 'horizontal' ? 80 : 20);
|
|
4661
|
+
const layerGap = Number.isFinite(Number(options.layerGap)) ? Number(options.layerGap) : defaultLayerGap;
|
|
4662
|
+
const nodeGap = Number.isFinite(Number(options.nodeGap)) ? Number(options.nodeGap) : defaultNodeGap;
|
|
4663
|
+
const projectionNodes = [];
|
|
4664
|
+
const projectionLabels = [];
|
|
4665
|
+
const points = new Map();
|
|
4666
|
+
const addProjectionNode = (node, role, x, y, labelDy, labelDx) => {
|
|
4667
|
+
const id = normalizeGraphViewText(node && node.id);
|
|
4668
|
+
if (!id || points.has(id)) {
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
points.set(id, { x, y });
|
|
4672
|
+
projectionNodes.push({
|
|
4673
|
+
id,
|
|
4674
|
+
label: getGraphViewNodeLabel(node) || id,
|
|
4675
|
+
role,
|
|
4676
|
+
x,
|
|
4677
|
+
y,
|
|
4678
|
+
score: Number(scoreNode(node).toFixed(4)),
|
|
4679
|
+
inDegree: Number(node && node.inDegree) || 0,
|
|
4680
|
+
outDegree: Number(node && node.outDegree) || 0,
|
|
4681
|
+
sourcePath: normalizeGraphViewText(node && (
|
|
4682
|
+
node.sourcePath
|
|
4683
|
+
|| node.filepath
|
|
4684
|
+
|| (node.metadata && (node.metadata.sourcePath || node.metadata.filepath))
|
|
4685
|
+
)),
|
|
4686
|
+
radius: role === 'anchor' ? 25 : 8,
|
|
4687
|
+
labelDy,
|
|
4688
|
+
labelDx,
|
|
4689
|
+
});
|
|
4690
|
+
};
|
|
4691
|
+
addProjectionNode(focusD, 'anchor', cx, cy, 35, layoutType === 'vertical' ? 25 : 29);
|
|
4692
|
+
if (layoutType === 'vertical') {
|
|
4693
|
+
const spreadVertical = (nodeList, baselineX, role) => {
|
|
4694
|
+
const totalHeight = (nodeList.length - 1) * nodeGap;
|
|
4695
|
+
const startY = cy - totalHeight / 2;
|
|
4696
|
+
nodeList.forEach((node, index) => {
|
|
4697
|
+
addProjectionNode(node, role, baselineX, startY + (index * nodeGap), 25, 25);
|
|
4698
|
+
});
|
|
4699
|
+
};
|
|
4700
|
+
spreadVertical(incomingNodes, cx - layerGap, 'incoming');
|
|
4701
|
+
spreadVertical(outgoingNodes, cx + layerGap, 'outgoing');
|
|
4702
|
+
projectionLabels.push({
|
|
4703
|
+
text: typeof t === 'function' ? t('focus_inbound') : 'Inbound',
|
|
4704
|
+
x: cx - layerGap,
|
|
4705
|
+
y: cy - (incomingNodes.length * nodeGap / 2) - 40,
|
|
4706
|
+
align: 'middle',
|
|
4707
|
+
role: 'incoming',
|
|
4708
|
+
});
|
|
4709
|
+
projectionLabels.push({
|
|
4710
|
+
text: typeof t === 'function' ? t('focus_outbound') : 'Outbound',
|
|
4711
|
+
x: cx + layerGap,
|
|
4712
|
+
y: cy - (outgoingNodes.length * nodeGap / 2) - 40,
|
|
4713
|
+
align: 'middle',
|
|
4714
|
+
role: 'outgoing',
|
|
4715
|
+
});
|
|
4716
|
+
} else {
|
|
4717
|
+
const spreadHorizontal = (nodeList, baselineY, role) => {
|
|
4718
|
+
const totalWidth = (nodeList.length - 1) * nodeGap;
|
|
4719
|
+
const startX = cx - totalWidth / 2;
|
|
4720
|
+
nodeList.forEach((node, index) => {
|
|
4721
|
+
const nodeScore = scoreNode(node);
|
|
4722
|
+
const stagger = (index % 2 === 0 ? -1 : 1) * 20;
|
|
4723
|
+
const nodeY = baselineY + stagger + (nodeScore * 20);
|
|
4724
|
+
addProjectionNode(node, role, startX + (index * nodeGap), nodeY, nodeY < baselineY ? -15 : 25, 12);
|
|
4725
|
+
});
|
|
4726
|
+
};
|
|
4727
|
+
spreadHorizontal(outgoingNodes, cy - layerGap, 'outgoing');
|
|
4728
|
+
spreadHorizontal(incomingNodes, cy + layerGap, 'incoming');
|
|
4729
|
+
projectionLabels.push({
|
|
4730
|
+
text: typeof t === 'function' ? t('focus_outbound') : 'Outbound',
|
|
4731
|
+
x: cx,
|
|
4732
|
+
y: cy - layerGap - 60,
|
|
4733
|
+
align: 'middle',
|
|
4734
|
+
role: 'outgoing',
|
|
4735
|
+
});
|
|
4736
|
+
projectionLabels.push({
|
|
4737
|
+
text: typeof t === 'function' ? t('focus_inbound') : 'Inbound',
|
|
4738
|
+
x: cx,
|
|
4739
|
+
y: cy + layerGap + 80,
|
|
4740
|
+
align: 'middle',
|
|
4741
|
+
role: 'incoming',
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4744
|
+
if (associatedNodes.length > 0) {
|
|
4745
|
+
const left = [];
|
|
4746
|
+
const right = [];
|
|
4747
|
+
associatedNodes.forEach((node, index) => {
|
|
4748
|
+
if (index % 2 === 0) {
|
|
4749
|
+
left.push(node);
|
|
4750
|
+
} else {
|
|
4751
|
+
right.push(node);
|
|
4752
|
+
}
|
|
4753
|
+
});
|
|
4754
|
+
const sideGap = layerGap * 1.2;
|
|
4755
|
+
const placeSide = (nodeList, direction) => {
|
|
4756
|
+
nodeList.forEach((node, index) => {
|
|
4757
|
+
addProjectionNode(
|
|
4758
|
+
node,
|
|
4759
|
+
'associated',
|
|
4760
|
+
cx + (direction * sideGap),
|
|
4761
|
+
cy + (index * 60) - (nodeList.length * 30),
|
|
4762
|
+
25,
|
|
4763
|
+
12
|
|
4764
|
+
);
|
|
4765
|
+
});
|
|
4766
|
+
};
|
|
4767
|
+
placeSide(left, -1);
|
|
4768
|
+
placeSide(right, 1);
|
|
4769
|
+
}
|
|
2372
4770
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
4771
|
+
const visibleIds = new Set(projectionNodes.map((node) => node.id));
|
|
4772
|
+
const projectionEdges = linkList
|
|
4773
|
+
.map((linkItem) => {
|
|
4774
|
+
const sourceId = getGraphViewLinkNodeId(linkItem && linkItem.source);
|
|
4775
|
+
const targetId = getGraphViewLinkNodeId(linkItem && linkItem.target);
|
|
4776
|
+
if (!sourceId || !targetId || !visibleIds.has(sourceId) || !visibleIds.has(targetId)) {
|
|
4777
|
+
return null;
|
|
4778
|
+
}
|
|
4779
|
+
return {
|
|
4780
|
+
sourceId,
|
|
4781
|
+
targetId,
|
|
4782
|
+
relationKind: getRelationKind(linkItem),
|
|
4783
|
+
confidence: getLinkWeight(linkItem),
|
|
4784
|
+
role: sourceId === focusD.id
|
|
4785
|
+
? 'outgoing'
|
|
4786
|
+
: targetId === focusD.id
|
|
4787
|
+
? 'incoming'
|
|
4788
|
+
: 'associated',
|
|
4789
|
+
};
|
|
4790
|
+
})
|
|
4791
|
+
.filter(Boolean);
|
|
4792
|
+
const maxContextNodes = Number.isFinite(Number(options.maxContextNodes))
|
|
4793
|
+
? Math.max(0, Math.min(500, Math.trunc(Number(options.maxContextNodes))))
|
|
4794
|
+
: 220;
|
|
4795
|
+
const contextNodes = maxContextNodes > 0
|
|
4796
|
+
? (Array.isArray(nodes) ? nodes : [])
|
|
4797
|
+
.map((node) => {
|
|
4798
|
+
const id = normalizeGraphViewText(node && node.id);
|
|
4799
|
+
const x = Number(node && node.x);
|
|
4800
|
+
const y = Number(node && node.y);
|
|
4801
|
+
if (!id || visibleIds.has(id) || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
4802
|
+
return null;
|
|
4803
|
+
}
|
|
4804
|
+
const dx = x - cx;
|
|
4805
|
+
const dy = y - cy;
|
|
4806
|
+
const distance = Math.sqrt((dx * dx) + (dy * dy));
|
|
4807
|
+
const degree = (Number(node && node.inDegree) || 0) + (Number(node && node.outDegree) || 0);
|
|
4808
|
+
const degreeScore = Math.min(degree, 40) / 40;
|
|
4809
|
+
const proximityScore = 1 / (1 + (distance / Math.max(layerGap, nodeGap, 1)));
|
|
4810
|
+
return {
|
|
4811
|
+
id,
|
|
4812
|
+
label: getGraphViewNodeLabel(node) || id,
|
|
4813
|
+
x,
|
|
4814
|
+
y,
|
|
4815
|
+
inDegree: Number(node && node.inDegree) || 0,
|
|
4816
|
+
outDegree: Number(node && node.outDegree) || 0,
|
|
4817
|
+
score: Number(((proximityScore * 0.72) + (degreeScore * 0.28)).toFixed(4)),
|
|
4818
|
+
};
|
|
4819
|
+
})
|
|
4820
|
+
.filter(Boolean)
|
|
4821
|
+
.sort((left, right) => right.score - left.score)
|
|
4822
|
+
.slice(0, maxContextNodes)
|
|
4823
|
+
: [];
|
|
4824
|
+
const boundsPoints = projectionNodes
|
|
4825
|
+
.map((node) => ({ x: node.x, y: node.y }))
|
|
4826
|
+
.concat(projectionLabels.map((label) => ({ x: label.x, y: label.y })));
|
|
4827
|
+
const contextBoundsPoints = contextNodes.length > 0
|
|
4828
|
+
? contextNodes.map((node) => ({ x: node.x, y: node.y }))
|
|
4829
|
+
: boundsPoints;
|
|
4830
|
+
const minX = Math.min(...boundsPoints.map((point) => point.x));
|
|
4831
|
+
const maxX = Math.max(...boundsPoints.map((point) => point.x));
|
|
4832
|
+
const minY = Math.min(...boundsPoints.map((point) => point.y));
|
|
4833
|
+
const maxY = Math.max(...boundsPoints.map((point) => point.y));
|
|
4834
|
+
const contextMinX = Math.min(...contextBoundsPoints.map((point) => point.x));
|
|
4835
|
+
const contextMaxX = Math.max(...contextBoundsPoints.map((point) => point.x));
|
|
4836
|
+
const contextMinY = Math.min(...contextBoundsPoints.map((point) => point.y));
|
|
4837
|
+
const contextMaxY = Math.max(...contextBoundsPoints.map((point) => point.y));
|
|
4838
|
+
return {
|
|
4839
|
+
anchorId: focusD.id,
|
|
4840
|
+
anchorLabel: getGraphViewNodeLabel(focusD) || focusD.id,
|
|
4841
|
+
layoutType,
|
|
4842
|
+
layerGap,
|
|
4843
|
+
nodeGap,
|
|
4844
|
+
stats: {
|
|
4845
|
+
inDegree: Number(focusD.inDegree) || incomingNodes.length,
|
|
4846
|
+
outDegree: Number(focusD.outDegree) || outgoingNodes.length,
|
|
4847
|
+
incomingCount: incomingNodes.length,
|
|
4848
|
+
outgoingCount: outgoingNodes.length,
|
|
4849
|
+
associatedCount: associatedNodes.length,
|
|
4850
|
+
contextCount: contextNodes.length,
|
|
4851
|
+
},
|
|
4852
|
+
controls: {
|
|
4853
|
+
layoutType,
|
|
4854
|
+
layerGap,
|
|
4855
|
+
nodeGap,
|
|
4856
|
+
rendererMode: document.querySelector('input[name="rendererMode"]:checked')
|
|
4857
|
+
? document.querySelector('input[name="rendererMode"]:checked').value
|
|
4858
|
+
: '',
|
|
4859
|
+
},
|
|
4860
|
+
labels: projectionLabels,
|
|
4861
|
+
contextNodes,
|
|
4862
|
+
nodes: projectionNodes,
|
|
4863
|
+
edges: projectionEdges,
|
|
4864
|
+
bounds: {
|
|
4865
|
+
minX,
|
|
4866
|
+
maxX,
|
|
4867
|
+
minY,
|
|
4868
|
+
maxY,
|
|
4869
|
+
width: Math.max(1, maxX - minX),
|
|
4870
|
+
height: Math.max(1, maxY - minY),
|
|
4871
|
+
},
|
|
4872
|
+
contextBounds: {
|
|
4873
|
+
minX: contextMinX,
|
|
4874
|
+
maxX: contextMaxX,
|
|
4875
|
+
minY: contextMinY,
|
|
4876
|
+
maxY: contextMaxY,
|
|
4877
|
+
width: Math.max(1, contextMaxX - contextMinX),
|
|
4878
|
+
height: Math.max(1, contextMaxY - contextMinY),
|
|
4879
|
+
},
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// Compatibility API for agent workspace graph-focus capabilities.
|
|
4884
|
+
// Keeps agent runtime decoupled from historical global helper names.
|
|
4885
|
+
window.NoteConnectionGraphView = {
|
|
4886
|
+
resolveNodeById: function(nodeId) {
|
|
4887
|
+
const id = String(nodeId || '').trim();
|
|
4888
|
+
if (!id) {
|
|
4889
|
+
return null;
|
|
4890
|
+
}
|
|
4891
|
+
return resolveGraphViewNodeByIdOrLabel(id);
|
|
4892
|
+
},
|
|
4893
|
+
resolveNodeByKnowledgePoint: function(payload) {
|
|
4894
|
+
return resolveGraphViewNodeByKnowledgePoint(payload);
|
|
4895
|
+
},
|
|
4896
|
+
openFocusModeById: function(nodeId) {
|
|
4897
|
+
const node = this.resolveNodeById(nodeId);
|
|
4898
|
+
if (!node) {
|
|
4899
|
+
return false;
|
|
4900
|
+
}
|
|
4901
|
+
if (window.highlightManager && typeof window.highlightManager.unhighlight === 'function') {
|
|
2381
4902
|
window.highlightManager.unhighlight({ force: true });
|
|
2382
4903
|
}
|
|
2383
|
-
|
|
2384
|
-
// Hide popup
|
|
2385
4904
|
const popup = document.getElementById('node-stats-popup');
|
|
2386
|
-
if (popup)
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
enterFocusMode(
|
|
2390
|
-
|
|
4905
|
+
if (popup) {
|
|
4906
|
+
popup.style.display = 'none';
|
|
4907
|
+
}
|
|
4908
|
+
enterFocusMode(node);
|
|
4909
|
+
return true;
|
|
4910
|
+
},
|
|
4911
|
+
getFocusNode: function() {
|
|
4912
|
+
return focusNode && typeof focusNode === 'object'
|
|
4913
|
+
? focusNode
|
|
4914
|
+
: null;
|
|
4915
|
+
},
|
|
4916
|
+
getFocusModeSnapshot: function(nodeId) {
|
|
4917
|
+
return buildGraphViewFocusModeSnapshot(nodeId || (focusNode && focusNode.id));
|
|
4918
|
+
},
|
|
4919
|
+
getFocusModeProjection: function(nodeId, options) {
|
|
4920
|
+
return buildGraphViewFocusModeProjection(nodeId || (focusNode && focusNode.id), options || {});
|
|
4921
|
+
},
|
|
4922
|
+
getNodeCount: function() {
|
|
4923
|
+
return Array.isArray(nodes) ? nodes.length : 0;
|
|
4924
|
+
},
|
|
2391
4925
|
};
|
|
2392
4926
|
|
|
2393
4927
|
|
|
@@ -2984,84 +5518,750 @@ function enterFocusMode(focusDInput) {
|
|
|
2984
5518
|
links: workerLinks,
|
|
2985
5519
|
restart: false // Set data, don't restart yet
|
|
2986
5520
|
}
|
|
2987
|
-
});
|
|
5521
|
+
});
|
|
5522
|
+
|
|
5523
|
+
// 4. Check Freeze Layout State & Restart if needed
|
|
5524
|
+
const isFrozen = document.getElementById('freeze-layout') ? document.getElementById('freeze-layout').checked : false;
|
|
5525
|
+
|
|
5526
|
+
if (isFrozen) {
|
|
5527
|
+
simulation.stop();
|
|
5528
|
+
ticked(); // Force one render to show all nodes in their current (restored) positions
|
|
5529
|
+
} else {
|
|
5530
|
+
simulation.alpha(1).restart();
|
|
5531
|
+
}
|
|
5532
|
+
scheduleGraphSemanticA11yRefresh('Focus mode exited');
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
// Expose Focus Mode functions for Tutorial and External Modules
|
|
5536
|
+
window.enterFocusMode = enterFocusMode;
|
|
5537
|
+
window.exitFocusMode = exitFocusMode;
|
|
5538
|
+
|
|
5539
|
+
// Max Workers (Performance)
|
|
5540
|
+
const workersSlider = document.getElementById('set-workers-slider');
|
|
5541
|
+
const workersInput = document.getElementById('set-workers-input');
|
|
5542
|
+
const gpuCheckbox = document.getElementById('set-gpu');
|
|
5543
|
+
|
|
5544
|
+
if (workersSlider && workersInput) {
|
|
5545
|
+
const updateWorkers = (val) => {
|
|
5546
|
+
const num = parseInt(val);
|
|
5547
|
+
if (isNaN(num) || num < 1) return;
|
|
5548
|
+
workersSlider.value = num;
|
|
5549
|
+
workersInput.value = num;
|
|
5550
|
+
settingsManager.set('performance', 'maxWorkers', num);
|
|
5551
|
+
};
|
|
5552
|
+
|
|
5553
|
+
workersSlider.addEventListener('input', (e) => updateWorkers(e.target.value));
|
|
5554
|
+
workersInput.addEventListener('change', (e) => updateWorkers(e.target.value));
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
if (gpuCheckbox) {
|
|
5558
|
+
gpuCheckbox.addEventListener('change', (e) => {
|
|
5559
|
+
settingsManager.set('performance', 'enableGPU', e.target.checked);
|
|
5560
|
+
});
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// --- Settings Integration ---
|
|
5564
|
+
function initSettingsUI() {
|
|
5565
|
+
const modal = document.getElementById('settings-modal');
|
|
5566
|
+
const agentModal = document.getElementById('agent-settings-modal');
|
|
5567
|
+
const openBtn = document.getElementById('btn-open-settings');
|
|
5568
|
+
const mainCloseBtns = modal ? modal.querySelectorAll('[data-settings-close="main"]') : [];
|
|
5569
|
+
const agentCloseBtns = agentModal ? agentModal.querySelectorAll('[data-settings-close="agent"]') : [];
|
|
5570
|
+
const openAgentSettingsBtn = document.getElementById('btn-open-agent-settings');
|
|
5571
|
+
const agentSettingsBackBtn = document.getElementById('btn-agent-settings-back');
|
|
5572
|
+
const agentSettingsBackFooterBtn = document.getElementById('btn-agent-settings-back-footer');
|
|
5573
|
+
const resetBtn = document.getElementById('btn-reset-settings');
|
|
5574
|
+
|
|
5575
|
+
// Track settings page state so simulation only resumes after every settings
|
|
5576
|
+
// surface is actually closed.
|
|
5577
|
+
let activeSettingsPage = null;
|
|
5578
|
+
|
|
5579
|
+
// Controls
|
|
5580
|
+
const inputs = {
|
|
5581
|
+
charge: document.getElementById('set-charge'),
|
|
5582
|
+
distance: document.getElementById('set-distance'),
|
|
5583
|
+
collision: document.getElementById('set-collision'),
|
|
5584
|
+
opacity: document.getElementById('set-opacity')
|
|
5585
|
+
};
|
|
5586
|
+
|
|
5587
|
+
const displays = {
|
|
5588
|
+
charge: document.getElementById('val-charge'),
|
|
5589
|
+
distance: document.getElementById('val-distance'),
|
|
5590
|
+
collision: document.getElementById('val-collision'),
|
|
5591
|
+
opacity: document.getElementById('val-opacity')
|
|
5592
|
+
};
|
|
5593
|
+
|
|
5594
|
+
// Performance Controls
|
|
5595
|
+
const workersSlider = document.getElementById('set-workers-slider');
|
|
5596
|
+
const workersInput = document.getElementById('set-workers-input');
|
|
5597
|
+
const gpuCheckbox = document.getElementById('set-gpu');
|
|
5598
|
+
const staticModeCheckbox = document.getElementById('set-static-mode');
|
|
5599
|
+
const gpuRenderingCheckbox = document.getElementById('set-gpu-rendering');
|
|
5600
|
+
const memorySavingCheckbox = document.getElementById('set-memory-saving');
|
|
5601
|
+
const compactModeCheckbox = document.getElementById('set-compact-mode');
|
|
5602
|
+
const deepDebugCheckbox = document.getElementById('set-deep-debug');
|
|
5603
|
+
|
|
5604
|
+
// Reader Settings
|
|
5605
|
+
const inputReadingMode = document.getElementById('set-reading-mode');
|
|
5606
|
+
|
|
5607
|
+
// Simplified NoteMD Provider Settings
|
|
5608
|
+
const providerTemplateSelect = document.getElementById('set-notemd-provider-template');
|
|
5609
|
+
const providerTemplateHint = document.getElementById('set-notemd-provider-template-hint');
|
|
5610
|
+
const providerNameSelect = document.getElementById('set-notemd-provider-name');
|
|
5611
|
+
const providerBaseUrlInput = document.getElementById('set-notemd-provider-base-url');
|
|
5612
|
+
const providerModelInput = document.getElementById('set-notemd-provider-model');
|
|
5613
|
+
const providerApiKeyInput = document.getElementById('set-notemd-provider-api-key');
|
|
5614
|
+
const providerApiVersionInput = document.getElementById('set-notemd-provider-api-version');
|
|
5615
|
+
const providerApiVersionRow = document.getElementById('set-notemd-provider-api-version-row');
|
|
5616
|
+
const providerApiVersionHint = document.getElementById('set-notemd-provider-api-version-hint');
|
|
5617
|
+
const providerStatus = document.getElementById('set-notemd-provider-status');
|
|
5618
|
+
const providerConfigPath = document.getElementById('set-notemd-provider-config-path');
|
|
5619
|
+
const applyProviderTemplateBtn = document.getElementById('btn-apply-notemd-provider-template');
|
|
5620
|
+
const testProviderBtn = document.getElementById('btn-test-notemd-provider');
|
|
5621
|
+
const saveProviderBtn = document.getElementById('btn-save-notemd-provider');
|
|
5622
|
+
const materializeProviderTemplatesBtn = document.getElementById('btn-materialize-notemd-provider-templates');
|
|
5623
|
+
const NOTEMD_PROVIDER_DRAFT_STORAGE_KEY = 'nc.notemdProviderDraft.v1';
|
|
5624
|
+
const NOTEMD_PROVIDER_AUTOSAVE_DEBOUNCE_MS = 900;
|
|
5625
|
+
|
|
5626
|
+
const notemdProviderModalState = {
|
|
5627
|
+
settings: null,
|
|
5628
|
+
templates: [],
|
|
5629
|
+
configPath: '',
|
|
5630
|
+
loading: false,
|
|
5631
|
+
autosaveTimer: null,
|
|
5632
|
+
lastSavedFingerprint: '',
|
|
5633
|
+
applyingDraft: false,
|
|
5634
|
+
};
|
|
5635
|
+
|
|
5636
|
+
const t = (key, fallback, params) => {
|
|
5637
|
+
if (window.i18n && typeof window.i18n.t === 'function') {
|
|
5638
|
+
const translated = window.i18n.t(key, params);
|
|
5639
|
+
if (translated && translated !== key) {
|
|
5640
|
+
return translated;
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
return fallback;
|
|
5644
|
+
};
|
|
5645
|
+
|
|
5646
|
+
const buildRuntimeUrl = (resourcePath) => {
|
|
5647
|
+
if (window.NoteConnectionRuntime && typeof window.NoteConnectionRuntime.buildUrl === 'function') {
|
|
5648
|
+
return window.NoteConnectionRuntime.buildUrl(resourcePath.replace(/^\/+/, ''));
|
|
5649
|
+
}
|
|
5650
|
+
return resourcePath;
|
|
5651
|
+
};
|
|
5652
|
+
|
|
5653
|
+
const buildRuntimeFetchOptions = (init) => {
|
|
5654
|
+
if (window.NoteConnectionRuntime && typeof window.NoteConnectionRuntime.buildFetchOptions === 'function') {
|
|
5655
|
+
return window.NoteConnectionRuntime.buildFetchOptions(init);
|
|
5656
|
+
}
|
|
5657
|
+
return init;
|
|
5658
|
+
};
|
|
5659
|
+
|
|
5660
|
+
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
|
|
5661
|
+
|
|
5662
|
+
const clearProviderAutosaveTimer = () => {
|
|
5663
|
+
if (notemdProviderModalState.autosaveTimer) {
|
|
5664
|
+
clearTimeout(notemdProviderModalState.autosaveTimer);
|
|
5665
|
+
notemdProviderModalState.autosaveTimer = null;
|
|
5666
|
+
}
|
|
5667
|
+
};
|
|
5668
|
+
|
|
5669
|
+
const readProviderDraftStore = () => {
|
|
5670
|
+
try {
|
|
5671
|
+
const raw = localStorage.getItem(NOTEMD_PROVIDER_DRAFT_STORAGE_KEY);
|
|
5672
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
5673
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
5674
|
+
} catch (_error) {
|
|
5675
|
+
return {};
|
|
5676
|
+
}
|
|
5677
|
+
};
|
|
5678
|
+
|
|
5679
|
+
const writeProviderDraftStore = (draftStore) => {
|
|
5680
|
+
try {
|
|
5681
|
+
localStorage.setItem(NOTEMD_PROVIDER_DRAFT_STORAGE_KEY, JSON.stringify(draftStore || {}));
|
|
5682
|
+
} catch (_error) {
|
|
5683
|
+
// Best effort only.
|
|
5684
|
+
}
|
|
5685
|
+
};
|
|
5686
|
+
|
|
5687
|
+
const getProviderDraft = (providerName) => {
|
|
5688
|
+
const draftStore = readProviderDraftStore();
|
|
5689
|
+
const key = String(providerName || '').trim();
|
|
5690
|
+
if (!key || !draftStore[key] || typeof draftStore[key] !== 'object') {
|
|
5691
|
+
return null;
|
|
5692
|
+
}
|
|
5693
|
+
return draftStore[key];
|
|
5694
|
+
};
|
|
5695
|
+
|
|
5696
|
+
const updateProviderDraft = (providerName, draftPatch) => {
|
|
5697
|
+
const key = String(providerName || '').trim();
|
|
5698
|
+
if (!key) {
|
|
5699
|
+
return;
|
|
5700
|
+
}
|
|
5701
|
+
const draftStore = readProviderDraftStore();
|
|
5702
|
+
const current = draftStore[key] && typeof draftStore[key] === 'object' ? draftStore[key] : {};
|
|
5703
|
+
draftStore[key] = {
|
|
5704
|
+
...current,
|
|
5705
|
+
...draftPatch,
|
|
5706
|
+
updatedAt: new Date().toISOString(),
|
|
5707
|
+
};
|
|
5708
|
+
writeProviderDraftStore(draftStore);
|
|
5709
|
+
};
|
|
5710
|
+
|
|
5711
|
+
const removeProviderDraft = (providerName) => {
|
|
5712
|
+
const key = String(providerName || '').trim();
|
|
5713
|
+
if (!key) {
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
const draftStore = readProviderDraftStore();
|
|
5717
|
+
if (Object.prototype.hasOwnProperty.call(draftStore, key)) {
|
|
5718
|
+
delete draftStore[key];
|
|
5719
|
+
writeProviderDraftStore(draftStore);
|
|
5720
|
+
}
|
|
5721
|
+
};
|
|
5722
|
+
|
|
5723
|
+
const setProviderModalStatus = (message, tone = 'muted') => {
|
|
5724
|
+
if (!providerStatus) {
|
|
5725
|
+
return;
|
|
5726
|
+
}
|
|
5727
|
+
providerStatus.textContent = String(message || '');
|
|
5728
|
+
providerStatus.style.color =
|
|
5729
|
+
tone === 'error'
|
|
5730
|
+
? '#fca5a5'
|
|
5731
|
+
: tone === 'success'
|
|
5732
|
+
? '#93c5fd'
|
|
5733
|
+
: '#aeb7c3';
|
|
5734
|
+
};
|
|
5735
|
+
|
|
5736
|
+
const setProviderModalBusy = (busy) => {
|
|
5737
|
+
notemdProviderModalState.loading = busy === true;
|
|
5738
|
+
const disabled = notemdProviderModalState.loading;
|
|
5739
|
+
[
|
|
5740
|
+
providerTemplateSelect,
|
|
5741
|
+
providerNameSelect,
|
|
5742
|
+
providerBaseUrlInput,
|
|
5743
|
+
providerModelInput,
|
|
5744
|
+
providerApiKeyInput,
|
|
5745
|
+
providerApiVersionInput,
|
|
5746
|
+
applyProviderTemplateBtn,
|
|
5747
|
+
testProviderBtn,
|
|
5748
|
+
saveProviderBtn,
|
|
5749
|
+
materializeProviderTemplatesBtn,
|
|
5750
|
+
].forEach((element) => {
|
|
5751
|
+
if (element) {
|
|
5752
|
+
element.disabled = disabled;
|
|
5753
|
+
}
|
|
5754
|
+
});
|
|
5755
|
+
};
|
|
5756
|
+
|
|
5757
|
+
const ensureRuntimeBridgeReady = async () => {
|
|
5758
|
+
if (
|
|
5759
|
+
window.NoteConnectionRuntime
|
|
5760
|
+
&& typeof window.NoteConnectionRuntime.whenReady === 'function'
|
|
5761
|
+
) {
|
|
5762
|
+
await window.NoteConnectionRuntime.whenReady();
|
|
5763
|
+
}
|
|
5764
|
+
};
|
|
5765
|
+
|
|
5766
|
+
const refreshRuntimeBridgeFromTauri = async () => {
|
|
5767
|
+
if (
|
|
5768
|
+
!window.NoteConnectionRuntime
|
|
5769
|
+
|| typeof window.NoteConnectionRuntime.refreshFromTauri !== 'function'
|
|
5770
|
+
) {
|
|
5771
|
+
return false;
|
|
5772
|
+
}
|
|
5773
|
+
try {
|
|
5774
|
+
await window.NoteConnectionRuntime.refreshFromTauri();
|
|
5775
|
+
return true;
|
|
5776
|
+
} catch (error) {
|
|
5777
|
+
console.warn('[Settings] Runtime bridge refresh failed before retrying agent settings request.', error);
|
|
5778
|
+
return false;
|
|
5779
|
+
}
|
|
5780
|
+
};
|
|
5781
|
+
|
|
5782
|
+
const fetchRuntimeJsonOnce = async (resourcePath, init) => {
|
|
5783
|
+
await ensureRuntimeBridgeReady();
|
|
5784
|
+
const response = await fetch(buildRuntimeUrl(resourcePath), buildRuntimeFetchOptions(init));
|
|
5785
|
+
const payload = await response.json().catch(() => null);
|
|
5786
|
+
return { response, payload };
|
|
5787
|
+
};
|
|
5788
|
+
|
|
5789
|
+
const isSuccessfulRuntimeJsonResponse = ({ response, payload }) => (
|
|
5790
|
+
response.ok && payload && payload.success === true
|
|
5791
|
+
);
|
|
5792
|
+
|
|
5793
|
+
const buildRuntimeJsonError = (resourcePath, { response, payload }) => {
|
|
5794
|
+
const message = payload && payload.error
|
|
5795
|
+
? String(payload.error)
|
|
5796
|
+
: `Request failed (${resourcePath} ${response.status})`;
|
|
5797
|
+
return new Error(message);
|
|
5798
|
+
};
|
|
5799
|
+
|
|
5800
|
+
const requestRuntimeJson = async (resourcePath, init = {}) => {
|
|
5801
|
+
const firstAttempt = await fetchRuntimeJsonOnce(resourcePath, init);
|
|
5802
|
+
if (isSuccessfulRuntimeJsonResponse(firstAttempt)) {
|
|
5803
|
+
return firstAttempt.payload;
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
if (firstAttempt.response.status === 401 && await refreshRuntimeBridgeFromTauri()) {
|
|
5807
|
+
const secondAttempt = await fetchRuntimeJsonOnce(resourcePath, init);
|
|
5808
|
+
if (isSuccessfulRuntimeJsonResponse(secondAttempt)) {
|
|
5809
|
+
return secondAttempt.payload;
|
|
5810
|
+
}
|
|
5811
|
+
throw buildRuntimeJsonError(resourcePath, secondAttempt);
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
throw buildRuntimeJsonError(resourcePath, firstAttempt);
|
|
5815
|
+
};
|
|
5816
|
+
|
|
5817
|
+
const isAnySettingsPanelOpen = () => activeSettingsPage !== null;
|
|
5818
|
+
|
|
5819
|
+
const resumeSimulationIfAllowed = () => {
|
|
5820
|
+
const isFrozen = document.getElementById('freeze-layout')
|
|
5821
|
+
? document.getElementById('freeze-layout').checked
|
|
5822
|
+
: false;
|
|
5823
|
+
if (!isFrozen) {
|
|
5824
|
+
simulation.alpha(0.3).restart();
|
|
5825
|
+
}
|
|
5826
|
+
};
|
|
5827
|
+
|
|
5828
|
+
const showMainSettingsPage = () => {
|
|
5829
|
+
if (modal) {
|
|
5830
|
+
modal.style.display = 'flex';
|
|
5831
|
+
const body = modal.querySelector('.settings-modal-body');
|
|
5832
|
+
if (body) {
|
|
5833
|
+
body.scrollTop = 0;
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
if (agentModal) {
|
|
5837
|
+
agentModal.style.display = 'none';
|
|
5838
|
+
}
|
|
5839
|
+
activeSettingsPage = 'main';
|
|
5840
|
+
simulation.stop();
|
|
5841
|
+
};
|
|
5842
|
+
|
|
5843
|
+
const showAgentSettingsPage = async () => {
|
|
5844
|
+
if (modal) {
|
|
5845
|
+
modal.style.display = 'none';
|
|
5846
|
+
}
|
|
5847
|
+
if (agentModal) {
|
|
5848
|
+
agentModal.style.display = 'flex';
|
|
5849
|
+
const body = agentModal.querySelector('.settings-modal-body');
|
|
5850
|
+
if (body) {
|
|
5851
|
+
body.scrollTop = 0;
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
activeSettingsPage = 'agent';
|
|
5855
|
+
simulation.stop();
|
|
5856
|
+
await loadNotemdProviderModalState();
|
|
5857
|
+
};
|
|
5858
|
+
|
|
5859
|
+
const closeAllSettingsPages = () => {
|
|
5860
|
+
clearProviderAutosaveTimer();
|
|
5861
|
+
const nextFingerprint = getCurrentProviderFingerprint();
|
|
5862
|
+
if (
|
|
5863
|
+
activeSettingsPage === 'agent'
|
|
5864
|
+
&& nextFingerprint
|
|
5865
|
+
&& nextFingerprint !== notemdProviderModalState.lastSavedFingerprint
|
|
5866
|
+
&& !notemdProviderModalState.loading
|
|
5867
|
+
) {
|
|
5868
|
+
void saveNotemdProviderModalState({ auto: true }).catch((error) => {
|
|
5869
|
+
setProviderModalStatus(
|
|
5870
|
+
t('notemd_provider_autosave_failed', 'Auto-save failed: {error}', {
|
|
5871
|
+
error: error && error.message ? error.message : String(error),
|
|
5872
|
+
}),
|
|
5873
|
+
'error'
|
|
5874
|
+
);
|
|
5875
|
+
});
|
|
5876
|
+
}
|
|
5877
|
+
if (modal) {
|
|
5878
|
+
modal.style.display = 'none';
|
|
5879
|
+
}
|
|
5880
|
+
if (agentModal) {
|
|
5881
|
+
agentModal.style.display = 'none';
|
|
5882
|
+
}
|
|
5883
|
+
activeSettingsPage = null;
|
|
5884
|
+
resumeSimulationIfAllowed();
|
|
5885
|
+
};
|
|
5886
|
+
|
|
5887
|
+
const getCurrentProviderFromModalState = () => {
|
|
5888
|
+
if (!notemdProviderModalState.settings || !providerNameSelect) {
|
|
5889
|
+
return null;
|
|
5890
|
+
}
|
|
5891
|
+
const providerName = String(providerNameSelect.value || '').trim();
|
|
5892
|
+
return notemdProviderModalState.settings.providers.find((provider) => provider.name === providerName) || null;
|
|
5893
|
+
};
|
|
5894
|
+
|
|
5895
|
+
const getCurrentProviderFingerprint = () => {
|
|
5896
|
+
const providerName = String(providerNameSelect ? providerNameSelect.value : '').trim();
|
|
5897
|
+
if (!providerName) {
|
|
5898
|
+
return '';
|
|
5899
|
+
}
|
|
5900
|
+
return JSON.stringify({
|
|
5901
|
+
providerName,
|
|
5902
|
+
templateId: String(providerTemplateSelect ? providerTemplateSelect.value : '').trim(),
|
|
5903
|
+
baseUrl: String(providerBaseUrlInput ? providerBaseUrlInput.value : '').trim(),
|
|
5904
|
+
model: String(providerModelInput ? providerModelInput.value : '').trim(),
|
|
5905
|
+
apiKey: String(providerApiKeyInput ? providerApiKeyInput.value : ''),
|
|
5906
|
+
apiVersion: String(providerApiVersionInput ? providerApiVersionInput.value : '').trim(),
|
|
5907
|
+
});
|
|
5908
|
+
};
|
|
5909
|
+
|
|
5910
|
+
const buildCurrentProviderRequestPayload = () => {
|
|
5911
|
+
const providerName = String(providerNameSelect ? providerNameSelect.value : '').trim();
|
|
5912
|
+
const persistedProvider = getCurrentProviderFromModalState();
|
|
5913
|
+
return {
|
|
5914
|
+
provider: {
|
|
5915
|
+
name: providerName,
|
|
5916
|
+
baseUrl: String(providerBaseUrlInput ? providerBaseUrlInput.value : '').trim(),
|
|
5917
|
+
model: String(providerModelInput ? providerModelInput.value : '').trim(),
|
|
5918
|
+
apiKey: String(providerApiKeyInput ? providerApiKeyInput.value : ''),
|
|
5919
|
+
apiVersion: String(providerApiVersionInput ? providerApiVersionInput.value : '').trim(),
|
|
5920
|
+
temperature: Number.isFinite(Number(persistedProvider && persistedProvider.temperature))
|
|
5921
|
+
? Number(persistedProvider.temperature)
|
|
5922
|
+
: 0.5,
|
|
5923
|
+
},
|
|
5924
|
+
providerName,
|
|
5925
|
+
};
|
|
5926
|
+
};
|
|
5927
|
+
|
|
5928
|
+
const syncCurrentProviderDraft = () => {
|
|
5929
|
+
const providerName = String(providerNameSelect ? providerNameSelect.value : '').trim();
|
|
5930
|
+
if (!providerName || notemdProviderModalState.applyingDraft) {
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
updateProviderDraft(providerName, {
|
|
5934
|
+
templateId: String(providerTemplateSelect ? providerTemplateSelect.value : '').trim(),
|
|
5935
|
+
baseUrl: String(providerBaseUrlInput ? providerBaseUrlInput.value : '').trim(),
|
|
5936
|
+
model: String(providerModelInput ? providerModelInput.value : '').trim(),
|
|
5937
|
+
apiKey: String(providerApiKeyInput ? providerApiKeyInput.value : ''),
|
|
5938
|
+
apiVersion: String(providerApiVersionInput ? providerApiVersionInput.value : '').trim(),
|
|
5939
|
+
});
|
|
5940
|
+
};
|
|
2988
5941
|
|
|
2989
|
-
|
|
2990
|
-
|
|
5942
|
+
const findTemplateForProvider = (providerName) => {
|
|
5943
|
+
const templates = Array.isArray(notemdProviderModalState.templates) ? notemdProviderModalState.templates : [];
|
|
5944
|
+
return templates.find((template) => template.providerName === providerName) || null;
|
|
5945
|
+
};
|
|
2991
5946
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
5947
|
+
const updateProviderApiVersionVisibility = (provider) => {
|
|
5948
|
+
if (!providerApiVersionRow) {
|
|
5949
|
+
return;
|
|
5950
|
+
}
|
|
5951
|
+
const isAzure = provider && String(provider.name || '').trim() === 'Azure OpenAI';
|
|
5952
|
+
providerApiVersionRow.style.display = isAzure ? '' : 'none';
|
|
5953
|
+
if (providerApiVersionInput) {
|
|
5954
|
+
providerApiVersionInput.placeholder = isAzure ? '2025-01-01-preview' : '';
|
|
5955
|
+
}
|
|
5956
|
+
if (providerApiVersionHint) {
|
|
5957
|
+
providerApiVersionHint.textContent = isAzure
|
|
5958
|
+
? t('notemd_provider_api_version_required', 'Required for Azure OpenAI. Example: 2025-01-01-preview.')
|
|
5959
|
+
: t('notemd_provider_api_version_optional', 'Optional. Leave blank for standard OpenAI-compatible endpoints. This is mainly used by Azure OpenAI.');
|
|
5960
|
+
}
|
|
5961
|
+
};
|
|
3000
5962
|
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
5963
|
+
const applyProviderDraftToFields = (providerName) => {
|
|
5964
|
+
const draft = getProviderDraft(providerName);
|
|
5965
|
+
if (!draft) {
|
|
5966
|
+
return;
|
|
5967
|
+
}
|
|
5968
|
+
notemdProviderModalState.applyingDraft = true;
|
|
5969
|
+
try {
|
|
5970
|
+
if (providerTemplateSelect && typeof draft.templateId === 'string' && draft.templateId) {
|
|
5971
|
+
providerTemplateSelect.value = draft.templateId;
|
|
5972
|
+
renderProviderTemplateHint();
|
|
5973
|
+
}
|
|
5974
|
+
if (providerBaseUrlInput && typeof draft.baseUrl === 'string') {
|
|
5975
|
+
providerBaseUrlInput.value = draft.baseUrl;
|
|
5976
|
+
}
|
|
5977
|
+
if (providerModelInput && typeof draft.model === 'string') {
|
|
5978
|
+
providerModelInput.value = draft.model;
|
|
5979
|
+
}
|
|
5980
|
+
if (providerApiKeyInput && typeof draft.apiKey === 'string') {
|
|
5981
|
+
providerApiKeyInput.value = draft.apiKey;
|
|
5982
|
+
}
|
|
5983
|
+
if (providerApiVersionInput && typeof draft.apiVersion === 'string') {
|
|
5984
|
+
providerApiVersionInput.value = draft.apiVersion;
|
|
5985
|
+
}
|
|
5986
|
+
} finally {
|
|
5987
|
+
notemdProviderModalState.applyingDraft = false;
|
|
5988
|
+
}
|
|
5989
|
+
};
|
|
3004
5990
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
5991
|
+
const renderProviderTemplateHint = () => {
|
|
5992
|
+
if (!providerTemplateHint) {
|
|
5993
|
+
return;
|
|
5994
|
+
}
|
|
5995
|
+
const templateId = providerTemplateSelect ? String(providerTemplateSelect.value || '').trim() : '';
|
|
5996
|
+
const templates = Array.isArray(notemdProviderModalState.templates) ? notemdProviderModalState.templates : [];
|
|
5997
|
+
const template = templates.find((item) => item.id === templateId) || null;
|
|
5998
|
+
if (!template) {
|
|
5999
|
+
providerTemplateHint.textContent = '';
|
|
6000
|
+
return;
|
|
6001
|
+
}
|
|
6002
|
+
const noteSummary = Array.isArray(template.notes) ? template.notes.slice(0, 2).join(' ') : '';
|
|
6003
|
+
providerTemplateHint.textContent = `${template.label} · ${template.category} · ${template.transport}. ${template.recommendedFor} ${template.hostHint} ${noteSummary}`.trim();
|
|
6004
|
+
};
|
|
3009
6005
|
|
|
3010
|
-
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
if (
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
6006
|
+
const renderSelectedProviderFields = () => {
|
|
6007
|
+
const provider = getCurrentProviderFromModalState();
|
|
6008
|
+
if (!provider) {
|
|
6009
|
+
if (providerBaseUrlInput) providerBaseUrlInput.value = '';
|
|
6010
|
+
if (providerModelInput) providerModelInput.value = '';
|
|
6011
|
+
if (providerApiKeyInput) providerApiKeyInput.value = '';
|
|
6012
|
+
if (providerApiVersionInput) providerApiVersionInput.value = '';
|
|
6013
|
+
updateProviderApiVersionVisibility(null);
|
|
6014
|
+
return;
|
|
6015
|
+
}
|
|
6016
|
+
if (providerBaseUrlInput) providerBaseUrlInput.value = String(provider.baseUrl || '');
|
|
6017
|
+
if (providerModelInput) providerModelInput.value = String(provider.model || '');
|
|
6018
|
+
if (providerApiKeyInput) providerApiKeyInput.value = String(provider.apiKey || '');
|
|
6019
|
+
if (providerApiVersionInput) providerApiVersionInput.value = String(provider.apiVersion || '');
|
|
6020
|
+
updateProviderApiVersionVisibility(provider);
|
|
6021
|
+
|
|
6022
|
+
const matchedTemplate = findTemplateForProvider(provider.name);
|
|
6023
|
+
if (providerTemplateSelect && matchedTemplate) {
|
|
6024
|
+
providerTemplateSelect.value = matchedTemplate.id;
|
|
6025
|
+
renderProviderTemplateHint();
|
|
6026
|
+
}
|
|
6027
|
+
applyProviderDraftToFields(provider.name);
|
|
6028
|
+
};
|
|
3018
6029
|
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
6030
|
+
const populateProviderTemplateSelect = () => {
|
|
6031
|
+
if (!providerTemplateSelect) {
|
|
6032
|
+
return;
|
|
6033
|
+
}
|
|
6034
|
+
providerTemplateSelect.innerHTML = '';
|
|
6035
|
+
const placeholder = document.createElement('option');
|
|
6036
|
+
placeholder.value = '';
|
|
6037
|
+
placeholder.textContent = t('notemd_provider_template_select', 'Choose a preset template');
|
|
6038
|
+
providerTemplateSelect.appendChild(placeholder);
|
|
6039
|
+
const templates = Array.isArray(notemdProviderModalState.templates) ? notemdProviderModalState.templates : [];
|
|
6040
|
+
templates.forEach((template) => {
|
|
6041
|
+
const option = document.createElement('option');
|
|
6042
|
+
option.value = template.id;
|
|
6043
|
+
option.textContent = `${template.label} (${template.category})`;
|
|
6044
|
+
providerTemplateSelect.appendChild(option);
|
|
3026
6045
|
});
|
|
3027
|
-
}
|
|
6046
|
+
};
|
|
3028
6047
|
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
6048
|
+
const populateProviderSelect = () => {
|
|
6049
|
+
if (!providerNameSelect) {
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
providerNameSelect.innerHTML = '';
|
|
6053
|
+
const settings = notemdProviderModalState.settings;
|
|
6054
|
+
const providers = settings && Array.isArray(settings.providers) ? settings.providers : [];
|
|
6055
|
+
providers.forEach((provider) => {
|
|
6056
|
+
const option = document.createElement('option');
|
|
6057
|
+
option.value = provider.name;
|
|
6058
|
+
option.textContent = provider.name;
|
|
6059
|
+
providerNameSelect.appendChild(option);
|
|
6060
|
+
});
|
|
6061
|
+
if (settings && settings.activeProvider) {
|
|
6062
|
+
providerNameSelect.value = settings.activeProvider;
|
|
6063
|
+
}
|
|
6064
|
+
renderSelectedProviderFields();
|
|
6065
|
+
};
|
|
3038
6066
|
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
6067
|
+
const loadNotemdProviderModalState = async (options = {}) => {
|
|
6068
|
+
if (notemdProviderModalState.loading) {
|
|
6069
|
+
return;
|
|
6070
|
+
}
|
|
6071
|
+
setProviderModalBusy(true);
|
|
6072
|
+
setProviderModalStatus(t('notemd_provider_loading', 'Loading provider settings...'));
|
|
6073
|
+
try {
|
|
6074
|
+
const templatesPayload = await requestRuntimeJson('/api/notemd/provider-templates?persist=1');
|
|
6075
|
+
const settingsPayload = await requestRuntimeJson('/api/notemd/settings');
|
|
6076
|
+
notemdProviderModalState.templates = Array.isArray(templatesPayload.templates)
|
|
6077
|
+
? cloneJson(templatesPayload.templates)
|
|
6078
|
+
: [];
|
|
6079
|
+
notemdProviderModalState.configPath = String(templatesPayload.configPath || '');
|
|
6080
|
+
notemdProviderModalState.settings = cloneJson(settingsPayload.settings || {});
|
|
6081
|
+
populateProviderTemplateSelect();
|
|
6082
|
+
populateProviderSelect();
|
|
6083
|
+
if (providerConfigPath) {
|
|
6084
|
+
providerConfigPath.textContent = notemdProviderModalState.configPath
|
|
6085
|
+
? `${t('notemd_provider_config_path', 'TOML path')}: ${notemdProviderModalState.configPath}`
|
|
6086
|
+
: '';
|
|
6087
|
+
}
|
|
6088
|
+
notemdProviderModalState.lastSavedFingerprint = getCurrentProviderFingerprint();
|
|
6089
|
+
setProviderModalStatus(
|
|
6090
|
+
t('notemd_provider_loaded', 'Provider presets and TOML templates are ready.'),
|
|
6091
|
+
'success'
|
|
6092
|
+
);
|
|
6093
|
+
} catch (error) {
|
|
6094
|
+
setProviderModalStatus(
|
|
6095
|
+
t('notemd_provider_load_failed', 'Provider settings load failed: {error}', {
|
|
6096
|
+
error: error && error.message ? error.message : String(error),
|
|
6097
|
+
}),
|
|
6098
|
+
'error'
|
|
6099
|
+
);
|
|
6100
|
+
} finally {
|
|
6101
|
+
setProviderModalBusy(false);
|
|
6102
|
+
}
|
|
3045
6103
|
};
|
|
3046
6104
|
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
6105
|
+
const saveNotemdProviderModalState = async (options = {}) => {
|
|
6106
|
+
if (!notemdProviderModalState.settings || !providerNameSelect) {
|
|
6107
|
+
return;
|
|
6108
|
+
}
|
|
6109
|
+
const auto = options.auto === true;
|
|
6110
|
+
const nextSettings = cloneJson(notemdProviderModalState.settings);
|
|
6111
|
+
const providerName = String(providerNameSelect.value || '').trim();
|
|
6112
|
+
const nextProvider = nextSettings.providers.find((provider) => provider.name === providerName);
|
|
6113
|
+
if (!nextProvider) {
|
|
6114
|
+
throw new Error(`Unsupported provider: ${providerName}`);
|
|
6115
|
+
}
|
|
6116
|
+
nextSettings.activeProvider = providerName;
|
|
6117
|
+
nextProvider.baseUrl = String(providerBaseUrlInput ? providerBaseUrlInput.value : '').trim();
|
|
6118
|
+
nextProvider.model = String(providerModelInput ? providerModelInput.value : '').trim();
|
|
6119
|
+
nextProvider.apiKey = String(providerApiKeyInput ? providerApiKeyInput.value : '');
|
|
6120
|
+
nextProvider.apiVersion = String(providerApiVersionInput ? providerApiVersionInput.value : '').trim();
|
|
6121
|
+
setProviderModalBusy(true);
|
|
6122
|
+
setProviderModalStatus(
|
|
6123
|
+
auto
|
|
6124
|
+
? t('notemd_provider_autosaving', 'Auto-saving provider settings...')
|
|
6125
|
+
: t('notemd_provider_saving', 'Saving provider settings...')
|
|
6126
|
+
);
|
|
6127
|
+
try {
|
|
6128
|
+
const payload = await requestRuntimeJson('/api/notemd/settings', {
|
|
6129
|
+
method: 'POST',
|
|
6130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6131
|
+
body: JSON.stringify({ settings: nextSettings }),
|
|
6132
|
+
});
|
|
6133
|
+
notemdProviderModalState.settings = cloneJson(payload.settings || nextSettings);
|
|
6134
|
+
populateProviderSelect();
|
|
6135
|
+
notemdProviderModalState.lastSavedFingerprint = getCurrentProviderFingerprint();
|
|
6136
|
+
removeProviderDraft(providerName);
|
|
6137
|
+
setProviderModalStatus(
|
|
6138
|
+
auto
|
|
6139
|
+
? t('notemd_provider_autosaved', 'Provider settings auto-saved.')
|
|
6140
|
+
: t('notemd_provider_saved', 'Provider settings saved to app_config.toml.'),
|
|
6141
|
+
'success'
|
|
6142
|
+
);
|
|
6143
|
+
} finally {
|
|
6144
|
+
setProviderModalBusy(false);
|
|
6145
|
+
}
|
|
6146
|
+
};
|
|
6147
|
+
|
|
6148
|
+
const scheduleProviderAutosave = () => {
|
|
6149
|
+
if (notemdProviderModalState.loading || notemdProviderModalState.applyingDraft) {
|
|
6150
|
+
return;
|
|
6151
|
+
}
|
|
6152
|
+
const providerName = String(providerNameSelect ? providerNameSelect.value : '').trim();
|
|
6153
|
+
if (!providerName || !notemdProviderModalState.settings) {
|
|
6154
|
+
return;
|
|
6155
|
+
}
|
|
6156
|
+
const nextFingerprint = getCurrentProviderFingerprint();
|
|
6157
|
+
if (!nextFingerprint || nextFingerprint === notemdProviderModalState.lastSavedFingerprint) {
|
|
6158
|
+
return;
|
|
6159
|
+
}
|
|
6160
|
+
clearProviderAutosaveTimer();
|
|
6161
|
+
notemdProviderModalState.autosaveTimer = setTimeout(() => {
|
|
6162
|
+
notemdProviderModalState.autosaveTimer = null;
|
|
6163
|
+
void saveNotemdProviderModalState({ auto: true }).catch((error) => {
|
|
6164
|
+
setProviderModalStatus(
|
|
6165
|
+
t('notemd_provider_autosave_failed', 'Auto-save failed: {error}', {
|
|
6166
|
+
error: error && error.message ? error.message : String(error),
|
|
6167
|
+
}),
|
|
6168
|
+
'error'
|
|
6169
|
+
);
|
|
6170
|
+
});
|
|
6171
|
+
}, NOTEMD_PROVIDER_AUTOSAVE_DEBOUNCE_MS);
|
|
6172
|
+
};
|
|
6173
|
+
|
|
6174
|
+
const applyNotemdProviderTemplate = async () => {
|
|
6175
|
+
if (!providerTemplateSelect) {
|
|
6176
|
+
return;
|
|
6177
|
+
}
|
|
6178
|
+
const templateId = String(providerTemplateSelect.value || '').trim();
|
|
6179
|
+
if (!templateId) {
|
|
6180
|
+
setProviderModalStatus(t('notemd_provider_template_required', 'Choose a preset template first.'), 'error');
|
|
6181
|
+
return;
|
|
6182
|
+
}
|
|
6183
|
+
setProviderModalBusy(true);
|
|
6184
|
+
setProviderModalStatus(t('notemd_provider_template_applying', 'Applying provider preset...'));
|
|
6185
|
+
try {
|
|
6186
|
+
const payload = await requestRuntimeJson('/api/notemd/provider-templates/apply', {
|
|
6187
|
+
method: 'POST',
|
|
6188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6189
|
+
body: JSON.stringify({ templateId }),
|
|
6190
|
+
});
|
|
6191
|
+
notemdProviderModalState.settings = cloneJson(payload.settings || {});
|
|
6192
|
+
if (payload.configPath) {
|
|
6193
|
+
notemdProviderModalState.configPath = String(payload.configPath);
|
|
6194
|
+
}
|
|
6195
|
+
populateProviderSelect();
|
|
6196
|
+
if (providerConfigPath) {
|
|
6197
|
+
providerConfigPath.textContent = notemdProviderModalState.configPath
|
|
6198
|
+
? `${t('notemd_provider_config_path', 'TOML path')}: ${notemdProviderModalState.configPath}`
|
|
6199
|
+
: '';
|
|
6200
|
+
}
|
|
6201
|
+
notemdProviderModalState.lastSavedFingerprint = getCurrentProviderFingerprint();
|
|
6202
|
+
removeProviderDraft(String(providerNameSelect ? providerNameSelect.value : '').trim());
|
|
6203
|
+
setProviderModalStatus(
|
|
6204
|
+
t('notemd_provider_template_applied', 'Preset applied and saved to app_config.toml.'),
|
|
6205
|
+
'success'
|
|
6206
|
+
);
|
|
6207
|
+
} finally {
|
|
6208
|
+
setProviderModalBusy(false);
|
|
6209
|
+
}
|
|
6210
|
+
};
|
|
6211
|
+
|
|
6212
|
+
const materializeNotemdProviderTemplates = async () => {
|
|
6213
|
+
setProviderModalBusy(true);
|
|
6214
|
+
setProviderModalStatus(t('notemd_provider_templates_writing', 'Writing TOML provider templates...'));
|
|
6215
|
+
try {
|
|
6216
|
+
const payload = await requestRuntimeJson('/api/notemd/provider-templates?persist=1');
|
|
6217
|
+
notemdProviderModalState.templates = Array.isArray(payload.templates)
|
|
6218
|
+
? cloneJson(payload.templates)
|
|
6219
|
+
: [];
|
|
6220
|
+
notemdProviderModalState.configPath = String(payload.configPath || notemdProviderModalState.configPath || '');
|
|
6221
|
+
populateProviderTemplateSelect();
|
|
6222
|
+
renderProviderTemplateHint();
|
|
6223
|
+
if (providerConfigPath) {
|
|
6224
|
+
providerConfigPath.textContent = notemdProviderModalState.configPath
|
|
6225
|
+
? `${t('notemd_provider_config_path', 'TOML path')}: ${notemdProviderModalState.configPath}`
|
|
6226
|
+
: '';
|
|
6227
|
+
}
|
|
6228
|
+
setProviderModalStatus(
|
|
6229
|
+
payload.persisted
|
|
6230
|
+
? t('notemd_provider_templates_written', 'Provider templates were written to app_config.toml.')
|
|
6231
|
+
: t('notemd_provider_templates_present', 'Provider templates are already present in app_config.toml.'),
|
|
6232
|
+
'success'
|
|
6233
|
+
);
|
|
6234
|
+
} finally {
|
|
6235
|
+
setProviderModalBusy(false);
|
|
6236
|
+
}
|
|
6237
|
+
};
|
|
6238
|
+
|
|
6239
|
+
const testNotemdProviderConnection = async () => {
|
|
6240
|
+
const payload = buildCurrentProviderRequestPayload();
|
|
6241
|
+
if (!payload.providerName) {
|
|
6242
|
+
throw new Error('Provider name is required.');
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
setProviderModalBusy(true);
|
|
6246
|
+
setProviderModalStatus(
|
|
6247
|
+
t('btn_test_provider_connection', 'Test Connection') + '...',
|
|
6248
|
+
'muted'
|
|
6249
|
+
);
|
|
6250
|
+
try {
|
|
6251
|
+
const response = await requestRuntimeJson('/api/notemd/test-llm', {
|
|
6252
|
+
method: 'POST',
|
|
6253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6254
|
+
body: JSON.stringify(payload),
|
|
6255
|
+
});
|
|
6256
|
+
const result = response.result || {};
|
|
6257
|
+
setProviderModalStatus(
|
|
6258
|
+
String(result.message || `Connected to ${payload.providerName}.`),
|
|
6259
|
+
result.success === false ? 'error' : 'success'
|
|
6260
|
+
);
|
|
6261
|
+
} finally {
|
|
6262
|
+
setProviderModalBusy(false);
|
|
6263
|
+
}
|
|
3052
6264
|
};
|
|
3053
|
-
|
|
3054
|
-
// Performance Controls
|
|
3055
|
-
const workersSlider = document.getElementById('set-workers-slider');
|
|
3056
|
-
const workersInput = document.getElementById('set-workers-input');
|
|
3057
|
-
const gpuCheckbox = document.getElementById('set-gpu');
|
|
3058
|
-
const gpuRenderingCheckbox = document.getElementById('set-gpu-rendering');
|
|
3059
|
-
const memorySavingCheckbox = document.getElementById('set-memory-saving');
|
|
3060
|
-
const compactModeCheckbox = document.getElementById('set-compact-mode');
|
|
3061
|
-
const deepDebugCheckbox = document.getElementById('set-deep-debug');
|
|
3062
|
-
|
|
3063
|
-
// Reader Settings
|
|
3064
|
-
const inputReadingMode = document.getElementById('set-reading-mode');
|
|
3065
6265
|
|
|
3066
6266
|
// Load initial values
|
|
3067
6267
|
const updateUIFromSettings = (settings) => {
|
|
@@ -3104,6 +6304,9 @@ function initSettingsUI() {
|
|
|
3104
6304
|
if (settings.performance.enableGPU !== undefined) {
|
|
3105
6305
|
if (gpuCheckbox) gpuCheckbox.checked = settings.performance.enableGPU;
|
|
3106
6306
|
}
|
|
6307
|
+
if (settings.performance.staticMode !== undefined) {
|
|
6308
|
+
if (staticModeCheckbox) staticModeCheckbox.checked = settings.performance.staticMode;
|
|
6309
|
+
}
|
|
3107
6310
|
if (settings.performance.gpuRendering !== undefined) {
|
|
3108
6311
|
if (gpuRenderingCheckbox) gpuRenderingCheckbox.checked = settings.performance.gpuRendering;
|
|
3109
6312
|
}
|
|
@@ -3166,6 +6369,12 @@ function initSettingsUI() {
|
|
|
3166
6369
|
});
|
|
3167
6370
|
}
|
|
3168
6371
|
|
|
6372
|
+
if (staticModeCheckbox) {
|
|
6373
|
+
staticModeCheckbox.addEventListener('change', (e) => {
|
|
6374
|
+
settingsManager.set('performance', 'staticMode', e.target.checked);
|
|
6375
|
+
});
|
|
6376
|
+
}
|
|
6377
|
+
|
|
3169
6378
|
if (gpuRenderingCheckbox) {
|
|
3170
6379
|
gpuRenderingCheckbox.addEventListener('change', (e) => {
|
|
3171
6380
|
settingsManager.set('performance', 'gpuRendering', e.target.checked);
|
|
@@ -3211,33 +6420,132 @@ function initSettingsUI() {
|
|
|
3211
6420
|
settingsManager.set('reading', 'mode', e.target.value);
|
|
3212
6421
|
});
|
|
3213
6422
|
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
6423
|
+
if (providerTemplateSelect) {
|
|
6424
|
+
providerTemplateSelect.addEventListener('change', () => {
|
|
6425
|
+
renderProviderTemplateHint();
|
|
6426
|
+
syncCurrentProviderDraft();
|
|
6427
|
+
scheduleProviderAutosave();
|
|
6428
|
+
});
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6431
|
+
if (providerNameSelect) {
|
|
6432
|
+
providerNameSelect.addEventListener('change', () => {
|
|
6433
|
+
renderSelectedProviderFields();
|
|
6434
|
+
syncCurrentProviderDraft();
|
|
6435
|
+
scheduleProviderAutosave();
|
|
6436
|
+
});
|
|
6437
|
+
}
|
|
6438
|
+
|
|
6439
|
+
[
|
|
6440
|
+
providerBaseUrlInput,
|
|
6441
|
+
providerModelInput,
|
|
6442
|
+
providerApiKeyInput,
|
|
6443
|
+
providerApiVersionInput,
|
|
6444
|
+
].forEach((input) => {
|
|
6445
|
+
if (!input) {
|
|
6446
|
+
return;
|
|
3222
6447
|
}
|
|
3223
|
-
|
|
6448
|
+
input.addEventListener('input', () => {
|
|
6449
|
+
syncCurrentProviderDraft();
|
|
6450
|
+
scheduleProviderAutosave();
|
|
6451
|
+
});
|
|
6452
|
+
input.addEventListener('change', () => {
|
|
6453
|
+
syncCurrentProviderDraft();
|
|
6454
|
+
scheduleProviderAutosave();
|
|
6455
|
+
});
|
|
6456
|
+
});
|
|
6457
|
+
|
|
6458
|
+
if (applyProviderTemplateBtn) {
|
|
6459
|
+
applyProviderTemplateBtn.addEventListener('click', () => {
|
|
6460
|
+
void applyNotemdProviderTemplate().catch((error) => {
|
|
6461
|
+
setProviderModalStatus(
|
|
6462
|
+
t('notemd_provider_template_apply_failed', 'Provider preset apply failed: {error}', {
|
|
6463
|
+
error: error && error.message ? error.message : String(error),
|
|
6464
|
+
}),
|
|
6465
|
+
'error'
|
|
6466
|
+
);
|
|
6467
|
+
});
|
|
6468
|
+
});
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6471
|
+
if (testProviderBtn) {
|
|
6472
|
+
testProviderBtn.addEventListener('click', () => {
|
|
6473
|
+
void testNotemdProviderConnection().catch((error) => {
|
|
6474
|
+
setProviderModalStatus(
|
|
6475
|
+
error && error.message ? error.message : String(error),
|
|
6476
|
+
'error'
|
|
6477
|
+
);
|
|
6478
|
+
});
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6482
|
+
if (saveProviderBtn) {
|
|
6483
|
+
saveProviderBtn.addEventListener('click', () => {
|
|
6484
|
+
void saveNotemdProviderModalState().catch((error) => {
|
|
6485
|
+
setProviderModalStatus(
|
|
6486
|
+
t('notemd_provider_save_failed', 'Provider save failed: {error}', {
|
|
6487
|
+
error: error && error.message ? error.message : String(error),
|
|
6488
|
+
}),
|
|
6489
|
+
'error'
|
|
6490
|
+
);
|
|
6491
|
+
});
|
|
6492
|
+
});
|
|
6493
|
+
}
|
|
6494
|
+
|
|
6495
|
+
if (materializeProviderTemplatesBtn) {
|
|
6496
|
+
materializeProviderTemplatesBtn.addEventListener('click', () => {
|
|
6497
|
+
void materializeNotemdProviderTemplates().catch((error) => {
|
|
6498
|
+
setProviderModalStatus(
|
|
6499
|
+
t('notemd_provider_templates_write_failed', 'Template write failed: {error}', {
|
|
6500
|
+
error: error && error.message ? error.message : String(error),
|
|
6501
|
+
}),
|
|
6502
|
+
'error'
|
|
6503
|
+
);
|
|
6504
|
+
});
|
|
6505
|
+
});
|
|
6506
|
+
}
|
|
3224
6507
|
|
|
3225
6508
|
// Modal Actions
|
|
3226
|
-
// v0.9.42: When opening settings, update UI to reflect current mode's values
|
|
3227
6509
|
openBtn.addEventListener('click', () => {
|
|
3228
6510
|
updateUIFromSettings(settingsManager.settings);
|
|
3229
|
-
|
|
3230
|
-
isSettingsModalOpen = true;
|
|
3231
|
-
simulation.stop(); // v0.9.41: Force freeze to save resources
|
|
3232
|
-
});
|
|
3233
|
-
|
|
3234
|
-
closeBtns.forEach(btn => btn.addEventListener('click', closeSettings));
|
|
3235
|
-
|
|
3236
|
-
// Close on click outside
|
|
3237
|
-
modal.addEventListener('click', (e) => {
|
|
3238
|
-
if (e.target === modal) closeSettings();
|
|
6511
|
+
showMainSettingsPage();
|
|
3239
6512
|
});
|
|
3240
6513
|
|
|
6514
|
+
if (openAgentSettingsBtn) {
|
|
6515
|
+
openAgentSettingsBtn.addEventListener('click', () => {
|
|
6516
|
+
void showAgentSettingsPage().catch((error) => {
|
|
6517
|
+
console.warn('[Settings] Failed to open agent settings.', error);
|
|
6518
|
+
});
|
|
6519
|
+
});
|
|
6520
|
+
}
|
|
6521
|
+
|
|
6522
|
+
if (agentSettingsBackBtn) {
|
|
6523
|
+
agentSettingsBackBtn.addEventListener('click', showMainSettingsPage);
|
|
6524
|
+
}
|
|
6525
|
+
|
|
6526
|
+
if (agentSettingsBackFooterBtn) {
|
|
6527
|
+
agentSettingsBackFooterBtn.addEventListener('click', showMainSettingsPage);
|
|
6528
|
+
}
|
|
6529
|
+
|
|
6530
|
+
mainCloseBtns.forEach((btn) => btn.addEventListener('click', closeAllSettingsPages));
|
|
6531
|
+
agentCloseBtns.forEach((btn) => btn.addEventListener('click', closeAllSettingsPages));
|
|
6532
|
+
|
|
6533
|
+
if (modal) {
|
|
6534
|
+
modal.addEventListener('click', (e) => {
|
|
6535
|
+
if (e.target === modal) {
|
|
6536
|
+
closeAllSettingsPages();
|
|
6537
|
+
}
|
|
6538
|
+
});
|
|
6539
|
+
}
|
|
6540
|
+
|
|
6541
|
+
if (agentModal) {
|
|
6542
|
+
agentModal.addEventListener('click', (e) => {
|
|
6543
|
+
if (e.target === agentModal) {
|
|
6544
|
+
closeAllSettingsPages();
|
|
6545
|
+
}
|
|
6546
|
+
});
|
|
6547
|
+
}
|
|
6548
|
+
|
|
3241
6549
|
resetBtn.addEventListener('click', () => {
|
|
3242
6550
|
settingsManager.reset();
|
|
3243
6551
|
updateUIFromSettings(settingsManager.settings);
|
|
@@ -3251,7 +6559,7 @@ function initSettingsUI() {
|
|
|
3251
6559
|
|
|
3252
6560
|
// v0.9.40: Check Freeze Layout State before restarting
|
|
3253
6561
|
const globalFreeze = document.getElementById('freeze-layout') ? document.getElementById('freeze-layout').checked : false;
|
|
3254
|
-
const isFrozen = globalFreeze ||
|
|
6562
|
+
const isFrozen = globalFreeze || isAnySettingsPanelOpen();
|
|
3255
6563
|
|
|
3256
6564
|
if (!isFrozen) {
|
|
3257
6565
|
simulation.alpha(0.3).restart();
|
|
@@ -3462,6 +6770,151 @@ function getTauriDialogApi() {
|
|
|
3462
6770
|
return null;
|
|
3463
6771
|
}
|
|
3464
6772
|
|
|
6773
|
+
function normalizePathModeRuntimeText(value) {
|
|
6774
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
6775
|
+
}
|
|
6776
|
+
|
|
6777
|
+
function normalizePathModeLanguage(value) {
|
|
6778
|
+
const raw = normalizePathModeRuntimeText(value).toLowerCase();
|
|
6779
|
+
return raw.startsWith('zh') ? 'zh' : 'en';
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6782
|
+
function resolvePathModeRuntimeTarget(targetId) {
|
|
6783
|
+
const normalizedTargetId = normalizePathModeRuntimeText(targetId);
|
|
6784
|
+
if (!normalizedTargetId) {
|
|
6785
|
+
return null;
|
|
6786
|
+
}
|
|
6787
|
+
const node = resolveGraphViewNodeByIdOrLabel(normalizedTargetId);
|
|
6788
|
+
if (!node) {
|
|
6789
|
+
return {
|
|
6790
|
+
id: normalizedTargetId,
|
|
6791
|
+
label: normalizedTargetId,
|
|
6792
|
+
resolved: false,
|
|
6793
|
+
};
|
|
6794
|
+
}
|
|
6795
|
+
return {
|
|
6796
|
+
id: node.id,
|
|
6797
|
+
label: getGraphViewNodeLabel(node) || node.id,
|
|
6798
|
+
resolved: true,
|
|
6799
|
+
};
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6802
|
+
function buildGodotFuturePathRuntimeConfig(targetId, options = {}) {
|
|
6803
|
+
const runtimeTarget = resolvePathModeRuntimeTarget(targetId);
|
|
6804
|
+
if (!runtimeTarget || !runtimeTarget.id) {
|
|
6805
|
+
return null;
|
|
6806
|
+
}
|
|
6807
|
+
const existingConfig = options.config && typeof options.config === 'object'
|
|
6808
|
+
? options.config
|
|
6809
|
+
: {};
|
|
6810
|
+
const targetIds = [];
|
|
6811
|
+
const seen = new Set();
|
|
6812
|
+
const appendTargetId = function(value) {
|
|
6813
|
+
const normalized = normalizePathModeRuntimeText(value);
|
|
6814
|
+
if (!normalized || seen.has(normalized)) {
|
|
6815
|
+
return;
|
|
6816
|
+
}
|
|
6817
|
+
seen.add(normalized);
|
|
6818
|
+
targetIds.push(normalized);
|
|
6819
|
+
};
|
|
6820
|
+
appendTargetId(runtimeTarget.id);
|
|
6821
|
+
if (Array.isArray(existingConfig.targetIds)) {
|
|
6822
|
+
existingConfig.targetIds.forEach(appendTargetId);
|
|
6823
|
+
}
|
|
6824
|
+
return {
|
|
6825
|
+
...existingConfig,
|
|
6826
|
+
mode: 'diffusion',
|
|
6827
|
+
strategy: 'core',
|
|
6828
|
+
layout: 'orbital',
|
|
6829
|
+
targetId: runtimeTarget.id,
|
|
6830
|
+
target_id: runtimeTarget.id,
|
|
6831
|
+
targetIds,
|
|
6832
|
+
focus_mode: true,
|
|
6833
|
+
language: normalizePathModeLanguage(
|
|
6834
|
+
existingConfig.language
|
|
6835
|
+
|| window.i18n && window.i18n.currentLanguage
|
|
6836
|
+
|| document.documentElement && document.documentElement.lang
|
|
6837
|
+
|| 'en'
|
|
6838
|
+
),
|
|
6839
|
+
};
|
|
6840
|
+
}
|
|
6841
|
+
|
|
6842
|
+
async function openGodotFuturePathById(targetId, options = {}) {
|
|
6843
|
+
const config = buildGodotFuturePathRuntimeConfig(targetId, options);
|
|
6844
|
+
if (!config) {
|
|
6845
|
+
throw new Error('Missing Godot Future Path target node.');
|
|
6846
|
+
}
|
|
6847
|
+
window.__NC_LAST_GODOT_FUTURE_PATH_REQUEST = { ...config };
|
|
6848
|
+
|
|
6849
|
+
const pathApp = window.pathApp;
|
|
6850
|
+
if (pathApp && typeof pathApp === 'object') {
|
|
6851
|
+
if (!window.__NC_AGENT_GODOT_FUTURE_PATH_INITIALIZED && typeof pathApp.init === 'function') {
|
|
6852
|
+
pathApp.init(config.targetId);
|
|
6853
|
+
window.__NC_AGENT_GODOT_FUTURE_PATH_INITIALIZED = true;
|
|
6854
|
+
}
|
|
6855
|
+
if (pathApp.runtimeConfig && typeof pathApp.runtimeConfig === 'object') {
|
|
6856
|
+
pathApp.runtimeConfig.mode = 'diffusion';
|
|
6857
|
+
pathApp.runtimeConfig.strategy = 'core';
|
|
6858
|
+
pathApp.runtimeConfig.layout = 'orbital';
|
|
6859
|
+
pathApp.runtimeConfig.targetId = config.targetId;
|
|
6860
|
+
pathApp.runtimeConfig.targetIds = config.targetIds.slice();
|
|
6861
|
+
}
|
|
6862
|
+
pathApp.currentTargetId = config.targetId;
|
|
6863
|
+
pathApp.currentTargetIds = config.targetIds.slice();
|
|
6864
|
+
pathApp.centralNodeId = config.targetId;
|
|
6865
|
+
if (typeof pathApp.applyRemoteConfigure === 'function') {
|
|
6866
|
+
pathApp.applyRemoteConfigure(config);
|
|
6867
|
+
}
|
|
6868
|
+
if (typeof pathApp._sendBridgeMessage === 'function') {
|
|
6869
|
+
pathApp._sendBridgeMessage('configure', config);
|
|
6870
|
+
}
|
|
6871
|
+
if (typeof pathApp.triggerUpdate === 'function') {
|
|
6872
|
+
pathApp.triggerUpdate();
|
|
6873
|
+
}
|
|
6874
|
+
if (typeof pathApp.requestBridgeWindowVisibility === 'function') {
|
|
6875
|
+
const bridgeReady = await pathApp.requestBridgeWindowVisibility(true, {
|
|
6876
|
+
waitMs: 1800,
|
|
6877
|
+
reason: options.source || 'open-godot-future-path',
|
|
6878
|
+
});
|
|
6879
|
+
if (bridgeReady && typeof pathApp._sendBridgeMessage === 'function') {
|
|
6880
|
+
pathApp._sendBridgeMessage('configure', config);
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6884
|
+
|
|
6885
|
+
const invoke = getTauriCoreInvoke();
|
|
6886
|
+
if (invoke) {
|
|
6887
|
+
const caps = window.__NC_RUNTIME_CAPS || {};
|
|
6888
|
+
if (caps.platform === 'android' && caps.supports_native_pathmode === true) {
|
|
6889
|
+
await invoke('open_native_pathmode', {
|
|
6890
|
+
request: {
|
|
6891
|
+
mode: 'diffusion',
|
|
6892
|
+
strategy: 'core',
|
|
6893
|
+
targetId: config.targetId,
|
|
6894
|
+
},
|
|
6895
|
+
});
|
|
6896
|
+
} else {
|
|
6897
|
+
await invoke('toggle_pathmode_window', { showGodot: true });
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6901
|
+
return {
|
|
6902
|
+
opened: true,
|
|
6903
|
+
targetId: config.targetId,
|
|
6904
|
+
targetIds: config.targetIds.slice(),
|
|
6905
|
+
strategy: config.strategy,
|
|
6906
|
+
mode: config.mode,
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6910
|
+
window.NoteConnectionPathMode = {
|
|
6911
|
+
...(window.NoteConnectionPathMode && typeof window.NoteConnectionPathMode === 'object'
|
|
6912
|
+
? window.NoteConnectionPathMode
|
|
6913
|
+
: {}),
|
|
6914
|
+
buildGodotFuturePathRuntimeConfig,
|
|
6915
|
+
openGodotFuturePathById,
|
|
6916
|
+
};
|
|
6917
|
+
|
|
3465
6918
|
function normalizeNotemdPickerPayload(payload) {
|
|
3466
6919
|
const safePayload = payload && typeof payload === 'object' ? { ...payload } : {};
|
|
3467
6920
|
const initialPath = typeof safePayload.initialPath === 'string'
|
|
@@ -3808,21 +7261,25 @@ if (btnPathMode) {
|
|
|
3808
7261
|
// 单窗口切换:隐藏 Tauri,显示 Godot。
|
|
3809
7262
|
if (window.__TAURI__ && window.__TAURI__.core && typeof window.__TAURI__.core.invoke === 'function') {
|
|
3810
7263
|
try {
|
|
3811
|
-
|
|
3812
|
-
await window.__TAURI__.core.invoke('toggle_pathmode_window', { showGodot: true });
|
|
3813
|
-
// 2. Send setWindowVisible to Godot via PathBridge WebSocket.
|
|
7264
|
+
let bridgeVisibilityReady = true;
|
|
3814
7265
|
if (
|
|
3815
7266
|
multiWindowOptions.singleWindowMode &&
|
|
3816
7267
|
window.pathApp &&
|
|
3817
|
-
window.pathApp.
|
|
3818
|
-
window.pathApp.ws.readyState === WebSocket.OPEN
|
|
7268
|
+
typeof window.pathApp.requestBridgeWindowVisibility === 'function'
|
|
3819
7269
|
) {
|
|
3820
|
-
window.pathApp.
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
})
|
|
7270
|
+
bridgeVisibilityReady = await window.pathApp.requestBridgeWindowVisibility(true, {
|
|
7271
|
+
waitMs: 2500,
|
|
7272
|
+
reason: 'enter-pathmode'
|
|
7273
|
+
});
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
if (multiWindowOptions.singleWindowMode && !bridgeVisibilityReady) {
|
|
7277
|
+
console.warn('[Path Mode] Bridge window visibility message was not delivered in time; skip hiding Tauri to avoid black-screen switch.');
|
|
7278
|
+
} else {
|
|
7279
|
+
// Hide the Tauri window via Rust IPC only after window-visibility intent is delivered.
|
|
7280
|
+
await window.__TAURI__.core.invoke('toggle_pathmode_window', { showGodot: true });
|
|
7281
|
+
console.log('[Path Mode] Single-window toggle: Tauri hidden, Godot shown.');
|
|
3824
7282
|
}
|
|
3825
|
-
console.log('[Path Mode] Single-window toggle: Tauri hidden, Godot shown.');
|
|
3826
7283
|
} catch (err) {
|
|
3827
7284
|
console.warn('[Path Mode] toggle_pathmode_window failed:', err);
|
|
3828
7285
|
}
|
|
@@ -3832,4 +7289,3 @@ if (btnPathMode) {
|
|
|
3832
7289
|
}
|
|
3833
7290
|
});
|
|
3834
7291
|
}
|
|
3835
|
-
|