import type { PageSection, PageSpecV1, PageValidationIssue, } from '@/lib/interactive-page' export type PagePlanDemoKind = 'svg-scene' | 'chart' | 'heatmap-matrix' | 'simulation' | 'none' export type PagePlanSection = { title: string goal: string demoKind: PagePlanDemoKind demoGoal?: string } export type PagePlan = { heroTitle: string heroSubtitle?: string overviewLead: string overviewCards: { badge: string title: string body: string intent?: string }[] sections: PagePlanSection[] } export type GenerateInteractivePageResponse = | { ok: true; page: PageSpecV1; attempts: number } | { ok: false error: string reason?: string issues?: PageValidationIssue[] quotaExceeded?: boolean status?: number } export type GeneratePlanResponse = | { ok: true; plan: PagePlan; attempts: number } | { ok: false error: string reason?: string quotaExceeded?: boolean status?: number } export type GenerateSectionResponse = | { ok: true; section: PageSection; attempts: number } | { ok: false; error: string; status?: number } async function postJson( body: Record, timeoutMs = 55_000 ): Promise<{ res: Response; data: Record } | { abortError: true }> { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), timeoutMs) try { const res = await fetch('/api/ai/interactive-page', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal, }) const data = await res.json().catch(() => ({})) return { res, data } } catch (err) { if (err instanceof Error && err.name === 'AbortError') { return { abortError: true } } throw err } finally { clearTimeout(timeout) } } /** LLM plan (billed — 20 crédits). */ export async function generateInteractivePagePlan(params: { content: string lang?: string noteId?: string }): Promise { let out: Awaited> try { out = await postJson({ ...params, action: 'plan' }) } catch (err) { return { ok: false, error: err instanceof Error ? err.message : 'Network error', } } if ('abortError' in out) { return { ok: false, error: 'Génération trop longue — réessaie' } } const { res, data } = out as { res: Response data: any } if (res.status === 402) { return { ok: false, error: data.error || 'Quota exceeded', quotaExceeded: true, status: 402, } } if (!res.ok) { return { ok: false, error: data.error || `HTTP ${res.status}`, reason: data.reason, status: res.status, } } return { ok: true, plan: data.plan, attempts: data.attempts ?? 1 } } /** LLM single section (not billed — page billed at plan time). */ export async function generateInteractivePageSection(params: { content: string lang?: string noteId?: string pageTitle: string sectionId: string section: PagePlanSection }): Promise { let out: Awaited> try { out = await postJson({ ...params, action: 'section' }) } catch (err) { return { ok: false, error: err instanceof Error ? err.message : 'Network error', } } if ('abortError' in out) { return { ok: false, error: 'timeout' } } const { res, data } = out as { res: Response data: any } if (!res.ok) { return { ok: false, error: data.error || `HTTP ${res.status}`, status: res.status } } return { ok: true, section: data.section, attempts: data.attempts ?? 1 } } /** Legacy deterministic full page (fallback, no quota). */ export async function generateInteractivePage(params: { content: string lang?: string noteId?: string notebookId?: string }): Promise { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 75_000) let res: Response try { res = await fetch('/api/ai/interactive-page', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), signal: controller.signal, }) } catch (err) { clearTimeout(timeout) const aborted = err instanceof Error && err.name === 'AbortError' return { ok: false, error: aborted ? 'Génération trop longue — réessaie' : err instanceof Error ? err.message : 'Network error', } } finally { clearTimeout(timeout) } const data = await res.json().catch(() => ({})) if (res.status === 402) { return { ok: false, error: data.error || 'Quota exceeded', quotaExceeded: true, status: 402, } } if (!res.ok) { return { ok: false, error: data.error || `HTTP ${res.status}`, reason: data.reason, issues: data.issues, status: res.status, } } return { ok: true, page: data.page, attempts: data.attempts ?? 1, } }