Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note) avec activation guidée, tableau éditable, kanban et suppression de colonnes. Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN. Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la robustesse du serveur MCP (config, validation, rate-limit, métriques). Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { Prisma } from '@prisma/client'
|
|
import { auth } from '@/auth'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { getOrCreateDeckForNotebook } from '@/lib/flashcards/deck-utils'
|
|
import type { FlashcardStyle } from '@/lib/flashcards/generate-flashcards'
|
|
|
|
interface CardInput {
|
|
front: string
|
|
back: string
|
|
type?: string
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
if (!prisma.flashcard || !prisma.flashcardDeck) {
|
|
return NextResponse.json(
|
|
{ error: 'Flashcards client outdated. Restart the app after prisma generate.', errorKey: 'flashcards.schemaMissing' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const body = await request.json()
|
|
const noteId = typeof body.noteId === 'string' ? body.noteId : null
|
|
const deckIdInput = typeof body.deckId === 'string' ? body.deckId : null
|
|
const cards = Array.isArray(body.cards) ? body.cards as CardInput[] : []
|
|
|
|
if (cards.length === 0) {
|
|
return NextResponse.json({ error: 'No cards to save' }, { status: 400 })
|
|
}
|
|
|
|
let notebookId: string | null = null
|
|
let fallbackDeckName: string | undefined
|
|
if (noteId) {
|
|
const note = await prisma.note.findFirst({
|
|
where: { id: noteId, userId: session.user.id },
|
|
select: { notebookId: true, title: true },
|
|
})
|
|
if (!note) {
|
|
return NextResponse.json({ error: 'Note not found' }, { status: 404 })
|
|
}
|
|
notebookId = note.notebookId
|
|
if (!notebookId) {
|
|
fallbackDeckName = note.title?.trim() || 'General'
|
|
}
|
|
}
|
|
|
|
let deckId = deckIdInput
|
|
if (!deckId) {
|
|
if (noteId) {
|
|
const existingFromNote = await prisma.flashcard.findFirst({
|
|
where: { noteId, deck: { userId: session.user.id } },
|
|
select: { deckId: true },
|
|
})
|
|
if (existingFromNote) {
|
|
deckId = existingFromNote.deckId
|
|
}
|
|
}
|
|
if (!deckId) {
|
|
const deck = await getOrCreateDeckForNotebook({
|
|
userId: session.user.id,
|
|
notebookId,
|
|
manualName: fallbackDeckName,
|
|
})
|
|
deckId = deck.id
|
|
}
|
|
} else {
|
|
const deck = await prisma.flashcardDeck.findFirst({
|
|
where: { id: deckId, userId: session.user.id },
|
|
})
|
|
if (!deck) {
|
|
return NextResponse.json({ error: 'Deck not found' }, { status: 404 })
|
|
}
|
|
}
|
|
|
|
const sanitized = cards
|
|
.map((c) => ({
|
|
front: typeof c.front === 'string' ? c.front.trim().slice(0, 500) : '',
|
|
back: typeof c.back === 'string' ? c.back.trim().slice(0, 800) : '',
|
|
type: (['qa', 'cloze', 'concept'].includes(c.type || '') ? c.type : 'qa') as FlashcardStyle,
|
|
}))
|
|
.filter((c) => c.front && c.back)
|
|
|
|
if (sanitized.length === 0) {
|
|
return NextResponse.json({ error: 'No valid cards' }, { status: 400 })
|
|
}
|
|
|
|
await prisma.flashcard.createMany({
|
|
data: sanitized.map((c) => ({
|
|
deckId: deckId!,
|
|
noteId,
|
|
front: c.front,
|
|
back: c.back,
|
|
type: c.type,
|
|
})),
|
|
})
|
|
|
|
await prisma.flashcardDeck.update({
|
|
where: { id: deckId! },
|
|
data: { updatedAt: new Date() },
|
|
})
|
|
|
|
return NextResponse.json({
|
|
deckId,
|
|
savedCount: sanitized.length,
|
|
})
|
|
} catch (error) {
|
|
console.error('[flashcards/save]', error)
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
if (error.code === 'P2021' || error.code === 'P2022') {
|
|
return NextResponse.json(
|
|
{ error: 'Flashcards schema not migrated. Run prisma migrate deploy.', errorKey: 'flashcards.schemaMissing' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
}
|
|
const message = error instanceof Error ? error.message : 'Failed to save flashcards'
|
|
return NextResponse.json({ error: message }, { status: 500 })
|
|
}
|
|
}
|