Briefing granulaire, pistes rapides puis enrichissement async, layout persisté v5, suggestions agents, intégration Gmail et navigation sidebar alignée sur /home. Co-authored-by: Cursor <cursoragent@cursor.com>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 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'
|
|
|
|
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(/ /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 provider.generateText(prompt)
|
|
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) {
|
|
console.error('[briefing/sentiment]', error)
|
|
return NextResponse.json({ available: false, reason: 'Analysis failed' })
|
|
}
|
|
}
|