|
@@ -5,6 +5,9 @@ self-correction from the heartbeat, command editing, bounded buffers.
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
from dataclasses import dataclass, field
|
|
|
|
|
+from datetime import datetime
|
|
|
|
|
+
|
|
|
|
|
+from rich.text import Text
|
|
|
|
|
|
|
|
MAX_TRANSCRIPTS = 200
|
|
MAX_TRANSCRIPTS = 200
|
|
|
MAX_CHAT = 200
|
|
MAX_CHAT = 200
|
|
@@ -17,6 +20,22 @@ MODE_STYLES = {
|
|
|
"chat": "magenta",
|
|
"chat": "magenta",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+# Time, language tag, and body text must use disjoint colors so they are never
|
|
|
|
|
+# confused: these fixed header styles never collide with the body palette.
|
|
|
|
|
+TIME_STYLE = "bold cyan"
|
|
|
|
|
+LANG_TAG_STYLE = "bold yellow"
|
|
|
|
|
+REASON_STYLE = "italic dim"
|
|
|
|
|
+
|
|
|
|
|
+LANG_TEXT_STYLES = {
|
|
|
|
|
+ "es": "green",
|
|
|
|
|
+ "en": "bright_blue",
|
|
|
|
|
+ "fr": "magenta",
|
|
|
|
|
+ "de": "bright_green",
|
|
|
|
|
+ "pt": "red",
|
|
|
|
|
+ "it": "bright_magenta",
|
|
|
|
|
+}
|
|
|
|
|
+DEFAULT_LANG_TEXT_STYLE = "white"
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def _num(value) -> float | None:
|
|
def _num(value) -> float | None:
|
|
|
"""Best-effort float coercion; None when missing/non-numeric."""
|
|
"""Best-effort float coercion; None when missing/non-numeric."""
|
|
@@ -26,6 +45,22 @@ def _num(value) -> float | None:
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _fmt_clock(ts) -> str:
|
|
|
|
|
+ """Render a transcript timestamp as local HH:MM:SS (epoch or ISO-8601)."""
|
|
|
|
|
+ value = _num(ts)
|
|
|
|
|
+ if value is not None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ return datetime.fromtimestamp(value).strftime("%H:%M:%S")
|
|
|
|
|
+ except (OverflowError, OSError, ValueError):
|
|
|
|
|
+ return "--:--:--"
|
|
|
|
|
+ if isinstance(ts, str):
|
|
|
|
|
+ try:
|
|
|
|
|
+ return datetime.fromisoformat(ts).strftime("%H:%M:%S")
|
|
|
|
|
+ except ValueError:
|
|
|
|
|
+ pass
|
|
|
|
|
+ return "--:--:--"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _fmt_gate(item: dict) -> str:
|
|
def _fmt_gate(item: dict) -> str:
|
|
|
"""Format the gating confidence defensively; '?' when absent."""
|
|
"""Format the gating confidence defensively; '?' when absent."""
|
|
|
for key in ("answer_confidence", "confidence"):
|
|
for key in ("answer_confidence", "confidence"):
|
|
@@ -186,8 +221,28 @@ class ViewState:
|
|
|
def mode_style(self) -> str:
|
|
def mode_style(self) -> str:
|
|
|
return MODE_STYLES.get(self.mode, "white")
|
|
return MODE_STYLES.get(self.mode, "white")
|
|
|
|
|
|
|
|
- def transcript_lines(self) -> list[str]:
|
|
|
|
|
- return [f"[{e.get('lang', '?')}] {e.get('text', '')}" for e in self.transcripts]
|
|
|
|
|
|
|
+ def transcript_lines(self) -> list[Text]:
|
|
|
|
|
+ """Render each transcript as a styled two-line block: header then text."""
|
|
|
|
|
+ blocks: list[Text] = []
|
|
|
|
|
+ for e in self.transcripts:
|
|
|
|
|
+ lang = str(e.get("lang") or "?").lower()
|
|
|
|
|
+ text_style = LANG_TEXT_STYLES.get(lang, DEFAULT_LANG_TEXT_STYLE)
|
|
|
|
|
+ reason = str(e.get("end_reason") or "").strip()
|
|
|
|
|
+ duration = _num(e.get("duration_s"))
|
|
|
|
|
+
|
|
|
|
|
+ block = Text()
|
|
|
|
|
+ block.append(_fmt_clock(e.get("ts")), style=TIME_STYLE)
|
|
|
|
|
+ block.append(" ")
|
|
|
|
|
+ block.append(lang.upper(), style=LANG_TAG_STYLE)
|
|
|
|
|
+ block.append(" ")
|
|
|
|
|
+ block.append(f"{duration:.1f}s" if duration is not None else "?", style="dim")
|
|
|
|
|
+ if reason:
|
|
|
|
|
+ block.append(" ")
|
|
|
|
|
+ block.append(reason, style=REASON_STYLE)
|
|
|
|
|
+ block.append("\n")
|
|
|
|
|
+ block.append(" " + str(e.get("text", "")), style=text_style)
|
|
|
|
|
+ blocks.append(block)
|
|
|
|
|
+ return blocks
|
|
|
|
|
|
|
|
def chat_lines(self) -> list[str]:
|
|
def chat_lines(self) -> list[str]:
|
|
|
return [f"{m.get('role', '?')}: {m.get('text', '')}" for m in self.chat]
|
|
return [f"{m.get('role', '?')}: {m.get('text', '')}" for m in self.chat]
|