← back to Voice Chat

public/index.html

229 lines

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <title>voice-chat · SA</title>
  <style>
    :root {
      --bg:        #0a0a0c;
      --panel:     #11131a;
      --fg:        #f4f4f5;
      --muted:     #9ca3af;
      --accent:    #ef4444;
      --green:     #22c55e;
      --gold:      #C9A14B;
      --border:    #1f2937;
    }
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    html, body {
      background: var(--bg);
      color: var(--fg);
      font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif;
      height: 100vh;
      overflow: hidden;
    }
    /* layout — three rows */
    body {
      display: grid;
      grid-template-rows: auto 1fr auto;
      gap: 16px;
      padding: 20px;
      max-width: 920px;
      margin: 0 auto;
    }

    /* ── top bar ─────────────────────────────────────────────── */
    .topbar {
      display: flex;
      align-items: center;
      gap: 14px;
      padding: 12px 16px;
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: 14px;
    }
    .avatar {
      width: 44px; height: 44px;
      border-radius: 50%;
      background: linear-gradient(135deg, var(--gold), #8b6914);
      display: flex; align-items: center; justify-content: center;
      font-weight: 700; font-size: 18px; color: #0a0a0c;
      flex-shrink: 0;
      box-shadow: 0 0 0 2px rgba(201, 161, 75, 0.2);
    }
    .who { flex: 1; }
    .who .name { font-size: 14px; font-weight: 600; }
    .who .sub  { font-size: 11px; color: var(--muted); letter-spacing: 0.4px; text-transform: uppercase; }
    .backend {
      font-size: 11px;
      color: var(--muted);
      padding: 4px 10px;
      border: 1px solid var(--border);
      border-radius: 999px;
      font-variant-numeric: tabular-nums;
    }
    .backend.up   { color: var(--green); border-color: rgba(34, 197, 94, 0.35); }
    .backend.down { color: var(--accent); border-color: rgba(239, 68, 68, 0.35); }

    /* ── transcript ─────────────────────────────────────────── */
    .transcript {
      overflow-y: auto;
      padding: 12px 16px;
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: 14px;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    .bubble {
      max-width: 80%;
      padding: 10px 14px;
      border-radius: 14px;
      font-size: 14px;
      line-height: 1.5;
      word-wrap: break-word;
    }
    .bubble.me {
      align-self: flex-end;
      background: var(--gold);
      color: #0a0a0c;
      border-bottom-right-radius: 4px;
    }
    .bubble.ai {
      align-self: flex-start;
      background: #1f2937;
      color: var(--fg);
      border-bottom-left-radius: 4px;
    }
    .bubble.interim {
      opacity: 0.5;
      font-style: italic;
    }
    .empty {
      color: var(--muted);
      font-size: 13px;
      text-align: center;
      padding: 24px;
      font-style: italic;
    }

    /* ── controls ───────────────────────────────────────────── */
    .controls {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 14px;
      padding: 12px;
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: 14px;
    }
    #mic {
      width: 84px; height: 84px;
      border-radius: 50%;
      background: var(--accent);
      border: none;
      color: white;
      font-size: 12px;
      font-weight: 700;
      letter-spacing: 1px;
      cursor: pointer;
      box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
      transition: transform 0.15s ease, background 0.2s;
      flex-shrink: 0;
    }
    #mic:hover  { transform: scale(1.04); }
    #mic:active { transform: scale(0.96); }
    #mic.listening {
      background: var(--green);
      box-shadow: 0 8px 24px rgba(34, 197, 94, 0.35);
      animation: pulse 1.8s ease-in-out infinite;
    }
    @keyframes pulse {
      0%, 100% { box-shadow: 0 8px 24px rgba(34, 197, 94, 0.35); }
      50%      { box-shadow: 0 8px 32px rgba(34, 197, 94, 0.7); }
    }
    .meta {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .status {
      color: var(--muted);
      font-size: 13px;
      min-height: 16px;
    }
    .latency {
      color: var(--muted);
      font-size: 11px;
      font-variant-numeric: tabular-nums;
      letter-spacing: 0.4px;
    }
    .countdown {
      color: var(--muted);
      font-size: 12px;
      font-variant-numeric: tabular-nums;
      letter-spacing: 0.4px;
    }
    .countdown.warn { color: #facc15; }
    .countdown.crit { color: var(--accent); font-weight: 600; }
    .options {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }
    select {
      background: #1f2937;
      color: var(--fg);
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 6px 10px;
      font-size: 12px;
      font-family: inherit;
    }
    .toggle {
      font-size: 11px;
      color: var(--muted);
      display: flex;
      align-items: center;
      gap: 4px;
      cursor: pointer;
    }
    .toggle input { accent-color: var(--gold); }
  </style>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
  </head>
<body>
  <header class="topbar">
    <div class="avatar">SA</div>
    <div class="who">
      <div class="name">Steve · voice-chat</div>
      <div class="sub">free stack · browser STT + ollama + browser TTS</div>
    </div>
    <div id="backend" class="backend">checking…</div>
  </header>

  <div id="transcript" class="transcript">
    <div class="empty">tap the mic and start talking</div>
  </div>

  <footer class="controls">
    <button id="mic">START</button>
    <div class="meta">
      <div id="status" class="status">idle</div>
      <div id="countdown" class="countdown"></div>
      <div id="latency" class="latency"></div>
      <div class="options">
        <select id="voicePicker" title="system voice"></select>
        <label class="toggle"><input id="autoSpeak" type="checkbox" checked>read aloud</label>
      </div>
    </div>
  </footer>

  <script type="module" src="voice-chat.js"></script>
</body>
</html>