diff --git a/CHANGELOG.md b/CHANGELOG.md
index e25dea1..be867de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
## [Unreleased]
### Changed
+- Conversation Playground: the round microphone button is red (pressed, "Stop microphone") while the mic is hot (live or recording) and blue ("Start microphone") when off; it returns to blue on stop, auto-stop and mic errors. The pulse ring only animates when reduced motion is not requested.
- Conversation Playground: the metrics (Token Usage and STT / LLM first token / TTS first audio TTFB charts) now sit in the right-hand Latency column below Turn history instead of a second "Metrics" tab; the Conversation | Metrics tablist is removed. Charts gain axis titles ("Time (ms)", "Time"), a legend and three y-ticks.
+### Fixed
+- Conversation Playground: Turn history rows (#n, time, error rows) were in the DOM but squeezed to zero height by the flex column once charts filled the Latency column; the list no longer shrinks.
+- Conversation Playground: the metrics charts render at the column width (re-rendered on resize) so axis, tick and legend text is 11px instead of ~8px; a single point is centred with a larger dot.
+
### Added
- Conversation Playground: Metrics view (Conversation | Metrics tabs) with Pipecat-style TTFB line charts for STT, LLM first token and TTS, plus session Token Usage (prompt/completion/total, "n/a" when the backend reports none). The stream now requests `stream_options.include_usage` (dropped and retried on 400/422) and forwards `usage` in the final stats event; the TTS value is now the time to the first audio chunk instead of including the remaining LLM generation time.
- Conversation Playground: a Thinking on/off switch next to the Language Model (default off, setting `conversation_thinking`). Off sends `chat_template_kwargs.enable_thinking=false` (plus `think:false` for Ollama) with each reply request and never touches the system prompt; the empty-reply error now says to turn Thinking off.
diff --git a/Manual.md b/Manual.md
index 1df28f3..380f267 100644
--- a/Manual.md
+++ b/Manual.md
@@ -886,7 +886,7 @@ offline cache yet.
**Usage:** scroll the right-hand Latency column below "Turn history" to Token Usage and the three chart boxes (STT, LLM first token, TTS first audio). Each completed turn adds one point (X = time of day HH:MM:SS, Y = "Time (ms)", legend "TTFB (STT|LLM|TTS)"). Use "Show as table" under a chart for the numbers. The Clear button resets charts and token totals.
-**Expected result:** three charts with one point per turn (last 50 kept) and summed token counts. Voice turns include STT; typed turns have no STT point. The Latency panel and Turn history above are unchanged.
+**Expected result:** three charts (sized to the column, text 11px) with one point per turn (last 50 kept) and summed token counts. Voice turns include STT; typed turns have no STT point. The Latency panel and Turn history above are unchanged.
**Errors:** a backend that sends no usage shows "n/a" (never 0). Failed turns add no point. With no turns yet, "No turns yet." is shown.
@@ -895,3 +895,9 @@ offline cache yet.
**Offline:** works with a local LLM; no network beyond your configured endpoint.
**Privacy:** metrics live only in the browser tab's memory and vanish on reload; nothing leaves the browser except the LLM call itself (which now asks the endpoint to include token usage).
+
+### Conversation Playground: microphone button and Turn history
+
+**Microphone colours:** the round mic button is blue when the microphone is off and red while it is hot (live listening or recording). Screen readers hear "Start microphone" / "Stop microphone" with a pressed state, and the status text still says "Listening…" or "Mic off", so colour is never the only cue. The button returns to blue on stop, auto-stop and when microphone access fails. The pulse ring is shown only if your system does not request reduced motion.
+
+**Turn history:** every finished turn adds a row "#n", a status icon and the total time (for example 1.88s) at the top of the list below the Latency bars; a failed turn adds a red row. Clear empties the list. Rows are text, so they work on mobile and with screen readers.
diff --git a/static/js/conversation.js b/static/js/conversation.js
index 3f22d30..0ceff33 100644
--- a/static/js/conversation.js
+++ b/static/js/conversation.js
@@ -595,24 +595,25 @@ $('s-import-voices-file')?.addEventListener('change', async function () {
renderMetrics();
}
- function chartSvg(pts, label, k) {
- const W = 300, H = 160, L = 46, R = 10, T = 8, B = 36;
+ let chartW = 0;
+ function chartSvg(pts, label, k, W) {
+ const H = 170, L = 46, R = 12, T = 10, B = 40; // W = rendered width, so SVG units are CSS px
const max = Math.max(...pts.map(p => p.v), 1);
const t0 = pts[0].ts, span = pts[pts.length - 1].ts - t0;
const x = ts => L + (span ? (ts - t0) / span : 0.5) * (W - L - R);
const y = v => T + (1 - v / max) * (H - T - B);
const line = pts.map(p => `${x(p.ts).toFixed(1)},${y(p.v).toFixed(1)}`).join(' ');
- const dots = pts.map(p => ``).join('');
+ const dots = pts.map(p => ``).join('');
const yt = [0, max / 2, max].map(v => `${Math.round(v)}`).join('');
return ``;
}
@@ -622,17 +623,23 @@ $('s-import-voices-file')?.addEventListener('change', async function () {
const tok = (id, v) => { const el = $(id); if (el) el.textContent = v == null ? tr('n/a') : String(v); };
tok('cm-tok-prompt', metricUsage?.p); tok('cm-tok-completion', metricUsage?.c); tok('cm-tok-total', metricUsage?.t);
if (!metricTurns.length) { wrap.innerHTML = ''; return; }
+ const W = Math.max(200, (wrap.clientWidth || 246) - 18); // minus chart padding+border
+ chartW = W;
wrap.innerHTML = [['stt', 'STT', 'STT'], ['llm', 'LLM', 'LLM first token'], ['tts', 'TTS', 'TTS first audio']].map(([k, title, what]) => {
const pts = metricTurns.filter(m => m[k] != null).map(m => ({ ts: m.ts, v: m[k] }));
const summary = pts.length ? `${tr(what)}, ${tr('last')} ${pts[pts.length - 1].v} ms, ${pts.length} ${tr('points')}` : `${tr(what)}: ${tr('no data')}`;
const rows = pts.map(p => `