Files
chat/app/templates/chat.html
T
marauder-actual b0893a3699 feat(transport): swap Anthropic → opencode; add persona switcher
Part 1 — Transport swap:
- Replace anthropic.AsyncAnthropic streaming with httpx SSE client
  calling opencode's OpenAI-compat /v1/chat/completions on sin:4096
- Auth: basic auth opencode:$OPENCODE_PASSWORD
- Env: OPENCODE_URL (default http://sin:4096), OPENCODE_PASSWORD
- Sidecar binding (sin:4098) consulted per message to resolve active
  persona; voice read from binding → cart → env default
- Helper _session_id_for_user: deterministic sha256 slug per email
  so sidecar binding survives WebSocket reconnects
- anthropic dep retained in pyproject.toml (not removed — P4 may use it)

Part 2 — Persona switcher:
- PERSONAS dict: bt7274, friday, samantha (slug → voice/backend/prompt)
- POST /api/persona  — bind persona via sidecar, maps slug → full config
- GET  /api/persona/current — return current binding
- GET  /api/personas — list available personas
- chat.html: persona <select> in topnav with server-rendered active state
- chat.js: onChange → fetch /api/persona, update __personaName +
  status badge + system message in conversation feed

TODO: add CSS polish for .topnav__persona-wrap (inherits base styles for now)
2026-05-29 14:18:47 +02:00

86 lines
3.4 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>chat.saiden.dev</title>
<meta name="description" content="A quiet channel.">
<meta name="robots" content="noindex, nofollow">
<link rel="icon" href="https://saiden.dev/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Caveat:wght@400;500&family=Inter:wght@300;400;500;600&family=Source+Serif+Pro:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="/static/chat.css">
</head>
<body
data-palette="{{ ui_palette }}"
data-typography="{{ ui_typography }}"
data-density="{{ ui_density }}"
data-labels="{{ ui_labels }}">
<script>
window.__pilotName = {{ pilot_name | tojson }};
window.__personaName = {{ persona_name | tojson }};
window.__personas = {{ personas | tojson }};
window.__boundSlug = {{ bound_slug | tojson }};
</script>
<nav class="topnav" aria-label="channel controls">
<button type="button" class="topnav__link" id="recalibrate-btn" title="reset calibration">recalibrate</button>
<span class="topnav__sep">·</span>
<!-- Persona switcher -->
<span class="topnav__persona-wrap">
<label class="topnav__persona-label" for="persona-select">persona</label>
<select class="topnav__persona-select" id="persona-select" title="Switch persona">
<option value="">— default —</option>
{% for p in personas %}
<option value="{{ p.slug }}"{% if p.slug == bound_slug %} selected{% endif %}>{{ p.display }}</option>
{% endfor %}
</select>
<span class="topnav__persona-status" id="persona-status">
{% if bound_display %}{{ bound_display }}{% endif %}
</span>
</span>
<span class="topnav__sep">·</span>
<a class="topnav__link" href="/auth/logout">sign out</a>
<a class="sigil" href="https://saiden.dev/" target="_blank" rel="noopener" title="Saiden">
<img src="https://saiden.dev/logo.png" alt="Saiden">
</a>
</nav>
<main class="page">
<section class="conversation" id="conversation" aria-live="polite">
</section>
<form class="prompt" id="prompt-form" autocomplete="off">
<hr class="prompt__line">
<div class="prompt__row">
<input
class="prompt__input"
id="prompt-input"
type="text"
placeholder="{% if cart %}speak to {{ persona_name }}{% else %}…{% endif %}"
autofocus
autocomplete="off"
spellcheck="true"
aria-label="message">
<button type="button" class="prompt__mic" id="mic-button" aria-label="speak"
title="hold space to speak · click to toggle">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"
stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="3" width="6" height="12" rx="3"></rect>
<path d="M5 11a7 7 0 0 0 14 0"></path>
<line x1="12" y1="18" x2="12" y2="22"></line>
</svg>
</button>
</div>
</form>
</main>
<script src="/static/chat.js"></script>
</body>
</html>