Rene Fichtmueller ee8b3c0779 feat: hot topics daily rotation — 30+ topic pool, seeded shuffle, next-refresh countdown
- Expanded research pool to 9 topics (was 3), evergreen to 12 (was 3)
- Conference topics: added Photonics West, CIOE, NFOEC follow-up, year-end review
- Standards topics: 3 rotating variants (IEEE tracker, SFF-8024 registry, OIF CEI-112G)
- seededShuffle(): day-of-year as seed — stable within the day, different every day
- API response adds refreshes_at (next midnight UTC) for frontend countdown
- Dashboard subtitle shows 'rotates daily · next refresh in Xh'
- Hot topic cards now pass full title + angle into generateBlog() correctly
2026-04-01 11:12:38 +02:00

216 lines
10 KiB
JavaScript

/**
* 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 pipelineEl = document.getElementById('blog-pipeline-status');
if (pipelineEl) {
pipelineEl.innerHTML =
'<div style="background:linear-gradient(135deg,#1a1a1a,#2a2a2a);color:white;padding:2rem;border-radius:12px;text-align:center;margin-bottom:1rem">' +
'<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;
var pipelineEl = document.getElementById('blog-pipeline-status');
if (pipelineEl) pipelineEl.innerHTML = '';
if (typeof showToast === 'function') showToast('Timeout', 'Pipeline took too long. Check the blog list.');
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
return;
}
// Poll progress endpoint (fast) + blog status endpoint (to detect completion)
setTimeout(function() {
// 1) Fetch real-time progress from server
fetch((API || '') + '/api/blog/' + id + '/progress')
.then(function(r) { return r.json(); })
.then(function(prog) {
if (prog.running) {
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 = prog.pct + '%';
if (pct) pct.textContent = prog.pct + '%';
if (status) status.textContent = prog.label || ('Step ' + prog.step + '/10');
if (step) step.textContent = 'Running on qwen2.5:14b via Ollama...';
}
}).catch(function() {});
// 2) Fetch blog draft to detect pipeline completion
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 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 fertig! ' + (d.word_count || '?') + ' Wörter'; status.style.color = '#2d6a4f'; }
if (step) step.textContent = 'Engine: ' + gen;
blogPipelineRunning = false;
if (typeof showToast === 'function') showToast('Blog Ready!', (d.title || 'Article') + ' — ' + (d.word_count || '?') + ' words');
setTimeout(function() {
var pipelineEl = document.getElementById('blog-pipeline-status');
if (pipelineEl) pipelineEl.innerHTML = '';
if (typeof loadBlogDrafts === 'function') loadBlogDrafts();
if (typeof viewBlogDraft === 'function') viewBlogDraft(id);
}, 2000);
} else {
pollPipeline(id, attempt + 1);
}
}).catch(function() { pollPipeline(id, attempt + 1); });
}, 8000); // Poll every 8s instead of 15s for snappier UI
}
// Hot topics loader
window.loadHotTopics = function() {
var grid = document.getElementById('hot-topics-grid');
var subtitle = document.getElementById('hot-topics-subtitle');
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;
}
// Update subtitle with next-refresh countdown
if (subtitle && data.refreshes_at) {
var nextRefresh = new Date(data.refreshes_at);
var hoursLeft = Math.round((nextRefresh - new Date()) / 3600000);
subtitle.textContent = 'auto-discovered · rotates daily · next refresh in ' + hoursLeft + 'h';
}
var colors = { breaking: '#c1121f', hot: '#FF8100', trending: '#e6a800', emerging: '#2d6a4f' };
grid.innerHTML = data.topics.map(function(t) {
var c = colors[t.urgency] || '#888';
// Pass full topic title and angle as data attributes to avoid quote-escaping hell
var cardId = 'ht-' + Math.random().toString(36).slice(2, 8);
// Store topic data for onclick
window['_ht_' + cardId] = t;
return '<div class="gen-card" style="cursor:pointer;border-left:3px solid ' + c + '" ' +
'onclick="window._generateFromHotTopic(\'' + cardId + '\')">' +
'<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, 90) + '</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>';
});
};
// Generate blog from hot topic card — uses title + angle from stored topic object
window._generateFromHotTopic = function(cardId) {
var t = window['_ht_' + cardId];
if (!t) return;
generateBlog(t.blog_type || 'hype_cycle', null, t.title, t.suggested_angle || t.description);
};
// Auto-load hot topics when blog tab activates
var origActivateTab = window.activateTab;
if (origActivateTab) {
window.activateTab = function(tabName) {
origActivateTab(tabName);
if (tabName === 'blog') loadHotTopics();
};
}
})();
// Called via data attributes to avoid quote-escaping issues in onclick
window.blogDeleteClick = function(el) {
var id = el.getAttribute('data-blog-id');
var title = el.getAttribute('data-blog-title');
if (id) window.deleteBlogDraft(id, title);
};
// 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...');
});
};