From 324bf40658236567268d0d55d538b63c143bf1c8 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 18 Jul 2026 17:32:00 +0000 Subject: [PATCH] fix: replace raw ai_consent_required code with translatable message and consent prompt - Centralize AI consent 403 responses in server-consent helpers - Return human-readable message + code + i18n errorKey from all AI APIs - Update dashboard and AI clients to detect code/errorKey and translate - Add consent.ai.requiredToast i18n keys (fr/en) --- memento-note/app/api/ai/auto-labels/route.ts | 7 +- .../app/api/ai/batch-organize/route.ts | 7 +- .../app/api/ai/describe-image/route.ts | 4 +- .../app/api/ai/echo/connections/route.ts | 7 +- memento-note/app/api/ai/echo/fusion/route.ts | 4 +- memento-note/app/api/ai/echo/route.ts | 7 +- .../app/api/ai/enrich-from-resource/route.ts | 4 +- .../app/api/ai/notebook-slides/route.ts | 62 +++++ .../app/api/ai/notebook-summary/route.ts | 7 +- memento-note/app/api/ai/personas/route.ts | 4 +- memento-note/app/api/ai/reformulate/route.ts | 4 +- .../app/api/ai/suggest-charts/route.ts | 7 +- .../app/api/ai/suggest-notebook/route.ts | 4 +- memento-note/app/api/ai/tags/route.ts | 4 +- .../app/api/ai/title-suggestions/route.ts | 4 +- .../app/api/ai/transform-markdown/route.ts | 4 +- memento-note/app/api/ai/translate/route.ts | 4 +- memento-note/app/api/chat/route.ts | 7 +- .../app/api/flashcards/generate/route.ts | 4 +- memento-note/app/api/notes/publish/route.ts | 4 +- memento-note/components/agents/agent-card.tsx | 2 +- .../auto-label-suggestion-dialog.tsx | 2 +- .../components/batch-organization-dialog.tsx | 2 +- .../components/contextual-ai-chat.tsx | 10 +- memento-note/components/dashboard-view.tsx | 2 +- .../flashcards/flashcard-generate-dialog.tsx | 6 +- .../components/flashcards/revision-view.tsx | 4 +- memento-note/components/fusion-modal.tsx | 2 +- memento-note/components/home-client.tsx | 22 +- .../note-editor/note-editor-context.tsx | 12 +- .../note-editor/note-editor-toolbar.tsx | 6 +- .../components/note-editor/publish-dialog.tsx | 2 +- .../components/notebook-summary-dialog.tsx | 2 +- memento-note/components/personas-panel.tsx | 2 +- memento-note/components/rich-text-editor.tsx | 2 +- .../components/wizard/ai-notebook-wizard.tsx | 2 +- .../wizard/notebook-site-dialog.tsx | 2 +- .../wizard/notebook-slides-dialog.tsx | 237 ++++++++++++++++++ memento-note/lib/consent/server-consent.ts | 17 +- memento-note/locales/en.json | 3 +- memento-note/locales/fr.json | 3 +- 41 files changed, 404 insertions(+), 97 deletions(-) create mode 100644 memento-note/app/api/ai/notebook-slides/route.ts create mode 100644 memento-note/components/wizard/notebook-slides-dialog.tsx diff --git a/memento-note/app/api/ai/auto-labels/route.ts b/memento-note/app/api/ai/auto-labels/route.ts index 08e20f9..03d8db5 100644 --- a/memento-note/app/api/ai/auto-labels/route.ts +++ b/memento-note/app/api/ai/auto-labels/route.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { autoLabelCreationService } from '@/lib/ai/services' import { getAISettings } from '@/app/actions/ai-settings' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenJson } from '@/lib/consent/server-consent' import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' /** @@ -23,10 +23,7 @@ export async function POST(request: NextRequest) { // GDPR AI Consent check if (!(await hasUserAiConsent())) { - return NextResponse.json( - { success: false, error: 'ai_consent_required' }, - { status: 403 } - ) + return aiConsentForbiddenJson() } // Respect user's autoLabeling toggle diff --git a/memento-note/app/api/ai/batch-organize/route.ts b/memento-note/app/api/ai/batch-organize/route.ts index 80bc869..7c4d9cc 100644 --- a/memento-note/app/api/ai/batch-organize/route.ts +++ b/memento-note/app/api/ai/batch-organize/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { batchOrganizationService } from '@/lib/ai/services' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenJson } from '@/lib/consent/server-consent' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' /** @@ -20,10 +20,7 @@ export async function POST(request: NextRequest) { // GDPR AI Consent check if (!(await hasUserAiConsent())) { - return NextResponse.json( - { success: false, error: 'ai_consent_required' }, - { status: 403 } - ) + return aiConsentForbiddenJson() } // Get language from request headers or body diff --git a/memento-note/app/api/ai/describe-image/route.ts b/memento-note/app/api/ai/describe-image/route.ts index 746c458..0c41818 100644 --- a/memento-note/app/api/ai/describe-image/route.ts +++ b/memento-note/app/api/ai/describe-image/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { getAISettings } from '@/app/actions/ai-settings' import { describeImages } from '@/lib/ai/services/image-description.service' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' export async function POST(req: NextRequest) { @@ -13,7 +13,7 @@ export async function POST(req: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const userSettings = await getAISettings(session.user.id) diff --git a/memento-note/app/api/ai/echo/connections/route.ts b/memento-note/app/api/ai/echo/connections/route.ts index 8f50809..09804b5 100644 --- a/memento-note/app/api/ai/echo/connections/route.ts +++ b/memento-note/app/api/ai/echo/connections/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { memoryEchoService } from '@/lib/ai/services/memory-echo.service' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' /** * GET /api/ai/echo/connections?noteId={id}&page={page}&limit={limit} @@ -19,10 +19,7 @@ export async function GET(req: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json( - { error: 'ai_consent_required' }, - { status: 403 } - ) + return aiConsentForbiddenResponse() } // Get query parameters diff --git a/memento-note/app/api/ai/echo/fusion/route.ts b/memento-note/app/api/ai/echo/fusion/route.ts index a940c49..1ff138a 100644 --- a/memento-note/app/api/ai/echo/fusion/route.ts +++ b/memento-note/app/api/ai/echo/fusion/route.ts @@ -3,7 +3,7 @@ import { auth } from '@/auth' import { getChatProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' import prisma from '@/lib/prisma' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' /** @@ -22,7 +22,7 @@ export async function POST(req: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const body = await req.json() diff --git a/memento-note/app/api/ai/echo/route.ts b/memento-note/app/api/ai/echo/route.ts index 23d6e7a..031a312 100644 --- a/memento-note/app/api/ai/echo/route.ts +++ b/memento-note/app/api/ai/echo/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { memoryEchoService } from '@/lib/ai/services/memory-echo.service' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' /** @@ -21,10 +21,7 @@ export async function GET(req: NextRequest) { // GDPR AI Consent check if (!(await hasUserAiConsent())) { - return NextResponse.json( - { error: 'ai_consent_required' }, - { status: 403 } - ) + return aiConsentForbiddenResponse() } // Get next insight (respects frequency limits) diff --git a/memento-note/app/api/ai/enrich-from-resource/route.ts b/memento-note/app/api/ai/enrich-from-resource/route.ts index fe7fe54..2fef03e 100644 --- a/memento-note/app/api/ai/enrich-from-resource/route.ts +++ b/memento-note/app/api/ai/enrich-from-resource/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { getSystemConfig } from '@/lib/config' import { getTagsProvider } from '@/lib/ai/factory' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' export async function POST(request: NextRequest) { @@ -13,7 +13,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const { existingContent, resourceText, mode, language, format } = await request.json() diff --git a/memento-note/app/api/ai/notebook-slides/route.ts b/memento-note/app/api/ai/notebook-slides/route.ts new file mode 100644 index 0000000..ed93bc7 --- /dev/null +++ b/memento-note/app/api/ai/notebook-slides/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' +import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' +import { generateSlideDeck } from '@/lib/ai/services/slide-deck-generator.service' + +export async function POST(request: NextRequest) { + try { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + if (!(await hasUserAiConsent())) { + return aiConsentForbiddenResponse() + } + const userId = session.user.id + + const body = await request.json() + const { notebookId, theme, purpose, audience, slideCount, language } = body + + if (!notebookId || typeof notebookId !== 'string') { + return NextResponse.json({ error: 'notebookId is required' }, { status: 400 }) + } + + const intent = + purpose || audience || slideCount + ? { + purpose: purpose || 'auto', + audience: audience || 'auto', + slideCount: typeof slideCount === 'number' ? slideCount : undefined, + template: 'auto' as const, + } + : null + + const result = await withAiQuota( + userId, + 'slide_generate', + () => + generateSlideDeck({ + userId, + sourceNotebookId: notebookId, + theme: theme && theme !== 'auto' ? theme : null, + lang: language === 'en' ? 'en' : 'fr', + intent, + }), + { lane: 'chat' }, + ) + + if (!result.success) { + return NextResponse.json({ error: result.error || 'Failed to generate slides' }, { status: 500 }) + } + + return NextResponse.json(result) + } catch (error: unknown) { + const quotaResp = handleQuotaHttpError(error) + if (quotaResp) return quotaResp + console.error('[NotebookSlides] Error:', error) + const message = error instanceof Error ? error.message : 'Failed to generate slides' + return NextResponse.json({ error: message }, { status: 500 }) + } +} diff --git a/memento-note/app/api/ai/notebook-summary/route.ts b/memento-note/app/api/ai/notebook-summary/route.ts index 3d391cb..d0baadf 100644 --- a/memento-note/app/api/ai/notebook-summary/route.ts +++ b/memento-note/app/api/ai/notebook-summary/route.ts @@ -2,7 +2,7 @@ import { rateLimit } from '@/lib/rate-limit' import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { notebookSummaryService } from '@/lib/ai/services' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenJson } from '@/lib/consent/server-consent' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' /** @@ -20,10 +20,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json( - { success: false, error: 'ai_consent_required' }, - { status: 403 } - ) + return aiConsentForbiddenJson() } const body = await request.json() diff --git a/memento-note/app/api/ai/personas/route.ts b/memento-note/app/api/ai/personas/route.ts index 013b831..fafccb7 100644 --- a/memento-note/app/api/ai/personas/route.ts +++ b/memento-note/app/api/ai/personas/route.ts @@ -3,7 +3,7 @@ import { auth } from '@/auth' import { getSystemConfig } from '@/lib/config' import { getTagsProvider } from '@/lib/ai/factory' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' export type PersonaId = 'engineer' | 'financial' | 'customer' | 'skeptic' | 'optimist' @@ -70,7 +70,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const { content, title, personaId } = await request.json() diff --git a/memento-note/app/api/ai/reformulate/route.ts b/memento-note/app/api/ai/reformulate/route.ts index f42dc1d..c35232e 100644 --- a/memento-note/app/api/ai/reformulate/route.ts +++ b/memento-note/app/api/ai/reformulate/route.ts @@ -3,7 +3,7 @@ import { auth } from '@/auth' import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service' import { getAISettings } from '@/app/actions/ai-settings' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' export async function POST(request: NextRequest) { try { @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } // Respect user's paragraphRefactor toggle (Assistant IA) diff --git a/memento-note/app/api/ai/suggest-charts/route.ts b/memento-note/app/api/ai/suggest-charts/route.ts index 8ee5283..1aaf160 100644 --- a/memento-note/app/api/ai/suggest-charts/route.ts +++ b/memento-note/app/api/ai/suggest-charts/route.ts @@ -5,7 +5,7 @@ import { getSystemConfig } from '@/lib/config' import { prisma } from '@/lib/prisma' import { auth } from '@/auth' import { reserveUsageOrThrow, QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' export const maxDuration = 30 @@ -41,10 +41,7 @@ export async function POST(req: Request) { } if (!(await hasUserAiConsent())) { - return new Response(JSON.stringify({ error: 'ai_consent_required' }), { - status: 403, - headers: { 'Content-Type': 'application/json' }, - }) + return aiConsentForbiddenResponse() } const userId = session.user.id diff --git a/memento-note/app/api/ai/suggest-notebook/route.ts b/memento-note/app/api/ai/suggest-notebook/route.ts index d7c207f..7af940f 100644 --- a/memento-note/app/api/ai/suggest-notebook/route.ts +++ b/memento-note/app/api/ai/suggest-notebook/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { notebookSuggestionService } from '@/lib/ai/services/notebook-suggestion.service' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' export async function POST(req: NextRequest) { @@ -13,7 +13,7 @@ export async function POST(req: NextRequest) { const userId = session.user.id if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const body = await req.json() diff --git a/memento-note/app/api/ai/tags/route.ts b/memento-note/app/api/ai/tags/route.ts index aedbc4f..fb50bde 100644 --- a/memento-note/app/api/ai/tags/route.ts +++ b/memento-note/app/api/ai/tags/route.ts @@ -6,7 +6,7 @@ import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-fo import { getSystemConfig } from '@/lib/config'; import { z } from 'zod'; import { checkEntitlementOrThrow, QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements'; -import { hasUserAiConsent } from '@/lib/consent/server-consent'; +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent'; import { getAISettings } from '@/app/actions/ai-settings'; @@ -25,7 +25,7 @@ export async function POST(req: NextRequest) { // GDPR AI Consent check if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }); + return aiConsentForbiddenResponse(); } const userSettings = await getAISettings(session.user.id); diff --git a/memento-note/app/api/ai/title-suggestions/route.ts b/memento-note/app/api/ai/title-suggestions/route.ts index 77da2d6..34d9056 100644 --- a/memento-note/app/api/ai/title-suggestions/route.ts +++ b/memento-note/app/api/ai/title-suggestions/route.ts @@ -7,7 +7,7 @@ import { getAISettings } from '@/app/actions/ai-settings' import { reserveAiUsageOrThrow } from '@/lib/ai-quota' import { QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements' import { z } from 'zod' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' const requestSchema = z.object({ content: z.string().min(1, "Le contenu ne peut pas être vide"), @@ -37,7 +37,7 @@ export async function POST(req: NextRequest) { // GDPR AI Consent check if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const settings = await getAISettings(session.user.id) diff --git a/memento-note/app/api/ai/transform-markdown/route.ts b/memento-note/app/api/ai/transform-markdown/route.ts index 4574035..441f5cf 100644 --- a/memento-note/app/api/ai/transform-markdown/route.ts +++ b/memento-note/app/api/ai/transform-markdown/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { getChatProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota' export async function POST(request: NextRequest) { @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const { text } = await request.json() diff --git a/memento-note/app/api/ai/translate/route.ts b/memento-note/app/api/ai/translate/route.ts index 61452a2..78b1d94 100644 --- a/memento-note/app/api/ai/translate/route.ts +++ b/memento-note/app/api/ai/translate/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import { getTagsProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' export async function POST(request: NextRequest) { @@ -13,7 +13,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const { text, targetLanguage } = await request.json() diff --git a/memento-note/app/api/chat/route.ts b/memento-note/app/api/chat/route.ts index bab3884..2d060ca 100644 --- a/memento-note/app/api/chat/route.ts +++ b/memento-note/app/api/chat/route.ts @@ -6,7 +6,7 @@ import { getChatProvider } from '@/lib/ai/factory' import { semanticSearchService } from '@/lib/ai/services/semantic-search.service' import { prisma } from '@/lib/prisma' import { auth } from '@/auth' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { loadTranslations, getTranslationValue, SupportedLanguage } from '@/lib/i18n' import { toolRegistry } from '@/lib/ai/tools' import { reserveAiUsageOrThrow, handleQuotaHttpError } from '@/lib/ai-quota' @@ -65,10 +65,7 @@ export async function POST(req: Request) { // GDPR AI Consent check if (!(await hasUserAiConsent())) { - return new Response(JSON.stringify({ error: 'ai_consent_required' }), { - status: 403, - headers: { 'Content-Type': 'application/json' }, - }) + return aiConsentForbiddenResponse() } // 1.5 Quota check (per-provider BYOK bypass — only when BYOK will be used for resolved provider) diff --git a/memento-note/app/api/flashcards/generate/route.ts b/memento-note/app/api/flashcards/generate/route.ts index c9a612a..6c1dcb9 100644 --- a/memento-note/app/api/flashcards/generate/route.ts +++ b/memento-note/app/api/flashcards/generate/route.ts @@ -3,7 +3,7 @@ import { auth } from '@/auth' import prisma from '@/lib/prisma' import { getAISettings } from '@/app/actions/ai-settings' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { generateFlashcardsFromNote, type FlashcardStyle } from '@/lib/flashcards/generate-flashcards' import { stripHtmlToText } from '@/lib/flashcards/deck-utils' @@ -15,7 +15,7 @@ export async function POST(request: NextRequest) { } if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } const userSettings = await getAISettings(session.user.id) diff --git a/memento-note/app/api/notes/publish/route.ts b/memento-note/app/api/notes/publish/route.ts index 5ecaeaa..57e32bd 100644 --- a/memento-note/app/api/notes/publish/route.ts +++ b/memento-note/app/api/notes/publish/route.ts @@ -4,7 +4,7 @@ import prisma from '@/lib/prisma' import { contentModerationService, type ModerationResult } from '@/lib/ai/services/content-moderation.service' import { publishEnhanceService } from '@/lib/ai/services/publish-enhance.service' import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements' -import { hasUserAiConsent } from '@/lib/consent/server-consent' +import { hasUserAiConsent, aiConsentForbiddenResponse } from '@/lib/consent/server-consent' import { isPublishTemplateId } from '@/lib/publish/types' import { computePublishedSourceHash, renderPublishedTemplate, renderRewrittenTemplate } from '@/lib/publish/template-render' @@ -115,7 +115,7 @@ export async function POST(request: NextRequest) { if (publishMode === 'ai') { if (!(await hasUserAiConsent())) { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) + return aiConsentForbiddenResponse() } if (!template || !isPublishTemplateId(template)) { return NextResponse.json({ error: 'Invalid template' }, { status: 400 }) diff --git a/memento-note/components/agents/agent-card.tsx b/memento-note/components/agents/agent-card.tsx index e47a6ff..11fb750 100644 --- a/memento-note/components/agents/agent-card.tsx +++ b/memento-note/components/agents/agent-card.tsx @@ -128,7 +128,7 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps if (pollRef.current) clearInterval(pollRef.current) pollRef.current = null setIsRunning(false) - toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { + toast.error(t('agents.toasts.runError', { error: data.errorKey ? t(data.errorKey) : (data.error || t('agents.toasts.runFailed')) }), { id: toastId, description: '' // Clear the loading description }) diff --git a/memento-note/components/auto-label-suggestion-dialog.tsx b/memento-note/components/auto-label-suggestion-dialog.tsx index c5f0330..d3d597e 100644 --- a/memento-note/components/auto-label-suggestion-dialog.tsx +++ b/memento-note/components/auto-label-suggestion-dialog.tsx @@ -122,7 +122,7 @@ export function AutoLabelSuggestionDialog({ onLabelsCreated() onOpenChange(false) } else { - toast.error(data.error || t('ai.autoLabels.error')) + toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.autoLabels.error'))) } } catch (error) { console.error('Failed to create labels:', error) diff --git a/memento-note/components/batch-organization-dialog.tsx b/memento-note/components/batch-organization-dialog.tsx index 455e3ed..8e4daf4 100644 --- a/memento-note/components/batch-organization-dialog.tsx +++ b/memento-note/components/batch-organization-dialog.tsx @@ -149,7 +149,7 @@ export function BatchOrganizationDialog({ onNotesMoved() onOpenChange(false) } else { - toast.error(data.error || t('ai.batchOrganization.applyFailed')) + toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.batchOrganization.applyFailed'))) } } catch (error) { console.error('Failed to apply organization plan:', error) diff --git a/memento-note/components/contextual-ai-chat.tsx b/memento-note/components/contextual-ai-chat.tsx index 47f6761..65df506 100644 --- a/memento-note/components/contextual-ai-chat.tsx +++ b/memento-note/components/contextual-ai-chat.tsx @@ -345,7 +345,7 @@ export function ContextualAIChat({ return } const data = await res.json() - if (!res.ok) throw new Error(data.error || t('ai.genericError')) + if (!res.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.genericError'))) const descs = data.descriptions || [] let resultText = descs.map((d: any) => noteImages.length > 1 ? `**Image ${d.index + 1}:** ${d.description}` : d.description @@ -381,7 +381,7 @@ export function ContextualAIChat({ return } const data = await res.json() - if (!res.ok) throw new Error(data.error || t('ai.genericError')) + if (!res.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.genericError'))) const result = data[action.resultKey] || '' setActionPreview({ label: t(action.i18nKey), text: result, asRichText: action.id === 'toRichText' }) } catch (e: any) { @@ -457,7 +457,7 @@ export function ContextualAIChat({ }) const data = await res.json() if (!res.ok || !data.success) { - mToast.error(data.error || t('ai.errorShort'), { id: toastId }) + mToast.error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.errorShort')), { id: toastId }) setGenerateLoading(null) return } @@ -577,7 +577,7 @@ export function ContextualAIChat({ }), }) const data = await res.json() - if (!res.ok) throw new Error(data.error || t('ai.resource.enrichError')) + if (!res.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.resource.enrichError'))) setResourcePreview({ text: data.enrichedContent, source: resourceMode }) } catch (e: any) { mToast.error(e.message || t('ai.resource.enrichError')) @@ -622,7 +622,7 @@ export function ContextualAIChat({ }), }) const data = await res.json() - if (!res.ok) throw new Error(data.error || t('ai.genericError')) + if (!res.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('ai.genericError'))) setResourcePreview({ text: data.enrichedContent, source: mode }) } catch (e: any) { mToast.error(e.message || t('ai.resource.enrichErrorShort')) diff --git a/memento-note/components/dashboard-view.tsx b/memento-note/components/dashboard-view.tsx index c6a6876..176a431 100644 --- a/memento-note/components/dashboard-view.tsx +++ b/memento-note/components/dashboard-view.tsx @@ -519,7 +519,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) { try { const res = await fetch('/api/ai/echo') const json = await res.json() - if (res.status === 403 && json.error === 'ai_consent_required') { await requestAiConsent(); return } + if (res.status === 403 && json.code === 'ai_consent_required') { await requestAiConsent(); return } if (!res.ok) throw new Error(json.error) if (json.insight) { await reloadBriefingAndPaths() diff --git a/memento-note/components/flashcards/flashcard-generate-dialog.tsx b/memento-note/components/flashcards/flashcard-generate-dialog.tsx index 732b662..e34a2f7 100644 --- a/memento-note/components/flashcards/flashcard-generate-dialog.tsx +++ b/memento-note/components/flashcards/flashcard-generate-dialog.tsx @@ -64,11 +64,7 @@ export function FlashcardGenerateDialog({ }) const data = await res.json() if (!res.ok) { - if (data.errorKey) { - toast.error(t(data.errorKey) || data.error) - } else { - toast.error(data.error || t('flashcards.generateFailed')) - } + toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('flashcards.generateFailed'))) return } setCards(data.cards || []) diff --git a/memento-note/components/flashcards/revision-view.tsx b/memento-note/components/flashcards/revision-view.tsx index 58839ce..ed61cf7 100644 --- a/memento-note/components/flashcards/revision-view.tsx +++ b/memento-note/components/flashcards/revision-view.tsx @@ -353,7 +353,7 @@ export function RevisionView() { const res = await fetch(`/api/flashcards/decks/${deckId}`) const data = await res.json() if (!res.ok) { - toast.error(data.error || t('flashcards.loadDeckFailed')) + toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('flashcards.loadDeckFailed'))) return } setActiveDeckId(deckId) @@ -432,7 +432,7 @@ export function RevisionView() { const res = await fetch(`/api/flashcards/decks/${deckId}`) const data = await res.json() if (!res.ok) { - toast.error(data.error || t('flashcards.loadDeckFailed')) + toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('flashcards.loadDeckFailed'))) setExpandedDeckId(null) return } diff --git a/memento-note/components/fusion-modal.tsx b/memento-note/components/fusion-modal.tsx index b0cbf06..2adccdc 100644 --- a/memento-note/components/fusion-modal.tsx +++ b/memento-note/components/fusion-modal.tsx @@ -69,7 +69,7 @@ export function FusionModal({ const data = await res.json() if (!res.ok) { - throw new Error(data.error || 'Failed to generate fusion') + throw new Error(data.errorKey ? t(data.errorKey) : (data.error || 'Failed to generate fusion')) } if (!data.fusedNote) { diff --git a/memento-note/components/home-client.tsx b/memento-note/components/home-client.tsx index cd2020d..8d7c5fc 100644 --- a/memento-note/components/home-client.tsx +++ b/memento-note/components/home-client.tsx @@ -20,7 +20,7 @@ import { import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast' import { Button } from '@/components/ui/button' -import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, ChevronDown, Tag as TagIcon, X, Menu, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload, Globe } from 'lucide-react' +import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, ChevronDown, Tag as TagIcon, X, Menu, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload, Globe, Presentation } from 'lucide-react' import { emitNoteChange, NOTE_CHANGE_EVENT, type NoteChangeEvent } from '@/lib/note-change-sync' import { useReminderCheck } from '@/hooks/use-reminder-check' import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion' @@ -67,6 +67,10 @@ const NotebookSiteDialog = dynamic( () => import('@/components/wizard/notebook-site-dialog').then(m => ({ default: m.NotebookSiteDialog })), { ssr: false } ) +const NotebookSlidesDialog = dynamic( + () => import('@/components/wizard/notebook-slides-dialog').then(m => ({ default: m.NotebookSlidesDialog })), + { ssr: false } +) const StructuredViewsIntro = dynamic( () => import('@/components/structured-views/structured-views-intro').then(m => ({ default: m.StructuredViewsIntro })), { ssr: false } @@ -151,6 +155,7 @@ export function HomeClient({ const [isEnablingStructured, setIsEnablingStructured] = useState(false) const [showStructuredWizard, setShowStructuredWizard] = useState(false) const [showNotebookSite, setShowNotebookSite] = useState(false) + const [showNotebookSlides, setShowNotebookSlides] = useState(false) const [aiMenuOpen, setAiMenuOpen] = useState(false) const aiMenuRef = useRef(null) const [showStudyPlanner, setShowStudyPlanner] = useState(false) @@ -179,7 +184,7 @@ export function HomeClient({ toast.success(`${data.created} notes importées !`) window.location.reload() } else { - toast.error(data.error || 'Erreur') + toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur')) } } input.click() @@ -1136,6 +1141,11 @@ export function HomeClient({ label: t('notebookSite.shortTitle') || 'Site web', action: () => { setShowNotebookSite(true); setAiMenuOpen(false) }, }, + { + icon: , + label: t('notebook.slides') || 'Présentation', + action: () => { setShowNotebookSlides(true); setAiMenuOpen(false) }, + }, ].map((item, i) => ( + + +
+ {!loading && ( + <> +

+ {t('notebook.slidesDescription') || `Génère une présentation à partir des notes du carnet « ${notebookName} ».`} +

+ +
+ + {t('ai.generate.theme') || 'Thème'} + + +
+ +
+ + {t('ai.generate.purpose') || 'Type de deck'} + + +
+ + + + )} + + {loading && ( +
+
+
+
+
+
+ + {t(STATUS_KEYS[statusIdx])} + + + {Math.round(progress)}% + +
+
+ +
+ {STATUS_KEYS.map((_, i) => ( +
+ ))} +
+ +

+ {t('notebook.slidesGeneratingHint') || 'Outline + rédaction des slides'} +

+
+ )} +
+
+
+ ) +} diff --git a/memento-note/lib/consent/server-consent.ts b/memento-note/lib/consent/server-consent.ts index bf7513a..2341d8e 100644 --- a/memento-note/lib/consent/server-consent.ts +++ b/memento-note/lib/consent/server-consent.ts @@ -25,6 +25,19 @@ export async function hasUserAiConsent(): Promise { return settings?.aiProcessingConsent ?? false } -export function aiConsentForbiddenResponse() { - return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) +const DEFAULT_AI_CONSENT_MESSAGE = 'AI processing consent is required to use this feature.' +const AI_CONSENT_ERROR_KEY = 'consent.ai.requiredToast' + +export function aiConsentForbiddenResponse(message?: string) { + return NextResponse.json( + { error: message ?? DEFAULT_AI_CONSENT_MESSAGE, code: 'ai_consent_required', errorKey: AI_CONSENT_ERROR_KEY }, + { status: 403 } + ) +} + +export function aiConsentForbiddenJson(message?: string) { + return NextResponse.json( + { success: false, error: message ?? DEFAULT_AI_CONSENT_MESSAGE, code: 'ai_consent_required', errorKey: AI_CONSENT_ERROR_KEY }, + { status: 403 } + ) } diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index 04e1f78..2283f15 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -3806,7 +3806,8 @@ "revoked": "Consent not granted", "revokedToast": "AI processing consent successfully revoked.", "complianceBadge": "GDPR compliance", - "auditFailed": "Could not record your consent. Please try again." + "auditFailed": "Could not record your consent. Please try again.", + "requiredToast": "Enable AI processing in Settings → AI to use this feature." } }, "account": { diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index 50ff2aa..99007fb 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -3812,7 +3812,8 @@ "revoked": "Consentement non accordé", "revokedToast": "Consentement IA révoqué avec succès.", "complianceBadge": "Conformité RGPD", - "auditFailed": "Impossible d'enregistrer votre consentement. Réessayez." + "auditFailed": "Impossible d'enregistrer votre consentement. Réessayez.", + "requiredToast": "Activez le traitement IA dans Paramètres → IA pour utiliser cette fonctionnalité." } }, "account": {