All files / src/views search-modal.ts

100% Statements 105/105
100% Branches 42/42
100% Functions 8/8
100% Lines 105/105

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 1261x         1x   1x 35x 35x 35x 35x   35x 35x 35x 35x 35x   35x 32x 32x 32x   32x 32x   32x 32x 32x 32x 32x   32x 32x   32x 21x 5x 5x 3x 5x 21x 32x 11x 3x 1x 1x 11x 11x     32x 32x   35x 3x 3x 3x 3x   35x 40x 39x 39x 39x 39x 39x 40x   35x 15x 14x 14x 15x   35x 9x 2x 2x 2x 7x 7x 7x 7x 7x 7x 6x 5x 9x 3x 3x 3x 9x 3x 3x 9x 1x 1x 9x   35x 7x 7x 7x 6x 5x   5x 5x 5x 5x   7x 2x 2x 2x 3x 3x 2x 7x 1x 1x 7x 35x  
import { App, Modal } from "obsidian";
import type LilbeePlugin from "../main";
import type { DocumentResult } from "../types";
import { renderDocumentResult, renderSourceChip } from "./results";
 
const SEARCH_DEBOUNCE_MS = 300;
 
export class SearchModal extends Modal {
    private plugin: LilbeePlugin;
    private mode: "search" | "ask";
    private debounceTimer: ReturnType<typeof setTimeout> | null = null;
    private resultsContainer: HTMLElement | null = null;
 
    constructor(app: App, plugin: LilbeePlugin, mode: "search" | "ask" = "search") {
        super(app);
        this.plugin = plugin;
        this.mode = mode;
    }
 
    onOpen(): void {
        const { contentEl } = this;
        contentEl.empty();
        contentEl.addClass("lilbee-modal");
 
        const title = this.mode === "search" ? "Search knowledge base" : "Ask a question";
        contentEl.createEl("h2", { text: title });
 
        const input = contentEl.createEl("input", {
            type: "text",
            cls: "lilbee-search-input",
            placeholder: this.mode === "search" ? "Type to search..." : "Ask anything...",
        });
 
        this.resultsContainer = contentEl.createDiv({ cls: "lilbee-modal-results" });
        this.renderEmptyState("Enter a query to begin.");
 
        if (this.mode === "search") {
            input.addEventListener("input", () => {
                if (this.debounceTimer) clearTimeout(this.debounceTimer);
                this.debounceTimer = setTimeout(() => {
                    this.runSearch(input.value.trim());
                }, SEARCH_DEBOUNCE_MS);
            });
        } else {
            input.addEventListener("keydown", (e) => {
                if (e.key === "Enter" && input.value.trim()) {
                    this.runAsk(input.value.trim());
                }
            });
        }
 
        // Focus input after open
        setTimeout(() => input.focus(), 0);
    }
 
    onClose(): void {
        if (this.debounceTimer) clearTimeout(this.debounceTimer);
        const { contentEl } = this;
        contentEl.empty();
    }
 
    private renderEmptyState(message: string): void {
        if (!this.resultsContainer) return;
        this.resultsContainer.empty();
        this.resultsContainer.createEl("p", {
            text: message,
            cls: "lilbee-empty-state",
        });
    }
 
    private renderLoading(): void {
        if (!this.resultsContainer) return;
        this.resultsContainer.empty();
        this.resultsContainer.createDiv({ cls: "lilbee-loading" });
    }
 
    private async runSearch(query: string): Promise<void> {
        if (!query) {
            this.renderEmptyState("Enter a query to begin.");
            return;
        }
        this.renderLoading();
        try {
            const results: DocumentResult[] = await this.plugin.api.search(
                query,
                this.plugin.settings.topK,
            );
            if (!this.resultsContainer) return;
            this.resultsContainer.empty();
            if (results.length === 0) {
                this.renderEmptyState("No results found.");
                return;
            }
            for (const result of results) {
                renderDocumentResult(this.resultsContainer, result, this.app);
            }
        } catch {
            this.renderEmptyState("Error: could not connect to lilbee server.");
        }
    }
 
    private async runAsk(question: string): Promise<void> {
        this.renderLoading();
        try {
            const response = await this.plugin.api.ask(question, this.plugin.settings.topK);
            if (!this.resultsContainer) return;
            this.resultsContainer.empty();
 
            this.resultsContainer.createEl("p", {
                text: response.answer,
                cls: "lilbee-ask-answer",
            });
 
            if (response.sources.length > 0) {
                const sourcesEl = this.resultsContainer.createDiv({ cls: "lilbee-ask-sources" });
                sourcesEl.createEl("span", { text: "Sources: " });
                for (const source of response.sources) {
                    renderSourceChip(sourcesEl, source);
                }
            }
        } catch {
            this.renderEmptyState("Error: could not connect to lilbee server.");
        }
    }
}