Files
oai/oai/tui/screens/conversation_selector.py
2026-02-04 11:22:53 +01:00

206 lines
6.9 KiB
Python

"""Conversation selector screen for oAI TUI."""
from typing import List, Optional
from textual.app import ComposeResult
from textual.containers import Container, Vertical
from textual.screen import ModalScreen
from textual.widgets import Button, DataTable, Input, Static
class ConversationSelectorScreen(ModalScreen[Optional[dict]]):
"""Modal screen for selecting a saved conversation."""
DEFAULT_CSS = """
ConversationSelectorScreen {
align: center middle;
}
ConversationSelectorScreen > Container {
width: 80%;
height: 70%;
background: #1e1e1e;
border: solid #555555;
layout: vertical;
}
ConversationSelectorScreen .header {
height: 3;
width: 100%;
background: #2d2d2d;
color: #cccccc;
padding: 0 2;
content-align: center middle;
}
ConversationSelectorScreen .search-input {
height: 3;
width: 100%;
background: #2a2a2a;
border: solid #555555;
margin: 0 0 1 0;
}
ConversationSelectorScreen .search-input:focus {
border: solid #888888;
}
ConversationSelectorScreen DataTable {
height: 1fr;
width: 100%;
background: #1e1e1e;
border: solid #555555;
}
ConversationSelectorScreen .footer {
height: 5;
width: 100%;
background: #2d2d2d;
padding: 1 2;
align: center middle;
}
ConversationSelectorScreen Button {
margin: 0 1;
}
"""
def __init__(self, conversations: List[dict]):
super().__init__()
self.all_conversations = conversations
self.filtered_conversations = conversations
self.selected_conversation: Optional[dict] = None
def compose(self) -> ComposeResult:
"""Compose the screen."""
with Container():
yield Static(
f"[bold]Load Conversation[/] [dim]({len(self.all_conversations)} saved)[/]",
classes="header"
)
yield Input(placeholder="Search conversations...", id="search-input", classes="search-input")
yield DataTable(id="conv-table", cursor_type="row", show_header=True, zebra_stripes=True)
with Vertical(classes="footer"):
yield Button("Load", id="load", variant="success")
yield Button("Cancel", id="cancel", variant="error")
def on_mount(self) -> None:
"""Initialize the table when mounted."""
table = self.query_one("#conv-table", DataTable)
# Add columns
table.add_column("#", width=5)
table.add_column("Name", width=40)
table.add_column("Messages", width=12)
table.add_column("Last Saved", width=20)
# Populate table
self._populate_table()
# Focus table if list is small (fits on screen), otherwise focus search
if len(self.all_conversations) <= 10:
table.focus()
else:
search_input = self.query_one("#search-input", Input)
search_input.focus()
def _populate_table(self) -> None:
"""Populate the table with conversations."""
table = self.query_one("#conv-table", DataTable)
table.clear()
for idx, conv in enumerate(self.filtered_conversations, 1):
name = conv.get("name", "Unknown")
message_count = str(conv.get("message_count", 0))
last_saved = conv.get("last_saved", "Unknown")
# Format timestamp if it's a full datetime
if "T" in last_saved or len(last_saved) > 20:
try:
# Truncate to just date and time
last_saved = last_saved[:19].replace("T", " ")
except:
pass
table.add_row(
str(idx),
name,
message_count,
last_saved,
key=str(idx)
)
def on_input_changed(self, event: Input.Changed) -> None:
"""Filter conversations based on search input."""
if event.input.id != "search-input":
return
search_term = event.value.lower()
if not search_term:
self.filtered_conversations = self.all_conversations
else:
self.filtered_conversations = [
c for c in self.all_conversations
if search_term in c.get("name", "").lower()
]
self._populate_table()
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
"""Handle row selection (click)."""
try:
row_index = int(event.row_key.value) - 1
if 0 <= row_index < len(self.filtered_conversations):
self.selected_conversation = self.filtered_conversations[row_index]
except (ValueError, IndexError):
pass
def on_data_table_row_highlighted(self, event) -> None:
"""Handle row highlight (arrow key navigation)."""
try:
table = self.query_one("#conv-table", DataTable)
if table.cursor_row is not None:
row_data = table.get_row_at(table.cursor_row)
if row_data:
row_index = int(row_data[0]) - 1
if 0 <= row_index < len(self.filtered_conversations):
self.selected_conversation = self.filtered_conversations[row_index]
except (ValueError, IndexError, AttributeError):
pass
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button press."""
if event.button.id == "load":
if self.selected_conversation:
self.dismiss(self.selected_conversation)
else:
self.dismiss(None)
else:
self.dismiss(None)
def on_key(self, event) -> None:
"""Handle keyboard shortcuts."""
if event.key == "escape":
self.dismiss(None)
elif event.key == "enter":
# If in search input, move to table
search_input = self.query_one("#search-input", Input)
if search_input.has_focus:
table = self.query_one("#conv-table", DataTable)
table.focus()
# If in table, select current row
else:
table = self.query_one("#conv-table", DataTable)
if table.cursor_row is not None:
try:
row_data = table.get_row_at(table.cursor_row)
if row_data:
row_index = int(row_data[0]) - 1
if 0 <= row_index < len(self.filtered_conversations):
selected = self.filtered_conversations[row_index]
self.dismiss(selected)
except (ValueError, IndexError, AttributeError):
if self.selected_conversation:
self.dismiss(self.selected_conversation)