| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- # -*- coding: utf-8 -*-
- """
- Satellite SubView: Conversation (F3).
- Zeigt Informationen zu Wakeword-Erkennung, Voice Recognition
- und aktiver Konversation.
- """
- from __future__ import annotations
- from textual.app import ComposeResult
- from textual.containers import Vertical
- from textual.widgets import Static
- from trixy_core.tui.views.base import MainView
- from trixy_core.tui.widgets.status_table import StatusTable
- class SatConversationView(MainView):
- """F3 im Satellite-Kontext: Konversations-Informationen."""
- TITLE = "Konversation"
- VIEW_ID = "sat_conversation"
- def __init__(self, **kwargs) -> None:
- super().__init__(**kwargs)
- self._sat_data: dict = {}
- self._detail_data: object | None = None
- self._refresh_counter: int = 0
- def set_satellite(self, sat_data: dict) -> None:
- """Setzt die Satellite-Basisdaten."""
- self._sat_data = sat_data
- def compose(self) -> ComposeResult:
- with Vertical(classes="section-box"):
- yield Static("[bold]Wakeword[/bold]", classes="section-header")
- yield StatusTable(id="sat-conv-wakeword")
- with Vertical(classes="section-box"):
- yield Static("[bold]Spracherkennung[/bold]", classes="section-header")
- yield StatusTable(id="sat-conv-voicerec")
- with Vertical(classes="section-box"):
- yield Static("[bold]Konversation[/bold]", classes="section-header")
- yield StatusTable(id="sat-conv-conversation")
- with Vertical(classes="section-box"):
- yield Static("[bold]Verlauf[/bold]", classes="section-header")
- yield Static("", id="sat-conv-history")
- async def on_activate(self) -> None:
- """Laedt und zeigt die Konversations-Infos."""
- await self._load_detail()
- async def refresh_data(self) -> None:
- """Periodisches Update (alle 4 Sekunden fuer Live-Status)."""
- self._refresh_counter += 1
- if self._refresh_counter % 2 == 0:
- await self._load_detail()
- async def _load_detail(self) -> None:
- """Laedt detaillierte Satellite-Daten vom Server."""
- if not self.connection or not self._sat_data:
- return
- sat_id = self._sat_data.get("satellite_id", "")
- if not sat_id:
- return
- try:
- detail = await self.connection.request_satellite_detail(sat_id)
- except Exception:
- return
- if detail is None or detail.error:
- return
- self._detail_data = detail
- self._update_display()
- def _update_display(self) -> None:
- """Aktualisiert die Anzeige."""
- d = self._detail_data
- if d is None:
- return
- # Wakeword
- wakeword = self.query_one("#sat-conv-wakeword", StatusTable)
- wakeword.update_data([
- ("Modell", d.wakeword_model or "Unbekannt"),
- ("Letzte Erkennung", d.wakeword_last_detection or "Nie"),
- ])
- # Spracherkennung
- voicerec = self.query_one("#sat-conv-voicerec", StatusTable)
- voicerec.update_data([
- ("Modell", d.voice_recognition_model or "Unbekannt"),
- ("Letzter Sprecher", d.voice_recognition_last_speaker or "Unbekannt"),
- ])
- # Konversation
- conv = self.query_one("#sat-conv-conversation", StatusTable)
- if d.has_active_conversation:
- conv_status = "[yellow]Aktiv[/yellow]"
- else:
- conv_status = "Keine"
- rows = [
- ("Status", conv_status),
- ]
- if d.last_conversation_at:
- rows.append(("Letzte Konversation", d.last_conversation_at))
- if d.last_conversation_duration > 0:
- rows.append(("Dauer", f"{d.last_conversation_duration:.1f}s"))
- conv.update_data(rows)
- # Conversation-History
- history_widget = self.query_one("#sat-conv-history", Static)
- history = getattr(d, "conversation_history", [])
- if history:
- lines = []
- for entry in history:
- ts = entry.get("created_at", "")[:19].replace("T", " ")
- state = entry.get("state", "")
- duration = entry.get("duration_seconds", 0)
- turn_count = entry.get("turn_count", 0)
- wakeword = entry.get("wakeword_type", "")
- header = f"[bold]{ts}[/bold] ({state}, {duration:.1f}s, {turn_count} Turns)"
- if wakeword:
- header += f" [dim]Wakeword: {wakeword}[/dim]"
- lines.append(header)
- # Turns anzeigen
- for turn in entry.get("turns", []):
- turn_type = turn.get("turn_type", "")
- content = turn.get("content", "")
- if not content:
- continue
- if turn_type in ("user_speech", "user_text"):
- lines.append(f" [cyan]User:[/cyan] {content}")
- elif turn_type == "follow_up":
- lines.append(f" [yellow]Rueckfrage:[/yellow] {content}")
- elif turn_type == "system":
- lines.append(f" [dim]System: {content}[/dim]")
- else:
- lines.append(f" [green]Assistent:[/green] {content}")
- lines.append("") # Leerzeile zwischen Sessions
- history_widget.update("\n".join(lines))
- else:
- history_widget.update("[dim]Keine Konversationen vorhanden[/dim]")
|