Initial commit: Add Local API Docs Search CLI tool
This commit is contained in:
216
src/cli/interactive.py
Normal file
216
src/cli/interactive.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
"""Interactive search mode with Rich-powered UI."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
from rich import box
|
||||||
|
|
||||||
|
from src.models.document import SourceType, Document, SearchResult
|
||||||
|
from src.search.searcher import Searcher
|
||||||
|
from src.utils.config import get_config
|
||||||
|
from src.utils.formatters import format_search_results, get_source_style
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveSession:
|
||||||
|
"""Interactive search session with history and navigation."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the interactive session."""
|
||||||
|
self._searcher = Searcher()
|
||||||
|
self._history: List[str] = []
|
||||||
|
self._history_index: int = -1
|
||||||
|
self._results: List[SearchResult] = []
|
||||||
|
self._result_index: int = 0
|
||||||
|
self._current_query: str = ""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the interactive session."""
|
||||||
|
self._print_welcome()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
query = self._get_input()
|
||||||
|
|
||||||
|
if query is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not query.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._history.append(query)
|
||||||
|
self._history_index = len(self._history)
|
||||||
|
|
||||||
|
self._execute_search(query)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[italic]Use 'exit' or 'quit' to leave[/]")
|
||||||
|
except EOFError:
|
||||||
|
break
|
||||||
|
|
||||||
|
console.print("\n[italic]Goodbye![/]")
|
||||||
|
|
||||||
|
def _print_welcome(self):
|
||||||
|
"""Print welcome message."""
|
||||||
|
welcome_text = Text.assemble(
|
||||||
|
("Local API Docs Search\n", "bold cyan"),
|
||||||
|
("-" * 40, "dim\n"),
|
||||||
|
("Type your query and press Enter to search.\n", "white"),
|
||||||
|
("Commands:\n", "bold yellow"),
|
||||||
|
(" :q, quit, exit - Leave interactive mode\n", "dim"),
|
||||||
|
(" :h, help - Show this help\n", "dim"),
|
||||||
|
(" :c, clear - Clear search results\n", "dim"),
|
||||||
|
(" :n, next - Next result\n", "dim"),
|
||||||
|
(" :p, prev - Previous result\n", "dim"),
|
||||||
|
(" ↑/↓ - History navigation\n", "dim"),
|
||||||
|
)
|
||||||
|
|
||||||
|
panel = Panel(welcome_text, title="Welcome", expand=False)
|
||||||
|
console.print(panel)
|
||||||
|
|
||||||
|
def _get_input(self) -> Optional[str]:
|
||||||
|
"""Get user input with history navigation."""
|
||||||
|
prompt = Prompt.ask(
|
||||||
|
"[bold cyan]Search[/]",
|
||||||
|
default="",
|
||||||
|
show_default=False,
|
||||||
|
accept_default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if prompt in (":q", ":quit", "quit", "exit", "exit()"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if prompt in (":h", ":help", "help"):
|
||||||
|
self._print_welcome()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if prompt in (":c", ":clear", "clear"):
|
||||||
|
self._results = []
|
||||||
|
console.print("[italic]Results cleared[/]")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if prompt in (":n", ":next", "next"):
|
||||||
|
self._navigate_results(1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if prompt in (":p", ":prev", "previous"):
|
||||||
|
self._navigate_results(-1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
def _execute_search(self, query: str):
|
||||||
|
"""Execute search and display results."""
|
||||||
|
self._current_query = query
|
||||||
|
self._result_index = 0
|
||||||
|
|
||||||
|
with console.status("Searching..."):
|
||||||
|
self._results = self._searcher.hybrid_search(query, limit=10)
|
||||||
|
|
||||||
|
if not self._results:
|
||||||
|
console.print("[italic]No results found[/]\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"\n[bold]Found {len(self._results)} result(s)[/]\n")
|
||||||
|
self._display_current_result()
|
||||||
|
|
||||||
|
def _display_current_result(self):
|
||||||
|
"""Display the current result."""
|
||||||
|
if not self._results:
|
||||||
|
return
|
||||||
|
|
||||||
|
result = self._results[self._result_index]
|
||||||
|
|
||||||
|
source_style = get_source_style(result.document.source_type)
|
||||||
|
|
||||||
|
content = Text()
|
||||||
|
content.append(f"Result {self._result_index + 1}/{len(self._results)}\n", "bold yellow")
|
||||||
|
content.append(f"Title: {result.document.title}\n", "bold")
|
||||||
|
content.append(f"Type: {result.document.source_type.value}\n", source_style)
|
||||||
|
content.append(f"Score: {result.score:.4f}\n\n", "dim")
|
||||||
|
|
||||||
|
preview = result.document.content[:500]
|
||||||
|
if len(result.document.content) > 500:
|
||||||
|
preview += "..."
|
||||||
|
content.append(preview)
|
||||||
|
|
||||||
|
if result.document.file_path:
|
||||||
|
content.append(f"\n\n[dim]File: {result.document.file_path}[/]")
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"Result {self._result_index + 1}",
|
||||||
|
expand=False,
|
||||||
|
box=box.ROUNDED,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(panel)
|
||||||
|
|
||||||
|
if result.highlights:
|
||||||
|
console.print("\n[bold]Highlights:[/]")
|
||||||
|
for highlight in result.highlights[:3]:
|
||||||
|
console.print(f" [dim]{highlight}[/]")
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
def _navigate_results(self, direction: int):
|
||||||
|
"""Navigate through search results."""
|
||||||
|
if not self._results:
|
||||||
|
console.print("[italic]No results to navigate[/]")
|
||||||
|
return
|
||||||
|
|
||||||
|
new_index = self._result_index + direction
|
||||||
|
|
||||||
|
if new_index < 0:
|
||||||
|
new_index = 0
|
||||||
|
elif new_index >= len(self._results):
|
||||||
|
new_index = len(self._results) - 1
|
||||||
|
|
||||||
|
self._result_index = new_index
|
||||||
|
self._display_current_result()
|
||||||
|
|
||||||
|
|
||||||
|
def run_interactive():
|
||||||
|
"""Run the interactive search mode."""
|
||||||
|
session = InteractiveSession()
|
||||||
|
session.run()
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveSearch:
|
||||||
|
"""Legacy interactive search class for compatibility."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the interactive search."""
|
||||||
|
self._searcher = Searcher()
|
||||||
|
self._history: List[str] = []
|
||||||
|
|
||||||
|
def search(self, query: str) -> List[SearchResult]:
|
||||||
|
"""Execute search.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of search results
|
||||||
|
"""
|
||||||
|
self._history.append(query)
|
||||||
|
return self._searcher.hybrid_search(query)
|
||||||
|
|
||||||
|
def get_history(self) -> List[str]:
|
||||||
|
"""Get search history.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of past queries
|
||||||
|
"""
|
||||||
|
return self._history
|
||||||
|
|
||||||
|
def clear_history(self):
|
||||||
|
"""Clear search history."""
|
||||||
|
self._history = []
|
||||||
Reference in New Issue
Block a user