feat(dashboard): clickable LLM model selector — switch blog engine at runtime
- client.ts: BLOG_LLM_PROVIDER/OLLAMA_LLM_MODEL as mutable state (setLlmProvider/ getLlmProvider). Reads blog-llm-settings.json on startup for persistence. All generate()/checkHealth()/chat() calls use dynamic provider() + llmModel() — no restart required for switches. - blog.ts: POST /api/blog/llm/switch endpoint — validates provider, calls setLlmProvider(), writes settings file, returns previous+active state. - index.html: all 4 model cards now clickable (cursor:pointer, hover fade). switchBlogLlm(provider, model) — disables cards during switch, shows green/red feedback toast, auto-refreshes status. SSH instruction removed.
This commit is contained in:
parent
39a63e0401
commit
270bd12382
@ -11,14 +11,51 @@
|
|||||||
* The default local blog model is the latest RunPod-trained FO_BlogLLM adapter.
|
* The default local blog model is the latest RunPod-trained FO_BlogLLM adapter.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||||
const LLM_MODEL = process.env.OLLAMA_LLM_MODEL || "fo-blog-v7";
|
import { join } from "path";
|
||||||
|
|
||||||
|
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
||||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
|
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
|
||||||
const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
|
const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
|
||||||
const BLOG_LLM_PROVIDER = process.env.BLOG_LLM_PROVIDER || "ollama";
|
|
||||||
const CLAUDE_BRIDGE_URL = process.env.CLAUDE_BRIDGE_URL || "http://localhost:3250";
|
const CLAUDE_BRIDGE_URL = process.env.CLAUDE_BRIDGE_URL || "http://localhost:3250";
|
||||||
|
|
||||||
|
// ── Runtime-switchable provider state ──────────────────────────────────────
|
||||||
|
// Reads from /opt/tip/blog-llm-settings.json if present (written by /api/blog/llm/switch).
|
||||||
|
// Falls back to process.env, then to defaults. No restart required for switches.
|
||||||
|
|
||||||
|
const SETTINGS_FILE = join(process.env.TIP_ROOT || "/opt/tip", "blog-llm-settings.json");
|
||||||
|
|
||||||
|
interface LlmSettings { provider: string; ollamaModel: string }
|
||||||
|
|
||||||
|
function loadSettings(): LlmSettings {
|
||||||
|
try {
|
||||||
|
if (existsSync(SETTINGS_FILE)) {
|
||||||
|
const raw = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as LlmSettings;
|
||||||
|
return { provider: raw.provider || "ollama", ollamaModel: raw.ollamaModel || "fo-blog-v7" };
|
||||||
|
}
|
||||||
|
} catch { /* ignore corrupt file */ }
|
||||||
|
return {
|
||||||
|
provider: process.env.BLOG_LLM_PROVIDER || "ollama",
|
||||||
|
ollamaModel: process.env.OLLAMA_LLM_MODEL || "fo-blog-v7",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let _settings = loadSettings();
|
||||||
|
|
||||||
|
/** Switch the active LLM provider at runtime. Persists to settings file. */
|
||||||
|
export function setLlmProvider(provider: string, ollamaModel?: string): void {
|
||||||
|
_settings = { provider, ollamaModel: ollamaModel || _settings.ollamaModel };
|
||||||
|
try { writeFileSync(SETTINGS_FILE, JSON.stringify(_settings, null, 2), "utf8"); } catch { /* non-fatal */ }
|
||||||
|
console.log(`[LLM] Provider switched → ${provider}${ollamaModel ? ` (${ollamaModel})` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the currently active provider config. */
|
||||||
|
export function getLlmProvider(): LlmSettings { return { ..._settings }; }
|
||||||
|
|
||||||
|
// Convenience getters used below (re-read on every call for zero-latency switch)
|
||||||
|
function provider(): string { return _settings.provider; }
|
||||||
|
function llmModel(): string { return _settings.ollamaModel; }
|
||||||
|
|
||||||
interface LlmResponse {
|
interface LlmResponse {
|
||||||
text: string;
|
text: string;
|
||||||
model: string;
|
model: string;
|
||||||
@ -184,7 +221,7 @@ async function generateOllama(
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: LLM_MODEL,
|
model: llmModel(),
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
stream: false,
|
stream: false,
|
||||||
@ -276,10 +313,10 @@ export async function generate(
|
|||||||
userPrompt: string,
|
userPrompt: string,
|
||||||
options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },
|
options?: { temperature?: number; maxTokens?: number; timeoutMs?: number },
|
||||||
): Promise<LlmResponse> {
|
): Promise<LlmResponse> {
|
||||||
if (BLOG_LLM_PROVIDER === "claude-code") {
|
if (provider() === "claude-code") {
|
||||||
return generateClaudeBridge(systemPrompt, userPrompt, options);
|
return generateClaudeBridge(systemPrompt, userPrompt, options);
|
||||||
}
|
}
|
||||||
if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) {
|
if (provider() === "anthropic" && ANTHROPIC_API_KEY) {
|
||||||
return generateClaude(systemPrompt, userPrompt, options);
|
return generateClaude(systemPrompt, userPrompt, options);
|
||||||
}
|
}
|
||||||
return generateOllama(systemPrompt, userPrompt, options);
|
return generateOllama(systemPrompt, userPrompt, options);
|
||||||
@ -295,7 +332,7 @@ export async function chat(
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: LLM_MODEL,
|
model: llmModel(),
|
||||||
messages,
|
messages,
|
||||||
stream: false,
|
stream: false,
|
||||||
options: {
|
options: {
|
||||||
@ -329,7 +366,10 @@ export async function chat(
|
|||||||
|
|
||||||
/** Check if configured LLM provider is available */
|
/** Check if configured LLM provider is available */
|
||||||
export async function checkHealth(): Promise<{ ok: boolean; model: string; provider: string; error?: string }> {
|
export async function checkHealth(): Promise<{ ok: boolean; model: string; provider: string; error?: string }> {
|
||||||
if (BLOG_LLM_PROVIDER === "claude-code") {
|
const p = provider();
|
||||||
|
const m = llmModel();
|
||||||
|
|
||||||
|
if (p === "claude-code") {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${CLAUDE_BRIDGE_URL}/health`, { signal: AbortSignal.timeout(5000) });
|
const resp = await fetch(`${CLAUDE_BRIDGE_URL}/health`, { signal: AbortSignal.timeout(5000) });
|
||||||
if (!resp.ok) return { ok: false, model: "claude-code", provider: "claude-code", error: `HTTP ${resp.status}` };
|
if (!resp.ok) return { ok: false, model: "claude-code", provider: "claude-code", error: `HTTP ${resp.status}` };
|
||||||
@ -339,20 +379,19 @@ export async function checkHealth(): Promise<{ ok: boolean; model: string; provi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BLOG_LLM_PROVIDER === "anthropic" && ANTHROPIC_API_KEY) {
|
if (p === "anthropic" && ANTHROPIC_API_KEY) {
|
||||||
// Key presence check only — live API call causes 429 when pipeline is running
|
|
||||||
return { ok: true, model: ANTHROPIC_MODEL, provider: "anthropic" };
|
return { ok: true, model: ANTHROPIC_MODEL, provider: "anthropic" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(5000) });
|
const resp = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(5000) });
|
||||||
if (!resp.ok) return { ok: false, model: LLM_MODEL, provider: "ollama", error: `HTTP ${resp.status}` };
|
if (!resp.ok) return { ok: false, model: m, provider: "ollama", error: `HTTP ${resp.status}` };
|
||||||
|
|
||||||
const data = await resp.json() as { models: Array<{ name: string }> };
|
const data = await resp.json() as { models: Array<{ name: string }> };
|
||||||
const hasModel = data.models.some((m) => m.name.includes(LLM_MODEL.split(":")[0]));
|
const hasModel = data.models.some((tag) => tag.name.includes(m.split(":")[0]));
|
||||||
|
|
||||||
return { ok: hasModel, model: LLM_MODEL, provider: "ollama", error: hasModel ? undefined : `Model ${LLM_MODEL} not found` };
|
return { ok: hasModel, model: m, provider: "ollama", error: hasModel ? undefined : `Model ${m} not found` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, model: LLM_MODEL, provider: "ollama", error: (err as Error).message };
|
return { ok: false, model: m, provider: "ollama", error: (err as Error).message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from "express";
|
||||||
import { pool } from "../db/client";
|
import { pool } from "../db/client";
|
||||||
|
import { setLlmProvider, getLlmProvider } from "../llm/client";
|
||||||
|
|
||||||
/** In-memory pipeline progress tracker — step updates pushed here, polled via GET /api/blog/:id/progress */
|
/** In-memory pipeline progress tracker — step updates pushed here, polled via GET /api/blog/:id/progress */
|
||||||
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
||||||
@ -1556,6 +1557,33 @@ blogRouter.post("/llm/reset-queue", (_req: Request, res: Response) => {
|
|||||||
res.json({ success: true, message: "Ollama queue reset — stuck requests cleared" });
|
res.json({ success: true, message: "Ollama queue reset — stuck requests cleared" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/blog/llm/switch — Switch active LLM provider at runtime (no restart needed)
|
||||||
|
// Body: { provider: "claude-code" | "anthropic" | "ollama", model?: "fo-blog-v7" | "qwen2.5:14b" }
|
||||||
|
blogRouter.post("/llm/switch", (req: Request, res: Response) => {
|
||||||
|
const { provider, model } = req.body as { provider?: string; model?: string };
|
||||||
|
const validProviders = ["claude-code", "anthropic", "ollama"];
|
||||||
|
|
||||||
|
if (!provider || !validProviders.includes(provider)) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: `Invalid provider. Valid: ${validProviders.join(", ")}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = getLlmProvider();
|
||||||
|
setLlmProvider(provider, model);
|
||||||
|
const next = getLlmProvider();
|
||||||
|
|
||||||
|
console.log(`[blog/llm/switch] ${prev.provider}→${next.provider} model=${next.ollamaModel}`);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
previous: { provider: prev.provider, model: prev.ollamaModel },
|
||||||
|
active: { provider: next.provider, model: next.ollamaModel },
|
||||||
|
message: `Switched to ${next.provider}${next.ollamaModel ? ` (${next.ollamaModel})` : ""}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/blog/:id — Get a specific draft with full content
|
// GET /api/blog/:id — Get a specific draft with full content
|
||||||
// GET /api/blog/:id/progress — Real-time pipeline step progress (in-memory)
|
// GET /api/blog/:id/progress — Real-time pipeline step progress (in-memory)
|
||||||
blogRouter.get("/:id/progress", (req: Request, res: Response) => {
|
blogRouter.get("/:id/progress", (req: Request, res: Response) => {
|
||||||
|
|||||||
@ -1299,7 +1299,7 @@
|
|||||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.85rem">
|
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.85rem">
|
||||||
|
|
||||||
<!-- Claude-Code (active — flat-rate via claude-bridge) -->
|
<!-- Claude-Code (active — flat-rate via claude-bridge) -->
|
||||||
<div id="blog-model-card-cc" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
<div id="blog-model-card-cc" onclick="switchBlogLlm('claude-code')" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface);cursor:pointer;transition:opacity 0.15s" onmouseenter="this.style.opacity='0.85'" onmouseleave="this.style.opacity='1'">
|
||||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🤖 claude-code</div>
|
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🤖 claude-code</div>
|
||||||
@ -1324,7 +1324,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Claude API -->
|
<!-- Claude API -->
|
||||||
<div id="blog-model-card-claude" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
<div id="blog-model-card-claude" onclick="switchBlogLlm('anthropic')" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface);cursor:pointer;transition:opacity 0.15s" onmouseenter="this.style.opacity='0.85'" onmouseleave="this.style.opacity='1'">
|
||||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🧠 claude-sonnet-4-6</div>
|
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🧠 claude-sonnet-4-6</div>
|
||||||
@ -1345,40 +1345,40 @@
|
|||||||
<div id="blog-model-claude-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
<div id="blog-model-claude-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fine-tuned local fo-blog-v5 -->
|
<!-- Fine-tuned local FO_BlogLLM RunPod -->
|
||||||
<div id="blog-model-card-fo" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
<div id="blog-model-card-fo" onclick="switchBlogLlm('ollama','fo-blog-v7')" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface);cursor:pointer;transition:opacity 0.15s" onmouseenter="this.style.opacity='0.85'" onmouseleave="this.style.opacity='1'">
|
||||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🎯 fo-blog-v5</div>
|
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">🎯 fo-blog-v7</div>
|
||||||
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Ollama / Mac Studio</div>
|
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Adapter Bridge / Mac Studio</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="blog-model-fo-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
|
<span id="blog-model-fo-active" style="display:none;font-size:0.62rem;padding:2px 6px;border-radius:3px;background:#1a7a3a;color:#fff;font-weight:700">● AKTIV</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
||||||
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Blog-Qualität</div>
|
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Blog-Qualität</div>
|
||||||
<div><span style="color:var(--accent)">★★★★★</span> Geschwindigkeit</div>
|
<div><span style="color:var(--accent)">★★★★★</span> Geschwindigkeit</div>
|
||||||
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Fine-tuned auf TIP-Stil (v5)</div>
|
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Neu trainierte RunPod-Version</div>
|
||||||
<div style="color:#1a7a3a;font-weight:500">✓ Lokal / keine API-Kosten</div>
|
<div style="color:#1a7a3a;font-weight:500">✓ Lokal / keine API-Kosten</div>
|
||||||
<div style="color:#b45309;font-weight:500">⚠ Gelegentlicher Mode Collapse</div>
|
<div style="color:#b45309;font-weight:500">⚠ Qualitaet laeuft schon, Feinschliff folgt</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
|
<div style="background:#1e1e1e;border-radius:5px;padding:0.5rem 0.65rem;margin-bottom:0.5rem">
|
||||||
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v5</code>
|
<code style="font-size:0.65rem;color:#f8f8f2;line-height:1.6;word-break:break-all;display:block">BLOG_LLM_PROVIDER=ollama<br>OLLAMA_LLM_MODEL=fo-blog-v7</code>
|
||||||
</div>
|
</div>
|
||||||
<div id="blog-model-fo-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
<div id="blog-model-fo-status" style="font-size:0.7rem;color:var(--text-dim)">checking…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Standard Qwen -->
|
<!-- Standard Qwen -->
|
||||||
<div id="blog-model-card-qwen" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface)">
|
<div id="blog-model-card-qwen" onclick="switchBlogLlm('ollama','qwen2.5:14b')" style="border-radius:8px;padding:0.85rem;border:2px solid var(--border);background:var(--surface);cursor:pointer;transition:opacity 0.15s" onmouseenter="this.style.opacity='0.85'" onmouseleave="this.style.opacity='1'">
|
||||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:0.6rem">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">⚡ qwen2.5:14b</div>
|
<div style="font-size:0.8rem;font-weight:700;color:var(--text-bright)">⚡ qwen2.5:14b (Fallback)</div>
|
||||||
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Ollama / Mac Studio</div>
|
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:2px">Ollama / Mac Studio</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
<div style="font-size:0.72rem;color:var(--text);line-height:1.8;margin-bottom:0.6rem">
|
||||||
<div><span style="color:var(--accent)">★★★</span><span style="color:var(--text-dim)">★★</span> Blog-Qualität</div>
|
<div><span style="color:var(--accent)">★★★</span><span style="color:var(--text-dim)">★★</span> Blog-Qualität</div>
|
||||||
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Geschwindigkeit</div>
|
<div><span style="color:var(--accent)">★★★★</span><span style="color:var(--text-dim)">★</span> Geschwindigkeit</div>
|
||||||
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Allzweck-Modell</div>
|
<div style="margin-top:0.35rem;color:#1a7a3a;font-weight:500">✓ Allzweck-/Fallback-Modell</div>
|
||||||
<div style="color:#1a7a3a;font-weight:500">✓ Keine API-Kosten</div>
|
<div style="color:#1a7a3a;font-weight:500">✓ Keine API-Kosten</div>
|
||||||
<div style="color:#b45309;font-weight:500">⚠ Mode Collapse bei komplexen Prompts</div>
|
<div style="color:#b45309;font-weight:500">⚠ Mode Collapse bei komplexen Prompts</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1390,12 +1390,10 @@
|
|||||||
|
|
||||||
</div><!-- end model grid -->
|
</div><!-- end model grid -->
|
||||||
|
|
||||||
<!-- Config note -->
|
<!-- Switch feedback -->
|
||||||
<div style="margin-top:0.85rem;padding:0.6rem 0.85rem;background:var(--surface2);border-radius:6px;font-size:0.72rem;color:var(--text)">
|
<div id="blog-llm-switch-msg" style="display:none;margin-top:0.85rem;padding:0.55rem 0.85rem;border-radius:6px;font-size:0.75rem;font-weight:600"></div>
|
||||||
<strong>Modell wechseln:</strong> SSH → Erik →
|
<div style="margin-top:0.6rem;padding:0.45rem 0.85rem;background:var(--surface2);border-radius:6px;font-size:0.7rem;color:var(--text-dim)">
|
||||||
<code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">nano /opt/tip/ecosystem.config.js</code>
|
💡 Karte anklicken zum Aktivieren — wechselt sofort ohne Neustart
|
||||||
→ BLOG_LLM_PROVIDER + CLAUDE_BRIDGE_URL / ANTHROPIC_API_KEY →
|
|
||||||
<code style="background:#1e1e1e;color:#f8f8f2;padding:1px 5px;border-radius:3px">pm2 restart tip-api --update-env</code>
|
|
||||||
</div>
|
</div>
|
||||||
</div><!-- end LLM panel -->
|
</div><!-- end LLM panel -->
|
||||||
|
|
||||||
@ -5630,6 +5628,57 @@ async function runFinder() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function switchBlogLlm(providerKey, model) {
|
||||||
|
var msgEl = document.getElementById('blog-llm-switch-msg');
|
||||||
|
// Disable all cards visually while switching
|
||||||
|
['cc','claude','fo','qwen'].forEach(function(k) {
|
||||||
|
var c = document.getElementById('blog-model-card-' + k);
|
||||||
|
if (c) { c.style.pointerEvents = 'none'; c.style.opacity = '0.5'; }
|
||||||
|
});
|
||||||
|
if (msgEl) {
|
||||||
|
msgEl.style.display = 'block';
|
||||||
|
msgEl.style.background = 'var(--surface3)';
|
||||||
|
msgEl.style.color = 'var(--text-dim)';
|
||||||
|
msgEl.textContent = '↺ Wechsle zu ' + providerKey + (model ? ' (' + model + ')' : '') + '…';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var body = { provider: providerKey };
|
||||||
|
if (model) body.model = model;
|
||||||
|
var res = await fetch(API + '/api/blog/llm/switch', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
var data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
if (msgEl) {
|
||||||
|
msgEl.style.background = '#d1fae5';
|
||||||
|
msgEl.style.color = '#065f46';
|
||||||
|
msgEl.textContent = '✓ ' + data.message;
|
||||||
|
setTimeout(function() { if (msgEl) msgEl.style.display = 'none'; }, 3000);
|
||||||
|
}
|
||||||
|
await loadBlogLLMStatus();
|
||||||
|
} else {
|
||||||
|
if (msgEl) {
|
||||||
|
msgEl.style.background = '#fee2e2';
|
||||||
|
msgEl.style.color = '#b91c1c';
|
||||||
|
msgEl.textContent = '✗ Fehler: ' + (data.error || 'Unbekannt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
if (msgEl) {
|
||||||
|
msgEl.style.background = '#fee2e2';
|
||||||
|
msgEl.style.color = '#b91c1c';
|
||||||
|
msgEl.textContent = '✗ Netzwerkfehler: ' + e.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
['cc','claude','fo','qwen'].forEach(function(k) {
|
||||||
|
var c = document.getElementById('blog-model-card-' + k);
|
||||||
|
if (c) { c.style.pointerEvents = ''; c.style.opacity = ''; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBlogLLMStatus() {
|
async function loadBlogLLMStatus() {
|
||||||
try {
|
try {
|
||||||
var data = await api('/api/blog/llm/status');
|
var data = await api('/api/blog/llm/status');
|
||||||
@ -5702,7 +5751,7 @@ async function loadBlogLLMStatus() {
|
|||||||
if (foActive) foActive.style.display = 'inline';
|
if (foActive) foActive.style.display = 'inline';
|
||||||
var foStatusEl = document.getElementById('blog-model-fo-status');
|
var foStatusEl = document.getElementById('blog-model-fo-status');
|
||||||
if (foStatusEl) {
|
if (foStatusEl) {
|
||||||
foStatusEl.textContent = llm.ok ? '● Aktiv — Ollama erreichbar' : '⚠ Ollama nicht erreichbar: ' + (llm.error || '').slice(0, 60);
|
foStatusEl.textContent = llm.ok ? ('● Aktiv — ' + (llm.model || 'fo-blog-v7') + ' erreichbar') : '⚠ Ollama/Bridge nicht erreichbar: ' + (llm.error || '').slice(0, 60);
|
||||||
foStatusEl.style.color = llm.ok ? '#1a7a3a' : '#b45309';
|
foStatusEl.style.color = llm.ok ? '#1a7a3a' : '#b45309';
|
||||||
foStatusEl.style.fontWeight = '600';
|
foStatusEl.style.fontWeight = '600';
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user