fix: batch priorités — thème, scroll, wizard, agents, slides, charts, packs
Anti-flash dark (color-scheme + applyDocumentTheme), scroll pages publiques, prompts wizard/slides moins génériques, cron agents rattrapage nextRun null, slides carnet via notebookId, graphiques à 2 crédits + hint, audit dashboard, packs Stripe marqués non configurés sans price ID.
This commit is contained in:
@@ -3,7 +3,7 @@ import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { QuotaExceededError } from '@/lib/entitlements'
|
||||
import { reserveAiUsageOrThrow } from '@/lib/ai-quota'
|
||||
import { slideGenerateCreditCost, resolveCreditCost } from '@/lib/credits'
|
||||
import { slideGenerateCreditCost, notebookSlideCreditCost, resolveCreditCost } from '@/lib/credits'
|
||||
import type { FeatureName } from '@/lib/quota-utils'
|
||||
import { logAuditEvent, getClientIp } from '@/lib/audit-log'
|
||||
import {
|
||||
@@ -42,8 +42,10 @@ export async function POST(req: NextRequest) {
|
||||
const userId = session.user.id
|
||||
|
||||
const body = await req.json()
|
||||
const { noteId, type, theme, style, language, template, purpose, audience, slideCount } = body as {
|
||||
noteId: string
|
||||
const { noteId, notebookId, type, theme, style, language, template, purpose, audience, slideCount } = body as {
|
||||
noteId?: string
|
||||
/** Si fourni (slides), génère un deck multi-notes du carnet */
|
||||
notebookId?: string
|
||||
type: GenerateType
|
||||
theme?: string
|
||||
style?: string
|
||||
@@ -54,21 +56,47 @@ export async function POST(req: NextRequest) {
|
||||
slideCount?: number
|
||||
}
|
||||
|
||||
if (!noteId || !type || !TYPE_DEFAULTS[type]) {
|
||||
if ((!noteId && !notebookId) || !type || !TYPE_DEFAULTS[type]) {
|
||||
return NextResponse.json({ error: 'Paramètres invalides' }, { status: 400 })
|
||||
}
|
||||
if (notebookId && type !== 'slide-generator') {
|
||||
return NextResponse.json({ error: 'notebookId réservé aux présentations' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Crédits globaux (respecte le BYOK) — slides = 1 + N
|
||||
let noteCountForPack = 1
|
||||
if (notebookId) {
|
||||
noteCountForPack = await prisma.note.count({
|
||||
where: {
|
||||
notebookId,
|
||||
userId,
|
||||
isArchived: false,
|
||||
trashedAt: null,
|
||||
},
|
||||
})
|
||||
if (noteCountForPack < 1) {
|
||||
return NextResponse.json({ error: 'Carnet vide ou introuvable' }, { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
// Crédits globaux (respecte le BYOK) — slides note = 1+N ; carnet = coût notebook
|
||||
const featureKey = (type === 'slide-generator' ? 'slide_generate' : 'excalidraw_generate') as FeatureName
|
||||
const creditCost =
|
||||
type === 'slide-generator'
|
||||
? slideGenerateCreditCost(slideCount)
|
||||
? notebookId
|
||||
? notebookSlideCreditCost({ slideCount, noteCount: noteCountForPack })
|
||||
: slideGenerateCreditCost(slideCount)
|
||||
: resolveCreditCost(featureKey)
|
||||
try {
|
||||
await reserveAiUsageOrThrow(userId, featureKey, {
|
||||
amount: creditCost,
|
||||
slideCount: type === 'slide-generator' ? slideCount : undefined,
|
||||
metadata: { noteId, type, slideCount: slideCount ?? null },
|
||||
metadata: {
|
||||
noteId: noteId ?? null,
|
||||
notebookId: notebookId ?? null,
|
||||
type,
|
||||
slideCount: slideCount ?? null,
|
||||
noteCount: noteCountForPack,
|
||||
},
|
||||
lane: 'chat',
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -86,12 +114,28 @@ export async function POST(req: NextRequest) {
|
||||
throw e
|
||||
}
|
||||
|
||||
const note = await prisma.note.findFirst({
|
||||
where: { id: noteId, userId },
|
||||
select: { id: true, title: true, notebookId: true },
|
||||
})
|
||||
if (!note) {
|
||||
return NextResponse.json({ error: 'Note introuvable' }, { status: 404 })
|
||||
let note: { id: string; title: string | null; notebookId: string | null } | null = null
|
||||
if (noteId) {
|
||||
note = await prisma.note.findFirst({
|
||||
where: { id: noteId, userId },
|
||||
select: { id: true, title: true, notebookId: true },
|
||||
})
|
||||
if (!note) {
|
||||
return NextResponse.json({ error: 'Note introuvable' }, { status: 404 })
|
||||
}
|
||||
} else if (notebookId) {
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: { id: notebookId, userId },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
if (!notebook) {
|
||||
return NextResponse.json({ error: 'Carnet introuvable' }, { status: 404 })
|
||||
}
|
||||
note = {
|
||||
id: notebookId,
|
||||
title: notebook.name,
|
||||
notebookId,
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = TYPE_DEFAULTS[type]
|
||||
@@ -110,6 +154,10 @@ export async function POST(req: NextRequest) {
|
||||
role = `Génère un deck exécutif de haute qualité (titres d'action, une idée par slide, pas de filler, graphiques uniquement avec vrais chiffres).${recipeHint}`
|
||||
}
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json({ error: 'Source introuvable' }, { status: 404 })
|
||||
}
|
||||
|
||||
const agentName = type === 'slide-generator'
|
||||
? `${isEn ? 'Slides' : 'Présentation'} — ${(note.title || 'Note').substring(0, 40)}`
|
||||
: `${isEn ? 'Diagram' : 'Diagramme'} — ${(note.title || 'Note').substring(0, 40)}`
|
||||
@@ -136,6 +184,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const sourceNotebookId = notebookId || note.notebookId || undefined
|
||||
const agent = await prisma.agent.create({
|
||||
data: {
|
||||
name: agentName,
|
||||
@@ -149,8 +198,9 @@ export async function POST(req: NextRequest) {
|
||||
maxSteps: defaults.maxSteps,
|
||||
frequency: 'one-shot',
|
||||
isEnabled: true,
|
||||
sourceNoteIds: JSON.stringify([noteId]),
|
||||
targetNotebookId: note.notebookId ?? undefined,
|
||||
sourceNoteIds: noteId ? JSON.stringify([noteId]) : JSON.stringify([]),
|
||||
sourceNotebookId: notebookId ? notebookId : undefined,
|
||||
targetNotebookId: sourceNotebookId,
|
||||
slideTheme: theme ?? 'keynote',
|
||||
slideStyle: style ?? 'soft',
|
||||
userId,
|
||||
@@ -168,11 +218,22 @@ export async function POST(req: NextRequest) {
|
||||
userId,
|
||||
action: 'AI_REQUEST',
|
||||
resource: featureKey,
|
||||
metadata: { agentId: agent.id, noteId, featureKey },
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
noteId: noteId ?? null,
|
||||
notebookId: notebookId ?? null,
|
||||
featureKey,
|
||||
creditCost,
|
||||
},
|
||||
ip: getClientIp(req),
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, agentId: agent.id, status: 'running' })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
agentId: agent.id,
|
||||
status: 'running',
|
||||
creditCost,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── GET : poll current agent status ──────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user