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,356 @@
1
+ // Analysis & Export Module
2
+ console.log("Analysis Module: Parsing...");
3
+
4
+ document.addEventListener("DOMContentLoaded", () => {
5
+ console.log("Analysis Module: DOMContentLoaded");
6
+
7
+ const UI = {
8
+ panel: document.getElementById("analysis-panel"),
9
+ resizer: document.getElementById("analysis-resizer"),
10
+ btn: document.getElementById("analysis-btn"),
11
+ closeBtn: document.querySelector(".close-panel"),
12
+ quickDist: document.getElementById("quick-distribution"),
13
+ histogram: document.getElementById("histogram-container"),
14
+ // Controls
15
+ strategy: document.getElementById("export-strategy"),
16
+ clusterFilter: document.getElementById("cluster-filter"),
17
+ slider: document.getElementById("export-threshold-slider"),
18
+ val: document.getElementById("export-threshold-val"),
19
+ count: document.getElementById("selected-count"),
20
+ btnJson: document.getElementById("export-json-btn"),
21
+ btnZip: document.getElementById("export-zip-btn"),
22
+ // Table
23
+ tableBody: document.getElementById("node-table-body"),
24
+ headers: document.querySelectorAll(".sortable")
25
+ };
26
+
27
+ // State
28
+ const AppState = {
29
+ threshold: 5,
30
+ strategy: 'top-percent',
31
+ cluster: 'all', // 'all' or specific clusterId
32
+ sortField: 'total', // 'name', 'cluster', 'in', 'out', 'total'
33
+ sortOrder: 'desc' // 'asc', 'desc'
34
+ };
35
+
36
+ // --- 0. Init Cluster Options ---
37
+ function initClusters() {
38
+ if (!UI.clusterFilter || typeof graphData === 'undefined') return;
39
+
40
+ // Find unique clusters
41
+ const clusters = new Set();
42
+ graphData.nodes.forEach(n => {
43
+ if (n.clusterId) clusters.add(n.clusterId);
44
+ });
45
+
46
+ // Sort
47
+ const sortedClusters = Array.from(clusters).sort();
48
+
49
+ // Populate
50
+ sortedClusters.forEach(c => {
51
+ const opt = document.createElement('option');
52
+ opt.value = c;
53
+ opt.textContent = c; // Or "Cluster " + c
54
+ UI.clusterFilter.appendChild(opt);
55
+ });
56
+ }
57
+ // Delay slightly to ensure data.js is parsed
58
+ setTimeout(initClusters, 0);
59
+
60
+
61
+ // --- 1. Quick Distribution (Immediate) ---
62
+ function initQuickDist() {
63
+ if (!UI.quickDist) return;
64
+ if (typeof graphData === 'undefined') return;
65
+
66
+ const degrees = graphData.nodes.map(n => n.inDegree + n.outDegree);
67
+ const maxDeg = Math.max(...degrees, 1);
68
+ const buckets = new Array(15).fill(0);
69
+
70
+ degrees.forEach(d => {
71
+ const idx = Math.min(Math.floor((d / maxDeg) * 15), 14);
72
+ buckets[idx]++;
73
+ });
74
+
75
+ const maxCount = Math.max(...buckets, 1);
76
+
77
+ UI.quickDist.innerHTML = buckets.map(count => {
78
+ const pct = (count / maxCount) * 100;
79
+ const bg = count > 0 ? '#4ecdc4' : '#333';
80
+ return `<div style="flex: 1; background: ${bg}; height: ${pct}%; border-radius: 1px;"></div>`;
81
+ }).join('');
82
+ }
83
+ setTimeout(initQuickDist, 0);
84
+
85
+ // --- 2. Panel Toggle ---
86
+ if (UI.btn && UI.panel && UI.resizer) {
87
+ UI.btn.addEventListener("click", () => {
88
+ const isOpen = UI.panel.classList.contains("open");
89
+
90
+ if (isOpen) {
91
+ UI.panel.classList.remove("open");
92
+ UI.resizer.style.display = "none";
93
+ } else {
94
+ UI.panel.classList.add("open");
95
+ UI.resizer.style.display = "block";
96
+
97
+ if (!UI.panel.style.height || UI.panel.style.height === '0px') {
98
+ UI.panel.style.height = "500px";
99
+ }
100
+
101
+ requestAnimationFrame(() => {
102
+ renderHistogram();
103
+ updateStats();
104
+ });
105
+ }
106
+ });
107
+
108
+ if (UI.closeBtn) {
109
+ UI.closeBtn.addEventListener("click", () => {
110
+ UI.panel.classList.remove("open");
111
+ UI.resizer.style.display = "none";
112
+ });
113
+ }
114
+ }
115
+
116
+ // --- 3. Resizer ---
117
+ let resizing = false;
118
+ if (UI.resizer) {
119
+ UI.resizer.addEventListener("mousedown", (e) => {
120
+ resizing = true;
121
+ document.body.style.cursor = "row-resize";
122
+ document.addEventListener("mousemove", onMouseMove);
123
+ document.addEventListener("mouseup", onMouseUp);
124
+ e.preventDefault();
125
+ });
126
+ }
127
+
128
+ function onMouseMove(e) {
129
+ if (!resizing) return;
130
+ const totalH = window.innerHeight;
131
+ const newH = totalH - e.clientY;
132
+ if (newH < 100 || newH > totalH - 100) return;
133
+ UI.panel.style.height = `${newH}px`;
134
+ }
135
+
136
+ function onMouseUp() {
137
+ resizing = false;
138
+ document.body.style.cursor = "";
139
+ document.removeEventListener("mousemove", onMouseMove);
140
+ document.removeEventListener("mouseup", onMouseUp);
141
+ window.dispatchEvent(new Event('resize'));
142
+ }
143
+
144
+ // --- 4. Histogram (D3) ---
145
+ function renderHistogram() {
146
+ if (!UI.histogram) return;
147
+ UI.histogram.innerHTML = "";
148
+ const w = UI.histogram.clientWidth;
149
+ const h = UI.histogram.clientHeight;
150
+ if (w === 0 || h === 0) return;
151
+
152
+ const margin = {top: 10, right: 10, bottom: 20, left: 30};
153
+ const counts = new Map();
154
+
155
+ // Use filtered nodes for histogram? Or all? Usually histogram shows ALL context.
156
+ // Let's stick to ALL nodes for the main histogram to show global context.
157
+ // OR: should it reflect the current cluster filter?
158
+ // Requirement: "support users to perform clustering filtering".
159
+ // It's better if the histogram reflects the CURRENTLY VIEWED set (filtered by cluster).
160
+
161
+ // Get nodes filtered ONLY by cluster (ignore threshold for histogram context)
162
+ let contextNodes = graphData.nodes;
163
+ if (AppState.cluster !== 'all') {
164
+ contextNodes = contextNodes.filter(n => n.clusterId === AppState.cluster);
165
+ }
166
+
167
+ contextNodes.forEach(n => {
168
+ const d = n.inDegree + n.outDegree;
169
+ counts.set(d, (counts.get(d)||0)+1);
170
+ });
171
+ const data = Array.from(counts, ([d, c]) => ({d, c})).sort((a,b)=>a.d - b.d);
172
+
173
+ const svg = d3.select(UI.histogram).append("svg").attr("width", w).attr("height", h);
174
+ const x = d3.scaleLinear().domain([0, d3.max(data, i=>i.d)||0]).range([margin.left, w-margin.right]);
175
+ const y = d3.scaleLinear().domain([0, d3.max(data, i=>i.c)||0]).range([h-margin.bottom, margin.top]);
176
+
177
+ svg.selectAll("rect").data(data).join("rect")
178
+ .attr("x", i => x(i.d)-2).attr("y", i => y(i.c))
179
+ .attr("width", 4).attr("height", i => y(0) - y(i.c))
180
+ .attr("fill", "#61dafb");
181
+
182
+ svg.append("g").attr("transform", `translate(0,${h-margin.bottom})`)
183
+ .call(d3.axisBottom(x).ticks(10).tickSizeOuter(0));
184
+ svg.append("g").attr("transform", `translate(${margin.left},0)`)
185
+ .call(d3.axisLeft(y).ticks(5));
186
+ }
187
+
188
+ // --- 5. Table Logic ---
189
+ function renderTable(nodes) {
190
+ if (!UI.tableBody) return;
191
+
192
+ // Sort
193
+ const sorted = [...nodes].sort((a, b) => {
194
+ let valA, valB;
195
+
196
+ switch(AppState.sortField) {
197
+ case 'name': valA = a.label.toLowerCase(); valB = b.label.toLowerCase(); break;
198
+ case 'cluster': valA = a.clusterId || ''; valB = b.clusterId || ''; break;
199
+ case 'in': valA = a.inDegree; valB = b.inDegree; break;
200
+ case 'out': valA = a.outDegree; valB = b.outDegree; break;
201
+ case 'total': default: valA = a.inDegree + a.outDegree; valB = b.inDegree + b.outDegree; break;
202
+ }
203
+
204
+ if (valA < valB) return AppState.sortOrder === 'asc' ? -1 : 1;
205
+ if (valA > valB) return AppState.sortOrder === 'asc' ? 1 : -1;
206
+ return 0;
207
+ });
208
+
209
+ // Update Header Indicators
210
+ UI.headers.forEach(h => {
211
+ const span = h.querySelector('span');
212
+ if (h.dataset.sort === AppState.sortField) {
213
+ h.style.color = '#61dafb';
214
+ span.innerText = AppState.sortOrder === 'asc' ? '▲' : '▼';
215
+ } else {
216
+ h.style.color = '';
217
+ span.innerText = '';
218
+ }
219
+ });
220
+
221
+ // Render Rows
222
+ UI.tableBody.innerHTML = sorted.map(n => `
223
+ <div class="node-row" title="${n.label}">
224
+ <div style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${n.label}</div>
225
+ <div style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color: #aaa;">${n.clusterId || '-'}</div>
226
+ <div>${n.inDegree}</div>
227
+ <div>${n.outDegree}</div>
228
+ <div>${n.inDegree + n.outDegree}</div>
229
+ </div>
230
+ `).join('');
231
+ }
232
+
233
+ UI.headers.forEach(h => {
234
+ h.addEventListener('click', () => {
235
+ const field = h.dataset.sort;
236
+ if (AppState.sortField === field) {
237
+ AppState.sortOrder = AppState.sortOrder === 'asc' ? 'desc' : 'asc';
238
+ } else {
239
+ AppState.sortField = field;
240
+ AppState.sortOrder = 'desc';
241
+ }
242
+ updateStats();
243
+ });
244
+ });
245
+
246
+
247
+ // --- 6. Export & Filter Logic ---
248
+ function getFilteredData() {
249
+ let nodes = graphData.nodes;
250
+
251
+ // 1. Cluster Filter
252
+ if (AppState.cluster !== 'all') {
253
+ nodes = nodes.filter(n => n.clusterId === AppState.cluster);
254
+ }
255
+
256
+ // 2. Degree/Rank Filter
257
+ let filteredNodes = [];
258
+ if (AppState.strategy === 'min-degree') {
259
+ filteredNodes = nodes.filter(n => (n.inDegree + n.outDegree) >= AppState.threshold);
260
+ } else {
261
+ // Top Percent
262
+ const sorted = [...nodes].sort((a,b)=>(b.inDegree+b.outDegree)-(a.inDegree+a.outDegree));
263
+ const cut = Math.ceil(nodes.length * (AppState.threshold/100));
264
+ filteredNodes = sorted.slice(0, cut);
265
+ }
266
+
267
+ // 3. Filter Edges
268
+ const nodeIds = new Set(filteredNodes.map(n => n.id));
269
+
270
+ // Requirement: Export complete in-degree and out-degree relationships of exported nodes.
271
+ // This means we include an edge if AT LEAST ONE of its endpoints is in our filtered node list.
272
+ const filteredEdges = graphData.edges.filter(e =>
273
+ nodeIds.has(e.source) || nodeIds.has(e.target)
274
+ );
275
+
276
+ return { nodes: filteredNodes, edges: filteredEdges };
277
+ }
278
+
279
+ function updateStats() {
280
+ const { nodes } = getFilteredData();
281
+ if (UI.count) {
282
+ const tot = graphData.nodes.length;
283
+ const pct = ((nodes.length/tot)*100).toFixed(1);
284
+ // Use translation for "Selected:" label is handled in HTML, here just numbers
285
+ // But wait, the label is separate <span data-i18n="selected">
286
+ UI.count.innerText = `${nodes.length} / ${tot} (${pct}%)`;
287
+ }
288
+ renderTable(nodes);
289
+ renderHistogram();
290
+ }
291
+
292
+ // Expose update function for Localization
293
+ window.updateAnalysisUI = function() {
294
+ // Re-render table to update headers if we were generating them via JS (we are not, they are HTML)
295
+ // But we need to update dynamic text that might contain English words
296
+ if (UI.val) {
297
+ const suffix = AppState.strategy === 'top-percent' ? '%' : '';
298
+ UI.val.innerText = AppState.threshold + suffix;
299
+ }
300
+ updateStats();
301
+ };
302
+
303
+ if (UI.clusterFilter) UI.clusterFilter.addEventListener("change", (e) => {
304
+ AppState.cluster = e.target.value;
305
+ updateStats();
306
+ });
307
+
308
+ if (UI.strategy) UI.strategy.addEventListener("change", (e) => {
309
+ AppState.strategy = e.target.value;
310
+ updateStats();
311
+ });
312
+
313
+ if (UI.slider) UI.slider.addEventListener("input", (e) => {
314
+ AppState.threshold = parseInt(e.target.value);
315
+ if (UI.val) {
316
+ const suffix = AppState.strategy === 'top-percent' ? '%' : '';
317
+ UI.val.innerText = AppState.threshold + suffix;
318
+ }
319
+ updateStats();
320
+ });
321
+
322
+ if (UI.btnJson) UI.btnJson.addEventListener("click", () => {
323
+ const data = getFilteredData();
324
+ // Export full graph structure with edge info
325
+ download(JSON.stringify(data, null, 2), "export.json", "application/json");
326
+ });
327
+
328
+ if (UI.btnZip) UI.btnZip.addEventListener("click", () => {
329
+ if (typeof JSZip === 'undefined') return alert("JSZip missing");
330
+ const zip = new JSZip();
331
+ const { nodes, edges } = getFilteredData();
332
+
333
+ // Add JSON export to ZIP
334
+ zip.file("graph_data.json", JSON.stringify({ nodes, edges }, null, 2));
335
+
336
+ const f = zip.folder("notes");
337
+ nodes.forEach(n => {
338
+ const name = n.id.endsWith(".md") ? n.id : n.id+".md";
339
+ f.file(name, (n.content||"") + `\n\n---\nDegree: ${n.inDegree+n.outDegree}\nCluster: ${n.clusterId}`);
340
+ });
341
+ zip.generateAsync({type:"blob"}).then(b => {
342
+ const url = URL.createObjectURL(b);
343
+ const a = document.createElement("a");
344
+ a.href = url; a.download = "notes.zip";
345
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
346
+ });
347
+ });
348
+
349
+ function download(content, name, type) {
350
+ const blob = new Blob([content], {type});
351
+ const url = URL.createObjectURL(blob);
352
+ const a = document.createElement("a");
353
+ a.href = url; a.download = name;
354
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
355
+ }
356
+ });