Files
Momento/memento-note/app/api/briefing/sentiment/route.ts
Antigravity 4fe31ebc99
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m55s
CI / Deploy production (on server) (push) Successful in 36s
fix(quotas): unifier le décompte IA (BYOK, rollback) et combler les fuites
Centralise la réserve via ai-quota, corrige admin unavailable (-1), brancher les routes sans quota et le host-pays brainstorm, avec usage-meter élargi, noms de clusters, MCP et ajustements dashboard/insights.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 20:42:25 +00:00

100 lines
3.5 KiB
TypeScript

import { NextResponse } from 'next/server'
import { auth } from '@/auth'
import prisma from '@/lib/prisma'
import { getChatProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { redis } from '@/lib/redis'
import { detectUserLanguage } from '@/lib/i18n/detect-user-language'
import { withAiQuota, handleQuotaHttpError } from '@/lib/ai-quota'
import { QuotaExceededError } from '@/lib/entitlements'
const CACHE_TTL_SEC = 3600
/**
* GET /api/briefing/sentiment
* Analyzes the emotional tone of recent notes (last 7 days) using LLM.
*/
export async function GET() {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const locale = await detectUserLanguage()
const cacheKey = `briefing:sentiment:${userId}:${locale}`
try {
const cached = await redis.get(cacheKey)
if (cached) return NextResponse.json(JSON.parse(cached))
} catch {}
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
const recentNotes = await prisma.note.findMany({
where: {
userId,
trashedAt: null,
isArchived: false,
updatedAt: { gte: weekAgo },
},
select: { title: true, content: true },
take: 20,
orderBy: { updatedAt: 'desc' },
})
if (recentNotes.length < 3) {
return NextResponse.json({
available: false,
reason: 'Not enough notes this week',
})
}
const snippets = recentNotes.map(n => {
const text = n.content.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
return `${n.title || ''}: ${text.slice(0, 200)}`
}).join('\n---\n')
try {
const config = await getSystemConfig()
const provider = getChatProvider(config)
if (!provider) {
return NextResponse.json({ available: false, reason: 'No AI provider' })
}
const localeLabel = locale === 'fr' ? 'French' : locale === 'en' ? 'English' : locale
const prompt = `Analyze the emotional patterns in these notes from the past week. Return ONLY valid JSON (no markdown, no code fences).
Write "summary" and "topTopic" in ${localeLabel} (locale code: ${locale}). Keep dominantEmotion keys in English as listed.
{"dominantEmotion":"focused|curious|enthusiastic|frustrated|calm|anxious|creative|reflective","sentimentScore":number from -1 to 1,"emotions":{"focused":number,"curious":number,"enthusiastic":number,"frustrated":number,"calm":number,"anxious":number,"creative":number,"reflective":number},"summary":"one sentence describing the emotional pattern","topTopic":"most discussed topic"}
Notes:
${snippets.slice(0, 3000)}`
const result = await withAiQuota(
userId,
'reformulate',
() => provider.generateText(prompt),
{ lane: 'chat' },
)
const jsonMatch = result.match(/\{[\s\S]*\}/)
if (!jsonMatch) {
return NextResponse.json({ available: false, reason: 'Parse error' })
}
const parsed = JSON.parse(jsonMatch[0])
const payload = { available: true, ...parsed }
try { await redis.setex(cacheKey, CACHE_TTL_SEC, JSON.stringify(payload)) } catch {}
return NextResponse.json(payload)
} catch (error) {
const quotaResp = handleQuotaHttpError(error)
if (quotaResp) return quotaResp
if (error instanceof QuotaExceededError) {
return NextResponse.json(error.toJSON(), { status: 402 })
}
console.error('[briefing/sentiment]', error)
return NextResponse.json({ available: false, reason: 'Analysis failed' })
}
}