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)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
62
memento-note/app/api/ai/notebook-slides/route.ts
Normal file
62
memento-note/app/api/ai/notebook-slides/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 || [])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<HTMLDivElement>(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: <Presentation size={14} />,
|
||||
label: t('notebook.slides') || 'Présentation',
|
||||
action: () => { setShowNotebookSlides(true); setAiMenuOpen(false) },
|
||||
},
|
||||
].map((item, i) => (
|
||||
<button
|
||||
key={i}
|
||||
@@ -1452,6 +1462,14 @@ export function HomeClient({
|
||||
onClose={() => setShowNotebookSite(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showNotebookSlides && currentNotebook && (
|
||||
<NotebookSlidesDialog
|
||||
notebookId={currentNotebook.id}
|
||||
notebookName={currentNotebook.name}
|
||||
onClose={() => setShowNotebookSlides(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
return
|
||||
}
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || t('ai.titleGenerationError'))
|
||||
throw new Error(errorData.errorKey ? t(errorData.errorKey) : (errorData.error || t('ai.titleGenerationError')))
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
@@ -502,7 +502,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
return
|
||||
}
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || t('ai.reformulationError'))
|
||||
throw new Error(errorData.errorKey ? t(errorData.errorKey) : (errorData.error || t('ai.reformulationError')))
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
@@ -542,7 +542,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
return
|
||||
}
|
||||
const data = await response.json()
|
||||
if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
|
||||
if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.clarifyFailed')))
|
||||
setContentImmediate(data.reformulatedText || data.text)
|
||||
toast.success(t('ai.reformulationApplied'))
|
||||
} catch (error) {
|
||||
@@ -575,7 +575,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
return
|
||||
}
|
||||
const data = await response.json()
|
||||
if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
|
||||
if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.shortenFailed')))
|
||||
setContentImmediate(data.reformulatedText || data.text)
|
||||
toast.success(t('ai.reformulationApplied'))
|
||||
} catch (error) {
|
||||
@@ -608,7 +608,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
return
|
||||
}
|
||||
const data = await response.json()
|
||||
if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
|
||||
if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.improveFailed')))
|
||||
setContentImmediate(data.reformulatedText || data.text)
|
||||
toast.success(t('ai.reformulationApplied'))
|
||||
} catch (error) {
|
||||
@@ -642,7 +642,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
body: JSON.stringify({ text: content })
|
||||
})
|
||||
const data = await response.json()
|
||||
if (!response.ok) throw new Error(data.error || t('notes.transformFailed'))
|
||||
if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.transformFailed')))
|
||||
|
||||
setContentImmediate(data.transformedText)
|
||||
setIsMarkdown(true)
|
||||
|
||||
@@ -330,7 +330,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
duration: 6000,
|
||||
})
|
||||
} else {
|
||||
toast.error(data.error || t('general.error'))
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('general.error')))
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('general.error'))
|
||||
@@ -408,7 +408,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
})
|
||||
} else {
|
||||
toast.dismiss(toastId)
|
||||
toast.error(data.error || t('general.error'))
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('general.error')))
|
||||
}
|
||||
} catch {
|
||||
toast.dismiss(toastId)
|
||||
@@ -437,7 +437,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
setPublishOpen(false)
|
||||
} else {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
toast.error(data.error || t('general.error'))
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('general.error')))
|
||||
}
|
||||
} catch {
|
||||
toast.error(t('general.error'))
|
||||
|
||||
@@ -46,7 +46,7 @@ export function PublishDialog({ open, onClose, noteId, noteTitle, isPublic: init
|
||||
duration: 6000,
|
||||
})
|
||||
} else {
|
||||
toast.error(data.error || 'Erreur')
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur'))
|
||||
}
|
||||
} catch { toast.error('Erreur') }
|
||||
finally { setLoading(false) }
|
||||
|
||||
@@ -68,7 +68,7 @@ export function NotebookSummaryDialog({
|
||||
if (data.success && data.data) {
|
||||
setSummary(data.data)
|
||||
} else {
|
||||
toast.error(data.error || t('notebook.summaryError'))
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || t('notebook.summaryError')))
|
||||
onOpenChange(false)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -124,7 +124,7 @@ export function PersonasPanel({ noteTitle, noteContent }: PersonasPanelProps) {
|
||||
body: JSON.stringify({ content: noteContent, title: noteTitle, personaId }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || t('common.error'))
|
||||
if (!res.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('common.error')))
|
||||
setResults(prev => new Map(prev).set(personaId, data))
|
||||
setExpanded(personaId)
|
||||
} catch {
|
||||
|
||||
@@ -2005,7 +2005,7 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) { toast.error(data.error || 'Erreur'); return }
|
||||
if (!res.ok) { toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur')); return }
|
||||
let html = data.reformulatedText || data.text || ''
|
||||
// Clean up excessive whitespace
|
||||
html = html
|
||||
|
||||
@@ -100,7 +100,7 @@ export function AiNotebookWizard({ onClose, onComplete }: { onClose: () => void;
|
||||
} else if (data.errorKey === 'ai.quotaExceeded') {
|
||||
toast.error(t('ai.quotaExceeded') || 'Quota IA dépassé')
|
||||
} else {
|
||||
toast.error(data.error || 'Erreur')
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur'))
|
||||
}
|
||||
setLoading(false)
|
||||
return
|
||||
|
||||
@@ -100,7 +100,7 @@ export function NotebookSiteDialog({ notebookId, notebookName, onClose }: Notebo
|
||||
body: JSON.stringify({ selectedNoteIds: selected, template, description }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) { toast.error(data.error || 'Erreur'); setStep('selection'); return }
|
||||
if (!res.ok) { toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur')); setStep('selection'); return }
|
||||
setExistingSlug(data.slug)
|
||||
setSiteUrl(data.url)
|
||||
setStep('done')
|
||||
|
||||
237
memento-note/components/wizard/notebook-slides-dialog.tsx
Normal file
237
memento-note/components/wizard/notebook-slides-dialog.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Presentation, X } from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { toast } from 'sonner'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const THEMES = [
|
||||
'auto',
|
||||
'Architectural SaaS',
|
||||
'Midnight Cathedral',
|
||||
'Aurora Borealis',
|
||||
'Tokyo Neon',
|
||||
'Sunlit Gallery',
|
||||
'Clinical Precision',
|
||||
'Venture Pitch',
|
||||
'Forest Floor',
|
||||
'Steel & Glass',
|
||||
'Cyberpunk Terminal',
|
||||
'Editorial Ink',
|
||||
'Coastal Morning',
|
||||
'Paper Studio',
|
||||
]
|
||||
|
||||
const PURPOSES = [
|
||||
{ value: 'auto', key: 'ai.generate.purposeAuto' },
|
||||
{ value: 'course', key: 'ai.generate.purposeCourse' },
|
||||
{ value: 'board', key: 'ai.generate.purposeBoard' },
|
||||
{ value: 'project', key: 'ai.generate.purposeProject' },
|
||||
{ value: 'strategy', key: 'ai.generate.purposeStrategy' },
|
||||
{ value: 'pitch', key: 'ai.generate.purposePitch' },
|
||||
{ value: 'summary', key: 'ai.generate.purposeSummary' },
|
||||
]
|
||||
|
||||
const STATUS_KEYS = [
|
||||
'notebook.slidesStatus1',
|
||||
'notebook.slidesStatus2',
|
||||
'notebook.slidesStatus3',
|
||||
'notebook.slidesStatus4',
|
||||
]
|
||||
|
||||
export function NotebookSlidesDialog({
|
||||
notebookId,
|
||||
notebookName,
|
||||
onClose,
|
||||
}: {
|
||||
notebookId: string
|
||||
notebookName: string
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t, language } = useLanguage()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [theme, setTheme] = useState('auto')
|
||||
const [purpose, setPurpose] = useState('auto')
|
||||
const [progress, setProgress] = useState(0)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval>>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setLoading(true)
|
||||
setProgress(0)
|
||||
|
||||
let elapsed = 0
|
||||
timerRef.current = setInterval(() => {
|
||||
elapsed += 0.6
|
||||
setProgress(() => {
|
||||
const target = 85 * (1 - Math.exp(-elapsed / 15))
|
||||
return Math.min(target, 85)
|
||||
})
|
||||
}, 600)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/ai/notebook-slides', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
notebookId,
|
||||
theme,
|
||||
purpose,
|
||||
language: language === 'fr' ? 'fr' : 'en',
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
setProgress(100)
|
||||
|
||||
if (!res.ok) {
|
||||
toast.error(
|
||||
data.errorKey === 'ai.featureLocked'
|
||||
? (t('ai.featureLocked') || 'Plan requis')
|
||||
: (data.error || 'Erreur'),
|
||||
)
|
||||
} else if (data.success && data.canvasId) {
|
||||
window.dispatchEvent(new Event('ai-usage-changed'))
|
||||
toast.success(t('notebook.slidesReady') || `Présentation générée (${data.slideCount} slides)`)
|
||||
setTimeout(() => {
|
||||
router.push(`/lab?canvas=${data.canvasId}`)
|
||||
onClose()
|
||||
}, 400)
|
||||
} else {
|
||||
toast.error(data.errorKey ? t(data.errorKey) : (data.error || 'Erreur lors de la génération'))
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
toast.error(e.message || 'Erreur')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const statusIdx = Math.min(Math.floor(progress / 22), STATUS_KEYS.length - 1)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[300] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
|
||||
dir="auto"
|
||||
onClick={loading ? undefined : onClose}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-lg rounded-2xl border border-border bg-card shadow-2xl overflow-hidden flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-brand-accent/5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Presentation className="h-5 w-5 text-brand-accent" />
|
||||
<h2 className="text-base font-semibold">
|
||||
{t('notebook.slides') || 'Présentation'}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
className="p-1 rounded-lg hover:bg-muted text-muted-foreground disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-5">
|
||||
{!loading && (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('notebook.slidesDescription') || `Génère une présentation à partir des notes du carnet « ${notebookName} ».`}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-foreground px-1">
|
||||
{t('ai.generate.theme') || 'Thème'}
|
||||
</span>
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
className="w-full bg-card/60 border border-border rounded-lg px-3 py-2 text-sm outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground"
|
||||
>
|
||||
{THEMES.map((th) => (
|
||||
<option key={th} value={th}>
|
||||
{th === 'auto' ? (t('ai.generate.themeAuto') || 'Auto') : th}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-foreground px-1">
|
||||
{t('ai.generate.purpose') || 'Type de deck'}
|
||||
</span>
|
||||
<select
|
||||
value={purpose}
|
||||
onChange={(e) => setPurpose(e.target.value)}
|
||||
className="w-full bg-card/60 border border-border rounded-lg px-3 py-2 text-sm outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground"
|
||||
>
|
||||
{PURPOSES.map((p) => (
|
||||
<option key={p.value} value={p.value}>
|
||||
{t(p.key) || p.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-sm rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90 transition-colors font-medium"
|
||||
>
|
||||
<Presentation className="h-4 w-4" />
|
||||
{t('notebook.slidesGenerate') || 'Générer la présentation'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="space-y-5 py-4">
|
||||
<div className="space-y-2">
|
||||
<div className="w-full h-2.5 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-brand-accent to-brand-accent/70 rounded-full transition-all duration-700 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground animate-pulse">
|
||||
{t(STATUS_KEYS[statusIdx])}
|
||||
</span>
|
||||
<span className="text-xs font-mono font-bold text-brand-accent tabular-nums">
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-2">
|
||||
{STATUS_KEYS.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-1 flex-1 mx-0.5 rounded-full transition-colors duration-300 ${
|
||||
i <= statusIdx ? 'bg-brand-accent' : 'bg-muted'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground/60 text-center">
|
||||
{t('notebook.slidesGeneratingHint') || 'Outline + rédaction des slides'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -25,6 +25,19 @@ export async function hasUserAiConsent(): Promise<boolean> {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user