sat_conversation_view.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. # -*- coding: utf-8 -*-
  2. """
  3. Satellite SubView: Conversation (F3).
  4. Zeigt Informationen zu Wakeword-Erkennung, Voice Recognition
  5. und aktiver Konversation.
  6. """
  7. from __future__ import annotations
  8. from textual.app import ComposeResult
  9. from textual.containers import Vertical
  10. from textual.widgets import Static
  11. from trixy_core.tui.views.base import MainView
  12. from trixy_core.tui.widgets.status_table import StatusTable
  13. class SatConversationView(MainView):
  14. """F3 im Satellite-Kontext: Konversations-Informationen."""
  15. TITLE = "Konversation"
  16. VIEW_ID = "sat_conversation"
  17. def __init__(self, **kwargs) -> None:
  18. super().__init__(**kwargs)
  19. self._sat_data: dict = {}
  20. self._detail_data: object | None = None
  21. self._refresh_counter: int = 0
  22. def set_satellite(self, sat_data: dict) -> None:
  23. """Setzt die Satellite-Basisdaten."""
  24. self._sat_data = sat_data
  25. def compose(self) -> ComposeResult:
  26. with Vertical(classes="section-box"):
  27. yield Static("[bold]Wakeword[/bold]", classes="section-header")
  28. yield StatusTable(id="sat-conv-wakeword")
  29. with Vertical(classes="section-box"):
  30. yield Static("[bold]Spracherkennung[/bold]", classes="section-header")
  31. yield StatusTable(id="sat-conv-voicerec")
  32. with Vertical(classes="section-box"):
  33. yield Static("[bold]Konversation[/bold]", classes="section-header")
  34. yield StatusTable(id="sat-conv-conversation")
  35. with Vertical(classes="section-box"):
  36. yield Static("[bold]Verlauf[/bold]", classes="section-header")
  37. yield Static("", id="sat-conv-history")
  38. async def on_activate(self) -> None:
  39. """Laedt und zeigt die Konversations-Infos."""
  40. await self._load_detail()
  41. async def refresh_data(self) -> None:
  42. """Periodisches Update (alle 4 Sekunden fuer Live-Status)."""
  43. self._refresh_counter += 1
  44. if self._refresh_counter % 2 == 0:
  45. await self._load_detail()
  46. async def _load_detail(self) -> None:
  47. """Laedt detaillierte Satellite-Daten vom Server."""
  48. if not self.connection or not self._sat_data:
  49. return
  50. sat_id = self._sat_data.get("satellite_id", "")
  51. if not sat_id:
  52. return
  53. try:
  54. detail = await self.connection.request_satellite_detail(sat_id)
  55. except Exception:
  56. return
  57. if detail is None or detail.error:
  58. return
  59. self._detail_data = detail
  60. self._update_display()
  61. def _update_display(self) -> None:
  62. """Aktualisiert die Anzeige."""
  63. d = self._detail_data
  64. if d is None:
  65. return
  66. # Wakeword
  67. wakeword = self.query_one("#sat-conv-wakeword", StatusTable)
  68. wakeword.update_data([
  69. ("Modell", d.wakeword_model or "Unbekannt"),
  70. ("Letzte Erkennung", d.wakeword_last_detection or "Nie"),
  71. ])
  72. # Spracherkennung
  73. voicerec = self.query_one("#sat-conv-voicerec", StatusTable)
  74. voicerec.update_data([
  75. ("Modell", d.voice_recognition_model or "Unbekannt"),
  76. ("Letzter Sprecher", d.voice_recognition_last_speaker or "Unbekannt"),
  77. ])
  78. # Konversation
  79. conv = self.query_one("#sat-conv-conversation", StatusTable)
  80. if d.has_active_conversation:
  81. conv_status = "[yellow]Aktiv[/yellow]"
  82. else:
  83. conv_status = "Keine"
  84. rows = [
  85. ("Status", conv_status),
  86. ]
  87. if d.last_conversation_at:
  88. rows.append(("Letzte Konversation", d.last_conversation_at))
  89. if d.last_conversation_duration > 0:
  90. rows.append(("Dauer", f"{d.last_conversation_duration:.1f}s"))
  91. conv.update_data(rows)
  92. # Conversation-History
  93. history_widget = self.query_one("#sat-conv-history", Static)
  94. history = getattr(d, "conversation_history", [])
  95. if history:
  96. lines = []
  97. for entry in history:
  98. ts = entry.get("created_at", "")[:19].replace("T", " ")
  99. state = entry.get("state", "")
  100. duration = entry.get("duration_seconds", 0)
  101. turn_count = entry.get("turn_count", 0)
  102. wakeword = entry.get("wakeword_type", "")
  103. header = f"[bold]{ts}[/bold] ({state}, {duration:.1f}s, {turn_count} Turns)"
  104. if wakeword:
  105. header += f" [dim]Wakeword: {wakeword}[/dim]"
  106. lines.append(header)
  107. # Turns anzeigen
  108. for turn in entry.get("turns", []):
  109. turn_type = turn.get("turn_type", "")
  110. content = turn.get("content", "")
  111. if not content:
  112. continue
  113. if turn_type in ("user_speech", "user_text"):
  114. lines.append(f" [cyan]User:[/cyan] {content}")
  115. elif turn_type == "follow_up":
  116. lines.append(f" [yellow]Rueckfrage:[/yellow] {content}")
  117. elif turn_type == "system":
  118. lines.append(f" [dim]System: {content}[/dim]")
  119. else:
  120. lines.append(f" [green]Assistent:[/green] {content}")
  121. lines.append("") # Leerzeile zwischen Sessions
  122. history_widget.update("\n".join(lines))
  123. else:
  124. history_widget.update("[dim]Keine Konversationen vorhanden[/dim]")