157 lines
4.9 KiB
Python
157 lines
4.9 KiB
Python
from typing import TYPE_CHECKING
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.binding import Binding
|
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
from textual.message import Message
|
|
from textual.screen import ModalScreen
|
|
from textual.widgets import Button, Markdown, Static, TextArea
|
|
|
|
if TYPE_CHECKING:
|
|
from haiku.rag.agents.chat.state import ChatSessionState
|
|
|
|
|
|
class ContextModal(ModalScreen): # pragma: no cover
|
|
"""Modal screen for viewing/editing context.
|
|
|
|
Before first message (not locked, no session context): Edit initial context
|
|
After first message (locked or has session context): View session context
|
|
"""
|
|
|
|
BINDINGS = [
|
|
Binding("escape", "cancel", "Close", show=False),
|
|
Binding("ctrl+o", "cancel", "Close", show=False),
|
|
]
|
|
|
|
CSS = """
|
|
ContextModal {
|
|
align: center middle;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
#context-container {
|
|
width: 70;
|
|
height: auto;
|
|
max-height: 32;
|
|
background: $surface;
|
|
border: tall $primary;
|
|
padding: 1 2;
|
|
}
|
|
|
|
#context-header {
|
|
height: auto;
|
|
margin-bottom: 1;
|
|
}
|
|
|
|
#context-description {
|
|
height: auto;
|
|
margin-bottom: 1;
|
|
color: $text-muted;
|
|
}
|
|
|
|
#context-editor {
|
|
height: 12;
|
|
min-height: 8;
|
|
max-height: 16;
|
|
}
|
|
|
|
#context-content {
|
|
height: 1fr;
|
|
max-height: 16;
|
|
scrollbar-gutter: stable;
|
|
}
|
|
|
|
#button-row {
|
|
height: auto;
|
|
margin-top: 1;
|
|
align: right middle;
|
|
}
|
|
|
|
#button-row Button {
|
|
margin-left: 1;
|
|
}
|
|
"""
|
|
|
|
class ContextUpdated(Message):
|
|
"""Emitted when the context is saved."""
|
|
|
|
def __init__(self, context: str) -> None:
|
|
super().__init__()
|
|
self.context = context
|
|
|
|
def __init__(
|
|
self, session_state: "ChatSessionState | None", is_locked: bool = False
|
|
) -> None:
|
|
super().__init__()
|
|
self.session_state = session_state
|
|
self._is_locked = is_locked
|
|
|
|
@property
|
|
def _is_edit_mode(self) -> bool:
|
|
"""Edit mode when not locked and no session context yet."""
|
|
has_session_context = (
|
|
self.session_state
|
|
and self.session_state.session_context
|
|
and self.session_state.session_context.summary
|
|
)
|
|
return not self._is_locked and not has_session_context
|
|
|
|
def compose(self) -> ComposeResult:
|
|
with Vertical(id="context-container"):
|
|
if self._is_edit_mode:
|
|
yield Static("[bold]Initial Context[/bold]", id="context-header")
|
|
yield Static(
|
|
"Set background context to guide the conversation. "
|
|
"This will be locked after you send your first message.",
|
|
id="context-description",
|
|
)
|
|
initial_value = ""
|
|
if self.session_state and self.session_state.initial_context:
|
|
initial_value = self.session_state.initial_context
|
|
yield TextArea(initial_value, id="context-editor")
|
|
with Horizontal(id="button-row"):
|
|
yield Button("Cancel", id="cancel-btn", variant="default")
|
|
yield Button("Save", id="save-btn", variant="primary")
|
|
else:
|
|
yield Static("[bold]Session Context[/bold]", id="context-header")
|
|
yield Static(
|
|
"What the assistant has learned from your conversation.",
|
|
id="context-description",
|
|
)
|
|
with VerticalScroll(id="context-content"):
|
|
yield Markdown(self._get_session_content())
|
|
with Horizontal(id="button-row"):
|
|
yield Button("Close", id="cancel-btn", variant="primary")
|
|
|
|
def _get_session_content(self) -> str:
|
|
if not self.session_state:
|
|
return "*No session state.*"
|
|
|
|
if not self.session_state.session_context:
|
|
return "*No session context yet. Ask a question first.*"
|
|
|
|
ctx = self.session_state.session_context
|
|
updated = (
|
|
ctx.last_updated.strftime("%Y-%m-%d %H:%M:%S")
|
|
if ctx.last_updated
|
|
else "unknown"
|
|
)
|
|
|
|
return f"**Last updated:** {updated}\n\n---\n\n{ctx.summary}"
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button presses."""
|
|
if event.button.id == "cancel-btn":
|
|
self.action_cancel()
|
|
elif event.button.id == "save-btn":
|
|
self.action_save()
|
|
|
|
def action_cancel(self) -> None:
|
|
"""Cancel and close without saving."""
|
|
self.app.pop_screen()
|
|
|
|
def action_save(self) -> None:
|
|
"""Save context and close."""
|
|
editor = self.query_one("#context-editor", TextArea)
|
|
self.post_message(self.ContextUpdated(editor.text))
|
|
self.app.pop_screen()
|