feat: dashboard Second Brain, essai 7 jours et vérification e-mail
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>
This commit is contained in:
@@ -17,14 +17,14 @@ export const maxDuration = 60
|
||||
const sectionPlanSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
goal: z.string().min(1),
|
||||
demoKind: z.enum(['svg-scene', 'chart', 'heatmap-matrix', 'simulation', 'none']),
|
||||
demoGoal: z.string().optional(),
|
||||
demoKind: z.enum(['steps', 'svg-scene', 'chart', 'heatmap-matrix', 'simulation', 'none']),
|
||||
demoGoal: z.string().nullish(),
|
||||
})
|
||||
|
||||
const requestSchema = z.object({
|
||||
/** undefined = legacy deterministic full-page (fallback path) */
|
||||
action: z.enum(['plan', 'section']).optional(),
|
||||
content: z.string().min(40),
|
||||
content: z.string().min(40).max(500_000),
|
||||
lang: z.string().optional(),
|
||||
noteId: z.string().optional(),
|
||||
notebookId: z.string().optional(),
|
||||
@@ -44,7 +44,10 @@ export async function POST(req: NextRequest) {
|
||||
return aiConsentForbiddenResponse()
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const body = await req.json().catch(() => null)
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
const parsed = requestSchema.parse(body)
|
||||
const wordCount = parsed.content
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
@@ -61,11 +64,12 @@ export async function POST(req: NextRequest) {
|
||||
const provider = getSlidesProvider(config)
|
||||
const lang = parsed.lang || 'fr'
|
||||
|
||||
// ── LLM plan (billed: page = 20 crédits, spec §8.5) ──────────────────
|
||||
// ── LLM plan (billed 5/20 crédits — le reste est facturé par section) ──
|
||||
if (parsed.action === 'plan') {
|
||||
try {
|
||||
await reserveAiUsageOrThrow(session.user.id, 'interactive_page', {
|
||||
lane: 'chat',
|
||||
amount: 5,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
@@ -97,7 +101,7 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ plan: result.plan, attempts: result.attempts })
|
||||
}
|
||||
|
||||
// ── LLM single section (already billed at plan time) ────────────────
|
||||
// ── LLM single section (billed 3/20 crédits per section — no free LLM) ──
|
||||
if (parsed.action === 'section') {
|
||||
if (!parsed.section || !parsed.sectionId || !parsed.pageTitle) {
|
||||
return NextResponse.json(
|
||||
@@ -105,6 +109,26 @@ export async function POST(req: NextRequest) {
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
try {
|
||||
await reserveAiUsageOrThrow(session.user.id, 'interactive_page', {
|
||||
lane: 'chat',
|
||||
amount: 3,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json(err.toJSON(), { status: 402 })
|
||||
}
|
||||
if (
|
||||
err instanceof QuotaServiceUnavailableError ||
|
||||
process.env.NODE_ENV === 'production'
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: 'QUOTA_SERVICE_UNAVAILABLE' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
console.error('[/api/ai/interactive-page] Quota check error (fail-open):', err)
|
||||
}
|
||||
const result = await generatePageSection({
|
||||
content: parsed.content,
|
||||
lang,
|
||||
@@ -171,7 +195,16 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: error.issues }, { status: 400 })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Requête invalide',
|
||||
issues: error.issues.slice(0, 10).map((i) => ({
|
||||
path: i.path.join('.'),
|
||||
message: i.message,
|
||||
})),
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Erreur génération interactive page'
|
||||
|
||||
Reference in New Issue
Block a user