fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
1. replaceAll (Find & Replace) — une seule transaction ProseMirror au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés. 2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs qui laissaient un nœud fantôme invisible dans le document. 3. Conversion Markdown → richtext — breaks: true dans marked.parse() Les simple newlines sont maintenant convertis en <br>. + préserve les blocs custom (toggle, callout, math, columns, outline, link-preview) en commentaires HTML lors de l'export MD. 4. emitNoteChange exercices — shape corrigée (type:'created' attend un objet Note, pas noteId/notebookId séparés). 5. Raccourcis clavier sans conflit : Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier) Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets) Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { logAuditEvent, getClientIp } from '@/lib/audit-log'
|
||||
|
||||
type GenerateType = 'slide-generator' | 'excalidraw-generator'
|
||||
@@ -48,7 +48,7 @@ export async function POST(req: NextRequest) {
|
||||
// Quota check — feature key depends on generation type
|
||||
const featureKey = type === 'slide-generator' ? 'slide_generate' : 'excalidraw_generate'
|
||||
try {
|
||||
await checkEntitlementOrThrow(userId, featureKey)
|
||||
await reserveUsageOrThrow(userId, featureKey)
|
||||
} catch (e) {
|
||||
if (e instanceof QuotaExceededError) {
|
||||
return NextResponse.json({ error: e.message }, { status: 402 })
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { exerciseGeneratorService } from '@/lib/ai/services/exercise-generator.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { preprocessMathInHtml } from '@/lib/text/math-preprocess'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -88,7 +88,6 @@ export async function POST(request: NextRequest) {
|
||||
})()
|
||||
}
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json({
|
||||
exercises: createdNotes.map(n => ({ id: n.id, title: n.title })),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { mathFromTextService } from '@/lib/ai/services/math-from-text.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -16,7 +16,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -29,7 +29,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const latex = await mathFromTextService.generate(description)
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json({ latex })
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { notebookWizardService, type WizardProfile } from '@/lib/ai/services/notebook-wizard.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -120,7 +120,6 @@ export async function POST(request: NextRequest) {
|
||||
})()
|
||||
}
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json({
|
||||
notebookId: notebook.id,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { notebookOrganizerService } from '@/lib/ai/services/notebook-organizer.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { syncNoteLabels } from '@/app/actions/notes'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -48,7 +48,6 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const result = await notebookOrganizerService.analyze(notesForAnalysis)
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -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 { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
|
||||
export type PersonaId = 'engineer' | 'financial' | 'customer' | 'skeptic' | 'optimist'
|
||||
@@ -90,7 +90,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Quota check (reuse reformulate quota)
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -114,7 +114,6 @@ export async function POST(request: NextRequest) {
|
||||
const fullPrompt = `${persona.systemPrompt}\n\n---\nNOTE À ANALYSER :\n${plainText}`
|
||||
const result = await provider.generateText(fullPrompt)
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json({
|
||||
personaId: persona.id,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -58,7 +58,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Check quota
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -75,8 +75,6 @@ export async function POST(request: NextRequest) {
|
||||
// Use the ParagraphRefactorService
|
||||
const result = await paragraphRefactorService.refactor(text, mode, format === 'html' ? 'html' : 'markdown', language, writePrompt)
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json({
|
||||
originalText: result.original,
|
||||
reformulatedText: result.refactored,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { studyPlannerService } from '@/lib/ai/services/study-planner.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -17,7 +17,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||
await reserveUsageOrThrow(session.user.id, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -61,7 +61,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
incrementUsageAsync(session.user.id, 'reformulate')
|
||||
|
||||
return NextResponse.json(plan)
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -4,8 +4,7 @@ import { willUseByokForLane } from '@/lib/ai/provider-for-user'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { trackFeatureUsage } from '@/lib/usage-tracker'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
|
||||
export const maxDuration = 30
|
||||
@@ -56,7 +55,7 @@ export async function POST(req: Request) {
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('chat', sysConfigEarly, userId)
|
||||
console.log('[suggest-charts] BYOK:', willUseByok)
|
||||
if (!willUseByok) {
|
||||
await checkEntitlementOrThrow(userId, 'suggest_charts')
|
||||
await reserveUsageOrThrow(userId, 'suggest_charts')
|
||||
console.log('[suggest-charts] Quota OK')
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -243,9 +242,6 @@ Response format (COPY this structure):
|
||||
parsed.hasData = false
|
||||
}
|
||||
|
||||
// Track usage
|
||||
await trackFeatureUsage(userId, 'suggest_charts', 'suggest-charts', 1)
|
||||
|
||||
return Response.json(parsed satisfies SuggestChartsResponse)
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.
|
||||
import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-for-user';
|
||||
import { getSystemConfig } from '@/lib/config';
|
||||
import { z } from 'zod';
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements';
|
||||
import { checkEntitlementOrThrow, QuotaExceededError } from '@/lib/entitlements';
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent';
|
||||
|
||||
import { getAISettings } from '@/app/actions/ai-settings';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-fo
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { auth } from '@/auth'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { z } from 'zod'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
|
||||
@@ -43,18 +43,6 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ suggestions: [] })
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('tags', config, session.user.id);
|
||||
if (!willUseByok) {
|
||||
await checkEntitlementOrThrow(session.user.id, 'auto_title');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json(err.toJSON(), { status: 402 });
|
||||
}
|
||||
console.error('[/api/ai/title-suggestions] Quota check error (fail-open):', err);
|
||||
}
|
||||
const body = await req.json()
|
||||
const { content: rawContent } = requestSchema.parse(body)
|
||||
|
||||
@@ -72,6 +60,17 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('tags', config, session.user.id)
|
||||
if (!willUseByok) {
|
||||
try {
|
||||
await reserveUsageOrThrow(session.user.id, 'auto_title')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json(err.toJSON(), { status: 402 })
|
||||
}
|
||||
console.error('[/api/ai/title-suggestions] Quota check error (fail-open):', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Détecter la langue du contenu (simple détection basée sur les caractères et mots)
|
||||
const isPersian = /[\u0600-\u06FF]/.test(content)
|
||||
@@ -130,13 +129,12 @@ CONTENT_START: ${content.substring(0, 3000)} CONTENT_END
|
||||
|
||||
Réponds SEULEMENT avec un tableau JSON: [{"title": "titre1", "confidence": 0.95}, {"title": "titre2", "confidence": 0.85}, {"title": "titre3", "confidence": 0.75}]`
|
||||
|
||||
const { result: titles, usedByok } = await runLaneWithBillingUser(
|
||||
const { result: titles } = await runLaneWithBillingUser(
|
||||
'tags',
|
||||
config,
|
||||
session.user.id,
|
||||
(provider) => provider.generateTitles(titlePrompt),
|
||||
)
|
||||
if (!usedByok) incrementUsageAsync(session.user.id, 'auto_title')
|
||||
|
||||
// Créer les suggestions
|
||||
const suggestions = titles.map((t: any) => ({
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function POST(req: NextRequest) {
|
||||
const userEmail = session.user.email;
|
||||
|
||||
try {
|
||||
const priceId = resolvePriceId(tier, interval);
|
||||
const priceId = await resolvePriceId(tier, interval);
|
||||
|
||||
const subscription = await prisma.subscription.findUnique({ where: { userId } });
|
||||
let customerId = subscription?.stripeCustomerId ?? undefined;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { auth } from '@/auth';
|
||||
import { getUserInfo, getEffectiveTier } from '@/lib/entitlements';
|
||||
import { stripe } from '@/lib/stripe';
|
||||
import type Stripe from 'stripe';
|
||||
import { priceIdToTier, getDynamicPrices } from '@/lib/billing/stripe-prices';
|
||||
import { priceIdToTier, getDynamicPrices, isBillingEnabled } from '@/lib/billing/stripe-prices';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
const sub = await stripe.subscriptions.retrieve(subId) as any;
|
||||
const priceId = sub.items.data[0].price.id;
|
||||
const tier = priceIdToTier(priceId) || (checkoutSession.metadata?.tier as any) || 'PRO';
|
||||
const tier = (await priceIdToTier(priceId)) || (checkoutSession.metadata?.tier as any) || 'PRO';
|
||||
|
||||
const currentPeriodStartTimestamp =
|
||||
sub.current_period_start ??
|
||||
@@ -75,6 +75,7 @@ export async function GET(req: NextRequest) {
|
||||
const effectiveTier = await getEffectiveTier(userId);
|
||||
const subscription = await prisma.subscription.findUnique({ where: { userId } });
|
||||
const prices = await getDynamicPrices();
|
||||
const billingEnabled = await isBillingEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
tier,
|
||||
@@ -85,6 +86,7 @@ export async function GET(req: NextRequest) {
|
||||
cancelAtPeriodEnd: subscription?.cancelAtPeriodEnd ?? false,
|
||||
hasStripeSubscription: !!subscription?.stripeSubscriptionId,
|
||||
prices,
|
||||
billingEnabled,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[billing/status]', error);
|
||||
|
||||
@@ -6,9 +6,8 @@ import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-fo
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { embeddingService } from '@/lib/ai/services/embedding.service'
|
||||
import {
|
||||
checkEntitlementOrThrow,
|
||||
reserveUsageOrThrow,
|
||||
QuotaExceededError,
|
||||
incrementUsageAsync,
|
||||
} from '@/lib/entitlements'
|
||||
import { logActivity, captureSnapshot } from '@/lib/brainstorm-collab'
|
||||
|
||||
@@ -221,7 +220,7 @@ export async function POST(request: NextRequest) {
|
||||
const config = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('tags', config, userId)
|
||||
if (!willUseByok) {
|
||||
await checkEntitlementOrThrow(userId, 'brainstorm_create')
|
||||
await reserveUsageOrThrow(userId, 'brainstorm_create')
|
||||
}
|
||||
|
||||
const classifiedNotes = await autoContextSearch(userId, seedIdea, contextNoteIds)
|
||||
@@ -233,7 +232,6 @@ export async function POST(request: NextRequest) {
|
||||
userId,
|
||||
(provider) => provider.generateText(prompt),
|
||||
)
|
||||
if (!usedByok) incrementUsageAsync(userId, 'brainstorm_create')
|
||||
|
||||
let ideas: any[]
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { auth } from '@/auth'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { loadTranslations, getTranslationValue, SupportedLanguage } from '@/lib/i18n'
|
||||
import { toolRegistry } from '@/lib/ai/tools'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { ByokUnavailableError } from '@/lib/byok'
|
||||
import { trackFeatureUsage } from '@/lib/usage-tracker'
|
||||
import { readFile } from 'fs/promises'
|
||||
@@ -66,7 +66,7 @@ export async function POST(req: Request) {
|
||||
const sysConfigEarly = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('chat', sysConfigEarly, userId)
|
||||
if (!willUseByok) {
|
||||
await checkEntitlementOrThrow(userId, 'chat')
|
||||
await reserveUsageOrThrow(userId, 'chat')
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
@@ -483,7 +483,6 @@ Focus ONLY on this note unless asked otherwise.`
|
||||
})
|
||||
if (!usedByok) {
|
||||
trackFeatureUsage(userId, 'chat', final.usage?.totalTokens ?? 0)
|
||||
incrementUsageAsync(userId, 'chat')
|
||||
}
|
||||
logAuditEvent({
|
||||
userId,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||
import { generateFlashcardsFromNote, type FlashcardStyle } from '@/lib/flashcards/generate-flashcards'
|
||||
import { stripHtmlToText } from '@/lib/flashcards/deck-utils'
|
||||
@@ -49,7 +49,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(session.user.id, 'ai_flashcard')
|
||||
await reserveUsageOrThrow(session.user.id, 'ai_flashcard')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
const isTierLocked = err.currentQuota === 0
|
||||
@@ -70,7 +70,6 @@ export async function POST(request: NextRequest) {
|
||||
language: note.language || undefined,
|
||||
})
|
||||
|
||||
incrementUsageAsync(session.user.id, 'ai_flashcard')
|
||||
|
||||
return NextResponse.json({ cards, noteId: note.id, style })
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getMobileUserId } from '@/lib/mobile-auth'
|
||||
import { paragraphRefactorService } from '@/lib/ai/services/paragraph-refactor.service'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
const MODE_MAP: Record<string, 'clarify' | 'shorten' | 'improveStyle' | 'fix_grammar'> = {
|
||||
improve: 'improveStyle',
|
||||
@@ -26,7 +26,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!validation.valid) return NextResponse.json({ error: validation.error }, { status: 400 })
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(userId, 'reformulate')
|
||||
await reserveUsageOrThrow(userId, 'reformulate')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json({ error: 'quota_exceeded' }, { status: 402 })
|
||||
@@ -35,7 +35,6 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const result = await paragraphRefactorService.refactor(text, refactorMode, 'markdown', undefined)
|
||||
incrementUsageAsync(userId, 'reformulate')
|
||||
|
||||
return NextResponse.json({ improved: result.refactored, original: result.original })
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getMobileUserId } from '@/lib/mobile-auth'
|
||||
import { runLaneWithBillingUser } from '@/lib/ai/provider-for-user'
|
||||
import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-for-user'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const userId = getMobileUserId(req)
|
||||
@@ -14,25 +14,27 @@ export async function POST(req: NextRequest) {
|
||||
const wordCount = content.split(/\s+/).length
|
||||
if (wordCount < 5) return NextResponse.json({ error: 'Contenu trop court (min 5 mots)' }, { status: 400 })
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(userId, 'auto_title')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json({ error: 'quota_exceeded' }, { status: 402 })
|
||||
const config = await getSystemConfig()
|
||||
const { usedByok: willUseByok } = await willUseByokForLane('tags', config, userId)
|
||||
if (!willUseByok) {
|
||||
try {
|
||||
await reserveUsageOrThrow(userId, 'auto_title')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json({ error: 'quota_exceeded' }, { status: 402 })
|
||||
}
|
||||
throw err
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const prompt = `Génère 3 titres concis pour ce texte. Réponds UNIQUEMENT avec un tableau JSON: [{"title":"titre1"},{"title":"titre2"},{"title":"titre3"}]\n\nTexte: ${content.slice(0, 400)}`
|
||||
|
||||
const { result: titles, usedByok } = await runLaneWithBillingUser(
|
||||
const { result: titles } = await runLaneWithBillingUser(
|
||||
'tags',
|
||||
config,
|
||||
userId,
|
||||
(provider) => provider.generateTitles(prompt),
|
||||
)
|
||||
if (!usedByok) incrementUsageAsync(userId, 'auto_title')
|
||||
|
||||
return NextResponse.json({ suggestions: (titles ?? []).map((t: any) => t.title ?? t) })
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import prisma from '@/lib/prisma'
|
||||
import { getMobileUserId } from '@/lib/mobile-auth'
|
||||
import { generateFlashcardsFromNote, type FlashcardStyle } from '@/lib/flashcards/generate-flashcards'
|
||||
import { stripHtmlToText } from '@/lib/flashcards/deck-utils'
|
||||
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||
import { reserveUsageOrThrow, QuotaExceededError } from '@/lib/entitlements'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const userId = getMobileUserId(req)
|
||||
@@ -29,7 +29,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkEntitlementOrThrow(userId, 'ai_flashcard')
|
||||
await reserveUsageOrThrow(userId, 'ai_flashcard')
|
||||
} catch (err) {
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json({ error: err.currentQuota === 0 ? 'Fonctionnalité non disponible sur votre abonnement' : 'Quota IA atteint' }, { status: 402 })
|
||||
@@ -78,7 +78,6 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
await prisma.flashcardDeck.update({ where: { id: deckId }, data: { updatedAt: new Date() } })
|
||||
incrementUsageAsync(userId, 'ai_flashcard')
|
||||
|
||||
return NextResponse.json({ deckId, count: cards.length, cards })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user