From ab811bda91d83dc36ab81e427f3f35d15dc5ae89 Mon Sep 17 00:00:00 2001 From: tcpsyn Date: Sun, 5 Apr 2026 12:42:59 -0600 Subject: [PATCH] Delete caller-model routing + preflight admin UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (~273 lines): - Removed GET/POST /api/caller-models endpoints (strategy/pool/map/fallback config) - Removed POST /api/caller-models/{caller_key} override endpoint - Removed GET /api/show/preflight diagnostics endpoint Frontend: - HTML: dropped Preflight header button, Caller Models settings panel, Preflight modal, and caller-model-badge/override from info panel. - JS (~328 lines): removed loadCallerModels, saveCallerModels, updateCallerModelUI/Badges, overrideCallerModel, showCallerModelBadge, populateCallerModelOverride, runPreflight + render helpers, MODEL_ABBREVS, CALLER_STYLES, and all their listeners and init calls. - CSS (~184 lines): removed .cm-*, .preflight-*, .caller-model-*, .model-tag, .info-badge.model, .caller-model-override rules. Session fields (caller_model_strategy/pool/map/fallback/models, caller_styles, caller_shapes) still present — they wire into shape picking and checkpoint restore and get deleted in the next commits. --- backend/main.py | 275 ------------------------------ frontend/css/style.css | 185 -------------------- frontend/index.html | 51 ------ frontend/js/app.js | 375 ----------------------------------------- 4 files changed, 886 deletions(-) diff --git a/backend/main.py b/backend/main.py index 0b4e943..8136522 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9884,281 +9884,6 @@ async def set_show_theme(data: dict): return {"theme": session.show_theme} -# --- Caller Model Routing --- - -@app.get("/api/caller-models") -async def get_caller_models(): - """Get current caller model routing config and per-caller assignments.""" - assignments = {} - for key in CALLER_BASES: - name = CALLER_BASES[key].get("name", key) - model = session.caller_models.get(key) - assignments[key] = {"name": name, "model": model or "(default)"} - return { - "strategy": session.caller_model_strategy, - "pool": session.caller_model_pool, - "map": session.caller_model_map, - "fallback": session.caller_model_fallback, - "assignments": assignments, - } - - -@app.post("/api/caller-models") -async def set_caller_models(data: dict): - """Update caller model routing strategy, pool, map, or fallback.""" - if "strategy" in data: - strategy = data["strategy"] - if strategy not in ("single", "cycle", "style_matched"): - raise HTTPException(400, f"Invalid strategy: {strategy}") - session.caller_model_strategy = strategy - print(f"[CallerModel] Strategy set to: {strategy}") - if "pool" in data: - pool = data["pool"] - if not isinstance(pool, list) or not pool: - raise HTTPException(400, "pool must be a non-empty list of model IDs") - session.caller_model_pool = pool - print(f"[CallerModel] Pool set to: {pool}") - if "map" in data: - session.caller_model_map = data["map"] - print(f"[CallerModel] Style map set: {len(data['map'])} entries") - if "fallback" in data: - session.caller_model_fallback = data["fallback"] - print(f"[CallerModel] Fallback set to: {data['fallback']}") - # Clear existing assignments so new strategy takes effect - if "strategy" in data or "pool" in data or "map" in data: - session.caller_models.clear() - session._caller_model_cycle_idx = 0 - print(f"[CallerModel] Cleared caller assignments (new config)") - _save_checkpoint() - return await get_caller_models() - - -@app.post("/api/caller-models/{caller_key}") -async def set_caller_model_override(caller_key: str, data: dict): - """Override the model for a specific caller mid-show.""" - if caller_key not in CALLER_BASES: - raise HTTPException(404, f"Unknown caller key: {caller_key}") - model = data.get("model", "").strip() - if not model: - # Clear override - session.caller_models.pop(caller_key, None) - name = CALLER_BASES[caller_key].get("name", caller_key) - print(f"[CallerModel] Cleared override for {name}") - else: - session.caller_models[caller_key] = model - name = CALLER_BASES[caller_key].get("name", caller_key) - print(f"[CallerModel] Override {name} → {model}") - _save_checkpoint() - return {"caller_key": caller_key, "model": session.caller_models.get(caller_key, "(default)")} - - -# --- Show Preflight --- - -@app.get("/api/show/preflight") -async def show_preflight(test_responses: bool = False): - """Run diagnostic checks before a show goes live.""" - from .services.tts import VOICE_PROFILES - - checks = {} - - # --- 1. Model diversity --- - caller_assignments = [] - model_counts: dict[str, int] = {} - for key, base in CALLER_BASES.items(): - raw_style = session.caller_styles.get(key, "") - style_key = _normalize_style_key(raw_style) if raw_style else "" - # Look up what model they'd get without persisting - model = session.caller_model_map.get(style_key) if style_key else None - if not model: - model = session.caller_model_fallback - caller_assignments.append({ - "name": base.get("name", key), - "style": style_key or "(none)", - "model": model, - }) - model_counts[model] = model_counts.get(model, 0) + 1 - - total_callers = len(caller_assignments) - max_same = max(model_counts.values()) if model_counts else 0 - diversity_status = "fail" if total_callers > 0 and max_same / total_callers > 0.5 else "pass" - checks["model_diversity"] = { - "status": diversity_status, - "details": { - "callers": caller_assignments, - "model_distribution": model_counts, - "max_same_model_pct": round(max_same / total_callers * 100) if total_callers else 0, - }, - } - - # --- 2. Theme penetration --- - if session.show_theme: - theme_words = [w.lower() for w in session.show_theme.split() if len(w) > 2] - hits = [] - misses = [] - for key, base in CALLER_BASES.items(): - bg = session.caller_backgrounds.get(key) - if not isinstance(bg, CallerBackground): - misses.append(base.get("name", key)) - continue - searchable = f"{bg.natural_description} {bg.situation_summary}".lower() - if any(tw in searchable for tw in theme_words): - hits.append(base.get("name", key)) - else: - misses.append(base.get("name", key)) - total_bg = len(hits) + len(misses) - hit_pct = round(len(hits) / total_bg * 100) if total_bg else 0 - theme_status = "warn" if total_bg > 0 and hit_pct < 40 else "pass" - checks["theme_penetration"] = { - "status": theme_status, - "details": { - "theme": session.show_theme, - "connected": hits, - "not_connected": misses, - "penetration_pct": hit_pct, - }, - } - else: - checks["theme_penetration"] = {"status": "skip", "details": {"reason": "No theme set"}} - - # --- 3. Voice-age alignment --- - mismatches = [] - alignments = [] - for key, base in CALLER_BASES.items(): - bg = session.caller_backgrounds.get(key) - if not isinstance(bg, CallerBackground): - continue - voice_name = base.get("voice", "") - profile = VOICE_PROFILES.get(voice_name, {}) - age_feel = profile.get("age_feel", "unknown") - is_mismatch = (bg.age >= 50 and age_feel == "young") or (bg.age < 25 and age_feel == "mature") - entry = { - "name": base.get("name", key), - "age": bg.age, - "voice": voice_name, - "voice_age_feel": age_feel, - "mismatch": is_mismatch, - } - alignments.append(entry) - if is_mismatch: - mismatches.append(entry) - - voice_status = "warn" if mismatches else "pass" - checks["voice_age_alignment"] = { - "status": voice_status, - "details": { - "callers": alignments, - "mismatches": len(mismatches), - }, - } - - # --- 4. Response coherence (optional) --- - if test_responses: - # Test ALL callers, not just a sample — we want confidence every caller works - test_callers = [] - for key, base in CALLER_BASES.items(): - raw_style = session.caller_styles.get(key, "") - style_key = _normalize_style_key(raw_style) if raw_style else "" - model = session.caller_model_map.get(style_key) if style_key else None - if not model: - model = session.caller_model_fallback - test_callers.append((key, base, model)) - - coherence_results = [] - coherence_fail = False - - # Run all tests in parallel for speed - async def _test_caller(key, base, model): - slim_caller = session.caller_backgrounds.get(key, {}) - prompt = get_caller_prompt(slim_caller) - # Simulate a real 3-exchange conversation - max_tok, _ = _pick_response_budget(session.caller_shapes.get(key, "standard")) - messages = [ - {"role": "user", "content": "Hey welcome to the show, what's going on tonight?"}, - ] - try: - # First response - r1 = await llm_service.generate( - messages=messages, system_prompt=prompt, - max_tokens=max_tok, category="caller_dialog", - caller_name=base.get("name", ""), model_override=model, - ) - r1 = await _retry_if_too_short( - r1, llm_service, messages, prompt, - max_tok, base.get("name", ""), model_override=model, - ) - r1_words = len(r1.split()) if r1 else 0 - - # Second exchange — host follow-up - messages.append({"role": "assistant", "content": r1}) - messages.append({"role": "user", "content": "Wait, tell me more about that."}) - r2 = await llm_service.generate( - messages=messages, system_prompt=prompt, - max_tokens=max_tok, category="caller_dialog", - caller_name=base.get("name", ""), model_override=model, - ) - r2 = await _retry_if_too_short( - r2, llm_service, messages, prompt, - max_tok, base.get("name", ""), model_override=model, - ) - r2_words = len(r2.split()) if r2 else 0 - - avg_words = (r1_words + r2_words) // 2 - passed = avg_words >= MIN_RESPONSE_WORDS and r1_words >= 30 and r2_words >= 30 - return { - "name": base.get("name", key), - "model": model, - "word_count": avg_words, - "r1_words": r1_words, - "r2_words": r2_words, - "pass": passed, - "snippet": (r1[:150] + "...") if r1 and len(r1) > 150 else r1, - } - except Exception as e: - return { - "name": base.get("name", key), - "model": model, - "error": str(e), - "pass": False, - } - - results = await asyncio.gather(*[_test_caller(k, b, m) for k, b, m in test_callers]) - for r in results: - coherence_results.append(r) - if not r.get("pass"): - coherence_fail = True - - checks["response_coherence"] = { - "status": "fail" if coherence_fail else "pass", - "details": {"results": coherence_results}, - } - else: - checks["response_coherence"] = {"status": "skip", "details": {"reason": "Use ?test_responses=true to enable"}} - - # --- Overall status --- - statuses = [c["status"] for c in checks.values()] - if "fail" in statuses: - overall = "fail" - elif "warn" in statuses: - overall = "warn" - else: - overall = "pass" - - # Summary - issues = [] - if checks["model_diversity"]["status"] == "fail": - issues.append(f"Model diversity: {checks['model_diversity']['details']['max_same_model_pct']}% on same model") - if checks["theme_penetration"]["status"] == "warn": - issues.append(f"Theme penetration: only {checks['theme_penetration']['details']['penetration_pct']}% connected") - if checks["voice_age_alignment"]["status"] == "warn": - issues.append(f"Voice-age mismatches: {len(mismatches)}") - if checks.get("response_coherence", {}).get("status") == "fail": - failed = [r["name"] for r in checks["response_coherence"]["details"]["results"] if not r.get("pass")] - issues.append(f"Response coherence failed: {', '.join(failed)}") - summary = "; ".join(issues) if issues else "All checks passed" - - return {"status": overall, "checks": checks, "summary": summary} - - # --- Cost Tracking Endpoints --- @app.get("/api/costs") diff --git a/frontend/css/style.css b/frontend/css/style.css index d6b1134..77482f4 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -449,84 +449,6 @@ section h2 { line-height: 1.3; } -/* Caller model indicator */ -.info-badge.model { - background: rgba(100, 140, 220, 0.2); - color: #7ab0e8; - font-size: 0.7rem; - cursor: pointer; -} - -.caller-model-override { - font-size: 0.7rem; - padding: 2px 4px; - background: var(--bg); - color: var(--text); - border: 1px solid rgba(100, 140, 220, 0.3); - border-radius: 4px; - max-width: 140px; -} - -/* Caller button model badge */ -.model-tag { - font-size: 0.55rem; - color: #7ab0e8; - background: rgba(100, 140, 220, 0.15); - padding: 0 3px; - border-radius: 2px; - font-weight: 700; - letter-spacing: 0.3px; - flex-shrink: 0; -} - -/* Caller Models settings section */ -.caller-model-row { - margin-bottom: 8px; -} - -.caller-model-row label { - margin-bottom: 0; -} - -.cm-pool-input { - font-size: 0.8rem; -} - -.cm-style-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px; - margin-bottom: 8px; - max-height: 200px; - overflow-y: auto; -} - -.cm-style-item { - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; - background: rgba(255, 255, 255, 0.05); - border-radius: 4px; - padding: 3px 6px; -} - -.cm-style-name { - font-size: 0.7rem; - color: var(--text-muted); - white-space: nowrap; -} - -.cm-style-select { - font-size: 0.7rem; - padding: 2px 3px; - background: var(--bg); - color: var(--text); - border: 1px solid rgba(232, 121, 29, 0.15); - border-radius: 4px; - max-width: 110px; -} - .caller-background-full { margin-top: 8px; font-size: 0.75rem; @@ -1915,110 +1837,3 @@ button:focus-visible { .log-toggle-btn:hover { color: var(--text); } - -/* Preflight */ -.preflight-btn { - background: rgba(90, 138, 60, 0.15); - color: var(--accent-green); - border: 1px solid rgba(90, 138, 60, 0.3); -} -.preflight-btn:hover { - background: rgba(90, 138, 60, 0.25); -} - -.preflight-content { - max-width: 700px; -} - -.preflight-status { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - border-radius: var(--radius-sm); - margin-bottom: 16px; - font-weight: 700; - font-size: 1.1rem; -} -.preflight-status.pass { background: rgba(90, 138, 60, 0.15); color: var(--accent-green); } -.preflight-status.warn { background: rgba(232, 169, 29, 0.15); color: #e8a91d; } -.preflight-status.fail { background: rgba(204, 34, 34, 0.15); color: var(--accent-red); } -.preflight-status.loading { background: rgba(232, 121, 29, 0.1); color: var(--text-muted); } - -.preflight-checks { - display: flex; - flex-direction: column; - gap: 12px; - max-height: 60vh; - overflow-y: auto; -} - -.preflight-check { - background: var(--bg); - border: 1px solid rgba(232, 121, 29, 0.1); - border-radius: var(--radius-sm); - padding: 12px 16px; -} -.preflight-check-header { - display: flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - user-select: none; -} -.preflight-check-name { - font-weight: 600; - font-size: 0.95rem; -} -.preflight-check-badge { - font-size: 0.75rem; - font-weight: 700; - padding: 2px 8px; - border-radius: 4px; - text-transform: uppercase; -} -.preflight-check-badge.pass { background: rgba(90, 138, 60, 0.2); color: var(--accent-green); } -.preflight-check-badge.warn { background: rgba(232, 169, 29, 0.2); color: #e8a91d; } -.preflight-check-badge.fail { background: rgba(204, 34, 34, 0.2); color: var(--accent-red); } -.preflight-check-badge.skip { background: rgba(154, 139, 120, 0.2); color: var(--text-muted); } - -.preflight-check-details { - margin-top: 10px; - font-size: 0.85rem; - color: var(--text-muted); - display: none; -} -.preflight-check.open .preflight-check-details { - display: block; -} - -.preflight-table { - width: 100%; - border-collapse: collapse; - margin-top: 8px; -} -.preflight-table th { - text-align: left; - color: var(--text-muted); - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - padding: 4px 8px; - border-bottom: 1px solid rgba(232, 121, 29, 0.1); -} -.preflight-table td { - padding: 4px 8px; - font-size: 0.8rem; - color: var(--text); - border-bottom: 1px solid rgba(232, 121, 29, 0.05); -} -.preflight-table tr.mismatch td { color: var(--accent-red); } -.preflight-table tr.connected td { color: var(--accent-green); } - -.preflight-test-btn { - background: rgba(232, 121, 29, 0.15); - color: var(--accent); - border: 1px solid rgba(232, 121, 29, 0.3); -} -.preflight-test-btn:hover { background: rgba(232, 121, 29, 0.25); } -.preflight-test-btn.loading { opacity: 0.6; pointer-events: none; } diff --git a/frontend/index.html b/frontend/index.html index 96dc68c..b3345db 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,7 +15,6 @@ -
@@ -72,10 +71,6 @@
No active call
- -
-

