Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
5.1 KiB
TypeScript
212 lines
5.1 KiB
TypeScript
import type {
|
|
PageSection,
|
|
PageSpecV1,
|
|
PageValidationIssue,
|
|
} from '@/lib/interactive-page'
|
|
|
|
export type PagePlanDemoKind = 'steps' | '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<string, unknown>,
|
|
timeoutMs = 55_000
|
|
): Promise<{ res: Response; data: Record<string, never> } | { 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<GeneratePlanResponse> {
|
|
let out: Awaited<ReturnType<typeof postJson>>
|
|
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<GenerateSectionResponse> {
|
|
let out: Awaited<ReturnType<typeof postJson>>
|
|
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<GenerateInteractivePageResponse> {
|
|
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,
|
|
}
|
|
}
|