6-source composite Market Signal Score (0-100) per transceiver technology. New GET /api/hype-cycle/market-signals blends: Norton-Bass hype_score, hyperscaler CapEx YoY (MSFT +68.8%, GOOG +107%, META +46.8%), price observation activity ratio 30d vs prior 30d, AI cluster transceiver demand, eBay secondary market sell-through velocity, internal fast-mover trend. All 6 queries run in parallel via Promise.all(). Recommendation engine maps hype phase × capex boom × speed class → Buy/Hold/Watch labels with tooltips. Dashboard Hype Cycle table now shows Market Signal ● LIVE column + Recommendation column. Hyperscaler CapEx panel + eBay panel added to hype tab. Procurement: new eBay Market section. Sourcing Hype Cycle replaced hardcoded seed with live price observation data.
304 lines
15 KiB
JavaScript
304 lines
15 KiB
JavaScript
/**
|
|
* Hot Topics + Blog Pipeline UX Enhancement (v0.3.0)
|
|
* Loaded after main dashboard script.
|
|
* Overrides generateBlog + pollBlogLlm with improved versions.
|
|
*
|
|
* v0.3.0: All fetch() calls now include Authorization Bearer token.
|
|
*/
|
|
(function() {
|
|
var API = window.API || '';
|
|
var blogPipelineRunning = false;
|
|
|
|
/** Get auth headers — uses obfuscated token helper from index.html (loadToken) */
|
|
function authHeaders(extra) {
|
|
var token = (window.loadToken ? window.loadToken() : localStorage.getItem('tip_token')) || '';
|
|
var h = { 'Authorization': 'Bearer ' + token };
|
|
if (extra) Object.assign(h, extra);
|
|
return h;
|
|
}
|
|
|
|
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;
|
|
|
|
// Fetch the current active model name so we never show a stale hardcoded version.
|
|
var initialModelLabel = (window._activeFoBlogModel) || 'FO_BlogLLM';
|
|
fetch(API + '/api/blog/llm/status', { headers: authHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
var m = data && data.llm && data.llm.model;
|
|
if (m) {
|
|
window._activeFoBlogModel = m;
|
|
var s = document.getElementById('bp-step');
|
|
if (s && s.textContent.indexOf('Connecting to FO_BlogLLM') === 0) {
|
|
s.textContent = 'Connecting to FO_BlogLLM (' + m + ')';
|
|
}
|
|
}
|
|
}).catch(function() {});
|
|
|
|
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 ' + initialModelLabel + '</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;
|
|
if (customTitle) body.custom_title = customTitle;
|
|
if (customAngle) body.additional_context = customAngle;
|
|
|
|
fetch(API + '/api/blog/generate', {
|
|
method: 'POST',
|
|
headers: authHeaders({ '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();
|
|
});
|
|
};
|
|
|
|
// Track how many consecutive polls had running=false (stall detection)
|
|
var _stallCount = 0;
|
|
|
|
function showStallWarning(id) {
|
|
var status = document.getElementById('bp-status');
|
|
var step = document.getElementById('bp-step');
|
|
if (status) {
|
|
status.style.color = '#e6a800';
|
|
status.textContent = '⚠ Pipeline nicht aktiv (API-Neustart?) — LLM läuft evtl. noch';
|
|
}
|
|
if (step) {
|
|
step.innerHTML = 'Status unklar · '
|
|
+ '<button onclick="window._resetAndRetry(\'' + id + '\')" '
|
|
+ 'style="background:#FF8100;color:white;border:none;padding:2px 10px;border-radius:4px;cursor:pointer;font-size:0.75rem;font-weight:600">'
|
|
+ '↺ Reset & Retry</button>';
|
|
}
|
|
}
|
|
|
|
window._resetAndRetry = function(id) {
|
|
_stallCount = 0;
|
|
// Reset Ollama queue then regenerate
|
|
fetch(API + '/api/blog/llm/reset-queue', { method: 'POST', headers: authHeaders() }).catch(function() {});
|
|
var step = document.getElementById('bp-step');
|
|
var status = document.getElementById('bp-status');
|
|
if (status) { status.style.color = '#FF8100'; status.textContent = 'Restarting pipeline…'; }
|
|
if (step) step.textContent = 'Sending to LLM…';
|
|
fetch(API + '/api/blog/' + id + '/regenerate', {
|
|
method: 'POST',
|
|
headers: authHeaders({ 'Content-Type': 'application/json' })
|
|
}).then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.success) {
|
|
if (typeof showToast === 'function') showToast('Pipeline gestartet', 'LLM läuft neu — warte auf Ergebnis');
|
|
pollPipeline(id, 0);
|
|
} else {
|
|
if (typeof showToast === 'function') showToast('Fehler', data.error || 'Regenerierung fehlgeschlagen', true);
|
|
}
|
|
}).catch(function(err) {
|
|
if (typeof showToast === 'function') showToast('Network Error', err.message, true);
|
|
});
|
|
};
|
|
|
|
function pollPipeline(id, attempt) {
|
|
if (attempt > 90) {
|
|
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;
|
|
}
|
|
setTimeout(function() {
|
|
// 1) Fetch real-time progress
|
|
fetch(API + '/api/blog/' + id + '/progress', { headers: authHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(prog) {
|
|
if (prog.running) {
|
|
_stallCount = 0;
|
|
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.style.color = '#FF8100'; status.textContent = prog.label || ('Step ' + prog.step + '/10'); }
|
|
if (step) step.textContent = 'Step ' + prog.step + '/10 · ' + (window._activeFoBlogModel || 'fo-blog-v10') + ' via adapter bridge';
|
|
} else {
|
|
_stallCount++;
|
|
// After 5 consecutive non-running polls (~40s), show stall warning
|
|
if (_stallCount >= 5) showStallWarning(id);
|
|
}
|
|
}).catch(function() { _stallCount++; });
|
|
|
|
// 2) Fetch blog draft to detect completion
|
|
fetch(API + '/api/blog/' + id, { headers: authHeaders() })
|
|
.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) {
|
|
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;
|
|
_stallCount = 0;
|
|
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);
|
|
}
|
|
|
|
// 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>';
|
|
|
|
var shuffle = Date.now().toString(36);
|
|
fetch(API + '/api/hot-topics?limit=20&shuffle=' + encodeURIComponent(shuffle), { headers: authHeaders({ 'Cache-Control': 'no-cache' }) })
|
|
.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;
|
|
}
|
|
|
|
if (subtitle && data.refreshes_at) {
|
|
var nextRefresh = new Date(data.refreshes_at);
|
|
var hoursLeft = Math.round((nextRefresh - new Date()) / 3600000);
|
|
subtitle.textContent = data.total + ' topics · refresh reshuffles · daily base rotation in ' + hoursLeft + 'h · sources: ' + (data.sources || []).join(', ');
|
|
}
|
|
|
|
var colors = { breaking: '#c1121f', hot: '#FF8100', trending: '#e6a800', emerging: '#2d6a4f' };
|
|
grid.innerHTML = data.topics.map(function(t) {
|
|
var c = colors[t.urgency] || '#888';
|
|
var cardId = 'ht-' + Math.random().toString(36).slice(2, 8);
|
|
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 || '') + (t.blog_title_created ? ' · <span style="background:#1b4332;color:#6ee7b7;font-size:0.6rem;padding:1px 5px;border-radius:3px;font-weight:700;text-transform:none">✓ Blog erstellt</span>' : '') + '</span>' +
|
|
'<span style="font-size:0.6rem;color:var(--text-dim)">' + (t.source_type || '') + ' · ' + (t.source || '') + '</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;line-height:1.4">' + (t.suggested_angle || t.description || '').slice(0, 100) + '</div>' +
|
|
(t.date ? '<div style="font-size:0.62rem;color:var(--text-dim);margin-top:5px">' + new Date(t.date).toLocaleDateString('de-DE', {day:'2-digit',month:'short',year:'numeric'}) + '</div>' : '') +
|
|
'</div>';
|
|
}).join('');
|
|
}).catch(function(err) {
|
|
console.error('[HotTopics] fetch error:', err);
|
|
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
|
|
window._generateFromHotTopic = function(cardId) {
|
|
var t = window['_ht_' + cardId];
|
|
if (!t) return;
|
|
generateBlog(t.blog_type || 'hype_cycle', null, t.title, t.llm_context || 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;
|
|
var token = (window.loadToken ? window.loadToken() : localStorage.getItem('tip_token')) || '';
|
|
fetch((window.API || '') + '/api/blog/' + id, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
}).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
|
|
window.deleteAllTemplateDrafts = function() {
|
|
if (!confirm('Delete ALL template drafts? LLM-generated articles will be kept.')) return;
|
|
var token = (window.loadToken ? window.loadToken() : localStorage.getItem('tip_token')) || '';
|
|
var authH = { 'Authorization': 'Bearer ' + token };
|
|
fetch((window.API || '') + '/api/blog', { headers: authH })
|
|
.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', headers: authH })
|
|
.then(function() {
|
|
count++;
|
|
if (count === templates.length && typeof loadBlogDrafts === 'function') loadBlogDrafts();
|
|
});
|
|
});
|
|
if (typeof showToast === 'function') showToast('Cleaning', 'Deleting ' + templates.length + ' template drafts...');
|
|
});
|
|
};
|