seo-intel 1.5.1 → 1.5.21
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/CHANGELOG.md +8 -0
- package/Start SEO Intel.command +10 -0
- package/analyses/blog-draft/index.js +62 -10
- package/cli.js +239 -0
- package/lib/scan-export.js +180 -0
- package/package.json +1 -1
- package/reports/generate-html.js +491 -51
- package/server.js +355 -126
package/reports/generate-html.js
CHANGED
|
@@ -33,6 +33,7 @@ function cardExportHtml(section, project) {
|
|
|
33
33
|
<button data-fmt="json"><i class="fa-solid fa-code"></i> JSON</button>
|
|
34
34
|
<button data-fmt="csv"><i class="fa-solid fa-table"></i> CSV</button>
|
|
35
35
|
<button data-fmt="zip"><i class="fa-solid fa-file-zipper"></i> ZIP (all)</button>
|
|
36
|
+
<label style="display:flex;align-items:center;gap:5px;padding:4px 10px;font-size:0.6rem;color:var(--accent-gold);cursor:pointer;border-top:1px solid var(--border-subtle);margin-top:2px;padding-top:6px;"><input type="checkbox" class="card-ai-toggle" style="accent-color:var(--accent-gold);" /> <i class="fa-solid fa-wand-magic-sparkles"></i> AI Smart</label>
|
|
36
37
|
</div>
|
|
37
38
|
</div>`;
|
|
38
39
|
}
|
|
@@ -104,7 +105,7 @@ export function gatherProjectData(db, project, config) {
|
|
|
104
105
|
const performanceBubbles = getPerformanceBubbleData(db, project);
|
|
105
106
|
const headingFlow = getHeadingFlowData(db, project, config);
|
|
106
107
|
const territoryTreemap = getTerritoryTreemapData(db, project, config);
|
|
107
|
-
const topicClusters = getTopicClusterData(project); // from
|
|
108
|
+
const topicClusters = getTopicClusterData(db, project); // auto-generates from DB if no file exists
|
|
108
109
|
const linkDna = getLinkDnaData(db, project, config);
|
|
109
110
|
const linkRadarPulse = getLinkRadarPulseData(db, project, config);
|
|
110
111
|
|
|
@@ -1648,15 +1649,16 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1648
1649
|
cursor: pointer;
|
|
1649
1650
|
}
|
|
1650
1651
|
.export-viewer {
|
|
1651
|
-
|
|
1652
|
-
padding: 12px;
|
|
1652
|
+
padding: 12px 16px;
|
|
1653
1653
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
1654
1654
|
font-size: 0.66rem;
|
|
1655
1655
|
line-height: 1.7;
|
|
1656
1656
|
color: var(--text-muted);
|
|
1657
1657
|
overflow-y: auto;
|
|
1658
|
-
|
|
1658
|
+
min-height: 60px;
|
|
1659
|
+
max-height: 600px;
|
|
1659
1660
|
}
|
|
1661
|
+
.export-viewer:empty, .export-viewer:has(> div:only-child) { max-height: 80px; }
|
|
1660
1662
|
.export-viewer h1, .export-viewer h2, .export-viewer h3 { color: var(--text-primary); margin: 12px 0 6px; font-family: var(--font-display); font-size: 0.8rem; }
|
|
1661
1663
|
.export-viewer h2 { font-size: 0.75rem; }
|
|
1662
1664
|
.export-viewer h3 { font-size: 0.7rem; }
|
|
@@ -1673,6 +1675,62 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1673
1675
|
|
|
1674
1676
|
/* Action exports integrated into terminal panel — CSS cleaned up */
|
|
1675
1677
|
|
|
1678
|
+
/* ── AI Smart Export Modal ── */
|
|
1679
|
+
.ai-export-overlay {
|
|
1680
|
+
position: fixed; inset: 0; z-index: 9999;
|
|
1681
|
+
background: rgba(0,0,0,0.85); backdrop-filter: blur(12px);
|
|
1682
|
+
display: flex; align-items: center; justify-content: center;
|
|
1683
|
+
opacity: 0; pointer-events: none; transition: opacity 0.4s ease;
|
|
1684
|
+
}
|
|
1685
|
+
.ai-export-overlay.active { opacity: 1; pointer-events: all; }
|
|
1686
|
+
.ai-export-card {
|
|
1687
|
+
position: relative; z-index: 2;
|
|
1688
|
+
background: rgba(18,18,18,0.85); border: 1px solid rgba(212,175,55,0.2);
|
|
1689
|
+
border-radius: 16px; padding: 32px 40px 28px; text-align: center;
|
|
1690
|
+
box-shadow: 0 0 60px rgba(212,175,55,0.08), 0 24px 48px rgba(0,0,0,0.5);
|
|
1691
|
+
min-width: 340px; max-width: 420px;
|
|
1692
|
+
}
|
|
1693
|
+
.ai-export-card h3 {
|
|
1694
|
+
font-family: var(--font-display); font-size: 1.1rem; color: var(--accent-gold);
|
|
1695
|
+
margin: 0 0 4px; letter-spacing: -0.02em;
|
|
1696
|
+
}
|
|
1697
|
+
.ai-export-card .ai-subtitle {
|
|
1698
|
+
font-size: 0.68rem; color: var(--text-muted); margin-bottom: 24px;
|
|
1699
|
+
}
|
|
1700
|
+
.ai-export-status {
|
|
1701
|
+
font-size: 0.72rem; color: var(--text-secondary); margin-bottom: 16px;
|
|
1702
|
+
min-height: 1.2em;
|
|
1703
|
+
}
|
|
1704
|
+
.ai-export-status i { color: var(--accent-gold); margin-right: 6px; }
|
|
1705
|
+
.ai-progress-track {
|
|
1706
|
+
width: 100%; height: 4px; background: rgba(255,255,255,0.06);
|
|
1707
|
+
border-radius: 4px; overflow: hidden; margin-bottom: 20px;
|
|
1708
|
+
position: relative;
|
|
1709
|
+
}
|
|
1710
|
+
.ai-progress-bar {
|
|
1711
|
+
height: 100%; width: 0%; border-radius: 4px;
|
|
1712
|
+
background: linear-gradient(90deg, var(--accent-gold), #f5c842, var(--accent-gold));
|
|
1713
|
+
background-size: 200% 100%;
|
|
1714
|
+
animation: ai-shimmer 1.5s ease infinite;
|
|
1715
|
+
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1716
|
+
}
|
|
1717
|
+
@keyframes ai-shimmer {
|
|
1718
|
+
0% { background-position: 200% 0; }
|
|
1719
|
+
100% { background-position: -200% 0; }
|
|
1720
|
+
}
|
|
1721
|
+
.ai-progress-pct {
|
|
1722
|
+
font-size: 0.6rem; color: var(--text-muted); font-family: 'SF Mono', monospace;
|
|
1723
|
+
margin-top: -14px; margin-bottom: 12px; text-align: right;
|
|
1724
|
+
}
|
|
1725
|
+
.ai-export-cancel {
|
|
1726
|
+
font-size: 0.62rem; color: var(--text-muted); background: none; border: 1px solid var(--border-subtle);
|
|
1727
|
+
padding: 4px 14px; border-radius: var(--radius); cursor: pointer; transition: all 0.2s;
|
|
1728
|
+
}
|
|
1729
|
+
.ai-export-cancel:hover { color: var(--text-primary); border-color: var(--text-muted); }
|
|
1730
|
+
#aiSwarmCanvas {
|
|
1731
|
+
position: absolute; inset: 0; z-index: 1; pointer-events: none; border-radius: 0;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1676
1734
|
</style>
|
|
1677
1735
|
</head>`;
|
|
1678
1736
|
|
|
@@ -2194,7 +2252,8 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2194
2252
|
<div class="draft-menu" id="draftMenu${suffix}">
|
|
2195
2253
|
<div class="draft-menu-section">Type</div>
|
|
2196
2254
|
<label class="draft-option"><input type="radio" name="draftType${suffix}" value="blog" checked /> <i class="fa-solid fa-blog"></i> Blog Post</label>
|
|
2197
|
-
<label class="draft-option"><input type="radio" name="draftType${suffix}" value="docs"
|
|
2255
|
+
<label class="draft-option"><input type="radio" name="draftType${suffix}" value="docs" /> <i class="fa-solid fa-book"></i> Documentation</label>
|
|
2256
|
+
<label class="draft-option"><input type="radio" name="draftType${suffix}" value="social" /> <i class="fa-solid fa-share-nodes"></i> Social Media</label>
|
|
2198
2257
|
<div class="draft-menu-section" style="margin-top:8px;">Topic <span style="font-size:0.55rem;opacity:0.4;">(optional)</span></div>
|
|
2199
2258
|
<input type="text" id="draftTopic${suffix}" class="draft-topic-input" placeholder="e.g. solana rpc, site speed..." />
|
|
2200
2259
|
<div class="draft-menu-section" style="margin-top:8px;">Language</div>
|
|
@@ -2214,34 +2273,20 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2214
2273
|
<div class="profile-export-picker" id="profilePicker${suffix}">
|
|
2215
2274
|
<button class="export-btn profile-export-trigger"><i class="fa-solid fa-download"></i> Export Report <i class="fa-solid fa-chevron-down" style="font-size:0.55rem;margin-left:auto;opacity:0.5;"></i></button>
|
|
2216
2275
|
<div class="profile-export-menu">
|
|
2217
|
-
<div class="draft-menu-section">
|
|
2218
|
-
<label class="draft-option"><input type="radio" name="exportProfile${suffix}" value="dev" /> <i class="fa-solid fa-wrench"></i> Developer <span style="font-size:0.5rem;opacity:0.45;margin-left:2px;">technical fixes, schema gaps</span></label>
|
|
2219
|
-
<label class="draft-option"><input type="radio" name="exportProfile${suffix}" value="content" checked /> <i class="fa-solid fa-pen-fancy"></i> Content <span style="font-size:0.5rem;opacity:0.45;margin-left:2px;">keyword gaps, opportunities</span></label>
|
|
2220
|
-
<label class="draft-option"><input type="radio" name="exportProfile${suffix}" value="ai-pipeline" /> <i class="fa-solid fa-robot"></i> AI Pipeline <span style="font-size:0.5rem;opacity:0.45;margin-left:2px;">structured JSON for LLMs</span></label>
|
|
2221
|
-
<div class="draft-menu-section" style="margin-top:8px;">Format</div>
|
|
2276
|
+
<div class="draft-menu-section">Format</div>
|
|
2222
2277
|
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
|
2223
2278
|
<label class="draft-option" style="flex:1;min-width:60px;"><input type="radio" name="exportFmt${suffix}" value="md" checked /> <i class="fa-solid fa-file-lines"></i> MD</label>
|
|
2224
2279
|
<label class="draft-option" style="flex:1;min-width:60px;"><input type="radio" name="exportFmt${suffix}" value="json" /> <i class="fa-solid fa-code"></i> JSON</label>
|
|
2225
2280
|
<label class="draft-option" style="flex:1;min-width:60px;"><input type="radio" name="exportFmt${suffix}" value="csv" /> <i class="fa-solid fa-table"></i> CSV</label>
|
|
2226
2281
|
<label class="draft-option" style="flex:1;min-width:60px;"><input type="radio" name="exportFmt${suffix}" value="zip" /> <i class="fa-solid fa-file-zipper"></i> ZIP</label>
|
|
2227
2282
|
</div>
|
|
2283
|
+
<label class="draft-option" style="margin-top:8px;border-color:var(--accent-gold);background:rgba(212,175,55,0.04);"><input type="checkbox" name="aiExport${suffix}" value="1" /> <i class="fa-solid fa-wand-magic-sparkles" style="color:var(--accent-gold);"></i> AI Smart Export</label>
|
|
2284
|
+
<div style="font-size:0.55rem;color:var(--text-muted);padding:2px 4px;">Fills gaps, adds priorities & action tips via AI</div>
|
|
2228
2285
|
<button class="draft-generate-btn profile-download-btn" data-project="${project}" style="margin-top:10px;"><i class="fa-solid fa-download"></i> Download</button>
|
|
2229
2286
|
</div>
|
|
2230
2287
|
</div>
|
|
2231
2288
|
<button class="export-btn download-all-btn" data-project="${project}" style="font-size:0.58rem;opacity:0.6;"><i class="fa-solid fa-file-zipper"></i> Raw Full Export (ZIP)</button>
|
|
2232
2289
|
</div>
|
|
2233
|
-
<div style="position:relative;">
|
|
2234
|
-
<div id="exportSaveStatus${suffix}" style="display:none;padding:4px 10px;font-size:.6rem;color:var(--color-success);background:rgba(80,200,120,0.06);border-bottom:1px solid rgba(80,200,120,0.15);font-family:'SF Mono',monospace;">
|
|
2235
|
-
<i class="fa-solid fa-check" style="margin-right:4px;"></i><span></span>
|
|
2236
|
-
</div>
|
|
2237
|
-
<button id="exportExpand${suffix}" class="export-expand-btn" title="Expand viewer"><i class="fa-solid fa-expand"></i></button>
|
|
2238
|
-
<div id="exportViewer${suffix}" class="export-viewer">
|
|
2239
|
-
<div style="color:#444;padding:20px 0;text-align:center;">
|
|
2240
|
-
<i class="fa-solid fa-file-export" style="font-size:1.2rem;margin-bottom:8px;display:block;"></i>
|
|
2241
|
-
Click an export to generate an<br/>implementation-ready action brief.
|
|
2242
|
-
</div>
|
|
2243
|
-
</div>
|
|
2244
|
-
</div>
|
|
2245
2290
|
` : `
|
|
2246
2291
|
<div style="padding:20px 14px;text-align:center;">
|
|
2247
2292
|
<i class="fa-solid fa-lock" style="font-size:1rem;color:var(--accent-gold);margin-bottom:8px;display:block;"></i>
|
|
@@ -2251,6 +2296,22 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2251
2296
|
`}
|
|
2252
2297
|
</div>
|
|
2253
2298
|
</div>
|
|
2299
|
+
${pro ? `
|
|
2300
|
+
<div class="viewer-row" style="max-width:var(--max-width);margin:0 auto;">
|
|
2301
|
+
<div style="position:relative;background:#0e0e0e;border:1px solid var(--border-card);border-radius:0 0 var(--radius) var(--radius);border-top:none;">
|
|
2302
|
+
<div id="exportSaveStatus${suffix}" style="display:none;padding:4px 10px;font-size:.6rem;color:var(--color-success);background:rgba(80,200,120,0.06);border-bottom:1px solid rgba(80,200,120,0.15);font-family:'SF Mono',monospace;">
|
|
2303
|
+
<i class="fa-solid fa-check" style="margin-right:4px;"></i><span></span>
|
|
2304
|
+
</div>
|
|
2305
|
+
<button id="exportExpand${suffix}" class="export-expand-btn" title="Expand viewer"><i class="fa-solid fa-expand"></i></button>
|
|
2306
|
+
<div id="exportViewer${suffix}" class="export-viewer">
|
|
2307
|
+
<div style="color:#444;padding:20px 0;text-align:center;">
|
|
2308
|
+
<i class="fa-solid fa-file-export" style="font-size:1.2rem;margin-bottom:8px;display:block;"></i>
|
|
2309
|
+
Click an export or generate a draft — output appears here.
|
|
2310
|
+
</div>
|
|
2311
|
+
</div>
|
|
2312
|
+
</div>
|
|
2313
|
+
</div>
|
|
2314
|
+
` : ''}
|
|
2254
2315
|
|
|
2255
2316
|
<script>
|
|
2256
2317
|
(function() {
|
|
@@ -2427,6 +2488,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2427
2488
|
if (msg.type === 'stdout') mdContent += msg.data + '\\n';
|
|
2428
2489
|
else if (msg.type === 'stderr') mdContent += msg.data + '\\n';
|
|
2429
2490
|
else if (msg.type === 'exit') {
|
|
2491
|
+
var exitCode = msg.data && msg.data.code;
|
|
2430
2492
|
running = false;
|
|
2431
2493
|
status.textContent = 'done';
|
|
2432
2494
|
status.style.color = 'var(--color-success)';
|
|
@@ -2449,11 +2511,17 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2449
2511
|
}
|
|
2450
2512
|
// Show save status
|
|
2451
2513
|
var saveEl = document.getElementById('exportSaveStatus' + suffix);
|
|
2452
|
-
if (saveEl &&
|
|
2453
|
-
var slugName = cmd === 'suggest-usecases' ? 'suggestions' : (scope || 'all');
|
|
2514
|
+
if (saveEl && exitCode === 0) {
|
|
2454
2515
|
var dateStr = new Date().toISOString().slice(0, 10);
|
|
2516
|
+
var savedName;
|
|
2517
|
+
if (cmd === 'aeo') {
|
|
2518
|
+
savedName = proj + '-aeo-' + dateStr + '.md';
|
|
2519
|
+
} else {
|
|
2520
|
+
var slugName = cmd === 'suggest-usecases' ? 'suggestions' : (scope || 'all');
|
|
2521
|
+
savedName = proj + '-' + slugName + '-' + dateStr + '.md';
|
|
2522
|
+
}
|
|
2455
2523
|
saveEl.style.display = 'block';
|
|
2456
|
-
saveEl.querySelector('span').textContent = 'Saved → reports/' +
|
|
2524
|
+
saveEl.querySelector('span').textContent = 'Saved → reports/' + savedName;
|
|
2457
2525
|
}
|
|
2458
2526
|
}
|
|
2459
2527
|
} catch (_) {}
|
|
@@ -2468,20 +2536,22 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2468
2536
|
});
|
|
2469
2537
|
});
|
|
2470
2538
|
|
|
2471
|
-
// Draft dropdown
|
|
2539
|
+
// Draft dropdown — use capture phase to match card-export handler
|
|
2472
2540
|
var draftTrigger = document.getElementById('draftTrigger' + suffix);
|
|
2473
2541
|
var draftMenu = document.getElementById('draftMenu' + suffix);
|
|
2474
2542
|
var draftGenerate = document.getElementById('draftGenerate' + suffix);
|
|
2475
2543
|
if (draftTrigger && draftMenu) {
|
|
2476
2544
|
draftTrigger.addEventListener('click', function(e) {
|
|
2477
|
-
e.
|
|
2545
|
+
e.stopImmediatePropagation();
|
|
2546
|
+
// Close other menus
|
|
2547
|
+
document.querySelectorAll('.draft-menu.open').forEach(function(m) { if (m !== draftMenu) m.classList.remove('open'); });
|
|
2548
|
+
document.querySelectorAll('.profile-export-menu').forEach(function(m) { m.style.display = 'none'; });
|
|
2478
2549
|
draftMenu.classList.toggle('open');
|
|
2479
|
-
});
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
});
|
|
2550
|
+
}, true);
|
|
2551
|
+
// Clicks inside the menu should not close it
|
|
2552
|
+
draftMenu.addEventListener('click', function(e) {
|
|
2553
|
+
e.stopImmediatePropagation();
|
|
2554
|
+
}, true);
|
|
2485
2555
|
}
|
|
2486
2556
|
if (draftGenerate) {
|
|
2487
2557
|
draftGenerate.addEventListener('click', function() {
|
|
@@ -2494,29 +2564,28 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2494
2564
|
var lang = langEl ? langEl.value : 'en';
|
|
2495
2565
|
var topic = topicEl ? topicEl.value.trim() : '';
|
|
2496
2566
|
|
|
2497
|
-
if (draftType !== 'blog') return; // docs not yet supported
|
|
2498
|
-
|
|
2499
2567
|
draftMenu.classList.remove('open');
|
|
2500
2568
|
|
|
2501
|
-
// Run blog-draft via terminal SSE
|
|
2502
|
-
var extra = { lang: lang };
|
|
2503
|
-
if (topic) extra.topic = topic;
|
|
2504
|
-
|
|
2569
|
+
// Run blog-draft via terminal SSE — type is passed so prompt builder can adapt
|
|
2505
2570
|
var params = new URLSearchParams({ command: 'blog-draft' });
|
|
2506
2571
|
params.set('project', proj);
|
|
2507
2572
|
params.set('lang', lang);
|
|
2573
|
+
params.set('type', draftType);
|
|
2508
2574
|
params.set('save', '1');
|
|
2509
2575
|
if (topic) params.set('topic', topic);
|
|
2510
2576
|
|
|
2577
|
+
var typeLabels = { blog: 'blog post', docs: 'documentation', social: 'social media post' };
|
|
2578
|
+
var typeLabel = typeLabels[draftType] || draftType;
|
|
2579
|
+
|
|
2511
2580
|
if (!isServed) {
|
|
2512
|
-
var cmd = 'seo-intel blog-draft ' + proj + (topic ? ' --topic "' + topic + '"' : '') + ' --lang ' + lang + ' --save';
|
|
2581
|
+
var cmd = 'seo-intel blog-draft ' + proj + (topic ? ' --topic "' + topic + '"' : '') + ' --lang ' + lang + ' --type ' + draftType + ' --save';
|
|
2513
2582
|
if (exportViewer) {
|
|
2514
2583
|
exportViewer.innerHTML = '<div style="color:var(--color-danger);padding:12px;">Not connected. Run in terminal:<br/><code style="color:var(--accent-gold);">' + cmd + '</code></div>';
|
|
2515
2584
|
}
|
|
2516
2585
|
return;
|
|
2517
2586
|
}
|
|
2518
2587
|
|
|
2519
|
-
if (exportViewer) exportViewer.innerHTML = '<div style="color:var(--text-muted);padding:20px;text-align:center;"><i class="fa-solid fa-wand-magic-sparkles fa-spin" style="margin-right:6px;color:var(--accent-gold);"></i>Generating
|
|
2588
|
+
if (exportViewer) exportViewer.innerHTML = '<div style="color:var(--text-muted);padding:20px;text-align:center;"><i class="fa-solid fa-wand-magic-sparkles fa-spin" style="margin-right:6px;color:var(--accent-gold);"></i>Generating ' + typeLabel + ' draft...</div>';
|
|
2520
2589
|
|
|
2521
2590
|
var mdContent = '';
|
|
2522
2591
|
var es = new EventSource('/api/terminal?' + params.toString());
|
|
@@ -2597,6 +2666,16 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2597
2666
|
document.addEventListener('click', function(e) {
|
|
2598
2667
|
var btn = e.target.closest('.card-export-btn');
|
|
2599
2668
|
var fmtBtn = e.target.closest('[data-fmt]');
|
|
2669
|
+
// Clicks on AI toggle checkbox or its label inside card-export — don't close dropdown
|
|
2670
|
+
var aiLabel = e.target.closest('label');
|
|
2671
|
+
if (aiLabel && aiLabel.querySelector('.card-ai-toggle')) {
|
|
2672
|
+
e.stopImmediatePropagation();
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (e.target.classList && e.target.classList.contains('card-ai-toggle')) {
|
|
2676
|
+
e.stopImmediatePropagation();
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2600
2679
|
if (btn) {
|
|
2601
2680
|
var wrap = btn.closest('.card-export');
|
|
2602
2681
|
if (wrap) {
|
|
@@ -2615,8 +2694,14 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2615
2694
|
var sec = wrap2.getAttribute('data-section');
|
|
2616
2695
|
var proj2 = wrap2.getAttribute('data-project');
|
|
2617
2696
|
var fmt = fmtBtn.getAttribute('data-fmt');
|
|
2697
|
+
var aiToggle = wrap2.querySelector('.card-ai-toggle');
|
|
2698
|
+
var useAi = aiToggle && aiToggle.checked;
|
|
2699
|
+
var exportUrl = '/api/export/download?project=' + encodeURIComponent(proj2) + '§ion=' + encodeURIComponent(sec) + '&format=' + encodeURIComponent(fmt) + (useAi ? '&ai=true' : '');
|
|
2618
2700
|
if (window.location.protocol.startsWith('http')) {
|
|
2619
|
-
|
|
2701
|
+
if (useAi) {
|
|
2702
|
+
var loaderUrl = '/ai-loader?url=' + encodeURIComponent(exportUrl);
|
|
2703
|
+
window.open(loaderUrl, 'ai-export', 'width=600,height=480,menubar=no,toolbar=no,status=no');
|
|
2704
|
+
} else { window.location = exportUrl; }
|
|
2620
2705
|
}
|
|
2621
2706
|
return;
|
|
2622
2707
|
}
|
|
@@ -2649,17 +2734,26 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2649
2734
|
if (picker2) {
|
|
2650
2735
|
e.stopImmediatePropagation();
|
|
2651
2736
|
var projP = profDl.getAttribute('data-project');
|
|
2652
|
-
var profVal = picker2.querySelector('input[name^="exportProfile"]:checked');
|
|
2653
2737
|
var fmtVal = picker2.querySelector('input[name^="exportFmt"]:checked');
|
|
2654
|
-
var prof = profVal ? profVal.value : 'content';
|
|
2655
2738
|
var fmt2 = fmtVal ? fmtVal.value : 'md';
|
|
2739
|
+
var aiCheck = picker2.querySelector('input[name^="aiExport"]');
|
|
2740
|
+
var useAi2 = aiCheck && aiCheck.checked;
|
|
2656
2741
|
picker2.querySelector('.profile-export-menu').style.display = 'none';
|
|
2742
|
+
var exportUrl2 = '/api/export/download?project=' + encodeURIComponent(projP) + '&format=' + encodeURIComponent(fmt2) + (useAi2 ? '&ai=true' : '');
|
|
2657
2743
|
if (window.location.protocol.startsWith('http')) {
|
|
2658
|
-
|
|
2744
|
+
if (useAi2) {
|
|
2745
|
+
var loaderUrl2 = '/ai-loader?url=' + encodeURIComponent(exportUrl2);
|
|
2746
|
+
window.open(loaderUrl2, 'ai-export', 'width=600,height=480,menubar=no,toolbar=no,status=no');
|
|
2747
|
+
} else { window.location = exportUrl2; }
|
|
2659
2748
|
}
|
|
2660
2749
|
return;
|
|
2661
2750
|
}
|
|
2662
2751
|
}
|
|
2752
|
+
// Clicks inside an open profile-export-menu (radio buttons etc) — don't close
|
|
2753
|
+
if (e.target.closest('.profile-export-menu')) {
|
|
2754
|
+
e.stopImmediatePropagation();
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2663
2757
|
// Outside click — close all open dropdowns
|
|
2664
2758
|
document.querySelectorAll('.card-export.open').forEach(function(el) { el.classList.remove('open'); });
|
|
2665
2759
|
document.querySelectorAll('.profile-export-menu').forEach(function(m) { m.style.display = 'none'; });
|
|
@@ -4756,8 +4850,214 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
4756
4850
|
|
|
4757
4851
|
</script>`;
|
|
4758
4852
|
|
|
4853
|
+
// ── AI Smart Export Modal ──
|
|
4854
|
+
const aiModalHtml = `
|
|
4855
|
+
<div class="ai-export-overlay" id="aiExportOverlay">
|
|
4856
|
+
<div id="aiSwarmCanvas"></div>
|
|
4857
|
+
<div class="ai-export-card">
|
|
4858
|
+
<h3><i class="fa-solid fa-wand-magic-sparkles"></i> AI Smart Export</h3>
|
|
4859
|
+
<p class="ai-subtitle">Enriching your report with AI intelligence</p>
|
|
4860
|
+
<div class="ai-export-status" id="aiExportStatus"><i class="fa-solid fa-brain fa-beat-fade"></i> Initializing...</div>
|
|
4861
|
+
<div class="ai-progress-track"><div class="ai-progress-bar" id="aiProgressBar"></div></div>
|
|
4862
|
+
<div class="ai-progress-pct" id="aiProgressPct">0%</div>
|
|
4863
|
+
<button class="ai-export-cancel" id="aiExportCancel">Cancel</button>
|
|
4864
|
+
</div>
|
|
4865
|
+
</div>
|
|
4866
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
4867
|
+
<script>
|
|
4868
|
+
(function(){
|
|
4869
|
+
// ── Swarm animation (compact, gold-themed) ──
|
|
4870
|
+
var overlay = document.getElementById('aiExportOverlay');
|
|
4871
|
+
var swarmEl = document.getElementById('aiSwarmCanvas');
|
|
4872
|
+
var swarmInited = false, swarmRaf = null, swarmRenderer, swarmScene, swarmCam;
|
|
4873
|
+
|
|
4874
|
+
function initSwarm() {
|
|
4875
|
+
if (swarmInited) return;
|
|
4876
|
+
swarmInited = true;
|
|
4877
|
+
var N = 300, sc = new THREE.Scene();
|
|
4878
|
+
sc.fog = new THREE.FogExp2(0x000000, 0.004);
|
|
4879
|
+
var cam = new THREE.PerspectiveCamera(60, swarmEl.clientWidth / Math.max(swarmEl.clientHeight, 1), 1, 800);
|
|
4880
|
+
cam.position.set(0, 0, 200);
|
|
4881
|
+
var r = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
4882
|
+
r.setSize(swarmEl.clientWidth, swarmEl.clientHeight);
|
|
4883
|
+
r.setPixelRatio(Math.min(devicePixelRatio, 1.5));
|
|
4884
|
+
r.setClearColor(0x000000, 0);
|
|
4885
|
+
swarmEl.appendChild(r.domElement);
|
|
4886
|
+
swarmRenderer = r; swarmScene = sc; swarmCam = cam;
|
|
4887
|
+
|
|
4888
|
+
// Dot texture
|
|
4889
|
+
var cv = document.createElement('canvas'); cv.width = cv.height = 64;
|
|
4890
|
+
var cx = cv.getContext('2d'), grd = cx.createRadialGradient(32,32,0,32,32,32);
|
|
4891
|
+
grd.addColorStop(0, 'rgba(255,255,255,1)');
|
|
4892
|
+
grd.addColorStop(0.3, 'rgba(212,175,55,0.9)');
|
|
4893
|
+
grd.addColorStop(1, 'rgba(212,175,55,0)');
|
|
4894
|
+
cx.fillStyle = grd; cx.fillRect(0,0,64,64);
|
|
4895
|
+
var tex = new THREE.CanvasTexture(cv);
|
|
4896
|
+
|
|
4897
|
+
// Particles — galaxy spiral
|
|
4898
|
+
var pos = new Float32Array(N*3), col = new Float32Array(N*3), szArr = new Float32Array(N);
|
|
4899
|
+
for (var i = 0; i < N; i++) {
|
|
4900
|
+
var t = i/N, ao = (i%4)*(Math.PI/2), rd = Math.pow(t,0.5)*100, a = t*Math.PI*5 + ao;
|
|
4901
|
+
pos[i*3] = Math.cos(a)*rd; pos[i*3+1] = (Math.random()-0.5)*12*(1-t); pos[i*3+2] = Math.sin(a)*rd;
|
|
4902
|
+
var isGold = Math.random() > 0.7;
|
|
4903
|
+
if (isGold) { col[i*3]=0.83; col[i*3+1]=0.69; col[i*3+2]=0.22; szArr[i]=3; }
|
|
4904
|
+
else { col[i*3]=0.38; col[i*3+1]=0.51; col[i*3+2]=0.96; szArr[i]=1.5; }
|
|
4905
|
+
}
|
|
4906
|
+
var geo = new THREE.BufferGeometry();
|
|
4907
|
+
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
4908
|
+
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
|
|
4909
|
+
geo.setAttribute('size', new THREE.BufferAttribute(szArr, 1));
|
|
4910
|
+
|
|
4911
|
+
var vs = 'attribute float size; attribute vec3 color; varying vec3 vColor; void main(){ vColor=color; vec4 mv=modelViewMatrix*vec4(position,1.0); gl_PointSize=size*2.0*(200.0/-mv.z); gl_Position=projectionMatrix*mv; }';
|
|
4912
|
+
var fs = 'uniform sampler2D pointTexture; varying vec3 vColor; void main(){ vec4 tc=texture2D(pointTexture,gl_PointCoord); if(tc.a<0.1) discard; gl_FragColor=vec4(vColor*1.8,1.0)*tc; }';
|
|
4913
|
+
var mat = new THREE.ShaderMaterial({ uniforms:{ pointTexture:{value:tex} }, vertexShader:vs, fragmentShader:fs, blending:THREE.AdditiveBlending, depthTest:false, transparent:true });
|
|
4914
|
+
var pts = new THREE.Points(geo, mat);
|
|
4915
|
+
sc.add(pts);
|
|
4916
|
+
|
|
4917
|
+
// Connection lines
|
|
4918
|
+
var maxD = 28*28, lPos = new Float32Array(N*36), lGeo = new THREE.BufferGeometry();
|
|
4919
|
+
lGeo.setAttribute('position', new THREE.BufferAttribute(lPos, 3));
|
|
4920
|
+
var lMat = new THREE.LineBasicMaterial({ color: 0xd4af37, transparent:true, opacity:0.08, blending:THREE.AdditiveBlending });
|
|
4921
|
+
var lines = new THREE.LineSegments(lGeo, lMat); sc.add(lines);
|
|
4922
|
+
var vi=0, cnt=0;
|
|
4923
|
+
for (var i=0; i<N && cnt<N*4; i++) {
|
|
4924
|
+
for (var j=i+1; j<N && cnt<N*4; j++) {
|
|
4925
|
+
var dx=pos[i*3]-pos[j*3], dy=pos[i*3+1]-pos[j*3+1], dz=pos[i*3+2]-pos[j*3+2];
|
|
4926
|
+
if (dx*dx+dy*dy+dz*dz < maxD) {
|
|
4927
|
+
lPos[vi++]=pos[i*3]; lPos[vi++]=pos[i*3+1]; lPos[vi++]=pos[i*3+2];
|
|
4928
|
+
lPos[vi++]=pos[j*3]; lPos[vi++]=pos[j*3+1]; lPos[vi++]=pos[j*3+2];
|
|
4929
|
+
cnt++;
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
lGeo.setDrawRange(0, cnt*2); lGeo.attributes.position.needsUpdate = true;
|
|
4934
|
+
|
|
4935
|
+
function anim() {
|
|
4936
|
+
swarmRaf = requestAnimationFrame(anim);
|
|
4937
|
+
sc.rotation.y += 0.003; sc.rotation.x += 0.001;
|
|
4938
|
+
r.render(sc, cam);
|
|
4939
|
+
}
|
|
4940
|
+
anim();
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4943
|
+
function stopSwarm() { if (swarmRaf) cancelAnimationFrame(swarmRaf); swarmRaf = null; }
|
|
4944
|
+
function startSwarm() { initSwarm(); if (!swarmRaf) { var sc=swarmScene, cam=swarmCam, r=swarmRenderer; (function a(){ swarmRaf=requestAnimationFrame(a); sc.rotation.y+=0.003; sc.rotation.x+=0.001; r.render(sc,cam); })(); } }
|
|
4945
|
+
|
|
4946
|
+
// Resize handler
|
|
4947
|
+
window.addEventListener('resize', function() {
|
|
4948
|
+
if (swarmRenderer && overlay.classList.contains('active')) {
|
|
4949
|
+
swarmCam.aspect = swarmEl.clientWidth / Math.max(swarmEl.clientHeight, 1);
|
|
4950
|
+
swarmCam.updateProjectionMatrix();
|
|
4951
|
+
swarmRenderer.setSize(swarmEl.clientWidth, swarmEl.clientHeight);
|
|
4952
|
+
}
|
|
4953
|
+
});
|
|
4954
|
+
|
|
4955
|
+
// ── Progress animation ──
|
|
4956
|
+
var STATUS_STEPS = [
|
|
4957
|
+
{ at: 0, icon: 'fa-brain fa-beat-fade', text: 'Analyzing report structure...' },
|
|
4958
|
+
{ at: 12, icon: 'fa-table-cells fa-fade', text: 'Filling empty table cells...' },
|
|
4959
|
+
{ at: 30, icon: 'fa-diagram-project fa-beat-fade', text: 'Mapping keyword clusters...' },
|
|
4960
|
+
{ at: 50, icon: 'fa-ranking-star fa-fade', text: 'Scoring priorities...' },
|
|
4961
|
+
{ at: 70, icon: 'fa-list-check fa-beat-fade', text: 'Building action plan...' },
|
|
4962
|
+
{ at: 88, icon: 'fa-file-export fa-fade', text: 'Finalizing export...' },
|
|
4963
|
+
];
|
|
4964
|
+
|
|
4965
|
+
var progressTimer = null, currentProgress = 0, abortCtrl = null;
|
|
4966
|
+
|
|
4967
|
+
function animateProgress(targetPct, durationMs) {
|
|
4968
|
+
var start = currentProgress, startTime = Date.now();
|
|
4969
|
+
clearInterval(progressTimer);
|
|
4970
|
+
progressTimer = setInterval(function() {
|
|
4971
|
+
var elapsed = Date.now() - startTime;
|
|
4972
|
+
var t = Math.min(elapsed / durationMs, 1);
|
|
4973
|
+
// Ease-out cubic
|
|
4974
|
+
var eased = 1 - Math.pow(1 - t, 3);
|
|
4975
|
+
currentProgress = start + (targetPct - start) * eased;
|
|
4976
|
+
updateProgressUI(currentProgress);
|
|
4977
|
+
if (t >= 1) clearInterval(progressTimer);
|
|
4978
|
+
}, 50);
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
function updateProgressUI(pct) {
|
|
4982
|
+
var bar = document.getElementById('aiProgressBar');
|
|
4983
|
+
var pctEl = document.getElementById('aiProgressPct');
|
|
4984
|
+
var statusEl = document.getElementById('aiExportStatus');
|
|
4985
|
+
if (bar) bar.style.width = pct + '%';
|
|
4986
|
+
if (pctEl) pctEl.textContent = Math.round(pct) + '%';
|
|
4987
|
+
// Update status text
|
|
4988
|
+
var step = STATUS_STEPS[0];
|
|
4989
|
+
for (var i = STATUS_STEPS.length - 1; i >= 0; i--) {
|
|
4990
|
+
if (pct >= STATUS_STEPS[i].at) { step = STATUS_STEPS[i]; break; }
|
|
4991
|
+
}
|
|
4992
|
+
if (statusEl) statusEl.innerHTML = '<i class="fa-solid ' + step.icon + '"></i> ' + step.text;
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
function showAiModal() {
|
|
4996
|
+
overlay.classList.add('active');
|
|
4997
|
+
currentProgress = 0;
|
|
4998
|
+
updateProgressUI(0);
|
|
4999
|
+
startSwarm();
|
|
5000
|
+
// Animate to 92% over ~60s (slowing down toward end)
|
|
5001
|
+
animateProgress(25, 8000);
|
|
5002
|
+
setTimeout(function() { animateProgress(55, 15000); }, 8000);
|
|
5003
|
+
setTimeout(function() { animateProgress(78, 15000); }, 23000);
|
|
5004
|
+
setTimeout(function() { animateProgress(92, 25000); }, 38000);
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
function hideAiModal() {
|
|
5008
|
+
clearInterval(progressTimer);
|
|
5009
|
+
stopSwarm();
|
|
5010
|
+
overlay.classList.remove('active');
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
function finishAiModal() {
|
|
5014
|
+
clearInterval(progressTimer);
|
|
5015
|
+
currentProgress = 100;
|
|
5016
|
+
updateProgressUI(100);
|
|
5017
|
+
var statusEl = document.getElementById('aiExportStatus');
|
|
5018
|
+
if (statusEl) statusEl.innerHTML = '<i class="fa-solid fa-check" style="color:#50c878;"></i> Export ready!';
|
|
5019
|
+
setTimeout(hideAiModal, 800);
|
|
5020
|
+
}
|
|
5021
|
+
|
|
5022
|
+
// Cancel button
|
|
5023
|
+
document.getElementById('aiExportCancel').addEventListener('click', function() {
|
|
5024
|
+
if (abortCtrl) abortCtrl.abort();
|
|
5025
|
+
hideAiModal();
|
|
5026
|
+
});
|
|
5027
|
+
|
|
5028
|
+
// ── Intercept AI export downloads ──
|
|
5029
|
+
window._triggerAiExport = function(url) {
|
|
5030
|
+
abortCtrl = new AbortController();
|
|
5031
|
+
showAiModal();
|
|
5032
|
+
fetch(url, { signal: abortCtrl.signal })
|
|
5033
|
+
.then(function(resp) {
|
|
5034
|
+
if (!resp.ok) throw new Error('Export failed: ' + resp.status);
|
|
5035
|
+
var cd = resp.headers.get('content-disposition') || '';
|
|
5036
|
+
var m = cd.match(/filename="?([^"]+)"?/);
|
|
5037
|
+
var filename = m ? m[1] : 'export.md';
|
|
5038
|
+
return resp.blob().then(function(blob) { return { blob: blob, filename: filename }; });
|
|
5039
|
+
})
|
|
5040
|
+
.then(function(result) {
|
|
5041
|
+
finishAiModal();
|
|
5042
|
+
setTimeout(function() {
|
|
5043
|
+
var a = document.createElement('a');
|
|
5044
|
+
a.href = URL.createObjectURL(result.blob);
|
|
5045
|
+
a.download = result.filename;
|
|
5046
|
+
a.click();
|
|
5047
|
+
URL.revokeObjectURL(a.href);
|
|
5048
|
+
}, 900);
|
|
5049
|
+
})
|
|
5050
|
+
.catch(function(err) {
|
|
5051
|
+
if (err.name === 'AbortError') return;
|
|
5052
|
+
hideAiModal();
|
|
5053
|
+
alert('AI Smart Export failed: ' + err.message);
|
|
5054
|
+
});
|
|
5055
|
+
};
|
|
5056
|
+
})();
|
|
5057
|
+
</script>`;
|
|
5058
|
+
|
|
4759
5059
|
// ── Compose full HTML ──
|
|
4760
|
-
return headHtml + '\n<body>\n' + panelHtml + '\n' + scriptHtml + '\n</body>\n</html>';
|
|
5060
|
+
return headHtml + '\n<body>\n' + panelHtml + '\n' + scriptHtml + '\n' + aiModalHtml + '\n</body>\n</html>';
|
|
4761
5061
|
}
|
|
4762
5062
|
|
|
4763
5063
|
// ─── Multi-Project Dashboard Builder ──────────────────────────────────────────
|
|
@@ -6587,8 +6887,8 @@ function getTerritoryTreemapData(db, project, config) {
|
|
|
6587
6887
|
});
|
|
6588
6888
|
}
|
|
6589
6889
|
|
|
6590
|
-
function getTopicClusterData(project) {
|
|
6591
|
-
//
|
|
6890
|
+
function getTopicClusterData(db, project) {
|
|
6891
|
+
// Try pre-generated file first (from topic-cluster-mapper.js)
|
|
6592
6892
|
const candidates = [
|
|
6593
6893
|
join(__dirname, `topic-clusters-${project}.json`),
|
|
6594
6894
|
join(__dirname, 'topic-clusters.json'),
|
|
@@ -6598,7 +6898,6 @@ function getTopicClusterData(project) {
|
|
|
6598
6898
|
try {
|
|
6599
6899
|
if (existsSync(path)) {
|
|
6600
6900
|
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
6601
|
-
// Verify this data is for the right project (if it has a project field)
|
|
6602
6901
|
if (raw.project && raw.project !== project) continue;
|
|
6603
6902
|
const data = raw.dashboard_data || null;
|
|
6604
6903
|
if (data) console.log(` 📊 Topic clusters loaded: ${data.length} clusters from ${path.split('/').pop()}`);
|
|
@@ -6609,8 +6908,149 @@ function getTopicClusterData(project) {
|
|
|
6609
6908
|
}
|
|
6610
6909
|
}
|
|
6611
6910
|
|
|
6612
|
-
|
|
6613
|
-
return
|
|
6911
|
+
// No file — auto-generate from DB (pure text scoring, no LLM)
|
|
6912
|
+
return generateTopicClustersFromDb(db, project);
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
function generateTopicClustersFromDb(db, project) {
|
|
6916
|
+
const CLUSTERS = [
|
|
6917
|
+
{ id: 'rpc', label: 'RPC & Node Infrastructure', seeds: ['rpc', 'node', 'endpoint', 'rpc node', 'rpc endpoint', 'json-rpc', 'jsonrpc', 'websocket', 'wss', 'connection', 'latency', 'uptime', 'reliability'] },
|
|
6918
|
+
{ id: 'dex', label: 'DEX & Swap', seeds: ['dex', 'swap', 'quote', 'amm', 'liquidity', 'pool', 'routing', 'slippage', 'token swap', 'dex api', 'swap api', 'jupiter', 'raydium', 'orca', 'gasless'] },
|
|
6919
|
+
{ id: 'data', label: 'Blockchain Data & Analytics', seeds: ['data', 'analytics', 'historical', 'indexer', 'index', 'stream', 'webhook', 'transaction data', 'on-chain', 'onchain', 'real-time', 'realtime', 'archive'] },
|
|
6920
|
+
{ id: 'validator', label: 'Validator & Staking', seeds: ['validator', 'stake', 'staking', 'delegation', 'epoch', 'rewards', 'apy', 'sol staking', 'validator node'] },
|
|
6921
|
+
{ id: 'api', label: 'API & Developer Tools', seeds: ['api', 'sdk', 'developer', 'documentation', 'quickstart', 'tutorial', 'integration', 'library', 'typescript', 'python', 'rust'] },
|
|
6922
|
+
{ id: 'trading', label: 'Trading & DeFi', seeds: ['trading', 'defi', 'bot', 'arbitrage', 'mev', 'sniper', 'frontrun', 'backrun', 'sandwich', 'jito', 'bundle', 'profit'] },
|
|
6923
|
+
{ id: 'pricing', label: 'Pricing & Plans', seeds: ['pricing', 'price', 'plan', 'tier', 'free', 'pro', 'enterprise', 'cost', 'credit', 'rate limit', 'quota'] },
|
|
6924
|
+
{ id: 'solana_ecosystem', label: 'Solana Ecosystem', seeds: ['solana', 'spl', 'token', 'program', 'account', 'transaction', 'block', 'slot', 'lamport', 'nft', 'metaplex'] },
|
|
6925
|
+
{ id: 'infrastructure', label: 'Infrastructure & Ops', seeds: ['infrastructure', 'bare metal', 'server', 'devops', 'monitoring', 'alerting', 'dashboard', 'status', 'sla', 'downtime'] },
|
|
6926
|
+
{ id: 'education', label: 'Education & Learning', seeds: ['learn', 'guide', 'tutorial', 'how to', 'getting started', 'beginner', 'explained', 'what is', 'introduction', 'course'] },
|
|
6927
|
+
{ id: 'ai', label: 'AI & Agents', seeds: ['ai', 'agent', 'skill', 'llm', 'gpt', 'claude', 'langchain', 'autonomous', 'agentic', 'artificial intelligence', 'machine learning'] },
|
|
6928
|
+
{ id: 'comparison', label: 'Comparisons & Alternatives', seeds: ['vs', 'versus', 'alternative', 'compare', 'comparison', 'better than', 'switch', 'migrate'] },
|
|
6929
|
+
];
|
|
6930
|
+
|
|
6931
|
+
const WEIGHTS = { title: 5, h1: 4, h2: 3, meta: 2, body: 1 };
|
|
6932
|
+
|
|
6933
|
+
// Check if project has crawl data
|
|
6934
|
+
const pageCount = db.prepare(`
|
|
6935
|
+
SELECT COUNT(*) as cnt FROM pages p
|
|
6936
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6937
|
+
WHERE d.project = ? AND p.status_code = 200
|
|
6938
|
+
`).get(project)?.cnt || 0;
|
|
6939
|
+
|
|
6940
|
+
if (pageCount === 0) return null;
|
|
6941
|
+
|
|
6942
|
+
// Load pages
|
|
6943
|
+
const pages = db.prepare(`
|
|
6944
|
+
SELECT p.id, p.url, p.word_count, p.click_depth,
|
|
6945
|
+
d.domain, d.role,
|
|
6946
|
+
e.title, e.meta_desc, e.h1
|
|
6947
|
+
FROM pages p
|
|
6948
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6949
|
+
LEFT JOIN extractions e ON e.page_id = p.id
|
|
6950
|
+
WHERE d.project = ? AND p.status_code = 200
|
|
6951
|
+
ORDER BY d.domain, p.click_depth
|
|
6952
|
+
`).all(project);
|
|
6953
|
+
|
|
6954
|
+
// Load keywords per page
|
|
6955
|
+
const keywordsByPage = new Map();
|
|
6956
|
+
const allKeywords = db.prepare(`
|
|
6957
|
+
SELECT k.page_id, k.keyword, k.location
|
|
6958
|
+
FROM keywords k
|
|
6959
|
+
JOIN pages p ON k.page_id = p.id
|
|
6960
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6961
|
+
WHERE d.project = ?
|
|
6962
|
+
`).all(project);
|
|
6963
|
+
for (const row of allKeywords) {
|
|
6964
|
+
if (!keywordsByPage.has(row.page_id)) keywordsByPage.set(row.page_id, []);
|
|
6965
|
+
keywordsByPage.get(row.page_id).push(row);
|
|
6966
|
+
}
|
|
6967
|
+
|
|
6968
|
+
// Load headings per page
|
|
6969
|
+
const headingsByPage = new Map();
|
|
6970
|
+
const allHeadings = db.prepare(`
|
|
6971
|
+
SELECT h.page_id, h.text
|
|
6972
|
+
FROM headings h
|
|
6973
|
+
JOIN pages p ON h.page_id = p.id
|
|
6974
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6975
|
+
WHERE d.project = ? AND h.level <= 3
|
|
6976
|
+
`).all(project);
|
|
6977
|
+
for (const row of allHeadings) {
|
|
6978
|
+
if (!headingsByPage.has(row.page_id)) headingsByPage.set(row.page_id, []);
|
|
6979
|
+
headingsByPage.get(row.page_id).push(row.text);
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6982
|
+
// Score each page
|
|
6983
|
+
const clusterStats = {};
|
|
6984
|
+
for (const c of CLUSTERS) {
|
|
6985
|
+
clusterStats[c.id] = { label: c.label, pages: [], byDomain: {}, targetPages: [], competitorPages: [] };
|
|
6986
|
+
}
|
|
6987
|
+
|
|
6988
|
+
for (const page of pages) {
|
|
6989
|
+
const keywords = keywordsByPage.get(page.id) || [];
|
|
6990
|
+
const headings = headingsByPage.get(page.id) || [];
|
|
6991
|
+
const scores = {};
|
|
6992
|
+
for (const c of CLUSTERS) scores[c.id] = 0;
|
|
6993
|
+
|
|
6994
|
+
// Score from keywords
|
|
6995
|
+
for (const { keyword, location } of keywords) {
|
|
6996
|
+
const kw = keyword.toLowerCase().trim();
|
|
6997
|
+
const w = WEIGHTS[location] || 1;
|
|
6998
|
+
for (const c of CLUSTERS) {
|
|
6999
|
+
for (const seed of c.seeds) {
|
|
7000
|
+
if (kw.includes(seed) || seed.includes(kw)) { scores[c.id] += w; break; }
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
|
|
7005
|
+
// Score from headings
|
|
7006
|
+
for (const text of headings) {
|
|
7007
|
+
const t = text.toLowerCase();
|
|
7008
|
+
for (const c of CLUSTERS) {
|
|
7009
|
+
for (const seed of c.seeds) {
|
|
7010
|
+
if (t.includes(seed)) { scores[c.id] += 3; break; }
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
|
|
7015
|
+
// Assign to primary cluster
|
|
7016
|
+
const primary = Object.entries(scores).sort((a, b) => b[1] - a[1]).filter(([, s]) => s > 0)[0];
|
|
7017
|
+
if (primary) {
|
|
7018
|
+
const cs = clusterStats[primary[0]];
|
|
7019
|
+
cs.pages.push(page);
|
|
7020
|
+
if (!cs.byDomain[page.domain]) cs.byDomain[page.domain] = 0;
|
|
7021
|
+
cs.byDomain[page.domain]++;
|
|
7022
|
+
if (page.role === 'target') cs.targetPages.push(page);
|
|
7023
|
+
else cs.competitorPages.push(page);
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
|
|
7027
|
+
// Build dashboard_data format
|
|
7028
|
+
const dashboardData = CLUSTERS.map(cluster => {
|
|
7029
|
+
const cs = clusterStats[cluster.id];
|
|
7030
|
+
const domainCounts = cs.byDomain;
|
|
7031
|
+
const dominant = Object.entries(domainCounts).sort((a, b) => b[1] - a[1])[0];
|
|
7032
|
+
const wcs = cs.pages.map(p => p.word_count || 0).filter(Boolean);
|
|
7033
|
+
const avgWc = wcs.length ? Math.round(wcs.reduce((a, b) => a + b, 0) / wcs.length) : 0;
|
|
7034
|
+
return {
|
|
7035
|
+
cluster: cluster.label,
|
|
7036
|
+
cluster_id: cluster.id,
|
|
7037
|
+
keywords: cs.pages.length,
|
|
7038
|
+
totalFreq: cs.pages.length,
|
|
7039
|
+
dominant: dominant ? { domain: dominant[0], freq: dominant[1] } : null,
|
|
7040
|
+
domains: domainCounts,
|
|
7041
|
+
target_pages: cs.targetPages.length,
|
|
7042
|
+
competitor_pages: cs.competitorPages.length,
|
|
7043
|
+
avg_word_count: avgWc,
|
|
7044
|
+
};
|
|
7045
|
+
}).filter(d => d.keywords > 0).sort((a, b) => b.totalFreq - a.totalFreq);
|
|
7046
|
+
|
|
7047
|
+
if (dashboardData.length > 0) {
|
|
7048
|
+
console.log(` 📊 Topic clusters auto-generated: ${dashboardData.length} clusters from DB for ${project}`);
|
|
7049
|
+
} else {
|
|
7050
|
+
console.log(` ⚠️ No topic clusters found for project: ${project} (no keyword data)`);
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
return dashboardData.length > 0 ? dashboardData : null;
|
|
6614
7054
|
}
|
|
6615
7055
|
|
|
6616
7056
|
function getLinkDnaData(db, project, config) {
|