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:
@@ -7,8 +7,7 @@ import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent'
|
||||
import { isPublishTemplateId, isInteractivePageTemplate } from '@/lib/publish/types'
|
||||
import { computePublishedSourceHash, renderPublishedTemplate, renderRewrittenTemplate } from '@/lib/publish/template-render'
|
||||
import { validateInteractivePage } from '@/lib/interactive-page'
|
||||
import { reserveAiUsageOrThrow } from '@/lib/ai-quota'
|
||||
import { validateInteractivePage, type PageSpecV1, type PageValidationResult } from '@/lib/interactive-page'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { getSlidesProvider } from '@/lib/ai/factory'
|
||||
import { generateInteractivePageFromContent } from '@/lib/ai/services/interactive-page-generate.service'
|
||||
@@ -93,11 +92,61 @@ async function updateNotePublishState(noteId: string, data: PublishUpdateData) {
|
||||
}
|
||||
}
|
||||
|
||||
/** All human-facing text of a PageSpecV1 — fed to moderation. */
|
||||
function collectPageText(page: PageSpecV1): string[] {
|
||||
const out: string[] = [
|
||||
page.hero.kicker,
|
||||
page.hero.title,
|
||||
page.hero.subtitle ?? '',
|
||||
page.hero.meta ?? '',
|
||||
page.footer ?? '',
|
||||
]
|
||||
if (page.overview) {
|
||||
out.push(page.overview.lead)
|
||||
for (const c of page.overview.cards) out.push(c.badge, c.title, c.body)
|
||||
}
|
||||
for (const section of page.sections) {
|
||||
out.push(section.title)
|
||||
for (const b of section.blocks) {
|
||||
if (b.type === 'prose') out.push(b.md)
|
||||
else if (b.type === 'formula') out.push(b.caption ?? '')
|
||||
else if (b.type === 'callout') out.push(b.title, b.md)
|
||||
else if (b.type === 'demo') {
|
||||
out.push(b.caption ?? '', b.demo.disclaimer ?? '')
|
||||
for (const act of b.demo.acts) {
|
||||
out.push(act.title)
|
||||
for (const st of act.steps) out.push(st.speak)
|
||||
}
|
||||
for (const panel of b.demo.scene.panels) {
|
||||
if (panel.type === 'svg-scene') {
|
||||
for (const n of panel.payload.nodes) out.push(n.label ?? '')
|
||||
}
|
||||
}
|
||||
} else if (b.type === 'chart') {
|
||||
out.push(b.caption ?? '')
|
||||
for (const s of b.payload.series) out.push(s.label ?? '')
|
||||
} else if (b.type === 'stats') {
|
||||
for (const it of b.items) out.push(it.value, it.label)
|
||||
} else if (b.type === 'table') {
|
||||
out.push(b.caption ?? '', ...b.columns, ...b.rows.flat())
|
||||
} else if (b.type === 'image') {
|
||||
out.push(b.alt, b.caption ?? '')
|
||||
} else if (b.type === 'sim') {
|
||||
out.push(b.caption ?? '', b.sim.title ?? '', b.sim.disclaimer ?? '')
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.filter((s) => s && s.trim())
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
const body = await request.json()
|
||||
const body = await request.json().catch(() => null)
|
||||
if (!body || typeof body !== 'object') {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||
}
|
||||
const { noteId, action, mode, template, language, rewrite, pageSpec } = body as {
|
||||
noteId?: string
|
||||
action?: string
|
||||
@@ -123,9 +172,21 @@ export async function POST(request: NextRequest) {
|
||||
return aiConsentForbiddenResponse()
|
||||
}
|
||||
|
||||
let validatedPage = pageSpec ? validateInteractivePage(pageSpec) : null
|
||||
let validatedPage: PageValidationResult | null = null
|
||||
if (pageSpec) {
|
||||
// Client-provided page (from the preview dialog): must validate as-is.
|
||||
// Never silently substitute a different page than the one previewed.
|
||||
const checked = validateInteractivePage(pageSpec)
|
||||
if (!checked.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: 'invalid_page_spec', issues: checked.issues.slice(0, 12) },
|
||||
{ status: 422 }
|
||||
)
|
||||
}
|
||||
validatedPage = checked
|
||||
}
|
||||
|
||||
if (!validatedPage?.ok) {
|
||||
if (!validatedPage) {
|
||||
// Deterministic generate (no LLM quota)
|
||||
const config = await getSystemConfig()
|
||||
const provider = getSlidesProvider(config)
|
||||
@@ -144,22 +205,12 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 422 }
|
||||
)
|
||||
}
|
||||
validatedPage = { ok: true, page: generated.page }
|
||||
validatedPage = { ok: true as const, page: generated.page }
|
||||
}
|
||||
|
||||
// Guaranteed valid PageSpec after generate-or-validate above
|
||||
if (!validatedPage?.ok) {
|
||||
return NextResponse.json({ error: 'invalid_page_spec' }, { status: 422 })
|
||||
}
|
||||
|
||||
const textForModeration = [
|
||||
validatedPage.page.hero.title,
|
||||
validatedPage.page.hero.subtitle,
|
||||
validatedPage.page.overview?.lead,
|
||||
...validatedPage.page.sections.map((s) => s.title),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
// Moderate ALL human-facing text of the page (prose, callouts, demo
|
||||
// narration, tables, captions) — not just titles.
|
||||
const textForModeration = collectPageText(validatedPage.page).join('\n')
|
||||
|
||||
const moderation = await moderateWithFallback(
|
||||
note.title || '',
|
||||
|
||||
Reference in New Issue
Block a user