Info modal

This commit is contained in:
Yiorgis Gozadinos 2025-12-11 15:30:31 +02:00
parent 09be31f021
commit c0cbc22137
No known key found for this signature in database
3 changed files with 225 additions and 0 deletions

View file

@ -13,6 +13,14 @@
- New `/api/documents` endpoint to list available documents
- Frontend document selector component with search and multi-select
- Demonstrates client-to-server state flow via AG-UI protocol
- **Inspector Info Modal**: New `i` keyboard shortcut opens a modal displaying database information
### Changed
- **Inspector Lazy Loading**: Chunks panel now loads chunks in batches of 50 with infinite scroll
- Fixes unresponsive UI when viewing documents with large numbers of chunks
- New `ChunkRepository.get_by_document_id()` pagination with `limit` and `offset` parameters
- New `ChunkRepository.count_by_document_id()` method
## [0.20.0] - 2025-12-10

View file

@ -69,6 +69,7 @@ class InspectorApp(App): # type: ignore[misc] # pragma: no cover
BINDINGS = [
Binding("q", "quit", "Quit", show=True),
Binding("/", "search", "Search", show=True),
Binding("i", "show_info", "Info", show=True),
Binding("v", "show_visual", "Visual", show=True),
Binding("c", "show_context", "Context", show=True),
]
@ -126,6 +127,13 @@ class InspectorApp(App): # type: ignore[misc] # pragma: no cover
if self.client:
await self._switch_modal(SearchModal(self.client))
async def action_show_info(self) -> None:
"""Show database info modal."""
if self.client:
from haiku.rag.inspector.widgets.info_modal import InfoModal
await self._switch_modal(InfoModal(self.client, self.db_path))
async def on_search_modal_chunk_selected(
self, message: SearchModal.ChunkSelected
) -> None:

View file

