// Secondary entry point: ng2-pdfjs-viewer/ai
//
// A minimal bring-your-own-endpoint AI client for chat-with-PDF and
// summarization flows. The library NEVER talks to any AI service on its own:
// this class only sends requests when the host application calls it, to the
// endpoint the host application configured, with the host application's key.
//
// Works with any OpenAI-compatible chat-completions endpoint: OpenAI, Azure
// OpenAI, Anthropic-compatible gateways, Ollama, vLLM, LM Studio, etc.
//
// This is a headless, framework-free entry point — it imports nothing from
// Angular, so it can be used outside an Angular context (a worker, a Node
// service, a plain script). The full component re-exports these symbols from
// the package root for backwards compatibility.
//
// SECURITY: this sends a fetch from wherever you construct it — in a browser
// app, that is the user's browser. Don't point `endpoint` directly at a hosted
// cloud LLM (OpenAI, Azure, …): the API key would be exposed to the client, and
// most providers block direct browser calls via CORS. Use a local model
// (Ollama, LM Studio) or your own backend proxy. See the AI Assistant guide.
//
// Usage:
//   import { PdfAiAssistant } from "ng2-pdfjs-viewer/ai";
//   const text = await viewer.getDocumentText();
//   const ai = new PdfAiAssistant({
//     endpoint: "http://localhost:11434/v1/chat/completions", // e.g. Ollama
//     model: "llama3.2",
//   });
//   const answer = await ai.ask("What is this document about?", text);
class PdfAiAssistant {
    config;
    constructor(config) {
        if (!config?.endpoint) {
            throw new Error("PdfAiAssistant requires an endpoint");
        }
        this.config = config;
    }
    /**
     * Ask a free-form question about the document. `documentText` is the output
     * of PdfJsViewerComponent.getDocumentText(); `history` carries prior turns
     * for multi-turn chat.
     */
    async ask(question, documentText, history = [], signal, onToken) {
        const context = this.buildContext(documentText);
        const messages = [
            {
                role: "system",
                content: "You answer questions about the provided PDF document. " +
                    "Cite page numbers like [p.3] when referencing content. " +
                    "If the answer is not in the document, say so.\n\n" +
                    `DOCUMENT:\n${context}`,
            },
            ...history,
            { role: "user", content: question },
        ];
        return this.complete(messages, signal, onToken);
    }
    /** One-shot document summary. */
    async summarize(documentText) {
        return this.ask("Summarize this document concisely. Lead with what it is, then the key points.", documentText);
    }
    /**
     * Raw chat-completions call for custom prompting. Pass `onToken` to stream the
     * answer token-by-token (the callback receives the running full text and the
     * latest delta); the Promise still resolves to the complete text. Streaming is
     * requested only when `onToken` is given and `config.stream !== false`, and it
     * falls back to a single JSON response if the endpoint doesn't stream.
     */
    async complete(messages, signal, onToken) {
        const headers = {
            "Content-Type": "application/json",
            ...(this.config.headers ?? {}),
        };
        if (this.config.apiKey) {
            headers["Authorization"] = `Bearer ${this.config.apiKey}`;
        }
        const wantStream = !!onToken && this.config.stream !== false;
        const response = await fetch(this.config.endpoint, {
            method: "POST",
            headers,
            signal,
            body: JSON.stringify({
                model: this.config.model,
                temperature: this.config.temperature ?? 0.2,
                messages,
                ...(wantStream ? { stream: true } : {}),
            }),
        });
        if (!response.ok) {
            const body = await response.text().catch(() => "");
            throw new Error(`AI endpoint returned ${response.status}: ${body.slice(0, 300)}`);
        }
        const contentType = response.headers?.get?.("Content-Type") ?? "";
        if (wantStream && response.body && contentType.includes("text/event-stream")) {
            return this.readStream(response, onToken);
        }
        // Non-streaming response, or the endpoint ignored `stream`: one JSON body.
        const json = await response.json();
        const content = json?.choices?.[0]?.message?.content;
        if (typeof content !== "string") {
            throw new Error("AI endpoint returned an unexpected response shape");
        }
        // Emit once so callers wired for streaming still receive their update.
        if (onToken)
            onToken(content, content);
        return content;
    }
    /**
     * Read an OpenAI-style Server-Sent Events stream, accumulating
     * choices[0].delta.content and emitting each delta through onToken. Returns the
     * full concatenated text. Tolerates chunk boundaries that split SSE lines, and
     * skips frames it can't parse (keep-alive comments, non-`data:` lines) rather
     * than failing the whole stream.
     */
    async readStream(response, onToken) {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        let full = "";
        const handleLine = (raw) => {
            const line = raw.trim();
            if (!line.startsWith("data:"))
                return false;
            const data = line.slice(5).trim();
            if (data === "[DONE]")
                return true;
            try {
                const delta = JSON.parse(data)?.choices?.[0]?.delta?.content;
                if (typeof delta === "string" && delta) {
                    full += delta;
                    onToken(full, delta);
                }
            }
            catch {
                // Ignore an unparseable frame rather than aborting the stream.
            }
            return false;
        };
        try {
            for (;;) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                buffer += decoder.decode(value, { stream: true });
                let nl;
                while ((nl = buffer.indexOf("\n")) !== -1) {
                    const line = buffer.slice(0, nl);
                    buffer = buffer.slice(nl + 1);
                    if (handleLine(line))
                        return full; // [DONE]
                }
            }
            if (buffer.trim())
                handleLine(buffer); // trailing line without a newline
        }
        finally {
            reader.releaseLock();
        }
        return full;
    }
    buildContext(documentText) {
        const max = this.config.maxContextChars ?? 100_000;
        let out = "";
        for (const page of documentText ?? []) {
            const chunk = `[page ${page.page}]\n${page.text}\n\n`;
            if (out.length + chunk.length > max) {
                out += `\n[truncated at ${max} characters]`;
                break;
            }
            out += chunk;
        }
        return out;
    }
}

/**
 * Generated bundle index. Do not edit.
 */

export { PdfAiAssistant };
//# sourceMappingURL=ng2-pdfjs-viewer-ai.mjs.map
