feat: add LLM model selector panel to Blog Engine tab

Shows active model (fo-blog-v3-qwen7b / claude-sonnet-4-6 / qwen2.5:14b),
live status from /api/blog/llm/status, ratings, config instructions,
and highlights which model is currently active.
This commit is contained in:
Rene Fichtmueller 2026-04-09 20:42:03 +02:00
parent 287eba1337
commit 692133f2ea

View File

@ -1206,6 +1206,99 @@
<!-- BLOG -->
<div id="tab-blog" class="hidden">
<!-- LLM ENGINE PANEL -->
<div class="card" style="margin-bottom:1.25rem;border:1px solid rgba(124,92,252,0.3);background:var(--surface2)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.85rem">
<div>
<span style="font-size:0.85rem;font-weight:700;color:#a78bfa">🤖 Blog Generation LLM</span>
<span id="blog-llm-status-badge" style="margin-left:0.5rem;font-size:0.68rem;padding:2px 7px;border-radius:10px;background:rgba(100,100,100,0.25);color:var(--text-dim)">loading…</span>
</div>
<button onclick="loadBlogLLMStatus()" style="background:transparent;color:var(--text-dim);border:1px solid var(--border);padding:3px 10px;border-radius:6px;cursor:pointer;font-size:0.72rem"></button>
</div>
<!-- Active model info bar -->
<div id="blog-llm-active-bar" style="background:rgba(124,92,252,0.08);border:1px solid rgba(124,92,252,0.2);border-radius:8px;padding:0.6rem 0.85rem;margin-bottom:0.9rem;display:flex;align-items:center;gap:0.75rem">
<span style="font-size:0.8rem;color:var(--text-dim)">Active model:</span>
<span id="blog-llm-active-model" style="font-family:monospace;font-size:0.8rem;color:#a78bfa;font-weight:600"></span>
<span id="blog-llm-active-provider" style="font-size:0.7rem;padding:2px 6px;border-radius:4px;background:rgba(124,92,252,0.15);color:#a78bfa"></span>
<span id="blog-llm-queue" style="margin-left:auto;font-size:0.72rem;color:var(--text-dim)"></span>
</div>
<!-- Model cards -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem">
<!-- Claude (recommended) -->
<div id="blog-model-card-claude" style="border-radius:8px;padding:0.75rem;border:1px solid rgba(124,92,252,0.25);background:rgba(124,92,252,0.06);position:relative">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.4rem">
<span style="font-size:0.85rem">🧠</span>
<span style="font-size:0.78rem;font-weight:700;color:#a78bfa">claude-sonnet-4-6</span>
<span style="margin-left:auto;font-size:0.65rem;padding:1px 5px;border-radius:3px;background:rgba(124,92,252,0.25);color:#a78bfa;font-weight:700">RECOMMENDED</span>
</div>
<div style="font-size:0.7rem;color:var(--text-dim);line-height:1.5">
<div>★★★★★ Blog quality</div>
<div>★★★★☆ Speed (API latency)</div>
<div style="margin-top:0.3rem;color:#86efac">✓ Multi-constraint prompts</div>
<div style="color:#86efac">✓ No mode collapse</div>
<div style="color:#86efac">✓ 4096 token output</div>
<div style="margin-top:0.3rem;color:var(--text-dim)">Provider: Anthropic API</div>
</div>
<div style="margin-top:0.6rem;padding-top:0.5rem;border-top:1px solid var(--border)">
<code style="font-size:0.65rem;color:#a78bfa;word-break:break-all">BLOG_LLM_PROVIDER=anthropic<br>ANTHROPIC_MODEL=claude-sonnet-4-6</code>
</div>
<div id="blog-model-claude-status" style="margin-top:0.5rem;font-size:0.68rem;color:var(--text-dim)">checking…</div>
</div>
<!-- Fine-tuned local (current default) -->
<div id="blog-model-card-fo" style="border-radius:8px;padding:0.75rem;border:1px solid rgba(34,197,94,0.25);background:rgba(34,197,94,0.04)">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.4rem">
<span style="font-size:0.85rem">🎯</span>
<span style="font-size:0.78rem;font-weight:700;color:#86efac">fo-blog-v3-qwen7b</span>
<span id="blog-model-fo-active" style="display:none;margin-left:auto;font-size:0.65rem;padding:1px 5px;border-radius:3px;background:rgba(34,197,94,0.25);color:#86efac;font-weight:700">ACTIVE</span>
</div>
<div style="font-size:0.7rem;color:var(--text-dim);line-height:1.5">
<div>★★★★☆ Blog quality</div>
<div>★★★★★ Speed (local GPU)</div>
<div style="margin-top:0.3rem;color:#86efac">✓ Fine-tuned on TIP style</div>
<div style="color:#86efac">✓ Privacy (runs locally)</div>
<div style="color:#fbbf24">⚠ Occasional mode collapse</div>
<div style="margin-top:0.3rem;color:var(--text-dim)">Provider: Ollama / Mac Studio</div>
</div>
<div style="margin-top:0.6rem;padding-top:0.5rem;border-top:1px solid var(--border)">
<code style="font-size:0.65rem;color:#86efac;word-break:break-all">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v3-qwen7b</code>
</div>
<div id="blog-model-fo-status" style="margin-top:0.5rem;font-size:0.68rem;color:var(--text-dim)">checking…</div>
</div>
<!-- Standard Qwen -->
<div id="blog-model-card-qwen" style="border-radius:8px;padding:0.75rem;border:1px solid rgba(100,100,100,0.2);background:rgba(100,100,100,0.04)">
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.4rem">
<span style="font-size:0.85rem"></span>
<span style="font-size:0.78rem;font-weight:700;color:var(--text)">qwen2.5:14b</span>
</div>
<div style="font-size:0.7rem;color:var(--text-dim);line-height:1.5">
<div>★★★☆☆ Blog quality</div>
<div>★★★★☆ Speed (local GPU)</div>
<div style="margin-top:0.3rem;color:#86efac">✓ General purpose</div>
<div style="color:#86efac">✓ No API cost</div>
<div style="color:#fbbf24">⚠ Mode collapse on complex prompts</div>
<div style="margin-top:0.3rem;color:var(--text-dim)">Provider: Ollama / Mac Studio</div>
</div>
<div style="margin-top:0.6rem;padding-top:0.5rem;border-top:1px solid var(--border)">
<code style="font-size:0.65rem;color:var(--text-dim);word-break:break-all">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=qwen2.5:14b</code>
</div>
<div id="blog-model-qwen-status" style="margin-top:0.5rem;font-size:0.68rem;color:var(--text-dim)">local model</div>
</div>
</div><!-- end model grid -->
<!-- Config note -->
<div style="margin-top:0.75rem;padding:0.6rem 0.85rem;background:rgba(0,0,0,0.15);border-radius:6px;font-size:0.7rem;color:var(--text-dim)">
<strong style="color:var(--text)">Modell wechseln:</strong>
SSH → Erik → <code style="color:#a78bfa">nano /opt/tip/ecosystem.config.js</code> → BLOG_LLM_PROVIDER + ANTHROPIC_API_KEY setzen → <code style="color:#a78bfa">pm2 restart tip-api --update-env</code>
</div>
</div><!-- end LLM panel -->
<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 id="hot-topics-subtitle" 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>
@ -1874,7 +1967,7 @@ function goToTab(tabName) {
if (tabName === 'news') loadNews(1);
if (tabName === 'vendors') loadVendors();
if (tabName === 'standards') loadStandardsList();
if (tabName === 'blog') { loadBlogDrafts(); loadSLLInsights(); }
if (tabName === 'blog') { loadBlogDrafts(); loadSLLInsights(); loadBlogLLMStatus(); }
if (tabName === 'finder') document.getElementById('finder-switch-input').focus();
if (tabName === 'crawlers') loadCrawlerStatus();
if (tabName === 'procurement') loadProcurement();
@ -4301,6 +4394,69 @@ async function runFinder() {
async function loadBlogLLMStatus() {
try {
var data = await api('/api/blog/llm/status');
var llm = data.llm || {};
var badge = document.getElementById('blog-llm-status-badge');
var activeModel = document.getElementById('blog-llm-active-model');
var activeProvider = document.getElementById('blog-llm-active-provider');
var queueEl = document.getElementById('blog-llm-queue');
if (badge) {
badge.textContent = llm.ok ? 'online' : 'offline';
badge.style.background = llm.ok ? 'rgba(34,197,94,0.2)' : 'rgba(193,18,31,0.2)';
badge.style.color = llm.ok ? '#86efac' : '#f87171';
}
if (activeModel) activeModel.textContent = llm.model || '—';
if (activeProvider) {
activeProvider.textContent = llm.provider || '—';
activeProvider.style.background = llm.provider === 'anthropic' ? 'rgba(124,92,252,0.2)' : 'rgba(34,197,94,0.15)';
activeProvider.style.color = llm.provider === 'anthropic' ? '#a78bfa' : '#86efac';
}
if (queueEl) {
var q = data.queue_depth || 0;
queueEl.textContent = q > 0 ? 'Queue: ' + q + ' jobs' : 'Queue: idle';
}
// Mark active model card
var foActive = document.getElementById('blog-model-fo-active');
var foCard = document.getElementById('blog-model-card-fo');
var claudeCard = document.getElementById('blog-model-card-claude');
if (llm.provider === 'anthropic') {
if (claudeCard) claudeCard.style.border = '1px solid rgba(124,92,252,0.6)';
claudeCard.style.background = 'rgba(124,92,252,0.1)';
var claudeStatusEl = document.getElementById('blog-model-claude-status');
if (claudeStatusEl) {
claudeStatusEl.textContent = '● ACTIVE — API key configured';
claudeStatusEl.style.color = '#a78bfa';
}
document.getElementById('blog-model-fo-status').textContent = 'available (not active)';
} else {
// Ollama active
if (foActive) foActive.style.display = 'inline';
if (foCard) {
foCard.style.border = '1px solid rgba(34,197,94,0.5)';
foCard.style.background = 'rgba(34,197,94,0.08)';
}
var foStatusEl = document.getElementById('blog-model-fo-status');
if (foStatusEl) {
foStatusEl.textContent = llm.ok ? '● ACTIVE — Ollama reachable' : '⚠ Ollama unreachable: ' + (llm.error || 'check connection');
foStatusEl.style.color = llm.ok ? '#86efac' : '#fbbf24';
}
var claudeStatusEl2 = document.getElementById('blog-model-claude-status');
if (claudeStatusEl2) {
claudeStatusEl2.textContent = 'available — set BLOG_LLM_PROVIDER=anthropic + ANTHROPIC_API_KEY';
claudeStatusEl2.style.color = 'var(--text-dim)';
}
}
} catch(e) {
var b = document.getElementById('blog-llm-status-badge');
if (b) { b.textContent = 'error'; b.style.color = '#f87171'; }
}
}
async function loadBlogDrafts() {
var data = await api('/api/blog');
// Check which drafts are still generating (in-progress pipelines)