206 lines
6.9 KiB
Python
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)
|