diff --git a/session/config.json b/session/config.json index 63565df..e0df9e0 100644 --- a/session/config.json +++ b/session/config.json @@ -1,8 +1,8 @@ { "llm": { - "model": "ollama/llama3.1", - "api_key": null, - "api_base": null, + "model": "openai/gpt-4o-mini", + "api_key": "not-needed", + "api_base": "http://localhost:8080/v1", "temperature": 0.8 } } diff --git a/tools/run.py b/tools/run.py index 91a7f88..58b8ec9 100755 --- a/tools/run.py +++ b/tools/run.py @@ -10,14 +10,14 @@ from __future__ import annotations import os import random import sys +import threading from datetime import date from pathlib import Path -from textual import on, work +from textual import on from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical, VerticalScroll from textual.widgets import Button, Input, Static, TabbedContent, TabPane -from textual.worker import Worker, WorkerState, get_current_worker from rich.markdown import Markdown as RichMarkdown from rich.theme import Theme @@ -327,6 +327,9 @@ class ChaosTUI(App): overflow-y: auto; scrollbar-size-vertical: 2; } + #main-tabs { + height: 1fr; + } TabbedContent { background: #1a1a2a; } @@ -366,8 +369,19 @@ class ChaosTUI(App): margin: 0 1; min-width: 12; } + #play-status { + background: #1a1a2a; + color: #e0b060; + padding: 0 2; + height: 1; + text-style: bold italic; + text-align: center; + } + #play-status.processing { + background: #2a1a0a; + color: #ffd93d; + } #play-input { - dock: bottom; height: 3; background: #222222; color: #e0d0c0; @@ -377,11 +391,10 @@ class ChaosTUI(App): #play-input:focus { border: solid #e0ad4c; } - #play-processing { - background: #1a1a2a; - color: #888888; - padding: 1 2; - text-style: italic; + #play-input:disabled { + background: #1a1a1a; + color: #666666; + border: solid #333333; } /* Book tab */ @@ -464,6 +477,10 @@ class ChaosTUI(App): self._last_result: GenerationResult | None = None self._is_processing: bool = False + # Thinking animation + self._thinking_dots = 0 + self._thinking_timer_handle = None + # Book viewer state self._book_page = 0 self._book_pages = [] @@ -480,6 +497,7 @@ class ChaosTUI(App): with VerticalScroll(id="play-scroll"): yield Static("*Awaiting the fates...*", id="play-narrative") yield Horizontal(id="play-choices") + yield Static("", id="play-status") yield Input( placeholder="Type your action and press Enter...", id="play-input", @@ -530,17 +548,21 @@ class ChaosTUI(App): input_widget = self.query_one("#play-input", Input) input_widget.disabled = True + input_widget.placeholder = "LLM is thinking..." - self._set_narrative("✦ *The fates conspire...* ✦") self._clear_choices() + self._show_thinking() - self._run_generation(player_action) + # Run generation in a daemon thread so it doesn't block the UI + t = threading.Thread( + target=self._run_generation, + args=(player_action,), + daemon=True, + ) + t.start() - @work(thread=True, exit_on_error=False) def _run_generation(self, player_action: str | None) -> None: """Worker thread: calls engine.generate() and posts result back.""" - worker = get_current_worker() - # Provide previous narrative as context on subsequent calls last_narrative = self._last_narrative if self._last_narrative else None @@ -549,16 +571,42 @@ class ChaosTUI(App): last_narrative=last_narrative, ) - if worker.is_cancelled: - return - self.call_from_thread(self._on_generation_done, result, player_action) + def _show_thinking(self) -> None: + """Show the thinking indicator and start the animation timer.""" + self._thinking_dots = 0 + status = self.query_one("#play-status", Static) + status.add_class("processing") + status.update("✦ LLM is weaving the narrative ✦") + self._thinking_timer_handle = self.set_interval( + 0.5, self._tick_thinking + ) + + def _hide_thinking(self) -> None: + """Stop the animation and hide the thinking indicator.""" + if self._thinking_timer_handle: + self._thinking_timer_handle.stop() + self._thinking_timer_handle = None + status = self.query_one("#play-status", Static) + status.remove_class("processing") + status.update("") + + def _tick_thinking(self) -> None: + """Animate the thinking dots.""" + if not self._is_processing: + return + self._thinking_dots = (self._thinking_dots + 1) % 4 + dots = "." * self._thinking_dots + status = self.query_one("#play-status", Static) + status.update(f"✦ LLM is weaving the narrative{dots} ✦") + def _on_generation_done( self, result: GenerationResult, player_action: str | None ) -> None: """Handle the completed generation on the main thread.""" self._is_processing = False + self._hide_thinking() if result.error: self._show_error(result.error) @@ -591,13 +639,10 @@ class ChaosTUI(App): self._set_choices(result.choices) self._enable_input() - # Focus the input - input_widget = self.query_one("#play-input", Input) - input_widget.focus() - def _enable_input(self) -> None: input_widget = self.query_one("#play-input", Input) input_widget.disabled = False + input_widget.placeholder = "Type your action and press Enter..." input_widget.value = "" input_widget.focus()