seo-intel 1.5.2 → 1.5.23
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 +26 -0
- package/Start SEO Intel.command +10 -0
- package/analyses/aeo/scorer.js +60 -6
- package/analyses/blog-draft/index.js +62 -10
- package/analyses/templates/index.js +1 -1
- package/analysis/prompt-builder.js +167 -2
- package/analysis/technical-audit.js +177 -0
- package/cli.js +446 -25
- package/crawler/index.js +36 -2
- package/crawler/sitemap.js +44 -0
- package/db/db.js +62 -9
- package/db/schema.sql +19 -0
- package/exports/queries.js +32 -0
- package/exports/technical.js +181 -1
- package/extractor/qwen.js +135 -13
- package/lib/scan-export.js +204 -0
- package/package.json +1 -1
- package/reports/generate-html.js +517 -50
- package/server.js +319 -25
- package/setup/checks.js +65 -5
- package/setup/engine.js +1 -0
- package/setup/web-routes.js +22 -3
- package/setup/wizard.html +8 -6
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
|
}
|
|
@@ -42,7 +43,7 @@ function cardExportHtml(section, project) {
|
|
|
42
43
|
*/
|
|
43
44
|
export function gatherProjectData(db, project, config) {
|
|
44
45
|
const targetDomain = config.target.domain;
|
|
45
|
-
const competitorDomains = config.competitors.map(c => c.domain);
|
|
46
|
+
const competitorDomains = (config.competitors || []).map(c => c.domain);
|
|
46
47
|
const allDomains = [targetDomain, ...competitorDomains];
|
|
47
48
|
|
|
48
49
|
// Domain architecture needs the raw owned domains, so gather BEFORE merge
|
|
@@ -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
|
|
|
@@ -178,9 +179,18 @@ export function generateHtmlDashboard(db, project, config) {
|
|
|
178
179
|
* @returns {string} Path to generated HTML file
|
|
179
180
|
*/
|
|
180
181
|
export function generateMultiDashboard(db, configs) {
|
|
181
|
-
const allProjectData = configs.
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
const allProjectData = configs.flatMap(config => {
|
|
183
|
+
try {
|
|
184
|
+
return [gatherProjectData(db, config.project, config)];
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(`[dashboard] Error gathering data for project "${config.project}":`, err.message);
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!allProjectData.length) {
|
|
192
|
+
throw new Error('No project data could be gathered — all projects failed.');
|
|
193
|
+
}
|
|
184
194
|
|
|
185
195
|
const html = buildMultiHtmlTemplate(allProjectData);
|
|
186
196
|
const outPath = join(__dirname, 'all-projects-dashboard.html');
|
|
@@ -1648,15 +1658,16 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1648
1658
|
cursor: pointer;
|
|
1649
1659
|
}
|
|
1650
1660
|
.export-viewer {
|
|
1651
|
-
|
|
1652
|
-
padding: 12px;
|
|
1661
|
+
padding: 12px 16px;
|
|
1653
1662
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
1654
1663
|
font-size: 0.66rem;
|
|
1655
1664
|
line-height: 1.7;
|
|
1656
1665
|
color: var(--text-muted);
|
|
1657
1666
|
overflow-y: auto;
|
|
1658
|
-
|
|
1667
|
+
min-height: 60px;
|
|
1668
|
+
max-height: 600px;
|
|
1659
1669
|
}
|
|
1670
|
+
.export-viewer:empty, .export-viewer:has(> div:only-child) { max-height: 80px; }
|
|
1660
1671
|
.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
1672
|
.export-viewer h2 { font-size: 0.75rem; }
|
|
1662
1673
|
.export-viewer h3 { font-size: 0.7rem; }
|
|
@@ -1673,6 +1684,62 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1673
1684
|
|
|
1674
1685
|
/* Action exports integrated into terminal panel — CSS cleaned up */
|
|
1675
1686
|
|
|
1687
|
+
/* ── AI Smart Export Modal ── */
|
|
1688
|
+
.ai-export-overlay {
|
|
1689
|
+
position: fixed; inset: 0; z-index: 9999;
|
|
1690
|
+
background: rgba(0,0,0,0.85); backdrop-filter: blur(12px);
|
|
1691
|
+
display: flex; align-items: center; justify-content: center;
|
|
1692
|
+
opacity: 0; pointer-events: none; transition: opacity 0.4s ease;
|
|
1693
|
+
}
|
|
1694
|
+
.ai-export-overlay.active { opacity: 1; pointer-events: all; }
|
|
1695
|
+
.ai-export-card {
|
|
1696
|
+
position: relative; z-index: 2;
|
|
1697
|
+
background: rgba(18,18,18,0.85); border: 1px solid rgba(212,175,55,0.2);
|
|
1698
|
+
border-radius: 16px; padding: 32px 40px 28px; text-align: center;
|
|
1699
|
+
box-shadow: 0 0 60px rgba(212,175,55,0.08), 0 24px 48px rgba(0,0,0,0.5);
|
|
1700
|
+
min-width: 340px; max-width: 420px;
|
|
1701
|
+
}
|
|
1702
|
+
.ai-export-card h3 {
|
|
1703
|
+
font-family: var(--font-display); font-size: 1.1rem; color: var(--accent-gold);
|
|
1704
|
+
margin: 0 0 4px; letter-spacing: -0.02em;
|
|
1705
|
+
}
|
|
1706
|
+
.ai-export-card .ai-subtitle {
|
|
1707
|
+
font-size: 0.68rem; color: var(--text-muted); margin-bottom: 24px;
|
|
1708
|
+
}
|
|
1709
|
+
.ai-export-status {
|
|
1710
|
+
font-size: 0.72rem; color: var(--text-secondary); margin-bottom: 16px;
|
|
1711
|
+
min-height: 1.2em;
|
|
1712
|
+
}
|
|
1713
|
+
.ai-export-status i { color: var(--accent-gold); margin-right: 6px; }
|
|
1714
|
+
.ai-progress-track {
|
|
1715
|
+
width: 100%; height: 4px; background: rgba(255,255,255,0.06);
|
|
1716
|
+
border-radius: 4px; overflow: hidden; margin-bottom: 20px;
|
|
1717
|
+
position: relative;
|
|
1718
|
+
}
|
|
1719
|
+
.ai-progress-bar {
|
|
1720
|
+
height: 100%; width: 0%; border-radius: 4px;
|
|
1721
|
+
background: linear-gradient(90deg, var(--accent-gold), #f5c842, var(--accent-gold));
|
|
1722
|
+
background-size: 200% 100%;
|
|
1723
|
+
animation: ai-shimmer 1.5s ease infinite;
|
|
1724
|
+
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1725
|
+
}
|
|
1726
|
+
@keyframes ai-shimmer {
|
|
1727
|
+
0% { background-position: 200% 0; }
|
|
1728
|
+
100% { background-position: -200% 0; }
|
|
1729
|
+
}
|
|
1730
|
+
.ai-progress-pct {
|
|
1731
|
+
font-size: 0.6rem; color: var(--text-muted); font-family: 'SF Mono', monospace;
|
|
1732
|
+
margin-top: -14px; margin-bottom: 12px; text-align: right;
|
|
1733
|
+
}
|
|
1734
|
+
.ai-export-cancel {
|
|
1735
|
+
font-size: 0.62rem; color: var(--text-muted); background: none; border: 1px solid var(--border-subtle);
|
|
1736
|
+
padding: 4px 14px; border-radius: var(--radius); cursor: pointer; transition: all 0.2s;
|
|
1737
|
+
}
|
|
1738
|
+
.ai-export-cancel:hover { color: var(--text-primary); border-color: var(--text-muted); }
|
|
1739
|
+
#aiSwarmCanvas {
|
|
1740
|
+
position: absolute; inset: 0; z-index: 1; pointer-events: none; border-radius: 0;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1676
1743
|
</style>
|
|
1677
1744
|
</head>`;
|
|
1678
1745
|
|
|
@@ -2194,7 +2261,8 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2194
2261
|
<div class="draft-menu" id="draftMenu${suffix}">
|
|
2195
2262
|
<div class="draft-menu-section">Type</div>
|
|
2196
2263
|
<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"
|
|
2264
|
+
<label class="draft-option"><input type="radio" name="draftType${suffix}" value="docs" /> <i class="fa-solid fa-book"></i> Documentation</label>
|
|
2265
|
+
<label class="draft-option"><input type="radio" name="draftType${suffix}" value="social" /> <i class="fa-solid fa-share-nodes"></i> Social Media</label>
|
|
2198
2266
|
<div class="draft-menu-section" style="margin-top:8px;">Topic <span style="font-size:0.55rem;opacity:0.4;">(optional)</span></div>
|
|
2199
2267
|
<input type="text" id="draftTopic${suffix}" class="draft-topic-input" placeholder="e.g. solana rpc, site speed..." />
|
|
2200
2268
|
<div class="draft-menu-section" style="margin-top:8px;">Language</div>
|
|
@@ -2221,23 +2289,13 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2221
2289
|
<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>
|
|
2222
2290
|
<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>
|
|
2223
2291
|
</div>
|
|
2292
|
+
<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>
|
|
2293
|
+
<div style="font-size:0.55rem;color:var(--text-muted);padding:2px 4px;">Fills gaps, adds priorities & action tips via AI</div>
|
|
2224
2294
|
<button class="draft-generate-btn profile-download-btn" data-project="${project}" style="margin-top:10px;"><i class="fa-solid fa-download"></i> Download</button>
|
|
2225
2295
|
</div>
|
|
2226
2296
|
</div>
|
|
2227
2297
|
<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>
|
|
2228
2298
|
</div>
|
|
2229
|
-
<div style="position:relative;">
|
|
2230
|
-
<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;">
|
|
2231
|
-
<i class="fa-solid fa-check" style="margin-right:4px;"></i><span></span>
|
|
2232
|
-
</div>
|
|
2233
|
-
<button id="exportExpand${suffix}" class="export-expand-btn" title="Expand viewer"><i class="fa-solid fa-expand"></i></button>
|
|
2234
|
-
<div id="exportViewer${suffix}" class="export-viewer">
|
|
2235
|
-
<div style="color:#444;padding:20px 0;text-align:center;">
|
|
2236
|
-
<i class="fa-solid fa-file-export" style="font-size:1.2rem;margin-bottom:8px;display:block;"></i>
|
|
2237
|
-
Click an export to generate an<br/>implementation-ready action brief.
|
|
2238
|
-
</div>
|
|
2239
|
-
</div>
|
|
2240
|
-
</div>
|
|
2241
2299
|
` : `
|
|
2242
2300
|
<div style="padding:20px 14px;text-align:center;">
|
|
2243
2301
|
<i class="fa-solid fa-lock" style="font-size:1rem;color:var(--accent-gold);margin-bottom:8px;display:block;"></i>
|
|
@@ -2247,6 +2305,22 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2247
2305
|
`}
|
|
2248
2306
|
</div>
|
|
2249
2307
|
</div>
|
|
2308
|
+
${pro ? `
|
|
2309
|
+
<div class="viewer-row" style="max-width:var(--max-width);margin:0 auto;">
|
|
2310
|
+
<div style="position:relative;background:#0e0e0e;border:1px solid var(--border-card);border-radius:0 0 var(--radius) var(--radius);border-top:none;">
|
|
2311
|
+
<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;">
|
|
2312
|
+
<i class="fa-solid fa-check" style="margin-right:4px;"></i><span></span>
|
|
2313
|
+
</div>
|
|
2314
|
+
<button id="exportExpand${suffix}" class="export-expand-btn" title="Expand viewer"><i class="fa-solid fa-expand"></i></button>
|
|
2315
|
+
<div id="exportViewer${suffix}" class="export-viewer">
|
|
2316
|
+
<div style="color:#444;padding:20px 0;text-align:center;">
|
|
2317
|
+
<i class="fa-solid fa-file-export" style="font-size:1.2rem;margin-bottom:8px;display:block;"></i>
|
|
2318
|
+
Click an export or generate a draft — output appears here.
|
|
2319
|
+
</div>
|
|
2320
|
+
</div>
|
|
2321
|
+
</div>
|
|
2322
|
+
</div>
|
|
2323
|
+
` : ''}
|
|
2250
2324
|
|
|
2251
2325
|
<script>
|
|
2252
2326
|
(function() {
|
|
@@ -2287,6 +2361,13 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2287
2361
|
return;
|
|
2288
2362
|
}
|
|
2289
2363
|
|
|
2364
|
+
// Serve is not a terminal command — the server is already running
|
|
2365
|
+
if (command === 'serve') {
|
|
2366
|
+
appendLine('Server is already running (you are connected to it).', 'stdout');
|
|
2367
|
+
appendLine('To restart: stop the server and run seo-intel serve again.', 'stdout');
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2290
2371
|
if (!isServed) {
|
|
2291
2372
|
appendLine('', 'cmd');
|
|
2292
2373
|
appendLine('Not connected to server. Run in your terminal:', 'error');
|
|
@@ -2301,7 +2382,12 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2301
2382
|
status.style.color = 'var(--color-warning)';
|
|
2302
2383
|
|
|
2303
2384
|
const params = new URLSearchParams({ command });
|
|
2304
|
-
|
|
2385
|
+
// scan uses domain param; all other commands use project
|
|
2386
|
+
if (command === 'scan') {
|
|
2387
|
+
if (proj) params.set('domain', proj);
|
|
2388
|
+
} else {
|
|
2389
|
+
if (proj) params.set('project', proj);
|
|
2390
|
+
}
|
|
2305
2391
|
if (extra?.scope) params.set('scope', extra.scope);
|
|
2306
2392
|
if (extra?.stealth) params.set('stealth', 'true');
|
|
2307
2393
|
if (extra?.format) params.set('format', extra.format);
|
|
@@ -2309,7 +2395,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2309
2395
|
var stealthFlag = extra?.stealth ? ' --stealth' : '';
|
|
2310
2396
|
appendLine('$ seo-intel ' + command + (proj ? ' ' + proj : '') + stealthFlag + (extra?.scope ? ' --scope ' + extra.scope : ''), 'cmd');
|
|
2311
2397
|
|
|
2312
|
-
var isCrawlOrExtract = (command === 'crawl' || command === 'extract');
|
|
2398
|
+
var isCrawlOrExtract = (command === 'crawl' || command === 'extract' || command === 'scan');
|
|
2313
2399
|
|
|
2314
2400
|
eventSource = new EventSource('/api/terminal?' + params.toString());
|
|
2315
2401
|
eventSource.onmessage = function(e) {
|
|
@@ -2423,6 +2509,7 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2423
2509
|
if (msg.type === 'stdout') mdContent += msg.data + '\\n';
|
|
2424
2510
|
else if (msg.type === 'stderr') mdContent += msg.data + '\\n';
|
|
2425
2511
|
else if (msg.type === 'exit') {
|
|
2512
|
+
var exitCode = msg.data && msg.data.code;
|
|
2426
2513
|
running = false;
|
|
2427
2514
|
status.textContent = 'done';
|
|
2428
2515
|
status.style.color = 'var(--color-success)';
|
|
@@ -2445,11 +2532,17 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2445
2532
|
}
|
|
2446
2533
|
// Show save status
|
|
2447
2534
|
var saveEl = document.getElementById('exportSaveStatus' + suffix);
|
|
2448
|
-
if (saveEl &&
|
|
2449
|
-
var slugName = cmd === 'suggest-usecases' ? 'suggestions' : (scope || 'all');
|
|
2535
|
+
if (saveEl && exitCode === 0) {
|
|
2450
2536
|
var dateStr = new Date().toISOString().slice(0, 10);
|
|
2537
|
+
var savedName;
|
|
2538
|
+
if (cmd === 'aeo') {
|
|
2539
|
+
savedName = proj + '-aeo-' + dateStr + '.md';
|
|
2540
|
+
} else {
|
|
2541
|
+
var slugName = cmd === 'suggest-usecases' ? 'suggestions' : (scope || 'all');
|
|
2542
|
+
savedName = proj + '-' + slugName + '-' + dateStr + '.md';
|
|
2543
|
+
}
|
|
2451
2544
|
saveEl.style.display = 'block';
|
|
2452
|
-
saveEl.querySelector('span').textContent = 'Saved → reports/' +
|
|
2545
|
+
saveEl.querySelector('span').textContent = 'Saved → reports/' + savedName;
|
|
2453
2546
|
}
|
|
2454
2547
|
}
|
|
2455
2548
|
} catch (_) {}
|
|
@@ -2464,20 +2557,22 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2464
2557
|
});
|
|
2465
2558
|
});
|
|
2466
2559
|
|
|
2467
|
-
// Draft dropdown
|
|
2560
|
+
// Draft dropdown — use capture phase to match card-export handler
|
|
2468
2561
|
var draftTrigger = document.getElementById('draftTrigger' + suffix);
|
|
2469
2562
|
var draftMenu = document.getElementById('draftMenu' + suffix);
|
|
2470
2563
|
var draftGenerate = document.getElementById('draftGenerate' + suffix);
|
|
2471
2564
|
if (draftTrigger && draftMenu) {
|
|
2472
2565
|
draftTrigger.addEventListener('click', function(e) {
|
|
2473
|
-
e.
|
|
2566
|
+
e.stopImmediatePropagation();
|
|
2567
|
+
// Close other menus
|
|
2568
|
+
document.querySelectorAll('.draft-menu.open').forEach(function(m) { if (m !== draftMenu) m.classList.remove('open'); });
|
|
2569
|
+
document.querySelectorAll('.profile-export-menu').forEach(function(m) { m.style.display = 'none'; });
|
|
2474
2570
|
draftMenu.classList.toggle('open');
|
|
2475
|
-
});
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
});
|
|
2571
|
+
}, true);
|
|
2572
|
+
// Clicks inside the menu should not close it
|
|
2573
|
+
draftMenu.addEventListener('click', function(e) {
|
|
2574
|
+
e.stopImmediatePropagation();
|
|
2575
|
+
}, true);
|
|
2481
2576
|
}
|
|
2482
2577
|
if (draftGenerate) {
|
|
2483
2578
|
draftGenerate.addEventListener('click', function() {
|
|
@@ -2490,29 +2585,28 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2490
2585
|
var lang = langEl ? langEl.value : 'en';
|
|
2491
2586
|
var topic = topicEl ? topicEl.value.trim() : '';
|
|
2492
2587
|
|
|
2493
|
-
if (draftType !== 'blog') return; // docs not yet supported
|
|
2494
|
-
|
|
2495
2588
|
draftMenu.classList.remove('open');
|
|
2496
2589
|
|
|
2497
|
-
// Run blog-draft via terminal SSE
|
|
2498
|
-
var extra = { lang: lang };
|
|
2499
|
-
if (topic) extra.topic = topic;
|
|
2500
|
-
|
|
2590
|
+
// Run blog-draft via terminal SSE — type is passed so prompt builder can adapt
|
|
2501
2591
|
var params = new URLSearchParams({ command: 'blog-draft' });
|
|
2502
2592
|
params.set('project', proj);
|
|
2503
2593
|
params.set('lang', lang);
|
|
2594
|
+
params.set('type', draftType);
|
|
2504
2595
|
params.set('save', '1');
|
|
2505
2596
|
if (topic) params.set('topic', topic);
|
|
2506
2597
|
|
|
2598
|
+
var typeLabels = { blog: 'blog post', docs: 'documentation', social: 'social media post' };
|
|
2599
|
+
var typeLabel = typeLabels[draftType] || draftType;
|
|
2600
|
+
|
|
2507
2601
|
if (!isServed) {
|
|
2508
|
-
var cmd = 'seo-intel blog-draft ' + proj + (topic ? ' --topic "' + topic + '"' : '') + ' --lang ' + lang + ' --save';
|
|
2602
|
+
var cmd = 'seo-intel blog-draft ' + proj + (topic ? ' --topic "' + topic + '"' : '') + ' --lang ' + lang + ' --type ' + draftType + ' --save';
|
|
2509
2603
|
if (exportViewer) {
|
|
2510
2604
|
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>';
|
|
2511
2605
|
}
|
|
2512
2606
|
return;
|
|
2513
2607
|
}
|
|
2514
2608
|
|
|
2515
|
-
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
|
|
2609
|
+
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>';
|
|
2516
2610
|
|
|
2517
2611
|
var mdContent = '';
|
|
2518
2612
|
var es = new EventSource('/api/terminal?' + params.toString());
|
|
@@ -2593,6 +2687,16 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2593
2687
|
document.addEventListener('click', function(e) {
|
|
2594
2688
|
var btn = e.target.closest('.card-export-btn');
|
|
2595
2689
|
var fmtBtn = e.target.closest('[data-fmt]');
|
|
2690
|
+
// Clicks on AI toggle checkbox or its label inside card-export — don't close dropdown
|
|
2691
|
+
var aiLabel = e.target.closest('label');
|
|
2692
|
+
if (aiLabel && aiLabel.querySelector('.card-ai-toggle')) {
|
|
2693
|
+
e.stopImmediatePropagation();
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
if (e.target.classList && e.target.classList.contains('card-ai-toggle')) {
|
|
2697
|
+
e.stopImmediatePropagation();
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2596
2700
|
if (btn) {
|
|
2597
2701
|
var wrap = btn.closest('.card-export');
|
|
2598
2702
|
if (wrap) {
|
|
@@ -2611,8 +2715,14 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2611
2715
|
var sec = wrap2.getAttribute('data-section');
|
|
2612
2716
|
var proj2 = wrap2.getAttribute('data-project');
|
|
2613
2717
|
var fmt = fmtBtn.getAttribute('data-fmt');
|
|
2718
|
+
var aiToggle = wrap2.querySelector('.card-ai-toggle');
|
|
2719
|
+
var useAi = aiToggle && aiToggle.checked;
|
|
2720
|
+
var exportUrl = '/api/export/download?project=' + encodeURIComponent(proj2) + '§ion=' + encodeURIComponent(sec) + '&format=' + encodeURIComponent(fmt) + (useAi ? '&ai=true' : '');
|
|
2614
2721
|
if (window.location.protocol.startsWith('http')) {
|
|
2615
|
-
|
|
2722
|
+
if (useAi) {
|
|
2723
|
+
var loaderUrl = '/ai-loader?url=' + encodeURIComponent(exportUrl);
|
|
2724
|
+
window.open(loaderUrl, 'ai-export', 'width=600,height=480,menubar=no,toolbar=no,status=no');
|
|
2725
|
+
} else { window.location = exportUrl; }
|
|
2616
2726
|
}
|
|
2617
2727
|
return;
|
|
2618
2728
|
}
|
|
@@ -2647,13 +2757,24 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2647
2757
|
var projP = profDl.getAttribute('data-project');
|
|
2648
2758
|
var fmtVal = picker2.querySelector('input[name^="exportFmt"]:checked');
|
|
2649
2759
|
var fmt2 = fmtVal ? fmtVal.value : 'md';
|
|
2760
|
+
var aiCheck = picker2.querySelector('input[name^="aiExport"]');
|
|
2761
|
+
var useAi2 = aiCheck && aiCheck.checked;
|
|
2650
2762
|
picker2.querySelector('.profile-export-menu').style.display = 'none';
|
|
2763
|
+
var exportUrl2 = '/api/export/download?project=' + encodeURIComponent(projP) + '&format=' + encodeURIComponent(fmt2) + (useAi2 ? '&ai=true' : '');
|
|
2651
2764
|
if (window.location.protocol.startsWith('http')) {
|
|
2652
|
-
|
|
2765
|
+
if (useAi2) {
|
|
2766
|
+
var loaderUrl2 = '/ai-loader?url=' + encodeURIComponent(exportUrl2);
|
|
2767
|
+
window.open(loaderUrl2, 'ai-export', 'width=600,height=480,menubar=no,toolbar=no,status=no');
|
|
2768
|
+
} else { window.location = exportUrl2; }
|
|
2653
2769
|
}
|
|
2654
2770
|
return;
|
|
2655
2771
|
}
|
|
2656
2772
|
}
|
|
2773
|
+
// Clicks inside an open profile-export-menu (radio buttons etc) — don't close
|
|
2774
|
+
if (e.target.closest('.profile-export-menu')) {
|
|
2775
|
+
e.stopImmediatePropagation();
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2657
2778
|
// Outside click — close all open dropdowns
|
|
2658
2779
|
document.querySelectorAll('.card-export.open').forEach(function(el) { el.classList.remove('open'); });
|
|
2659
2780
|
document.querySelectorAll('.profile-export-menu').forEach(function(m) { m.style.display = 'none'; });
|
|
@@ -4750,8 +4871,214 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
4750
4871
|
|
|
4751
4872
|
</script>`;
|
|
4752
4873
|
|
|
4874
|
+
// ── AI Smart Export Modal ──
|
|
4875
|
+
const aiModalHtml = `
|
|
4876
|
+
<div class="ai-export-overlay" id="aiExportOverlay">
|
|
4877
|
+
<div id="aiSwarmCanvas"></div>
|
|
4878
|
+
<div class="ai-export-card">
|
|
4879
|
+
<h3><i class="fa-solid fa-wand-magic-sparkles"></i> AI Smart Export</h3>
|
|
4880
|
+
<p class="ai-subtitle">Enriching your report with AI intelligence</p>
|
|
4881
|
+
<div class="ai-export-status" id="aiExportStatus"><i class="fa-solid fa-brain fa-beat-fade"></i> Initializing...</div>
|
|
4882
|
+
<div class="ai-progress-track"><div class="ai-progress-bar" id="aiProgressBar"></div></div>
|
|
4883
|
+
<div class="ai-progress-pct" id="aiProgressPct">0%</div>
|
|
4884
|
+
<button class="ai-export-cancel" id="aiExportCancel">Cancel</button>
|
|
4885
|
+
</div>
|
|
4886
|
+
</div>
|
|
4887
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
4888
|
+
<script>
|
|
4889
|
+
(function(){
|
|
4890
|
+
// ── Swarm animation (compact, gold-themed) ──
|
|
4891
|
+
var overlay = document.getElementById('aiExportOverlay');
|
|
4892
|
+
var swarmEl = document.getElementById('aiSwarmCanvas');
|
|
4893
|
+
var swarmInited = false, swarmRaf = null, swarmRenderer, swarmScene, swarmCam;
|
|
4894
|
+
|
|
4895
|
+
function initSwarm() {
|
|
4896
|
+
if (swarmInited) return;
|
|
4897
|
+
swarmInited = true;
|
|
4898
|
+
var N = 300, sc = new THREE.Scene();
|
|
4899
|
+
sc.fog = new THREE.FogExp2(0x000000, 0.004);
|
|
4900
|
+
var cam = new THREE.PerspectiveCamera(60, swarmEl.clientWidth / Math.max(swarmEl.clientHeight, 1), 1, 800);
|
|
4901
|
+
cam.position.set(0, 0, 200);
|
|
4902
|
+
var r = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
4903
|
+
r.setSize(swarmEl.clientWidth, swarmEl.clientHeight);
|
|
4904
|
+
r.setPixelRatio(Math.min(devicePixelRatio, 1.5));
|
|
4905
|
+
r.setClearColor(0x000000, 0);
|
|
4906
|
+
swarmEl.appendChild(r.domElement);
|
|
4907
|
+
swarmRenderer = r; swarmScene = sc; swarmCam = cam;
|
|
4908
|
+
|
|
4909
|
+
// Dot texture
|
|
4910
|
+
var cv = document.createElement('canvas'); cv.width = cv.height = 64;
|
|
4911
|
+
var cx = cv.getContext('2d'), grd = cx.createRadialGradient(32,32,0,32,32,32);
|
|
4912
|
+
grd.addColorStop(0, 'rgba(255,255,255,1)');
|
|
4913
|
+
grd.addColorStop(0.3, 'rgba(212,175,55,0.9)');
|
|
4914
|
+
grd.addColorStop(1, 'rgba(212,175,55,0)');
|
|
4915
|
+
cx.fillStyle = grd; cx.fillRect(0,0,64,64);
|
|
4916
|
+
var tex = new THREE.CanvasTexture(cv);
|
|
4917
|
+
|
|
4918
|
+
// Particles — galaxy spiral
|
|
4919
|
+
var pos = new Float32Array(N*3), col = new Float32Array(N*3), szArr = new Float32Array(N);
|
|
4920
|
+
for (var i = 0; i < N; i++) {
|
|
4921
|
+
var t = i/N, ao = (i%4)*(Math.PI/2), rd = Math.pow(t,0.5)*100, a = t*Math.PI*5 + ao;
|
|
4922
|
+
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;
|
|
4923
|
+
var isGold = Math.random() > 0.7;
|
|
4924
|
+
if (isGold) { col[i*3]=0.83; col[i*3+1]=0.69; col[i*3+2]=0.22; szArr[i]=3; }
|
|
4925
|
+
else { col[i*3]=0.38; col[i*3+1]=0.51; col[i*3+2]=0.96; szArr[i]=1.5; }
|
|
4926
|
+
}
|
|
4927
|
+
var geo = new THREE.BufferGeometry();
|
|
4928
|
+
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
4929
|
+
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
|
|
4930
|
+
geo.setAttribute('size', new THREE.BufferAttribute(szArr, 1));
|
|
4931
|
+
|
|
4932
|
+
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; }';
|
|
4933
|
+
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; }';
|
|
4934
|
+
var mat = new THREE.ShaderMaterial({ uniforms:{ pointTexture:{value:tex} }, vertexShader:vs, fragmentShader:fs, blending:THREE.AdditiveBlending, depthTest:false, transparent:true });
|
|
4935
|
+
var pts = new THREE.Points(geo, mat);
|
|
4936
|
+
sc.add(pts);
|
|
4937
|
+
|
|
4938
|
+
// Connection lines
|
|
4939
|
+
var maxD = 28*28, lPos = new Float32Array(N*36), lGeo = new THREE.BufferGeometry();
|
|
4940
|
+
lGeo.setAttribute('position', new THREE.BufferAttribute(lPos, 3));
|
|
4941
|
+
var lMat = new THREE.LineBasicMaterial({ color: 0xd4af37, transparent:true, opacity:0.08, blending:THREE.AdditiveBlending });
|
|
4942
|
+
var lines = new THREE.LineSegments(lGeo, lMat); sc.add(lines);
|
|
4943
|
+
var vi=0, cnt=0;
|
|
4944
|
+
for (var i=0; i<N && cnt<N*4; i++) {
|
|
4945
|
+
for (var j=i+1; j<N && cnt<N*4; j++) {
|
|
4946
|
+
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];
|
|
4947
|
+
if (dx*dx+dy*dy+dz*dz < maxD) {
|
|
4948
|
+
lPos[vi++]=pos[i*3]; lPos[vi++]=pos[i*3+1]; lPos[vi++]=pos[i*3+2];
|
|
4949
|
+
lPos[vi++]=pos[j*3]; lPos[vi++]=pos[j*3+1]; lPos[vi++]=pos[j*3+2];
|
|
4950
|
+
cnt++;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
lGeo.setDrawRange(0, cnt*2); lGeo.attributes.position.needsUpdate = true;
|
|
4955
|
+
|
|
4956
|
+
function anim() {
|
|
4957
|
+
swarmRaf = requestAnimationFrame(anim);
|
|
4958
|
+
sc.rotation.y += 0.003; sc.rotation.x += 0.001;
|
|
4959
|
+
r.render(sc, cam);
|
|
4960
|
+
}
|
|
4961
|
+
anim();
|
|
4962
|
+
}
|
|
4963
|
+
|
|
4964
|
+
function stopSwarm() { if (swarmRaf) cancelAnimationFrame(swarmRaf); swarmRaf = null; }
|
|
4965
|
+
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); })(); } }
|
|
4966
|
+
|
|
4967
|
+
// Resize handler
|
|
4968
|
+
window.addEventListener('resize', function() {
|
|
4969
|
+
if (swarmRenderer && overlay.classList.contains('active')) {
|
|
4970
|
+
swarmCam.aspect = swarmEl.clientWidth / Math.max(swarmEl.clientHeight, 1);
|
|
4971
|
+
swarmCam.updateProjectionMatrix();
|
|
4972
|
+
swarmRenderer.setSize(swarmEl.clientWidth, swarmEl.clientHeight);
|
|
4973
|
+
}
|
|
4974
|
+
});
|
|
4975
|
+
|
|
4976
|
+
// ── Progress animation ──
|
|
4977
|
+
var STATUS_STEPS = [
|
|
4978
|
+
{ at: 0, icon: 'fa-brain fa-beat-fade', text: 'Analyzing report structure...' },
|
|
4979
|
+
{ at: 12, icon: 'fa-table-cells fa-fade', text: 'Filling empty table cells...' },
|
|
4980
|
+
{ at: 30, icon: 'fa-diagram-project fa-beat-fade', text: 'Mapping keyword clusters...' },
|
|
4981
|
+
{ at: 50, icon: 'fa-ranking-star fa-fade', text: 'Scoring priorities...' },
|
|
4982
|
+
{ at: 70, icon: 'fa-list-check fa-beat-fade', text: 'Building action plan...' },
|
|
4983
|
+
{ at: 88, icon: 'fa-file-export fa-fade', text: 'Finalizing export...' },
|
|
4984
|
+
];
|
|
4985
|
+
|
|
4986
|
+
var progressTimer = null, currentProgress = 0, abortCtrl = null;
|
|
4987
|
+
|
|
4988
|
+
function animateProgress(targetPct, durationMs) {
|
|
4989
|
+
var start = currentProgress, startTime = Date.now();
|
|
4990
|
+
clearInterval(progressTimer);
|
|
4991
|
+
progressTimer = setInterval(function() {
|
|
4992
|
+
var elapsed = Date.now() - startTime;
|
|
4993
|
+
var t = Math.min(elapsed / durationMs, 1);
|
|
4994
|
+
// Ease-out cubic
|
|
4995
|
+
var eased = 1 - Math.pow(1 - t, 3);
|
|
4996
|
+
currentProgress = start + (targetPct - start) * eased;
|
|
4997
|
+
updateProgressUI(currentProgress);
|
|
4998
|
+
if (t >= 1) clearInterval(progressTimer);
|
|
4999
|
+
}, 50);
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
function updateProgressUI(pct) {
|
|
5003
|
+
var bar = document.getElementById('aiProgressBar');
|
|
5004
|
+
var pctEl = document.getElementById('aiProgressPct');
|
|
5005
|
+
var statusEl = document.getElementById('aiExportStatus');
|
|
5006
|
+
if (bar) bar.style.width = pct + '%';
|
|
5007
|
+
if (pctEl) pctEl.textContent = Math.round(pct) + '%';
|
|
5008
|
+
// Update status text
|
|
5009
|
+
var step = STATUS_STEPS[0];
|
|
5010
|
+
for (var i = STATUS_STEPS.length - 1; i >= 0; i--) {
|
|
5011
|
+
if (pct >= STATUS_STEPS[i].at) { step = STATUS_STEPS[i]; break; }
|
|
5012
|
+
}
|
|
5013
|
+
if (statusEl) statusEl.innerHTML = '<i class="fa-solid ' + step.icon + '"></i> ' + step.text;
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
function showAiModal() {
|
|
5017
|
+
overlay.classList.add('active');
|
|
5018
|
+
currentProgress = 0;
|
|
5019
|
+
updateProgressUI(0);
|
|
5020
|
+
startSwarm();
|
|
5021
|
+
// Animate to 92% over ~60s (slowing down toward end)
|
|
5022
|
+
animateProgress(25, 8000);
|
|
5023
|
+
setTimeout(function() { animateProgress(55, 15000); }, 8000);
|
|
5024
|
+
setTimeout(function() { animateProgress(78, 15000); }, 23000);
|
|
5025
|
+
setTimeout(function() { animateProgress(92, 25000); }, 38000);
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
function hideAiModal() {
|
|
5029
|
+
clearInterval(progressTimer);
|
|
5030
|
+
stopSwarm();
|
|
5031
|
+
overlay.classList.remove('active');
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
function finishAiModal() {
|
|
5035
|
+
clearInterval(progressTimer);
|
|
5036
|
+
currentProgress = 100;
|
|
5037
|
+
updateProgressUI(100);
|
|
5038
|
+
var statusEl = document.getElementById('aiExportStatus');
|
|
5039
|
+
if (statusEl) statusEl.innerHTML = '<i class="fa-solid fa-check" style="color:#50c878;"></i> Export ready!';
|
|
5040
|
+
setTimeout(hideAiModal, 800);
|
|
5041
|
+
}
|
|
5042
|
+
|
|
5043
|
+
// Cancel button
|
|
5044
|
+
document.getElementById('aiExportCancel').addEventListener('click', function() {
|
|
5045
|
+
if (abortCtrl) abortCtrl.abort();
|
|
5046
|
+
hideAiModal();
|
|
5047
|
+
});
|
|
5048
|
+
|
|
5049
|
+
// ── Intercept AI export downloads ──
|
|
5050
|
+
window._triggerAiExport = function(url) {
|
|
5051
|
+
abortCtrl = new AbortController();
|
|
5052
|
+
showAiModal();
|
|
5053
|
+
fetch(url, { signal: abortCtrl.signal })
|
|
5054
|
+
.then(function(resp) {
|
|
5055
|
+
if (!resp.ok) throw new Error('Export failed: ' + resp.status);
|
|
5056
|
+
var cd = resp.headers.get('content-disposition') || '';
|
|
5057
|
+
var m = cd.match(/filename="?([^"]+)"?/);
|
|
5058
|
+
var filename = m ? m[1] : 'export.md';
|
|
5059
|
+
return resp.blob().then(function(blob) { return { blob: blob, filename: filename }; });
|
|
5060
|
+
})
|
|
5061
|
+
.then(function(result) {
|
|
5062
|
+
finishAiModal();
|
|
5063
|
+
setTimeout(function() {
|
|
5064
|
+
var a = document.createElement('a');
|
|
5065
|
+
a.href = URL.createObjectURL(result.blob);
|
|
5066
|
+
a.download = result.filename;
|
|
5067
|
+
a.click();
|
|
5068
|
+
URL.revokeObjectURL(a.href);
|
|
5069
|
+
}, 900);
|
|
5070
|
+
})
|
|
5071
|
+
.catch(function(err) {
|
|
5072
|
+
if (err.name === 'AbortError') return;
|
|
5073
|
+
hideAiModal();
|
|
5074
|
+
alert('AI Smart Export failed: ' + err.message);
|
|
5075
|
+
});
|
|
5076
|
+
};
|
|
5077
|
+
})();
|
|
5078
|
+
</script>`;
|
|
5079
|
+
|
|
4753
5080
|
// ── Compose full HTML ──
|
|
4754
|
-
return headHtml + '\n<body>\n' + panelHtml + '\n' + scriptHtml + '\n</body>\n</html>';
|
|
5081
|
+
return headHtml + '\n<body>\n' + panelHtml + '\n' + scriptHtml + '\n' + aiModalHtml + '\n</body>\n</html>';
|
|
4755
5082
|
}
|
|
4756
5083
|
|
|
4757
5084
|
// ─── Multi-Project Dashboard Builder ──────────────────────────────────────────
|
|
@@ -6581,8 +6908,8 @@ function getTerritoryTreemapData(db, project, config) {
|
|
|
6581
6908
|
});
|
|
6582
6909
|
}
|
|
6583
6910
|
|
|
6584
|
-
function getTopicClusterData(project) {
|
|
6585
|
-
//
|
|
6911
|
+
function getTopicClusterData(db, project) {
|
|
6912
|
+
// Try pre-generated file first (from topic-cluster-mapper.js)
|
|
6586
6913
|
const candidates = [
|
|
6587
6914
|
join(__dirname, `topic-clusters-${project}.json`),
|
|
6588
6915
|
join(__dirname, 'topic-clusters.json'),
|
|
@@ -6592,7 +6919,6 @@ function getTopicClusterData(project) {
|
|
|
6592
6919
|
try {
|
|
6593
6920
|
if (existsSync(path)) {
|
|
6594
6921
|
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
6595
|
-
// Verify this data is for the right project (if it has a project field)
|
|
6596
6922
|
if (raw.project && raw.project !== project) continue;
|
|
6597
6923
|
const data = raw.dashboard_data || null;
|
|
6598
6924
|
if (data) console.log(` 📊 Topic clusters loaded: ${data.length} clusters from ${path.split('/').pop()}`);
|
|
@@ -6603,8 +6929,149 @@ function getTopicClusterData(project) {
|
|
|
6603
6929
|
}
|
|
6604
6930
|
}
|
|
6605
6931
|
|
|
6606
|
-
|
|
6607
|
-
return
|
|
6932
|
+
// No file — auto-generate from DB (pure text scoring, no LLM)
|
|
6933
|
+
return generateTopicClustersFromDb(db, project);
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
function generateTopicClustersFromDb(db, project) {
|
|
6937
|
+
const CLUSTERS = [
|
|
6938
|
+
{ id: 'rpc', label: 'RPC & Node Infrastructure', seeds: ['rpc', 'node', 'endpoint', 'rpc node', 'rpc endpoint', 'json-rpc', 'jsonrpc', 'websocket', 'wss', 'connection', 'latency', 'uptime', 'reliability'] },
|
|
6939
|
+
{ id: 'dex', label: 'DEX & Swap', seeds: ['dex', 'swap', 'quote', 'amm', 'liquidity', 'pool', 'routing', 'slippage', 'token swap', 'dex api', 'swap api', 'jupiter', 'raydium', 'orca', 'gasless'] },
|
|
6940
|
+
{ id: 'data', label: 'Blockchain Data & Analytics', seeds: ['data', 'analytics', 'historical', 'indexer', 'index', 'stream', 'webhook', 'transaction data', 'on-chain', 'onchain', 'real-time', 'realtime', 'archive'] },
|
|
6941
|
+
{ id: 'validator', label: 'Validator & Staking', seeds: ['validator', 'stake', 'staking', 'delegation', 'epoch', 'rewards', 'apy', 'sol staking', 'validator node'] },
|
|
6942
|
+
{ id: 'api', label: 'API & Developer Tools', seeds: ['api', 'sdk', 'developer', 'documentation', 'quickstart', 'tutorial', 'integration', 'library', 'typescript', 'python', 'rust'] },
|
|
6943
|
+
{ id: 'trading', label: 'Trading & DeFi', seeds: ['trading', 'defi', 'bot', 'arbitrage', 'mev', 'sniper', 'frontrun', 'backrun', 'sandwich', 'jito', 'bundle', 'profit'] },
|
|
6944
|
+
{ id: 'pricing', label: 'Pricing & Plans', seeds: ['pricing', 'price', 'plan', 'tier', 'free', 'pro', 'enterprise', 'cost', 'credit', 'rate limit', 'quota'] },
|
|
6945
|
+
{ id: 'solana_ecosystem', label: 'Solana Ecosystem', seeds: ['solana', 'spl', 'token', 'program', 'account', 'transaction', 'block', 'slot', 'lamport', 'nft', 'metaplex'] },
|
|
6946
|
+
{ id: 'infrastructure', label: 'Infrastructure & Ops', seeds: ['infrastructure', 'bare metal', 'server', 'devops', 'monitoring', 'alerting', 'dashboard', 'status', 'sla', 'downtime'] },
|
|
6947
|
+
{ id: 'education', label: 'Education & Learning', seeds: ['learn', 'guide', 'tutorial', 'how to', 'getting started', 'beginner', 'explained', 'what is', 'introduction', 'course'] },
|
|
6948
|
+
{ id: 'ai', label: 'AI & Agents', seeds: ['ai', 'agent', 'skill', 'llm', 'gpt', 'claude', 'langchain', 'autonomous', 'agentic', 'artificial intelligence', 'machine learning'] },
|
|
6949
|
+
{ id: 'comparison', label: 'Comparisons & Alternatives', seeds: ['vs', 'versus', 'alternative', 'compare', 'comparison', 'better than', 'switch', 'migrate'] },
|
|
6950
|
+
];
|
|
6951
|
+
|
|
6952
|
+
const WEIGHTS = { title: 5, h1: 4, h2: 3, meta: 2, body: 1 };
|
|
6953
|
+
|
|
6954
|
+
// Check if project has crawl data
|
|
6955
|
+
const pageCount = db.prepare(`
|
|
6956
|
+
SELECT COUNT(*) as cnt FROM pages p
|
|
6957
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6958
|
+
WHERE d.project = ? AND p.status_code = 200
|
|
6959
|
+
`).get(project)?.cnt || 0;
|
|
6960
|
+
|
|
6961
|
+
if (pageCount === 0) return null;
|
|
6962
|
+
|
|
6963
|
+
// Load pages
|
|
6964
|
+
const pages = db.prepare(`
|
|
6965
|
+
SELECT p.id, p.url, p.word_count, p.click_depth,
|
|
6966
|
+
d.domain, d.role,
|
|
6967
|
+
e.title, e.meta_desc, e.h1
|
|
6968
|
+
FROM pages p
|
|
6969
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6970
|
+
LEFT JOIN extractions e ON e.page_id = p.id
|
|
6971
|
+
WHERE d.project = ? AND p.status_code = 200
|
|
6972
|
+
ORDER BY d.domain, p.click_depth
|
|
6973
|
+
`).all(project);
|
|
6974
|
+
|
|
6975
|
+
// Load keywords per page
|
|
6976
|
+
const keywordsByPage = new Map();
|
|
6977
|
+
const allKeywords = db.prepare(`
|
|
6978
|
+
SELECT k.page_id, k.keyword, k.location
|
|
6979
|
+
FROM keywords k
|
|
6980
|
+
JOIN pages p ON k.page_id = p.id
|
|
6981
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6982
|
+
WHERE d.project = ?
|
|
6983
|
+
`).all(project);
|
|
6984
|
+
for (const row of allKeywords) {
|
|
6985
|
+
if (!keywordsByPage.has(row.page_id)) keywordsByPage.set(row.page_id, []);
|
|
6986
|
+
keywordsByPage.get(row.page_id).push(row);
|
|
6987
|
+
}
|
|
6988
|
+
|
|
6989
|
+
// Load headings per page
|
|
6990
|
+
const headingsByPage = new Map();
|
|
6991
|
+
const allHeadings = db.prepare(`
|
|
6992
|
+
SELECT h.page_id, h.text
|
|
6993
|
+
FROM headings h
|
|
6994
|
+
JOIN pages p ON h.page_id = p.id
|
|
6995
|
+
JOIN domains d ON p.domain_id = d.id
|
|
6996
|
+
WHERE d.project = ? AND h.level <= 3
|
|
6997
|
+
`).all(project);
|
|
6998
|
+
for (const row of allHeadings) {
|
|
6999
|
+
if (!headingsByPage.has(row.page_id)) headingsByPage.set(row.page_id, []);
|
|
7000
|
+
headingsByPage.get(row.page_id).push(row.text);
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
// Score each page
|
|
7004
|
+
const clusterStats = {};
|
|
7005
|
+
for (const c of CLUSTERS) {
|
|
7006
|
+
clusterStats[c.id] = { label: c.label, pages: [], byDomain: {}, targetPages: [], competitorPages: [] };
|
|
7007
|
+
}
|
|
7008
|
+
|
|
7009
|
+
for (const page of pages) {
|
|
7010
|
+
const keywords = keywordsByPage.get(page.id) || [];
|
|
7011
|
+
const headings = headingsByPage.get(page.id) || [];
|
|
7012
|
+
const scores = {};
|
|
7013
|
+
for (const c of CLUSTERS) scores[c.id] = 0;
|
|
7014
|
+
|
|
7015
|
+
// Score from keywords
|
|
7016
|
+
for (const { keyword, location } of keywords) {
|
|
7017
|
+
const kw = keyword.toLowerCase().trim();
|
|
7018
|
+
const w = WEIGHTS[location] || 1;
|
|
7019
|
+
for (const c of CLUSTERS) {
|
|
7020
|
+
for (const seed of c.seeds) {
|
|
7021
|
+
if (kw.includes(seed) || seed.includes(kw)) { scores[c.id] += w; break; }
|
|
7022
|
+
}
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
|
|
7026
|
+
// Score from headings
|
|
7027
|
+
for (const text of headings) {
|
|
7028
|
+
const t = text.toLowerCase();
|
|
7029
|
+
for (const c of CLUSTERS) {
|
|
7030
|
+
for (const seed of c.seeds) {
|
|
7031
|
+
if (t.includes(seed)) { scores[c.id] += 3; break; }
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
|
|
7036
|
+
// Assign to primary cluster
|
|
7037
|
+
const primary = Object.entries(scores).sort((a, b) => b[1] - a[1]).filter(([, s]) => s > 0)[0];
|
|
7038
|
+
if (primary) {
|
|
7039
|
+
const cs = clusterStats[primary[0]];
|
|
7040
|
+
cs.pages.push(page);
|
|
7041
|
+
if (!cs.byDomain[page.domain]) cs.byDomain[page.domain] = 0;
|
|
7042
|
+
cs.byDomain[page.domain]++;
|
|
7043
|
+
if (page.role === 'target') cs.targetPages.push(page);
|
|
7044
|
+
else cs.competitorPages.push(page);
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
7047
|
+
|
|
7048
|
+
// Build dashboard_data format
|
|
7049
|
+
const dashboardData = CLUSTERS.map(cluster => {
|
|
7050
|
+
const cs = clusterStats[cluster.id];
|
|
7051
|
+
const domainCounts = cs.byDomain;
|
|
7052
|
+
const dominant = Object.entries(domainCounts).sort((a, b) => b[1] - a[1])[0];
|
|
7053
|
+
const wcs = cs.pages.map(p => p.word_count || 0).filter(Boolean);
|
|
7054
|
+
const avgWc = wcs.length ? Math.round(wcs.reduce((a, b) => a + b, 0) / wcs.length) : 0;
|
|
7055
|
+
return {
|
|
7056
|
+
cluster: cluster.label,
|
|
7057
|
+
cluster_id: cluster.id,
|
|
7058
|
+
keywords: cs.pages.length,
|
|
7059
|
+
totalFreq: cs.pages.length,
|
|
7060
|
+
dominant: dominant ? { domain: dominant[0], freq: dominant[1] } : null,
|
|
7061
|
+
domains: domainCounts,
|
|
7062
|
+
target_pages: cs.targetPages.length,
|
|
7063
|
+
competitor_pages: cs.competitorPages.length,
|
|
7064
|
+
avg_word_count: avgWc,
|
|
7065
|
+
};
|
|
7066
|
+
}).filter(d => d.keywords > 0).sort((a, b) => b.totalFreq - a.totalFreq);
|
|
7067
|
+
|
|
7068
|
+
if (dashboardData.length > 0) {
|
|
7069
|
+
console.log(` 📊 Topic clusters auto-generated: ${dashboardData.length} clusters from DB for ${project}`);
|
|
7070
|
+
} else {
|
|
7071
|
+
console.log(` ⚠️ No topic clusters found for project: ${project} (no keyword data)`);
|
|
7072
|
+
}
|
|
7073
|
+
|
|
7074
|
+
return dashboardData.length > 0 ? dashboardData : null;
|
|
6608
7075
|
}
|
|
6609
7076
|
|
|
6610
7077
|
function getLinkDnaData(db, project, config) {
|