feat(kontext): URL import helpers — API client + appendContent

Frontend plumbing for the Kontext "Aus URL" inline panel (the UI
itself was committed earlier as part of 003f75f7e):

- kontext/api.ts: new module-scoped fetch wrapper that talks to
  /api/v1/context/import-url with a Bearer token. Kept in kontext/
  rather than a shared helper because it's the only caller; moving
  it to `context/` would mix two unrelated modules again.
- kontext/stores/kontext.svelte.ts: new appendContent(chunk) method.
  The singleton row is encrypted at rest, so we decrypt the current
  content before concatenating with a '\n\n---\n\n' separator and
  writing back — going through setContent() to keep encryption +
  Dexie hook behaviour consistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 14:24:38 +02:00
parent 121a0c0a6f
commit 12072c6b6c
2 changed files with 54 additions and 1 deletions

View file

@ -0,0 +1,44 @@
/**
* Kontext API client talks to apps/api `POST /api/v1/context/import-url`.
*
* The server route lives under /context for historical reasons (shared
* crawler + LLM wrapper). Only the kontext singleton consumes it.
*/
import { authStore } from '$lib/stores/auth.svelte';
import { getManaApiUrl } from '$lib/api/config';
export type CrawlMode = 'single' | 'deep';
export interface ImportInput {
url: string;
mode: CrawlMode;
summarize: boolean;
}
export interface ImportResponse {
title: string;
content: string;
sourceUrl: string;
crawlMode: CrawlMode;
crawledAt: string;
pageCount: number;
}
export async function crawlUrlViaApi(input: ImportInput): Promise<ImportResponse> {
const token = await authStore.getValidToken();
if (!token) throw new Error('not authenticated');
const res = await fetch(`${getManaApiUrl()}/api/v1/context/import-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(input),
});
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error || `import failed (${res.status})`);
}
return (await res.json()) as ImportResponse;
}

View file

@ -6,7 +6,7 @@
*/
import { kontextDocTable } from '../collections';
import { encryptRecord } from '$lib/data/crypto';
import { encryptRecord, decryptRecords } from '$lib/data/crypto';
import { KONTEXT_SINGLETON_ID, type LocalKontextDoc } from '../types';
export const kontextStore = {
@ -30,4 +30,13 @@ export const kontextStore = {
await encryptRecord('kontextDoc', diff);
await kontextDocTable.update(KONTEXT_SINGLETON_ID, diff);
},
async appendContent(chunk: string): Promise<void> {
await this.ensureDoc();
const row = await kontextDocTable.get(KONTEXT_SINGLETON_ID);
const [decrypted] = row ? await decryptRecords('kontextDoc', [row]) : [];
const current = decrypted?.content ?? '';
const separator = current.trim() ? '\n\n---\n\n' : '';
await this.setContent(`${current}${separator}${chunk}`);
},
};