Caller Models

-
- -
- - -
- -
-
-

TTS Provider

@@ -357,22 +322,6 @@
- - diff --git a/frontend/js/app.js b/frontend/js/app.js index e082d32..6652248 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -131,7 +131,6 @@ document.addEventListener('DOMContentLoaded', async () => { initEventListeners(); initClock(); loadShowTheme(); - loadCallerModels(); loadVoicemails(); setInterval(loadVoicemails, 30000); loadEmails(); @@ -356,27 +355,6 @@ function initEventListeners() { else if (e.key === 'Escape') e.target.blur(); }); - // Caller Models - document.getElementById('cm-strategy')?.addEventListener('change', () => { - callerModelSettings.strategy = document.getElementById('cm-strategy').value; - updateCallerModelUI(); - }); - document.getElementById('caller-model-badge')?.addEventListener('click', () => { - const sel = document.getElementById('caller-model-override'); - if (!sel || !currentCaller) return; - sel.classList.toggle('hidden'); - if (!sel.classList.contains('hidden')) { - const current = callerModelAssignments[currentCaller.key]; - if (current) sel.value = current; - } - }); - document.getElementById('caller-model-override')?.addEventListener('change', (e) => { - if (currentCaller && e.target.value) { - overrideCallerModel(currentCaller.key, e.target.value); - e.target.classList.add('hidden'); - } - }); - // Settings document.getElementById('settings-btn')?.addEventListener('click', async () => { document.getElementById('settings-modal')?.classList.remove('hidden'); @@ -392,17 +370,6 @@ function initEventListeners() { }); document.getElementById('refresh-ollama')?.addEventListener('click', refreshOllamaModels); - // Preflight - document.getElementById('preflight-btn')?.addEventListener('click', () => { - document.getElementById('preflight-modal')?.classList.remove('hidden'); - runPreflight(false); - }); - document.getElementById('preflight-test-btn')?.addEventListener('click', () => runPreflight(true)); - document.getElementById('preflight-rerun-btn')?.addEventListener('click', () => runPreflight(false)); - document.getElementById('close-preflight')?.addEventListener('click', () => { - document.getElementById('preflight-modal')?.classList.add('hidden'); - }); - // Wrap-up button document.getElementById('wrapup-btn')?.addEventListener('click', wrapUp); @@ -655,7 +622,6 @@ async function loadCallers() { } console.log('Loaded', data.callers.length, 'callers, session:', data.session_id); - updateCallerModelBadges(); } catch (err) { console.error('loadCallers error:', err); } @@ -742,10 +708,6 @@ async function startCall(key, name) { if (secretWant) secretWant.textContent = ci.secret_want ? `secretly wants: ${ci.secret_want}` : ''; infoPanel.classList.remove('hidden'); } - try { - showCallerModelBadge(callerModelAssignments[key] || data.model); - } catch(e) { console.error('[startCall] showCallerModelBadge error:', e); } - document.getElementById('caller-model-override')?.classList.add('hidden'); const bgEl = document.getElementById('caller-background'); if (bgEl && data.background) bgEl.textContent = data.background; @@ -792,7 +754,6 @@ async function newSession() { // Reload callers to get new session ID await loadCallers(); await loadShowTheme(); - await loadCallerModels(); log('New session started - all callers have fresh backgrounds'); } @@ -831,8 +792,6 @@ async function hangup() { document.getElementById('caller-info-panel')?.classList.add('hidden'); const bgDetails2 = document.getElementById('caller-background-details'); if (bgDetails2) bgDetails2.classList.add('hidden'); - showCallerModelBadge(null); - document.getElementById('caller-model-override')?.classList.add('hidden'); // Hide AI caller indicator document.getElementById('ai-caller-info')?.classList.add('hidden'); @@ -1420,188 +1379,6 @@ async function clearShowTheme() { } -// --- Caller Model Routing --- -const MODEL_ABBREVS = { - 'claude-sonnet-4-5': 'Son', 'claude-haiku-4.5': 'Hai', 'claude-3-haiku': 'H3', - 'grok-4': 'Grk', 'grok-4-fast': 'GrF', - 'minimax-m2-her': 'MnM', 'mistral-small-creative': 'Mis', - 'deepseek-v3.2': 'DSk', 'gemini-2.5-flash': 'Gem', 'gemini-flash-1.5': 'Gm1', - 'gpt-4o-mini': '4oM', 'gpt-4o': '4o', 'llama-3.1-8b-instruct': 'Lla', -}; - -const CALLER_STYLES = [ - 'quiet_nervous', 'storyteller', 'deadpan', 'high_energy', 'confrontational', - 'oversharer', 'philosopher', 'bragger', 'first_time', 'emotional', - 'world_weary', 'conspiracy', 'comedian', 'angry_venting', 'sweet_earnest', - 'mysterious', 'know_it_all', 'rambling', -]; - -let callerModelSettings = { strategy: 'single', pool: [], fallback: '', style_map: {} }; -let callerModelAssignments = {}; // key -> model_id - -function modelAbbrev(modelId) { - const name = (modelId || '').split('/').pop(); - return MODEL_ABBREVS[name] || name.substring(0, 3).toUpperCase(); -} - -async function loadCallerModels() { - try { - const res = await fetch('/api/caller-models'); - if (!res.ok) return; - const data = await res.json(); - callerModelSettings = { - strategy: data.strategy || 'single', - pool: data.pool || [], - fallback: data.fallback || '', - style_map: data.map || data.style_map || {}, - }; - callerModelAssignments = data.assignments || {}; - updateCallerModelUI(); - updateCallerModelBadges(); - } catch (e) { - console.error('Failed to load caller models:', e); - } -} - -function updateCallerModelUI() { - const strategyEl = document.getElementById('cm-strategy'); - if (strategyEl) strategyEl.value = callerModelSettings.strategy; - - const poolSection = document.getElementById('cm-pool-section'); - const styleMap = document.getElementById('cm-style-map'); - if (poolSection) poolSection.classList.toggle('hidden', callerModelSettings.strategy === 'single'); - if (styleMap) styleMap.classList.toggle('hidden', callerModelSettings.strategy !== 'style_matched'); - - const poolInput = document.getElementById('cm-pool'); - if (poolInput) poolInput.value = callerModelSettings.pool.join(', '); - - // Populate style map grid - const grid = document.getElementById('cm-style-grid'); - if (grid && callerModelSettings.strategy === 'style_matched') { - grid.innerHTML = ''; - for (const style of CALLER_STYLES) { - const item = document.createElement('div'); - item.className = 'cm-style-item'; - const label = style.replace(/_/g, ' '); - item.innerHTML = `${label}`; - const sel = document.createElement('select'); - sel.className = 'cm-style-select'; - sel.dataset.style = style; - const models = window._openrouterModels || callerModelSettings.pool; - for (const m of models) { - const opt = document.createElement('option'); - opt.value = m; - opt.textContent = m.split('/').pop(); - if (m === callerModelSettings.style_map[style]) opt.selected = true; - sel.appendChild(opt); - } - item.appendChild(sel); - grid.appendChild(item); - } - } - - // Fallback dropdown - const fallbackEl = document.getElementById('cm-fallback'); - if (fallbackEl) { - const currentVal = fallbackEl.value; - fallbackEl.innerHTML = ''; - const models = callerModelSettings.pool.length > 0 - ? callerModelSettings.pool - : (window._openrouterModels || []); - for (const m of models) { - const opt = document.createElement('option'); - opt.value = m; - opt.textContent = m.split('/').pop(); - if (m === callerModelSettings.fallback) opt.selected = true; - fallbackEl.appendChild(opt); - } - if (!fallbackEl.value && currentVal) fallbackEl.value = currentVal; - } -} - -function updateCallerModelBadges() { - document.querySelectorAll('.caller-btn').forEach(btn => { - const key = btn.dataset.key; - const model = callerModelAssignments[key]; - let tag = btn.querySelector('.model-tag'); - if (model) { - if (!tag) { - tag = document.createElement('span'); - tag.className = 'model-tag'; - btn.appendChild(tag); - } - tag.textContent = modelAbbrev(model); - tag.title = model; - } else if (tag) { - tag.remove(); - } - }); -} - -function showCallerModelBadge(model) { - const badge = document.getElementById('caller-model-badge'); - if (badge) { - badge.textContent = model ? `via ${modelAbbrev(model)}` : ''; - badge.title = model || ''; - badge.classList.toggle('hidden', !model); - } -} - -function populateCallerModelOverride() { - const sel = document.getElementById('caller-model-override'); - if (!sel) return; - sel.innerHTML = ''; - const models = window._openrouterModels || []; - for (const m of models) { - const opt = document.createElement('option'); - opt.value = m; - opt.textContent = m.split('/').pop(); - sel.appendChild(opt); - } -} - -async function overrideCallerModel(callerKey, modelId) { - try { - const res = await fetch(`/api/caller-models/${callerKey}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: modelId }) - }); - if (!res.ok) throw new Error(res.status); - callerModelAssignments[callerKey] = modelId; - showCallerModelBadge(modelId); - updateCallerModelBadges(); - log(`Model override: ${currentCaller?.name || callerKey} → ${modelAbbrev(modelId)}`); - } catch (err) { - log('Model override failed: ' + err.message); - } -} - -async function saveCallerModels() { - const strategy = document.getElementById('cm-strategy')?.value || 'single'; - const poolRaw = document.getElementById('cm-pool')?.value || ''; - const pool = poolRaw.split(',').map(s => s.trim()).filter(Boolean); - const fallback = document.getElementById('cm-fallback')?.value || ''; - - const style_map = {}; - document.querySelectorAll('.cm-style-select').forEach(sel => { - if (sel.value) style_map[sel.dataset.style] = sel.value; - }); - - try { - const res = await fetch('/api/caller-models', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ strategy, pool, fallback, map: style_map }) - }); - if (!res.ok) throw new Error(res.status); - callerModelSettings = { strategy, pool, fallback, style_map }; - } catch (err) { - log('Caller model save failed: ' + err.message); - } -} - - // --- Settings --- async function loadSettings() { try { @@ -1658,7 +1435,6 @@ async function loadSettings() { // Category model routing const models = data.available_openrouter_models || []; window._openrouterModels = models; - populateCallerModelOverride(); const categoryModels = data.category_models || {}; const categories = ['caller_dialog', 'devon_monitor', 'devon_ask', 'background_gen', 'call_summary', 'news_summary']; for (const cat of categories) { @@ -1692,9 +1468,6 @@ async function saveSettings() { // Save audio devices await saveAudioDevices(); - // Save caller model routing - await saveCallerModels(); - // Collect category model routing const categoryModels = {}; const categories = ['caller_dialog', 'devon_monitor', 'devon_ask', 'background_gen', 'call_summary', 'news_summary']; @@ -2414,151 +2187,3 @@ async function dismissDevonSuggestion() { } -// --- Preflight --- - -const PREFLIGHT_STATUS_ICONS = { pass: '✓', warn: '⚠', fail: '✗', skip: '—' }; - -const PREFLIGHT_CHECK_NAMES = { - model_diversity: 'Model Diversity', - theme_penetration: 'Theme Penetration', - voice_age_alignment: 'Voice-Age Alignment', - response_coherence: 'Response Coherence', -}; - -async function runPreflight(testResponses) { - const statusEl = document.getElementById('preflight-status'); - const checksEl = document.getElementById('preflight-checks'); - const testBtn = document.getElementById('preflight-test-btn'); - - statusEl.className = 'preflight-status loading'; - statusEl.querySelector('.preflight-status-icon').textContent = '...'; - statusEl.querySelector('.preflight-status-text').textContent = 'Running checks...'; - checksEl.innerHTML = ''; - - if (testResponses && testBtn) testBtn.classList.add('loading'); - - try { - const url = '/api/show/preflight' + (testResponses ? '?test_responses=true' : ''); - const data = await safeFetch(url, {}, 120000); - renderPreflightResults(data, statusEl, checksEl); - } catch (err) { - statusEl.className = 'preflight-status fail'; - statusEl.querySelector('.preflight-status-icon').textContent = '✗'; - statusEl.querySelector('.preflight-status-text').textContent = 'Error: ' + err.message; - } finally { - if (testBtn) testBtn.classList.remove('loading'); - } -} - -function renderPreflightResults(data, statusEl, checksEl) { - const overall = data.status || 'pass'; - statusEl.className = 'preflight-status ' + overall; - statusEl.querySelector('.preflight-status-icon').textContent = PREFLIGHT_STATUS_ICONS[overall] || '✓'; - statusEl.querySelector('.preflight-status-text').textContent = - overall === 'pass' ? 'All checks passed' : - overall === 'warn' ? 'Passed with warnings' : 'Issues found'; - - checksEl.innerHTML = ''; - const checksObj = data.checks || {}; - for (const [checkKey, check] of Object.entries(checksObj)) { - const card = document.createElement('div'); - card.className = 'preflight-check'; - - const status = check.status || 'skip'; - const name = PREFLIGHT_CHECK_NAMES[checkKey] || checkKey; - - card.innerHTML = ` -
- ${escapeHtml(name)} - ${status.toUpperCase()} -
-
${renderCheckDetails(checkKey, check)}
- `; - - card.querySelector('.preflight-check-header').addEventListener('click', () => { - card.classList.toggle('open'); - }); - - checksEl.appendChild(card); - } -} - -function renderCheckDetails(name, check) { - const d = check.details || {}; - switch (name) { - case 'model_diversity': return renderModelDiversity(d); - case 'theme_penetration': return renderThemePenetration(d); - case 'voice_age_alignment': return renderVoiceAgeAlignment(d); - case 'response_coherence': return renderResponseCoherence(check); - default: return `
${escapeHtml(JSON.stringify(d, null, 2))}
`; - } -} - -function renderModelDiversity(d) { - const callers = d.callers || []; - if (!callers.length) return '

No callers to check.

'; - let html = ` - `; - for (const c of callers) { - html += ``; - } - html += '
CallerStyleModel
${escapeHtml(c.name || '')}${escapeHtml(c.style || '')}${escapeHtml(c.model || '')}
'; - if (d.max_same_model_pct != null) { - html += `

${d.max_same_model_pct}% on same model

`; - } - return html; -} - -function renderThemePenetration(d) { - let html = ''; - if (d.theme) html += `

Theme: ${escapeHtml(d.theme)}

`; - if (d.connected?.length) { - html += `

Connected: ${d.connected.map(n => escapeHtml(n)).join(', ')}

`; - } - if (d.not_connected?.length) { - html += `

Not connected: ${d.not_connected.map(n => escapeHtml(n)).join(', ')}

`; - } - if (d.penetration_pct != null) { - html += `

${d.penetration_pct}% penetration

`; - } - return html || '

No theme set.

'; -} - -function renderVoiceAgeAlignment(d) { - const callers = d.callers || []; - if (!callers.length) return '

No callers to check.

'; - let html = ` - `; - for (const c of callers) { - const cls = c.mismatch ? ' class="mismatch"' : ''; - html += ``; - } - html += '
CallerAgeVoiceAge Feel
${escapeHtml(c.name || '')}${c.age || ''}${escapeHtml(c.voice || '')}${escapeHtml(c.age_feel || '')}
'; - return html; -} - -function renderResponseCoherence(check) { - if (check.status === 'skip') { - return '

Use Test Responses button to run this check.

'; - } - const d = check.details || {}; - const results = d.results || []; - if (!results.length) return '

No test results.

'; - let html = ` - `; - for (const c of results) { - const cls = c.pass ? '' : ' class="mismatch"'; - if (c.error) { - html += ``; - } else { - html += ``; - if (c.snippet) { - html += ``; - } - } - } - html += '
CallerModelR1R2Avg
${escapeHtml(c.name || '')}${escapeHtml(c.model || '')}${escapeHtml(c.error)}
${escapeHtml(c.name || '')}${escapeHtml(c.model || '')}${c.r1_words || 0}${c.r2_words || 0}${c.word_count || 0}${c.pass ? '✓' : '✗'}
${escapeHtml(c.snippet)}
'; - const passed = results.filter(r => r.pass).length; - html += `

${passed}/${results.length} callers passed (min ${50} words per response)

`; - return html; -}