/** * Reactive app store for the doocus browser. * * Reads the local docs-output tree via /api/docs (see server/doocusApi.ts). * Holds the doc list, the currently open doc (meta + textified content), and the * selection used by the package builder. Read-only over the pipeline output — * the UI never mutates extracted artifacts. */ import { reactive, computed } from 'vue' export interface DocSummary { id: string name: string type: string | null family: string | null title: string | null extractedAt: string | null wordCount: number bytes: number hasThumb: boolean warnings: number } export interface DocDetail { id: string meta: Record content: string hasThumb: boolean thumbUrl: string | null hasOriginal: boolean originalUrl: string | null } /** Package targets and their (soft) limits. Surfaced, not hard-enforced. */ export interface Target { key: string label: string maxFiles: number maxBytes: number } export const TARGETS: Target[] = [ { key: 'gemini', label: 'Gemini web', maxFiles: 10, maxBytes: 100 * 1024 * 1024 }, { key: 'notebooklm', label: 'NotebookLM', maxFiles: 50, maxBytes: 200 * 1024 * 1024 }, ] interface State { docs: DocSummary[] outputDir: string currentId: string | null current: DocDetail | null loading: boolean selected: Set targetKey: string } export const store = reactive({ docs: [], outputDir: '', currentId: null, current: null, loading: false, selected: new Set(), targetKey: TARGETS[0].key, }) export async function loadDocs(): Promise { const res = await fetch('/api/docs') const data = await res.json() store.docs = data.docs ?? [] store.outputDir = data.outputDir ?? '' if (!store.currentId && store.docs.length) { await openDoc(store.docs[0].id) } } export async function openDoc(id: string): Promise { store.loading = true try { const res = await fetch(`/api/docs/${encodeURIComponent(id)}`) if (!res.ok) throw new Error(`open failed: ${res.status}`) store.current = await res.json() store.currentId = id } finally { store.loading = false } } export function toggleSelect(id: string): void { if (store.selected.has(id)) store.selected.delete(id) else store.selected.add(id) } export function isSelected(id: string): boolean { return store.selected.has(id) } export const target = computed(() => TARGETS.find((t) => t.key === store.targetKey) ?? TARGETS[0]) /** Rough package estimate: each doc contributes original + content.md + meta.json. */ export const packageEstimate = computed(() => { const chosen = store.docs.filter((d) => store.selected.has(d.id)) const files = chosen.length * 3 // original + content.md + meta.json // originals dominate size; content/meta are small — approximate with wordCount. const bytes = chosen.reduce((sum, d) => sum + d.bytes + d.wordCount * 6 + 512, 0) const t = target.value return { docCount: chosen.length, files, bytes, overFiles: files > t.maxFiles, overBytes: bytes > t.maxBytes, } }) export function humanBytes(n: number): string { if (n < 1024) return `${n} B` if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB` return `${(n / 1024 / 1024).toFixed(1)} MB` }