Delete caller-model routing + preflight admin UI
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.
This commit is contained in:
-275
@@ -9884,281 +9884,6 @@ async def set_show_theme(data: dict):
|
|||||||
return {"theme": session.show_theme}
|
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 ---
|
# --- Cost Tracking Endpoints ---
|
||||||
|
|
||||||
@app.get("/api/costs")
|
@app.get("/api/costs")
|
||||||
|
|||||||
@@ -449,84 +449,6 @@ section h2 {
|
|||||||
line-height: 1.3;
|
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 {
|
.caller-background-full {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -1915,110 +1837,3 @@ button:focus-visible {
|
|||||||
.log-toggle-btn:hover {
|
.log-toggle-btn:hover {
|
||||||
color: var(--text);
|
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; }
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<button id="rec-btn" class="rec-btn" title="Record stems for post-production">REC</button>
|
<button id="rec-btn" class="rec-btn" title="Record stems for post-production">REC</button>
|
||||||
<button id="new-session-btn" class="new-session-btn">New Session</button>
|
<button id="new-session-btn" class="new-session-btn">New Session</button>
|
||||||
<button id="export-session-btn">Export</button>
|
<button id="export-session-btn">Export</button>
|
||||||
<button id="preflight-btn" class="preflight-btn">Preflight</button>
|
|
||||||
<button id="settings-btn">Settings</button>
|
<button id="settings-btn">Settings</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-bar">
|
<div class="theme-bar">
|
||||||
@@ -72,10 +71,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="call-status" class="call-status">No active call</div>
|
<div id="call-status" class="call-status">No active call</div>
|
||||||
<div id="caller-info-panel" class="caller-info-panel hidden">
|
<div id="caller-info-panel" class="caller-info-panel hidden">
|
||||||
<div class="caller-info-row">
|
|
||||||
<span id="caller-model-badge" class="info-badge model"></span>
|
|
||||||
<select id="caller-model-override" class="caller-model-override hidden"></select>
|
|
||||||
</div>
|
|
||||||
<div id="caller-identity" class="caller-identity"></div>
|
<div id="caller-identity" class="caller-identity"></div>
|
||||||
<div id="caller-situation" class="caller-situation"></div>
|
<div id="caller-situation" class="caller-situation"></div>
|
||||||
<div id="caller-signature" class="caller-signature"></div>
|
<div id="caller-signature" class="caller-signature"></div>
|
||||||
@@ -287,36 +282,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Caller Model Routing -->
|
|
||||||
<div class="settings-group">
|
|
||||||
<h3>Caller Models</h3>
|
|
||||||
<div class="caller-model-row">
|
|
||||||
<label>
|
|
||||||
Strategy
|
|
||||||
<select id="cm-strategy">
|
|
||||||
<option value="single">Single Model</option>
|
|
||||||
<option value="cycle">Cycle Models</option>
|
|
||||||
<option value="style_matched">Style-Matched</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="cm-pool-section" class="hidden">
|
|
||||||
<label>
|
|
||||||
Model Pool
|
|
||||||
<input type="text" id="cm-pool" class="cm-pool-input" placeholder="x-ai/grok-4, deepseek/deepseek-v3.2, ...">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div id="cm-style-map" class="hidden">
|
|
||||||
<div class="cm-style-grid" id="cm-style-grid"></div>
|
|
||||||
</div>
|
|
||||||
<div class="caller-model-row">
|
|
||||||
<label>
|
|
||||||
Fallback Model
|
|
||||||
<select id="cm-fallback" class="model-select"></select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TTS Settings -->
|
<!-- TTS Settings -->
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<h3>TTS Provider</h3>
|
<h3>TTS Provider</h3>
|
||||||
@@ -357,22 +322,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Preflight Modal -->
|
|
||||||
<div id="preflight-modal" class="modal hidden">
|
|
||||||
<div class="modal-content preflight-content">
|
|
||||||
<h2>Show Preflight</h2>
|
|
||||||
<div id="preflight-status" class="preflight-status loading">
|
|
||||||
<span class="preflight-status-icon">...</span>
|
|
||||||
<span class="preflight-status-text">Running checks...</span>
|
|
||||||
</div>
|
|
||||||
<div id="preflight-checks" class="preflight-checks"></div>
|
|
||||||
<div class="modal-buttons">
|
|
||||||
<button id="preflight-test-btn" class="preflight-test-btn">Test Responses</button>
|
|
||||||
<button id="preflight-rerun-btn">Re-run</button>
|
|
||||||
<button id="close-preflight">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js?v=27"></script>
|
<script src="/js/app.js?v=27"></script>
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
initEventListeners();
|
initEventListeners();
|
||||||
initClock();
|
initClock();
|
||||||
loadShowTheme();
|
loadShowTheme();
|
||||||
loadCallerModels();
|
|
||||||
loadVoicemails();
|
loadVoicemails();
|
||||||
setInterval(loadVoicemails, 30000);
|
setInterval(loadVoicemails, 30000);
|
||||||
loadEmails();
|
loadEmails();
|
||||||
@@ -356,27 +355,6 @@ function initEventListeners() {
|
|||||||
else if (e.key === 'Escape') e.target.blur();
|
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
|
// Settings
|
||||||
document.getElementById('settings-btn')?.addEventListener('click', async () => {
|
document.getElementById('settings-btn')?.addEventListener('click', async () => {
|
||||||
document.getElementById('settings-modal')?.classList.remove('hidden');
|
document.getElementById('settings-modal')?.classList.remove('hidden');
|
||||||
@@ -392,17 +370,6 @@ function initEventListeners() {
|
|||||||
});
|
});
|
||||||
document.getElementById('refresh-ollama')?.addEventListener('click', refreshOllamaModels);
|
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
|
// Wrap-up button
|
||||||
document.getElementById('wrapup-btn')?.addEventListener('click', wrapUp);
|
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);
|
console.log('Loaded', data.callers.length, 'callers, session:', data.session_id);
|
||||||
updateCallerModelBadges();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('loadCallers error:', 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}` : '';
|
if (secretWant) secretWant.textContent = ci.secret_want ? `secretly wants: ${ci.secret_want}` : '';
|
||||||
infoPanel.classList.remove('hidden');
|
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');
|
const bgEl = document.getElementById('caller-background');
|
||||||
if (bgEl && data.background) bgEl.textContent = data.background;
|
if (bgEl && data.background) bgEl.textContent = data.background;
|
||||||
|
|
||||||
@@ -792,7 +754,6 @@ async function newSession() {
|
|||||||
// Reload callers to get new session ID
|
// Reload callers to get new session ID
|
||||||
await loadCallers();
|
await loadCallers();
|
||||||
await loadShowTheme();
|
await loadShowTheme();
|
||||||
await loadCallerModels();
|
|
||||||
|
|
||||||
log('New session started - all callers have fresh backgrounds');
|
log('New session started - all callers have fresh backgrounds');
|
||||||
}
|
}
|
||||||
@@ -831,8 +792,6 @@ async function hangup() {
|
|||||||
document.getElementById('caller-info-panel')?.classList.add('hidden');
|
document.getElementById('caller-info-panel')?.classList.add('hidden');
|
||||||
const bgDetails2 = document.getElementById('caller-background-details');
|
const bgDetails2 = document.getElementById('caller-background-details');
|
||||||
if (bgDetails2) bgDetails2.classList.add('hidden');
|
if (bgDetails2) bgDetails2.classList.add('hidden');
|
||||||
showCallerModelBadge(null);
|
|
||||||
document.getElementById('caller-model-override')?.classList.add('hidden');
|
|
||||||
|
|
||||||
// Hide AI caller indicator
|
// Hide AI caller indicator
|
||||||
document.getElementById('ai-caller-info')?.classList.add('hidden');
|
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 = `<span class="cm-style-name">${label}</span>`;
|
|
||||||
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 ---
|
// --- Settings ---
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
@@ -1658,7 +1435,6 @@ async function loadSettings() {
|
|||||||
// Category model routing
|
// Category model routing
|
||||||
const models = data.available_openrouter_models || [];
|
const models = data.available_openrouter_models || [];
|
||||||
window._openrouterModels = models;
|
window._openrouterModels = models;
|
||||||
populateCallerModelOverride();
|
|
||||||
const categoryModels = data.category_models || {};
|
const categoryModels = data.category_models || {};
|
||||||
const categories = ['caller_dialog', 'devon_monitor', 'devon_ask', 'background_gen', 'call_summary', 'news_summary'];
|
const categories = ['caller_dialog', 'devon_monitor', 'devon_ask', 'background_gen', 'call_summary', 'news_summary'];
|
||||||
for (const cat of categories) {
|
for (const cat of categories) {
|
||||||
@@ -1692,9 +1468,6 @@ async function saveSettings() {
|
|||||||
// Save audio devices
|
// Save audio devices
|
||||||
await saveAudioDevices();
|
await saveAudioDevices();
|
||||||
|
|
||||||
// Save caller model routing
|
|
||||||
await saveCallerModels();
|
|
||||||
|
|
||||||
// Collect category model routing
|
// Collect category model routing
|
||||||
const categoryModels = {};
|
const categoryModels = {};
|
||||||
const categories = ['caller_dialog', 'devon_monitor', 'devon_ask', 'background_gen', 'call_summary', 'news_summary'];
|
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 = `
|
|
||||||
<div class="preflight-check-header">
|
|
||||||
<span class="preflight-check-name">${escapeHtml(name)}</span>
|
|
||||||
<span class="preflight-check-badge ${status}">${status.toUpperCase()}</span>
|
|
||||||
</div>
|
|
||||||
<div class="preflight-check-details">${renderCheckDetails(checkKey, check)}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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 `<pre>${escapeHtml(JSON.stringify(d, null, 2))}</pre>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderModelDiversity(d) {
|
|
||||||
const callers = d.callers || [];
|
|
||||||
if (!callers.length) return '<p>No callers to check.</p>';
|
|
||||||
let html = `<table class="preflight-table">
|
|
||||||
<thead><tr><th>Caller</th><th>Style</th><th>Model</th></tr></thead><tbody>`;
|
|
||||||
for (const c of callers) {
|
|
||||||
html += `<tr><td>${escapeHtml(c.name || '')}</td><td>${escapeHtml(c.style || '')}</td><td>${escapeHtml(c.model || '')}</td></tr>`;
|
|
||||||
}
|
|
||||||
html += '</tbody></table>';
|
|
||||||
if (d.max_same_model_pct != null) {
|
|
||||||
html += `<p style="margin-top:8px">${d.max_same_model_pct}% on same model</p>`;
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderThemePenetration(d) {
|
|
||||||
let html = '';
|
|
||||||
if (d.theme) html += `<p><strong>Theme:</strong> ${escapeHtml(d.theme)}</p>`;
|
|
||||||
if (d.connected?.length) {
|
|
||||||
html += `<p style="color:var(--accent-green);margin-top:6px">Connected: ${d.connected.map(n => escapeHtml(n)).join(', ')}</p>`;
|
|
||||||
}
|
|
||||||
if (d.not_connected?.length) {
|
|
||||||
html += `<p style="color:var(--text-muted);margin-top:4px">Not connected: ${d.not_connected.map(n => escapeHtml(n)).join(', ')}</p>`;
|
|
||||||
}
|
|
||||||
if (d.penetration_pct != null) {
|
|
||||||
html += `<p style="margin-top:6px">${d.penetration_pct}% penetration</p>`;
|
|
||||||
}
|
|
||||||
return html || '<p>No theme set.</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderVoiceAgeAlignment(d) {
|
|
||||||
const callers = d.callers || [];
|
|
||||||
if (!callers.length) return '<p>No callers to check.</p>';
|
|
||||||
let html = `<table class="preflight-table">
|
|
||||||
<thead><tr><th>Caller</th><th>Age</th><th>Voice</th><th>Age Feel</th></tr></thead><tbody>`;
|
|
||||||
for (const c of callers) {
|
|
||||||
const cls = c.mismatch ? ' class="mismatch"' : '';
|
|
||||||
html += `<tr${cls}><td>${escapeHtml(c.name || '')}</td><td>${c.age || ''}</td><td>${escapeHtml(c.voice || '')}</td><td>${escapeHtml(c.age_feel || '')}</td></tr>`;
|
|
||||||
}
|
|
||||||
html += '</tbody></table>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResponseCoherence(check) {
|
|
||||||
if (check.status === 'skip') {
|
|
||||||
return '<p>Use <strong>Test Responses</strong> button to run this check.</p>';
|
|
||||||
}
|
|
||||||
const d = check.details || {};
|
|
||||||
const results = d.results || [];
|
|
||||||
if (!results.length) return '<p>No test results.</p>';
|
|
||||||
let html = `<table class="preflight-table">
|
|
||||||
<thead><tr><th>Caller</th><th>Model</th><th>R1</th><th>R2</th><th>Avg</th><th></th></tr></thead><tbody>`;
|
|
||||||
for (const c of results) {
|
|
||||||
const cls = c.pass ? '' : ' class="mismatch"';
|
|
||||||
if (c.error) {
|
|
||||||
html += `<tr class="mismatch"><td>${escapeHtml(c.name || '')}</td><td>${escapeHtml(c.model || '')}</td><td colspan="3">${escapeHtml(c.error)}</td><td>✗</td></tr>`;
|
|
||||||
} else {
|
|
||||||
html += `<tr${cls}><td>${escapeHtml(c.name || '')}</td><td>${escapeHtml(c.model || '')}</td><td>${c.r1_words || 0}</td><td>${c.r2_words || 0}</td><td>${c.word_count || 0}</td><td>${c.pass ? '✓' : '✗'}</td></tr>`;
|
|
||||||
if (c.snippet) {
|
|
||||||
html += `<tr><td colspan="6" style="color:var(--text-muted);font-size:0.75rem;padding-left:16px">${escapeHtml(c.snippet)}</td></tr>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '</tbody></table>';
|
|
||||||
const passed = results.filter(r => r.pass).length;
|
|
||||||
html += `<p style="margin-top:8px">${passed}/${results.length} callers passed (min ${50} words per response)</p>`;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user