Files
meetus/ui/doocus-app/src/store.ts
Mariano Gabriel ca8b3a784d doocus first ver
2026-07-05 10:08:42 -03:00

121 lines
3.2 KiB
TypeScript

/**
* 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<string, unknown>
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<string>
targetKey: string
}
export const store = reactive<State>({
docs: [],
outputDir: '',
currentId: null,
current: null,
loading: false,
selected: new Set<string>(),
targetKey: TARGETS[0].key,
})
export async function loadDocs(): Promise<void> {
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<void> {
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`
}