noteconnection 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/dist/backend/CommunityDetection.js +58 -0
  4. package/dist/backend/FileLoader.js +110 -0
  5. package/dist/backend/GraphBuilder.js +347 -0
  6. package/dist/backend/GraphMetrics.js +70 -0
  7. package/dist/backend/algorithms/CycleDetection.js +63 -0
  8. package/dist/backend/algorithms/HybridEngine.js +70 -0
  9. package/dist/backend/algorithms/StatisticalAnalyzer.js +123 -0
  10. package/dist/backend/algorithms/TopologicalSort.js +69 -0
  11. package/dist/backend/algorithms/VectorSpace.js +87 -0
  12. package/dist/backend/build_dag.js +164 -0
  13. package/dist/backend/config.js +17 -0
  14. package/dist/backend/graph.js +108 -0
  15. package/dist/backend/main.js +67 -0
  16. package/dist/backend/parser.js +94 -0
  17. package/dist/backend/test_robustness/test_hybrid.js +60 -0
  18. package/dist/backend/test_robustness/test_statistics.js +58 -0
  19. package/dist/backend/test_robustness/test_vector.js +54 -0
  20. package/dist/backend/test_robustness.js +113 -0
  21. package/dist/backend/types.js +3 -0
  22. package/dist/backend/utils/frontmatterParser.js +121 -0
  23. package/dist/backend/utils/stringUtils.js +66 -0
  24. package/dist/backend/workers/keywordMatchWorker.js +22 -0
  25. package/dist/core/Graph.js +121 -0
  26. package/dist/core/Graph.test.js +37 -0
  27. package/dist/core/types.js +2 -0
  28. package/dist/frontend/analysis.js +356 -0
  29. package/dist/frontend/app.js +1447 -0
  30. package/dist/frontend/data.js +8356 -0
  31. package/dist/frontend/graph_data.json +8356 -0
  32. package/dist/frontend/index.html +279 -0
  33. package/dist/frontend/reader.js +177 -0
  34. package/dist/frontend/settings.js +84 -0
  35. package/dist/frontend/source_manager.js +61 -0
  36. package/dist/frontend/styles.css +577 -0
  37. package/dist/frontend/styles_analysis.css +145 -0
  38. package/dist/index.js +121 -0
  39. package/dist/server.js +149 -0
  40. package/package.json +39 -0
