feat(v0.2.6): hot topics + pipeline lock + blog delete + clean external JS
Hot Topics: - Dynamic topics from /api/hot-topics loaded in Blog Engine tab - 7 data sources (prices, competitors, hype cycle, news, conferences, research, evergreen) - Urgency badges: BREAKING (red), HOT (orange), TRENDING (yellow), EMERGING (green) Pipeline Lock: - Only 1 generation at a time, 'Pipeline Busy' toast on double-click - Progress bar with step names (external hot-topics.js, no inline hacks) Blog Delete: - DELETE /api/blog/:id endpoint - Delete button (✕) on each blog in list - 'Delete All Templates' button to clean up test drafts Fix: dashboard JS extracted to external hot-topics.js to avoid sed quote hell
This commit is contained in:
parent
3132b58309
commit
12d12aab4f
@ -69,7 +69,7 @@ app.get("/", (_req, res) => {
|
||||
app.get("/api", (_req, res) => {
|
||||
res.json({
|
||||
name: "Transceiver Intelligence Platform",
|
||||
version: "0.2.5",
|
||||
version: "0.2.6",
|
||||
endpoints: [
|
||||
"GET /api/transceivers?q=&form_factor=&speed=&category=&fiber_type=&wdm_type=&coherent=",
|
||||
"GET /api/transceivers/:id",
|
||||
|
||||
@ -1291,3 +1291,21 @@ blogRouter.get("/feedback/training-data", async (_req: Request, res: Response) =
|
||||
res.json({ entries: result.rows, count: result.rowCount });
|
||||
} catch (err) { res.status(500).json({ error: "Failed" }); }
|
||||
});
|
||||
|
||||
// DELETE /api/blog/:id — Delete a blog draft
|
||||
blogRouter.delete("/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Delete feedback first (FK constraint)
|
||||
await pool.query("DELETE FROM blog_feedback WHERE blog_id = $1::uuid", [req.params.id]);
|
||||
const result = await pool.query(
|
||||
"DELETE FROM blog_drafts WHERE id = $1::uuid RETURNING id, title",
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: "Draft not found" });
|
||||
}
|
||||
res.json({ success: true, deleted: result.rows[0] });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ healthRouter.get("/", async (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
success: true,
|
||||
status: "healthy",
|
||||
version: "0.2.5",
|
||||
version: "0.2.6",
|
||||
uptime: process.uptime(),
|
||||
database: {
|
||||
connected: true,
|
||||
|
||||
176
packages/dashboard/hot-topics.js
Normal file
176
packages/dashboard/hot-topics.js
Normal file
@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Hot Topics + Blog Pipeline UX Enhancement (v0.2.5)
|
||||
* Loaded after main dashboard script.
|
||||
* Overrides generateBlog + pollBlogLlm with improved versions.
|
||||
*/
|
||||
(function() {
|
||||
var API = window.API || '';
|
||||
var blogPipelineRunning = false;
|
||||
|
||||
var STEP_NAMES = [
|
||||
'Topic Expansion', 'Angle Selection', 'Outline Generation',
|
||||
'Master Draft (writing...)', 'Reality Injection', 'Technical Deepening',
|
||||
'Opinion Layer', 'Kill AI Tone', 'QA Check', 'Quality Score'
|
||||
];
|
||||
|
||||
// Override generateBlog with pipeline lock + progress UI
|
||||
window.generateBlog = function(topic, speed, customTitle, customAngle) {
|
||||
if (blogPipelineRunning) {
|
||||
if (typeof showToast === 'function') showToast('Pipeline Busy', 'A blog is already being generated. Please wait.');
|
||||
return;
|
||||
}
|
||||
blogPipelineRunning = true;
|
||||
|
||||
var blogList = document.getElementById('blog-list');
|
||||
if (blogList) {
|
||||
blogList.innerHTML =
|
||||
'<div style="background:linear-gradient(135deg,#1a1a1a,#2a2a2a);color:white;padding:2rem;border-radius:12px;text-align:center">' +
|
||||
'<div style="font-size:1.4rem;font-weight:700;margin-bottom:1rem">Generating Blog with AI...</div>' +
|
||||
'<div id="bp-status" style="font-size:1rem;color:#FF8100;margin-bottom:0.5rem">Starting 10-step Flexoptix Style pipeline...</div>' +
|
||||
'<div id="bp-step" style="font-size:0.85rem;color:#aaa">Connecting to LLM (qwen2.5:14b)</div>' +
|
||||
'<div style="margin-top:1.5rem;background:#333;border-radius:8px;height:8px;overflow:hidden">' +
|
||||
'<div id="bp-bar" style="width:2%;height:100%;background:#FF8100;transition:width 0.5s ease"></div></div>' +
|
||||
'<div id="bp-pct" style="font-size:0.8rem;color:#666;margin-top:0.5rem">0%</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
var body = { topic: topic };
|
||||
if (speed) body.speed = speed;
|
||||
|
||||
fetch((API || '') + '/api/blog/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.success && data.draft) {
|
||||
var s = document.getElementById('bp-status');
|
||||
if (s) s.textContent = 'Template created. LLM pipeline running (~10 min)...';
|
||||
var b = document.getElementById('bp-bar');
|
||||
if (b) b.style.width = '5%';
|
||||
pollPipeline(data.draft.id, 0);
|
||||
} else {
|
||||
blogPipelineRunning = false;
|
||||
if (typeof showToast === 'function') showToast('Error', (data.error || 'Generation failed'), true);
|
||||
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
|
||||
}
|
||||
}).catch(function(err) {
|
||||
blogPipelineRunning = false;
|
||||
if (typeof showToast === 'function') showToast('Network Error', err.message, true);
|
||||
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
|
||||
});
|
||||
};
|
||||
|
||||
function pollPipeline(id, attempt) {
|
||||
if (attempt > 80) {
|
||||
blogPipelineRunning = false;
|
||||
if (typeof showToast === 'function') showToast('Timeout', 'Pipeline took too long. Check the blog list.');
|
||||
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
|
||||
return;
|
||||
}
|
||||
setTimeout(function() {
|
||||
fetch((API || '') + '/api/blog/' + id).then(function(r) { return r.json(); }).then(function(data) {
|
||||
var d = data.draft || data;
|
||||
var gen = d.generated_by || '';
|
||||
var steps = d.pipeline_steps_completed || 0;
|
||||
var done = gen && gen !== 'tip-blog-engine-template' && gen.length > 0;
|
||||
|
||||
if (done) {
|
||||
// Pipeline complete!
|
||||
var bar = document.getElementById('bp-bar');
|
||||
var pct = document.getElementById('bp-pct');
|
||||
var status = document.getElementById('bp-status');
|
||||
var step = document.getElementById('bp-step');
|
||||
if (bar) bar.style.width = '100%';
|
||||
if (pct) pct.textContent = '100%';
|
||||
if (status) { status.textContent = 'Blog generated! ' + (d.word_count || '?') + ' words'; status.style.color = '#2d6a4f'; }
|
||||
if (step) step.textContent = 'Generated by: ' + gen;
|
||||
blogPipelineRunning = false;
|
||||
if (typeof showToast === 'function') showToast('Blog Ready!', (d.title || 'Article') + ' — ' + (d.word_count || '?') + ' words');
|
||||
setTimeout(function() {
|
||||
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
|
||||
if (typeof viewBlogDraft === 'function') viewBlogDraft(id);
|
||||
}, 2000);
|
||||
} else {
|
||||
// Still processing
|
||||
var pctVal = Math.min(95, steps * 10 + 5);
|
||||
var bar = document.getElementById('bp-bar');
|
||||
var pct = document.getElementById('bp-pct');
|
||||
var status = document.getElementById('bp-status');
|
||||
var step = document.getElementById('bp-step');
|
||||
if (bar) bar.style.width = pctVal + '%';
|
||||
if (pct) pct.textContent = pctVal + '%';
|
||||
if (status) status.textContent = 'Step ' + steps + '/10';
|
||||
if (step && steps > 0 && steps <= 10) step.textContent = STEP_NAMES[steps - 1] || 'Processing...';
|
||||
pollPipeline(id, attempt + 1);
|
||||
}
|
||||
}).catch(function() { pollPipeline(id, attempt + 1); });
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
// Hot topics loader
|
||||
window.loadHotTopics = function() {
|
||||
var grid = document.getElementById('hot-topics-grid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div class="loading pulse">Discovering hot topics...</div>';
|
||||
|
||||
fetch((API || '') + '/api/hot-topics').then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (!data.topics || data.topics.length === 0) {
|
||||
grid.innerHTML = '<div class="gen-card" style="cursor:pointer" onclick="generateBlog(\'hype_cycle\',\'800G\')"><div class="gen-card-title">Hype Cycle Analysis</div></div>';
|
||||
return;
|
||||
}
|
||||
var colors = { breaking: '#c1121f', hot: '#FF8100', trending: '#e6a800', emerging: '#2d6a4f' };
|
||||
grid.innerHTML = data.topics.slice(0, 6).map(function(t) {
|
||||
var c = colors[t.urgency] || '#888';
|
||||
var escaped = encodeURIComponent(t.blog_type || 'hype_cycle');
|
||||
var title = (t.title || '').replace(/'/g, "\\'").replace(/"/g, '"');
|
||||
return '<div class="gen-card" style="cursor:pointer;border-left:3px solid ' + c + '" onclick="generateBlog(\'' + escaped + '\')">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">' +
|
||||
'<span style="font-size:0.65rem;text-transform:uppercase;font-weight:600;color:' + c + '">' + t.urgency + '</span>' +
|
||||
'<span style="font-size:0.6rem;color:var(--text-dim)">' + (t.source_type || '') + '</span></div>' +
|
||||
'<div class="gen-card-title" style="font-size:0.85rem;line-height:1.3">' + t.title + '</div>' +
|
||||
'<div class="gen-card-sub" style="font-size:0.7rem;margin-top:4px">' + ((t.suggested_angle || t.description || '')).slice(0, 80) + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}).catch(function() {
|
||||
grid.innerHTML =
|
||||
'<div class="gen-card" style="cursor:pointer" onclick="generateBlog(\'hype_cycle\',\'800G\')"><div class="gen-card-title">Hype Cycle</div></div>' +
|
||||
'<div class="gen-card" style="cursor:pointer" onclick="generateBlog(\'comparison\',\'400G\')"><div class="gen-card-title">Comparison</div></div>' +
|
||||
'<div class="gen-card" style="cursor:pointer" onclick="generateBlog(\'tutorial\')"><div class="gen-card-title">Tutorial</div></div>';
|
||||
});
|
||||
};
|
||||
|
||||
// Auto-load hot topics when blog tab activates
|
||||
var origActivateTab = window.activateTab;
|
||||
if (origActivateTab) {
|
||||
window.activateTab = function(tabName) {
|
||||
origActivateTab(tabName);
|
||||
if (tabName === 'blog') loadHotTopics();
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
// Delete a single blog draft
|
||||
window.deleteBlogDraft = function(id, title) {
|
||||
if (!confirm('Delete "' + title + '"?')) return;
|
||||
fetch((window.API || '') + '/api/blog/' + id, { method: 'DELETE' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
if (typeof showToast === 'function') showToast('Deleted', title);
|
||||
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Delete all template drafts (keep LLM-generated ones)
|
||||
window.deleteAllTemplateDrafts = function() {
|
||||
if (!confirm('Delete ALL template drafts? LLM-generated articles will be kept.')) return;
|
||||
fetch((window.API || '') + '/api/blog').then(function(r) { return r.json(); }).then(function(data) {
|
||||
var templates = (data.drafts || []).filter(function(d) { return d.generated_by === 'tip-blog-engine-template' || !d.generated_by; });
|
||||
var count = 0;
|
||||
templates.forEach(function(d) {
|
||||
fetch((window.API || '') + '/api/blog/' + d.id, { method: 'DELETE' }).then(function() { count++; if (count === templates.length && typeof loadBlogDrafts === 'function') loadBlogDrafts(); });
|
||||
});
|
||||
if (typeof showToast === 'function') showToast('Cleaning', 'Deleting ' + templates.length + ' template drafts...');
|
||||
});
|
||||
};
|
||||
@ -873,14 +873,14 @@
|
||||
|
||||
<!-- BLOG -->
|
||||
<div id="tab-blog" class="hidden">
|
||||
<div style="margin-bottom:1rem;display:flex;justify-content:space-between;align-items:center">
|
||||
<h3 style="font-size:1.1rem;font-weight:600">Hot Topics <span style="font-size:0.75rem;color:var(--text-dim);font-weight:400">(auto-discovered from market data, conferences, research)</span></h3>
|
||||
<button onclick="loadHotTopics()" style="background:var(--accent);color:white;border:none;padding:6px 14px;border-radius:8px;cursor:pointer;font-size:0.8rem">Refresh Topics</button>
|
||||
<div style="margin-bottom:0.8rem;display:flex;justify-content:space-between;align-items:center">
|
||||
<h3 style="font-size:1rem;font-weight:600">Hot Topics <span style="font-size:0.7rem;color:var(--text-dim);font-weight:400">auto-discovered from market data + conferences</span></h3>
|
||||
<button onclick="loadHotTopics()" style="background:var(--accent);color:white;border:none;padding:5px 12px;border-radius:6px;cursor:pointer;font-size:0.75rem">Refresh</button>
|
||||
</div>
|
||||
<div id="hot-topics-grid" class="grid g3 mb" style="min-height:80px">
|
||||
<div class="loading pulse">Loading hot topics...</div>
|
||||
<div id="hot-topics-grid" class="grid g3 mb">
|
||||
<div class="loading pulse">Loading topics...</div>
|
||||
</div>
|
||||
<div class="card"><div id="blog-list"></div></div>
|
||||
<div style="margin-bottom:0.5rem;text-align:right"><button onclick="deleteAllTemplateDrafts()" style="background:#c1121f;color:white;border:none;padding:5px 12px;border-radius:6px;cursor:pointer;font-size:0.7rem">Delete All Templates</button></div><div class="card"><div id="blog-list"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1164,7 +1164,7 @@ function goToTab(tabName) {
|
||||
if (tabName === 'transceivers') searchTransceivers();
|
||||
if (tabName === 'switches') searchSwitches();
|
||||
if (tabName === 'news') loadNews();
|
||||
if (tabName === 'blog') { loadBlogDrafts(); if (typeof loadHotTopics === 'function') loadHotTopics(); }
|
||||
if (tabName === 'blog') loadBlogDrafts();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.tab').forEach(function(tab) {
|
||||
@ -2279,23 +2279,8 @@ function copyBlogContent(id) {
|
||||
}
|
||||
|
||||
// BLOG
|
||||
function generateBlog(topic, speed, customTitle, customAngle) {
|
||||
// Pipeline lock: prevent multiple simultaneous generations
|
||||
if (blogPipelineRunning) {
|
||||
showToast('Pipeline Busy', 'A blog is already being generated. Wait for it to finish.', true);
|
||||
return;
|
||||
}
|
||||
blogPipelineRunning = true;
|
||||
// Show prominent progress overlay
|
||||
el('blog-list').innerHTML = '<div style="background:linear-gradient(135deg,#1a1a1a,#2a2a2a);color:white;padding:2rem;border-radius:12px;text-align:center">' +
|
||||
'<div style="font-size:1.5rem;font-weight:700;margin-bottom:1rem">🔄 Generating Blog with AI...</div>' +
|
||||
'<div id="blog-pipeline-status" style="font-size:1rem;color:#FF8100;margin-bottom:0.5rem">Starting 10-step Flexoptix Style pipeline...</div>' +
|
||||
'<div id="blog-pipeline-step" style="font-size:0.85rem;color:#888">Connecting to LLM (qwen2.5:14b on Mac Studio)</div>' +
|
||||
'<div style="margin-top:1.5rem;background:#333;border-radius:8px;height:8px;overflow:hidden">' +
|
||||
'<div id="blog-pipeline-bar" style="width:0%;height:100%;background:#FF8100;transition:width 0.5s ease"></div></div>' +
|
||||
'<div id="blog-pipeline-pct" style="font-size:0.8rem;color:#666;margin-top:0.5rem">0%</div>' +
|
||||
'</div>';
|
||||
|
||||
function generateBlog(topic, speed) {
|
||||
el('blog-list').innerHTML = '<div class="loading pulse">Generating article...</div>';
|
||||
var body = { topic: topic };
|
||||
if (speed) body.speed = speed;
|
||||
fetch(API + '/api/blog/generate', {
|
||||
@ -2304,109 +2289,34 @@ function generateBlog(topic, speed, customTitle, customAngle) {
|
||||
body: JSON.stringify(body)
|
||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.success) {
|
||||
var statusEl = document.getElementById('blog-pipeline-status');
|
||||
if (statusEl) statusEl.textContent = 'Template created. LLM pipeline running...';
|
||||
var stepEl = document.getElementById('blog-pipeline-step');
|
||||
if (stepEl) stepEl.textContent = 'Step 1/10: Topic Expansion — analyzing real-world scenarios...';
|
||||
var barEl = document.getElementById('blog-pipeline-bar');
|
||||
if (barEl) barEl.style.width = '5%';
|
||||
var msg = data.draft.title + ' — ' + data.draft.word_count + ' words';
|
||||
if (data.draft.llm_enhancing) msg += ' (LLM enhancing in background...)';
|
||||
showToast('Draft generated', msg);
|
||||
if (data.draft.llm_enhancing) {
|
||||
pollBlogLlm(data.draft.id, 0);
|
||||
} else {
|
||||
showToast('Failed', data.error || 'Unknown error', true);
|
||||
}
|
||||
} else showToast('Failed', data.error || 'Unknown error', true);
|
||||
loadBlogDrafts();
|
||||
}).catch(function(err) { showToast('Network error', err.message, true); });
|
||||
}
|
||||
}).catch(function(err) { blogPipelineRunning = false; showToast('Network error', err.message, true); loadBlogDrafts(); });
|
||||
}
|
||||
|
||||
var STEP_NAMES = [
|
||||
'Topic Expansion — analyzing real-world scenarios',
|
||||
'Angle Selection — picking the strongest angle',
|
||||
'Outline Generation — decision-driven structure',
|
||||
'Master Draft — writing 2500+ word article',
|
||||
'Reality Injection — adding production failures',
|
||||
'Technical Deepening — specific optics + numbers',
|
||||
'Opinion Layer — removing neutrality',
|
||||
'Kill AI Tone — making it sound human',
|
||||
'QA Check — fixing weak sections',
|
||||
'Quality Score — rating 1-10'
|
||||
];
|
||||
|
||||
function pollBlogLlm(id, attempt) {
|
||||
if (attempt > 60) {
|
||||
blogPipelineRunning = false; showToast('Timeout', 'LLM pipeline took too long. Refresh to check.');
|
||||
loadBlogDrafts();
|
||||
return;
|
||||
}
|
||||
if (attempt > 30) return;
|
||||
setTimeout(function() {
|
||||
api('/api/blog/' + id).then(function(data) {
|
||||
var d = data.draft || data;
|
||||
var done = d.generated_by && d.generated_by !== 'tip-blog-engine-template' && d.generated_by !== null;
|
||||
var steps = d.pipeline_steps_completed || 0;
|
||||
|
||||
if (done) {
|
||||
// Pipeline finished! Show the result
|
||||
var pct = document.getElementById('blog-pipeline-pct');
|
||||
var bar = document.getElementById('blog-pipeline-bar');
|
||||
var status = document.getElementById('blog-pipeline-status');
|
||||
if (bar) bar.style.width = '100%';
|
||||
if (pct) pct.textContent = '100%';
|
||||
if (status) {
|
||||
status.textContent = '✅ Blog generated! ' + (d.word_count || '?') + ' words, QA Score: ' + (d.auto_qa_score ? JSON.parse(typeof d.auto_qa_score === 'string' ? d.auto_qa_score : JSON.stringify(d.auto_qa_score)).overall || '?' : '?') + '/10';
|
||||
status.style.color = '#2d6a4f';
|
||||
}
|
||||
showToast('Blog Ready!', d.title + ' — ' + d.word_count + ' words (LLM-generated)');
|
||||
blogPipelineRunning = false; setTimeout(function() { loadBlogDrafts(); viewBlogDraft(id); }, 2000);
|
||||
if (data.draft && data.draft.generated_by && data.draft.generated_by.includes('llm')) {
|
||||
showToast('LLM Enhanced', data.draft.title + ' — ' + data.draft.word_count + ' words');
|
||||
loadBlogDrafts();
|
||||
} else {
|
||||
// Still processing — update progress
|
||||
var pctVal = Math.min(95, steps * 10 + 5);
|
||||
var bar = document.getElementById('blog-pipeline-bar');
|
||||
var pct = document.getElementById('blog-pipeline-pct');
|
||||
var stepEl = document.getElementById('blog-pipeline-step');
|
||||
var status = document.getElementById('blog-pipeline-status');
|
||||
if (bar) bar.style.width = pctVal + '%';
|
||||
if (pct) pct.textContent = pctVal + '%';
|
||||
if (stepEl && steps > 0 && steps <= 10) stepEl.textContent = 'Step ' + steps + '/10: ' + (STEP_NAMES[steps-1] || 'Processing...');
|
||||
if (status) status.textContent = 'LLM Pipeline: Step ' + steps + '/10';
|
||||
pollBlogLlm(id, attempt + 1);
|
||||
}
|
||||
}).catch(function() { pollBlogLlm(id, attempt + 1); });
|
||||
}, 15000);
|
||||
}).catch(function() {});
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Hot Topics: load dynamically from API
|
||||
var blogPipelineRunning = false;
|
||||
function loadHotTopics() {
|
||||
var grid = el('hot-topics-grid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div class="loading pulse">Discovering hot topics...</div>';
|
||||
api('/api/hot-topics').then(function(data) {
|
||||
if (!data.topics || data.topics.length === 0) {
|
||||
grid.innerHTML = '<div class="gen-card" onclick="generateBlog('hype_cycle','800G')"><div class="gen-card-title">Hype Cycle Analysis</div><div class="gen-card-sub">800G technology position</div></div>';
|
||||
return;
|
||||
}
|
||||
var urgencyColors = { breaking: '#c1121f', hot: '#FF8100', trending: '#e6a800', emerging: '#2d6a4f' };
|
||||
grid.innerHTML = data.topics.slice(0, 6).map(function(t) {
|
||||
var color = urgencyColors[t.urgency] || '#888';
|
||||
return '<div class="gen-card" onclick="generateBlogFromTopic('' + encodeURIComponent(JSON.stringify(t)) + '')" style="cursor:pointer;border-left:3px solid ' + color + '">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">' +
|
||||
'<span style="font-size:0.65rem;text-transform:uppercase;font-weight:600;color:' + color + '">' + t.urgency + '</span>' +
|
||||
'<span style="font-size:0.6rem;color:var(--text-dim)">' + t.source_type + '</span></div>' +
|
||||
'<div class="gen-card-title" style="font-size:0.85rem;line-height:1.3">' + t.title + '</div>' +
|
||||
'<div class="gen-card-sub" style="font-size:0.7rem;margin-top:4px">' + (t.suggested_angle || t.description).slice(0, 80) + '...</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}).catch(function() {
|
||||
grid.innerHTML = '<div class="gen-card" onclick="generateBlog('hype_cycle','800G')"><div class="gen-card-title">Hype Cycle Analysis</div></div><div class="gen-card" onclick="generateBlog('comparison','400G')"><div class="gen-card-title">Product Comparison</div></div><div class="gen-card" onclick="generateBlog('tutorial')"><div class="gen-card-title">Tutorial</div></div>';
|
||||
});
|
||||
}
|
||||
// Hot topics loaded dynamically via hot-topics.js
|
||||
|
||||
function generateBlogFromTopic(encodedTopic) {
|
||||
var t = JSON.parse(decodeURIComponent(encodedTopic));
|
||||
generateBlog(t.blog_type, null, t.title, t.suggested_angle);
|
||||
}
|
||||
|
||||
// Load hot topics when blog tab opens
|
||||
var origLoadBlog = loadBlogDrafts;
|
||||
|
||||
async function loadBlogDrafts() {
|
||||
var data = await api('/api/blog');
|
||||
@ -2550,5 +2460,6 @@ el('compare-overlay').addEventListener('click', function(e) {
|
||||
// INIT
|
||||
loadOverview();
|
||||
</script>
|
||||
<script src="/dashboard/hot-topics.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user