@ -0,0 +1,209 @@
import json
from importlib.metadata import version as pkg_version
from pathlib import Path
from typing import TYPE_CHECKING
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical, VerticalScroll
from textual.screen import ModalScreen
from textual.widgets import Static
from haiku.rag.utils import format_bytes
if TYPE_CHECKING:
from haiku.rag.client import HaikuRAG
class InfoModal(ModalScreen): # pragma: no cover
"""Modal screen for displaying database information."""
BINDINGS = [
Binding("escape", "dismiss", "Close", show=True),
Binding("i", "dismiss", "Close", show=True),
]
CSS = """
InfoModal {
align: center middle;
}
#info-container {
width: 80;
height: auto;
max-height: 80%;
background: $surface;
border: solid $primary;
padding: 1 2;
}
#info-header {
height: auto;
margin-bottom: 1;
}
#info-content {
height: auto;
max-height: 100%;
}
"""
def __init__(self, client: "HaikuRAG", db_path: Path):
super().__init__()
self.client = client
self.db_path = db_path
self._content_widget = Static("Loading...")
def compose(self) -> ComposeResult:
with Vertical(id="info-container"):
yield Static("[bold]Database Info[/bold]", id="info-header")
with VerticalScroll(id="info-content"):
yield self._content_widget
async def on_mount(self) -> None:
"""Load and display database info."""
import lancedb
lines: list[str] = []
# Path
lines.append(f"[bold $accent]path[/bold $accent]: {self.db_path}")
if not self.db_path.exists():
lines.append("[red]Database path does not exist.[/red]")
self._content_widget.update("\n".join(lines))
return
# Connect to get table info
try:
db = lancedb.connect(self.db_path)
table_names = set(db.table_names())
except Exception as e:
lines.append(f"[red]Failed to open database: {e}[/red]")
self._content_widget.update("\n".join(lines))
return
# Get versions
try:
ldb_version = pkg_version("lancedb")
except Exception:
ldb_version = "unknown"
try:
hr_version = pkg_version("haiku.rag-slim")
except Exception:
hr_version = "unknown"
try:
docling_version = pkg_version("docling")
except Exception:
docling_version = "unknown"
# Get stats from store
table_stats = self.client.store.get_stats()
# Read settings
stored_version = "unknown"
embed_provider: str | None = None
embed_model: str | None = None
vector_dim: int | None = None
if "settings" in table_names:
settings_tbl = db.open_table("settings")
arrow = settings_tbl.search().where("id = 'settings'").limit(1).to_arrow()
rows = arrow.to_pylist() if arrow is not None else []
if rows:
raw = rows[0].get("settings") or "{}"
data = json.loads(raw) if isinstance(raw, str) else (raw or {})
stored_version = str(data.get("version", stored_version))
embeddings = data.get("embeddings", {})
embed_model_obj = embeddings.get("model", {})
embed_provider = embed_model_obj.get("provider")
embed_model = embed_model_obj.get("name")
vector_dim = embed_model_obj.get("vector_dim")
num_docs = table_stats["documents"].get("num_rows", 0)
doc_bytes = table_stats["documents"].get("total_bytes", 0)
num_chunks = table_stats["chunks"].get("num_rows", 0)
chunk_bytes = table_stats["chunks"].get("total_bytes", 0)
has_vector_index = table_stats["chunks"].get("has_vector_index", False)
num_indexed_rows = table_stats["chunks"].get("num_indexed_rows", 0)
num_unindexed_rows = table_stats["chunks"].get("num_unindexed_rows", 0)
# Table versions
doc_versions = (
len(list(db.open_table("documents").list_versions()))
if "documents" in table_names
else 0
)
chunk_versions = (
len(list(db.open_table("chunks").list_versions()))
if "chunks" in table_names
else 0
)
# Build output
lines.append(
f"[bold $accent]haiku.rag version (db)[/bold $accent]: {stored_version}"
)
if embed_provider or embed_model or vector_dim:
provider_part = embed_provider or "unknown"
model_part = embed_model or "unknown"
dim_part = f"{vector_dim}" if vector_dim is not None else "unknown"
lines.append(
f"[bold $accent]embeddings[/bold $accent]: "
f"{provider_part}/{model_part} (dim: {dim_part})"
)
else:
lines.append("[bold $accent]embeddings[/bold $accent]: unknown")
lines.append(
f"[bold $accent]documents[/bold $accent]: {num_docs} ({format_bytes(doc_bytes)})"
)
lines.append(
f"[bold $accent]chunks[/bold $accent]: {num_chunks} ({format_bytes(chunk_bytes)})"
)
# Vector index info
if has_vector_index:
lines.append("[bold $accent]vector index[/bold $accent]: ✓ exists")
lines.append(
f"[bold $accent]indexed chunks[/bold $accent]: {num_indexed_rows}"
)
if num_unindexed_rows > 0:
lines.append(
f"[bold $accent]unindexed chunks[/bold $accent]: [yellow]{num_unindexed_rows}[/yellow]"
)
else:
lines.append(
f"[bold $accent]unindexed chunks[/bold $accent]: {num_unindexed_rows}"
)
else:
if num_chunks >= 256:
lines.append(
"[bold $accent]vector index[/bold $accent]: [yellow]✗ not created[/yellow]"
)
else:
lines.append(
f"[bold $accent]vector index[/bold $accent]: ✗ not created "
f"(need {256 - num_chunks} more chunks)"
)
lines.append(
f"[bold $accent]versions (documents)[/bold $accent]: {doc_versions}"
)
lines.append(
f"[bold $accent]versions (chunks)[/bold $accent]: {chunk_versions}"
)
lines.append("")
lines.append("[bold]Versions[/bold]")
lines.append(f"[bold $accent]haiku.rag[/bold $accent]: {hr_version}")
lines.append(f"[bold $accent]lancedb[/bold $accent]: {ldb_version}")
lines.append(f"[bold $accent]docling[/bold $accent]: {docling_version}")
self._content_widget.update("\n".join(lines))
async def action_dismiss(self, result=None) -> None:
self.app.pop_screen()