@@ -0,0 +1,279 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NoteConnection Knowledge Graph</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <script src="https://d3js.org/d3.v7.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
10
+ <!-- Reading Mode Dependencies -->
11
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
13
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
16
+
17
+ <script src="data.js"></script> <!-- Defines const graphData -->
18
+ </head>
19
+ <body>
20
+ <!-- Top Graph Area -->
21
+ <div id="graph-wrapper">
22
+ <button id="exit-focus-btn" style="display: none;">Exit Focus Mode</button>
23
+ <div id="source-control" style="position: absolute; top: 10px; left: 20px; z-index: 1000; display: flex; gap: 5px;">
24
+ <select id="folder-select" style="background: #333; color: white; border: 1px solid #555; padding: 2px;">
25
+ <option value="" disabled selected>Select Knowledge Base...</option>
26
+ <option value="ALL_FOLDERS">All Folders</option>
27
+ </select>
28
+ <button id="btn-load-source" style="background: #2c5282; color: white; border: 1px solid #555; padding: 2px 8px; cursor: pointer;">Load</button>
29
+ </div>
30
+ <div style="position: absolute; top: 10px; right: 20px; z-index: 1000;">
31
+ <select id="lang-select" style="background: #333; color: white; border: 1px solid #555; padding: 2px;">
32
+ <option value="en">English</option>
33
+ <option value="zh">中文</option>
34
+ </select>
35
+ </div>
36
+ <div id="controls">
37
+ <h3>NoteConnection</h3>
38
+ <div class="stats" style="display: flex; align-items: center;">
39
+ <span><span data-i18n="nodes">Nodes:</span> <span id="node-count">0</span> | <span data-i18n="edges">Edges:</span> <span id="edge-count">0</span></span>
40
+ <div id="quick-distribution" title="Degree Distribution"></div>
41
+ </div> <div class="filter-controls">
42
+ <label><input type="radio" name="mode" value="all" checked> <span data-i18n="show_all">Show All</span></label>
43
+ <label><input type="radio" name="mode" value="in"> <span data-i18n="show_in">Incoming Only</span></label>
44
+ <label><input type="radio" name="mode" value="out"> <span data-i18n="show_out">Outgoing Only</span></label>
45
+ </div>
46
+
47
+ <div class="view-mode-controls" style="margin-top: 10px; border-top: 1px solid #555; padding-top: 5px;">
48
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="view_mode">View Mode:</span>
49
+ <label style="font-size: 0.9rem; margin-right: 10px;"><input type="radio" name="viewMode" value="nodes" checked> <span data-i18n="view_nodes">Nodes</span></label>
50
+ <label style="font-size: 0.9rem;"><input type="radio" name="viewMode" value="clusters"> <span data-i18n="view_clusters">Clusters</span></label>
51
+ </div>
52
+
53
+ <div class="degree-controls" style="margin-top: 10px; border-top: 1px solid #555; padding-top: 5px;">
54
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="degree_basis">Degree Basis:</span>
55
+ <label style="font-size: 0.9rem; margin-right: 5px;"><input type="radio" name="degreeMode" value="all" checked> <span data-i18n="all">All</span></label>
56
+ <label style="font-size: 0.9rem; margin-right: 5px;"><input type="radio" name="degreeMode" value="in"> <span data-i18n="in">In</span></label>
57
+ <label style="font-size: 0.9rem;"><input type="radio" name="degreeMode" value="out"> <span data-i18n="out">Out</span></label>
58
+ </div>
59
+
60
+ <div class="color-controls" style="margin-top: 5px; padding-top: 5px;">
61
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="color_by">Color By:</span>
62
+ <label style="font-size: 0.9rem; display: inline-block; margin-right: 10px;"><input type="radio" name="colorMode" value="degree" checked> <span data-i18n="degree">Degree</span></label>
63
+ <label style="font-size: 0.9rem; display: inline-block;"><input type="radio" name="colorMode" value="cluster"> <span data-i18n="cluster">Cluster</span></label>
64
+ </div>
65
+
66
+ <div class="size-controls" style="margin-top: 5px; padding-top: 5px;">
67
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="size_by">Size By:</span>
68
+ <label style="font-size: 0.9rem; display: inline-block; margin-right: 10px;"><input type="radio" name="sizeMode" value="uniform"> <span data-i18n="uniform">Uniform</span></label>
69
+ <label style="font-size: 0.9rem; display: inline-block; margin-right: 10px;"><input type="radio" name="sizeMode" value="degree" checked> <span data-i18n="degree">Degree</span></label>
70
+ <label style="font-size: 0.9rem; display: inline-block;"><input type="radio" name="sizeMode" value="centrality"> <span data-i18n="centrality">Centrality</span></label>
71
+ </div>
72
+
73
+ <div class="opacity-controls" style="margin-top: 5px; padding-top: 5px;">
74
+ <label style="font-size: 0.85rem; color: #aaa; display: block;"><span data-i18n="label_opacity">Label Opacity:</span> <span id="label-opacity-val">100%</span></label>
75
+ <input type="range" id="label-opacity-slider" min="0" max="100" value="100" style="width: 100%;">
76
+ </div>
77
+
78
+ <div class="search-box">
79
+ <input type="text" id="search-input" placeholder="Search node..." data-i18n-placeholder="search_placeholder">
80
+ </div>
81
+
82
+ <div class="renderer-controls" style="margin-top: 10px; border-top: 1px solid #555; padding-top: 5px;">
83
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="renderer">Renderer:</span>
84
+ <label style="font-size: 0.9rem; margin-right: 10px;"><input type="radio" name="rendererMode" value="svg" checked> SVG</label>
85
+ <label style="font-size: 0.9rem;"><input type="radio" name="rendererMode" value="canvas"> Canvas</label>
86
+ </div>
87
+
88
+ <div class="layout-controls" style="margin-top: 10px; border-top: 1px solid #555; padding-top: 5px;">
89
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="layout">Layout:</span>
90
+ <label style="font-size: 0.9rem; margin-right: 10px;"><input type="radio" name="layoutMode" value="force" checked> <span data-i18n="layout_force">Force</span></label>
91
+ <label style="font-size: 0.9rem;"><input type="radio" name="layoutMode" value="dag"> <span data-i18n="layout_dag">DAG (Hierarchical)</span></label>
92
+ </div>
93
+
94
+ <div class="simulation-controls" style="margin-top: 10px; border-top: 1px solid #555; padding-top: 5px;">
95
+ <span style="font-size: 0.85rem; color: #aaa; display: block; margin-bottom: 3px;" data-i18n="simulation">Simulation:</span>
96
+ <label style="display: flex; align-items: center; margin-bottom: 5px;">
97
+ <input type="checkbox" id="freeze-layout">
98
+ <span style="margin-left: 5px; font-size: 0.9rem;" data-i18n="freeze_layout">Freeze Layout</span>
99
+ </label>
100
+ <label style="font-size: 0.8rem; color: #aaa; display: block;"><span data-i18n="speed">Speed (Damping):</span> <span id="sim-speed-val">0.4</span></label>
101
+ <input type="range" id="sim-speed-slider" min="0" max="1" step="0.01" value="0.4" style="width: 100%;">
102
+ </div>
103
+
104
+ <div class="advanced-controls" style="margin-top: 15px; border-top: 1px solid #555; padding-top: 10px;">
105
+ <label style="font-size: 0.9rem; color: #aaa;"><span data-i18n="min_degree">Min Degree:</span> <span id="min-degree-val">0</span></label>
106
+ <input type="range" id="min-degree-slider" min="0" max="20" value="0" style="width: 100%;">
107
+
108
+ <label style="display: flex; align-items: center; margin-top: 5px;">
109
+ <input type="checkbox" id="show-orphans">
110
+ <span style="margin-left: 5px;" data-i18n="show_orphans">Show Orphans</span>
111
+ </label>
112
+
113
+ <button id="export-btn" style="margin-top: 10px; width: 100%; padding: 5px; cursor: pointer; background: #333; color: white; border: 1px solid #555; border-radius: 4px;" data-i18n="export_image">Export Image</button>
114
+ <button id="save-layout-btn" style="margin-top: 5px; width: 100%; padding: 5px; cursor: pointer; background: #333; color: white; border: 1px solid #555; border-radius: 4px;" data-i18n="save_layout">Save Layout (JSON)</button>
115
+ <button id="analysis-btn" style="margin-top: 5px; width: 100%; padding: 5px; cursor: pointer; background: #2c5282; color: white; border: 1px solid #555; border-radius: 4px;" data-i18n="analysis_export">Analysis & Export</button>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Focus Mode Exit Button -->
120
+ <div id="focus-exit-btn" style="display: none; align-items: center; gap: 15px;">
121
+ <span id="focus-node-name" style="color: #61dafb; font-weight: bold;"></span>
122
+ <div style="display: flex; align-items: center; gap: 5px;">
123
+ <label for="focus-spacing-slider" style="color: #aaa; font-size: 0.8rem;">V-Space:</label>
124
+ <input type="range" id="focus-spacing-slider" min="50" max="500" value="250" style="width: 80px;" title="Vertical Spacing">
125
+ </div>
126
+ <div style="display: flex; align-items: center; gap: 5px;">
127
+ <label for="focus-h-spacing-slider" style="color: #aaa; font-size: 0.8rem;">H-Space:</label>
128
+ <input type="range" id="focus-h-spacing-slider" min="20" max="300" value="80" style="width: 80px;" title="Horizontal Spacing">
129
+ </div>
130
+ <button id="btn-arrange-focus" data-i18n="auto_arrange" style="margin-right: 5px;">Auto-Arrange</button>
131
+ <button id="btn-exit-focus" data-i18n="exit_focus">Exit Focus Mode</button>
132
+ </div>
133
+
134
+ <div id="graph-container">
135
+ <canvas id="graph-canvas" style="display: none; width: 100%; height: 100%;"></canvas>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- Resizer Handle -->
140
+ <div id="analysis-resizer"></div>
141
+
142
+ <!-- Bottom Analysis Panel -->
143
+ <div id="analysis-panel">
144
+ <div class="panel-header">
145
+ <h2 data-i18n="analysis_title">Degree Analysis</h2>
146
+ <span class="close-panel">&times;</span>
147
+ </div>
148
+ <div class="panel-body">
149
+ <div id="histogram-container"></div>
150
+
151
+ <div class="export-controls">
152
+ <div class="control-group">
153
+ <label data-i18n="filter_strategy">Filter Strategy:</label>
154
+ <select id="export-strategy" style="width: 100%; margin-top: 5px;">
155
+ <option value="top-percent" data-i18n="strat_top">Top X% (by Degree)</option>
156
+ <option value="min-degree" data-i18n="strat_min">Min Degree > X</option>
157
+ </select>
158
+ </div>
159
+ <div class="control-group">
160
+ <label data-i18n="cluster_filter">Cluster Filter:</label>
161
+ <select id="cluster-filter" style="width: 100%; margin-top: 5px;">
162
+ <option value="all" data-i18n="cluster_all">All Clusters</option>
163
+ <!-- Clusters injected here -->
164
+ </select>
165
+ </div>
166
+ <div class="control-group">
167
+ <label><span data-i18n="threshold">Threshold:</span> <span id="export-threshold-val">5</span></label>
168
+ <input type="range" id="export-threshold-slider" min="1" max="100" value="5" style="width: 100%;">
169
+ </div>
170
+ <div class="stats-preview">
171
+ <span data-i18n="selected">Selected:</span> <span id="selected-count">0</span>
172
+ </div>
173
+ <div class="action-buttons">
174
+ <button id="export-json-btn" style="flex: 1;" data-i18n="export_json">JSON</button>
175
+ <button id="export-zip-btn" style="flex: 1;" data-i18n="export_zip">ZIP (MD)</button>
176
+ </div>
177
+ </div>
178
+
179
+ <!-- Node List Table -->
180
+ <div id="node-list-wrapper" style="margin-top: 15px; flex: 1; overflow: hidden; display: flex; flex-direction: column;">
181
+ <h3 style="margin: 0 0 5px 0; font-size: 0.9rem; color: #aaa;" data-i18n="filtered_nodes">Filtered Nodes</h3>
182
+ <div class="table-header" style="display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr; gap: 5px; padding: 5px; background: #222; font-weight: bold; font-size: 0.85rem;">
183
+ <div class="sortable" data-sort="name"><span data-i18n="th_name">Name</span> <span></span></div>
184
+ <div class="sortable" data-sort="cluster"><span data-i18n="th_cluster">Cluster</span> <span></span></div>
185
+ <div class="sortable" data-sort="in"><span data-i18n="th_in">In</span> <span></span></div>
186
+ <div class="sortable" data-sort="out"><span data-i18n="th_out">Out</span> <span></span></div>
187
+ <div class="sortable" data-sort="total"><span data-i18n="th_total">Total</span> <span></span></div>
188
+ </div>
189
+ <div id="node-table-body" style="overflow-y: auto; flex: 1; border: 1px solid #444;">
190
+ <!-- Rows injected here -->
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Reading Window -->
197
+ <div id="reading-window" class="reading-overlay" style="display: none;">
198
+ <div id="reading-content-box" class="reading-box window-mode">
199
+ <div class="reading-header">
200
+ <span id="reading-title">Title</span>
201
+ <div class="reading-controls">
202
+ <button id="btn-reader-lock" class="reader-btn" title="Lock/Unlock Sizing">🔒</button>
203
+ <button id="btn-reader-zoom-out" class="reader-btn">A-</button>
204
+ <button id="btn-reader-zoom-in" class="reader-btn">A+</button>
205
+ <button id="btn-reader-close" class="reader-btn close">&times;</button>
206
+ </div>
207
+ </div>
208
+ <div id="reading-body" class="reading-body locked">
209
+ <!-- Content injected here -->
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <!-- Settings Modal -->
215
+ <div id="settings-modal" class="modal-overlay" style="display: none;">
216
+ <div class="modal-content">
217
+ <div class="modal-header">
218
+ <h2 data-i18n="settings_title">Settings</h2>
219
+ <button class="modal-close">&times;</button>
220
+ </div>
221
+ <div class="modal-body">
222
+ <div class="settings-group">
223
+ <h3 data-i18n="grp_physics">Physics</h3>
224
+ <div class="setting-item">
225
+ <label data-i18n="lbl_repulsion">Repulsion Strength</label>
226
+ <input type="range" id="set-charge" min="-1000" max="-50" step="50">
227
+ <span class="value-display" id="val-charge"></span>
228
+ </div>
229
+ <div class="setting-item">
230
+ <label data-i18n="lbl_distance">Link Distance</label>
231
+ <input type="range" id="set-distance" min="20" max="300" step="10">
232
+ <span class="value-display" id="val-distance"></span>
233
+ </div>
234
+ <div class="setting-item">
235
+ <label data-i18n="lbl_collision">Collision Radius</label>
236
+ <input type="range" id="set-collision" min="5" max="50" step="5">
237
+ <span class="value-display" id="val-collision"></span>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="settings-group">
242
+ <h3 data-i18n="grp_visuals">Visuals</h3>
243
+ <div class="setting-item">
244
+ <label data-i18n="lbl_opacity">Edge Opacity</label>
245
+ <input type="range" id="set-opacity" min="0.1" max="1.0" step="0.1">
246
+ <span class="value-display" id="val-opacity"></span>
247
+ </div>
248
+ </div>
249
+
250
+ <div class="settings-group">
251
+ <h3 data-i18n="grp_reading">Reading</h3>
252
+ <div class="setting-item">
253
+ <label data-i18n="lbl_reading_mode">Open Mode</label>
254
+ <select id="set-reading-mode" style="background: #444; color: white; border: 1px solid #555; padding: 2px;">
255
+ <option value="window" data-i18n="opt_window">Window</option>
256
+ <option value="fullscreen" data-i18n="opt_fullscreen">Full Screen</option>
257
+ </select>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ <div class="modal-footer">
262
+ <button class="btn btn-secondary" id="btn-reset-settings" data-i18n="btn_reset">Reset</button>
263
+ <button class="btn btn-primary modal-close" data-i18n="btn_done">Done</button>
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- Settings Toggle Button (Absolute Position top right) -->
269
+ <button id="btn-open-settings" style="position: absolute; top: 10px; right: 110px; z-index: 1000; background: #333; color: white; border: 1px solid #555; padding: 3px 8px; cursor: pointer;">
270
+ ⚙️ <span data-i18n="btn_settings">Settings</span>
271
+ </button>
272
+
273
+ <script src="settings.js"></script>
274
+ <script src="reader.js"></script>
275
+ <script src="app.js"></script>
276
+ <script src="analysis.js"></script>
277
+ <script src="source_manager.js"></script>
278
+ </body>
279
+ </html>
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Reader Module
3
+ * Handles rendering of Markdown, Math, Mermaid, and window management.
4
+ */
5
+
6
+ class Reader {
7
+ constructor() {
8
+ this.window = document.getElementById('reading-window');
9
+ this.contentBox = document.getElementById('reading-content-box');
10
+ this.body = document.getElementById('reading-body');
11
+ this.title = document.getElementById('reading-title');
12
+
13
+ this.isLocked = true;
14
+ this.currentZoom = 1.0;
15
+
16
+ this.init();
17
+ }
18
+
19
+ init() {
20
+ // Close Button
21
+ document.getElementById('btn-reader-close').addEventListener('click', () => {
22
+ this.close();
23
+ });
24
+
25
+ // Close on overlay click
26
+ this.window.addEventListener('click', (e) => {
27
+ if (e.target === this.window) this.close();
28
+ });
29
+
30
+ // Lock Toggle
31
+ document.getElementById('btn-reader-lock').addEventListener('click', () => {
32
+ this.toggleLock();
33
+ });
34
+
35
+ // Zoom Controls
36
+ document.getElementById('btn-reader-zoom-in').addEventListener('click', () => this.zoom(0.1));
37
+ document.getElementById('btn-reader-zoom-out').addEventListener('click', () => this.zoom(-0.1));
38
+
39
+ // Initialize Mermaid
40
+ if (window.mermaid) {
41
+ mermaid.initialize({ startOnLoad: false, theme: 'dark' });
42
+ }
43
+ }
44
+
45
+ open(node) {
46
+ // 1. Set Title
47
+ this.title.innerText = node.label;
48
+
49
+ // 2. Prepare Content
50
+ let rawContent = node.content || "*No content available.*";
51
+ let htmlContent = marked.parse(rawContent);
52
+ this.body.innerHTML = htmlContent;
53
+
54
+ // 3. Show Window EARLY (Crucial for Mermaid layout calculation)
55
+ const settings = window.settingsManager.get('reading', 'mode');
56
+ this.contentBox.className = `reading-box ${settings === 'fullscreen' ? 'fullscreen-mode' : 'window-mode'}`;
57
+ this.window.style.display = 'flex';
58
+
59
+ // 4. Render Latex (KaTeX)
60
+ if (window.renderMathInElement) {
61
+ renderMathInElement(this.body, {
62
+ delimiters: [
63
+ {left: '$$', right: '$$', display: true},
64
+ {left: '$', right: '$', display: false}
65
+ ]
66
+ });
67
+ }
68
+
69
+ // 5. Render Mermaid
70
+ if (window.mermaid) {
71
+ // Re-configure for robustness & High Contrast
72
+ mermaid.initialize({
73
+ startOnLoad: false,
74
+ theme: 'dark', // Switch to 'dark' base for better defaults
75
+ themeVariables: {
76
+ darkMode: true,
77
+ background: '#1e1e1e', // Match app bg
78
+ mainBkg: '#1e1e1e', // Container bg
79
+
80
+ // Node Colors (High Contrast)
81
+ primaryColor: '#2d2d2d',
82
+ primaryTextColor: '#ffffff',
83
+ primaryBorderColor: '#61dafb',
84
+
85
+ // Edges & Arrows
86
+ lineColor: '#a0a0a0',
87
+ secondaryColor: '#333',
88
+ tertiaryColor: '#2d2d2d',
89
+
90
+ // Text
91
+ textColor: '#ffffff',
92
+ fontSize: '16px' // Bump base size
93
+ },
94
+ securityLevel: 'loose', // Allow HTML in nodes
95
+ htmlLabels: true
96
+ });
97
+
98
+ const mermaidBlocks = this.body.querySelectorAll('pre code.language-mermaid');
99
+ mermaidBlocks.forEach((block, index) => {
100
+ // Robust Decoding: Handle HTML entities (e.g., &gt; -> >)
101
+ const txt = document.createElement("textarea");
102
+ txt.innerHTML = block.innerHTML;
103
+ const graphDefinition = txt.value;
104
+
105
+ const parentPre = block.parentElement;
106
+
107
+ // Create container
108
+ const div = document.createElement('div');
109
+ div.className = 'mermaid';
110
+ div.id = `mermaid-${index}`;
111
+ div.textContent = graphDefinition; // Use textContent to set raw text
112
+
113
+ parentPre.parentNode.replaceChild(div, parentPre);
114
+ });
115
+
116
+ if (mermaidBlocks.length > 0) {
117
+ // Slight delay to ensure DOM is reflowed after show
118
+ setTimeout(() => {
119
+ mermaid.run().catch(err => {
120
+ console.error("Mermaid error:", err);
121
+ // Fallback: Show error in UI
122
+ // Mermaid usually handles this, but we can log it.
123
+ });
124
+ }, 10);
125
+ }
126
+ }
127
+
128
+ // 6. Reset State
129
+ this.isLocked = true;
130
+ this.currentZoom = 1.0;
131
+ this.updateLockState();
132
+ this.updateZoom();
133
+ }
134
+
135
+ close() {
136
+ this.window.style.display = 'none';
137
+ }
138
+
139
+ toggleLock() {
140
+ this.isLocked = !this.isLocked;
141
+ this.updateLockState();
142
+ }
143
+
144
+ updateLockState() {
145
+ const btn = document.getElementById('btn-reader-lock');
146
+ const zoomBtns = document.querySelectorAll('#btn-reader-zoom-in, #btn-reader-zoom-out');
147
+
148
+ if (this.isLocked) {
149
+ this.body.classList.add('locked');
150
+ this.body.classList.remove('unlocked');
151
+ btn.innerText = "🔒";
152
+ btn.title = "Locked: Sizing fixed";
153
+ zoomBtns.forEach(b => b.disabled = true);
154
+ } else {
155
+ this.body.classList.add('unlocked');
156
+ this.body.classList.remove('locked');
157
+ btn.innerText = "🔓";
158
+ btn.title = "Unlocked: Adjust size enabled";
159
+ zoomBtns.forEach(b => b.disabled = false);
160
+ }
161
+ }
162
+
163
+ zoom(delta) {
164
+ if (this.isLocked) return;
165
+ this.currentZoom = Math.max(0.5, Math.min(3.0, this.currentZoom + delta));
166
+ this.updateZoom();
167
+ }
168
+
169
+ updateZoom() {
170
+ this.body.style.fontSize = `${this.currentZoom}rem`;
171
+ // Scale images if needed, but CSS 'resize' handles explicit image resizing in unlocked mode.
172
+ // Font size scaling handles text content scaling.
173
+ }
174
+ }
175
+
176
+ const reader = new Reader();
177
+ window.reader = reader;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Settings Manager Module
3
+ * Handles configuration persistence, defaults, and application.
4
+ */
5
+
6
+ const defaultSettings = {
7
+ physics: {
8
+ chargeStrength: -300,
9
+ linkDistance: 100,
10
+ collisionRadius: 20
11
+ },
12
+ visuals: {
13
+ edgeOpacity: 0.6,
14
+ baseNodeSize: 5
15
+ },
16
+ reading: {
17
+ mode: 'window' // 'window' or 'fullscreen'
18
+ }
19
+ };
20
+
21
+ class SettingsManager {
22
+ constructor() {
23
+ this.settings = this.load();
24
+ this.listeners = [];
25
+ }
26
+
27
+ load() {
28
+ const saved = localStorage.getItem('nc_settings');
29
+ if (saved) {
30
+ try {
31
+ // Merge saved settings with defaults to ensure new keys exist
32
+ const parsed = JSON.parse(saved);
33
+ return this.deepMerge(defaultSettings, parsed);
34
+ } catch (e) {
35
+ console.error("Failed to parse settings", e);
36
+ return JSON.parse(JSON.stringify(defaultSettings));
37
+ }
38
+ }
39
+ return JSON.parse(JSON.stringify(defaultSettings));
40
+ }
41
+
42
+ save() {
43
+ localStorage.setItem('nc_settings', JSON.stringify(this.settings));
44
+ this.notify();
45
+ }
46
+
47
+ get(category, key) {
48
+ return this.settings[category][key];
49
+ }
50
+
51
+ set(category, key, value) {
52
+ if (!this.settings[category]) this.settings[category] = {};
53
+ this.settings[category][key] = value;
54
+ this.save();
55
+ }
56
+
57
+ // Helper for deep merging defaults
58
+ deepMerge(target, source) {
59
+ for (const key of Object.keys(source)) {
60
+ if (source[key] instanceof Object && key in target) {
61
+ Object.assign(source[key], this.deepMerge(target[key], source[key]));
62
+ }
63
+ }
64
+ Object.assign(target || {}, source);
65
+ return target;
66
+ }
67
+
68
+ // Observer pattern for live updates
69
+ subscribe(callback) {
70
+ this.listeners.push(callback);
71
+ }
72
+
73
+ notify() {
74
+ this.listeners.forEach(cb => cb(this.settings));
75
+ }
76
+
77
+ reset() {
78
+ this.settings = JSON.parse(JSON.stringify(defaultSettings));
79
+ this.save();
80
+ }
81
+ }
82
+
83
+ const settingsManager = new SettingsManager();
84
+ window.settingsManager = settingsManager; // Expose globally
@@ -0,0 +1,61 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const folderSelect = document.getElementById('folder-select');
3
+ const loadBtn = document.getElementById('btn-load-source');
4
+
5
+ if (!folderSelect || !loadBtn) return;
6
+
7
+ // Fetch folders
8
+ fetch('/api/folders')
9
+ .then(response => response.json())
10
+ .then(data => {
11
+ if (data.folders) {
12
+ data.folders.forEach(folder => {
13
+ const option = document.createElement('option');
14
+ option.value = folder;
15
+ option.textContent = folder;
16
+ folderSelect.appendChild(option);
17
+ });
18
+ }
19
+ })
20
+ .catch(err => {
21
+ console.warn('Backend API not available (likely running static).', err);
22
+ // Optional: Hide controls if API fails
23
+ const container = document.getElementById('source-control');
24
+ if (container) container.style.display = 'none';
25
+ });
26
+
27
+ // Handle Load
28
+ loadBtn.addEventListener('click', () => {
29
+ const target = folderSelect.value;
30
+ if (!target) {
31
+ alert('Please select a folder first.');
32
+ return;
33
+ }
34
+
35
+ loadBtn.disabled = true;
36
+ loadBtn.textContent = '...';
37
+
38
+ fetch('/api/build', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ target: target })
42
+ })
43
+ .then(response => response.json())
44
+ .then(data => {
45
+ if (data.success) {
46
+ // Reload graph data logic.
47
+ // Reload page to refresh data.js
48
+ window.location.reload();
49
+ } else {
50
+ alert('Build Failed: ' + data.error);
51
+ }
52
+ })
53
+ .catch(err => {
54
+ alert('Error: ' + err);
55
+ })
56
+ .finally(() => {
57
+ loadBtn.disabled = false;
58
+ loadBtn.textContent = 'Load';
59
+ });
60
+ });
61
+ });