Files
Momento/memento-note/app/api/briefing/route.ts
Antigravity 30da592ba2
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m3s
CI / Deploy production (on server) (push) Successful in 23s
feat(dashboard): tableau de bord Second Brain configurable avec chargement progressif
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>
2026-07-14 16:50:53 +00:00

241 lines
7.6 KiB
TypeScript

import { NextResponse } from 'next/server'
import { auth } from '@/auth'
import prisma from '@/lib/prisma'
import { agentSuggestionService } from '@/lib/ai/services/agent-suggestion.service'
import { bridgeNotesService } from '@/lib/ai/services/bridge-notes.service'
import { gmailScannerService } from '@/lib/integrations/gmail-scanner.service'
import { hasUserAiConsent } from '@/lib/consent/server-consent'
import { getSystemConfig } from '@/lib/config'
import { getChatProvider } from '@/lib/ai/factory'
function excerptNoteContent(content: string | null | undefined, max = 200): string {
if (!content) return ''
const plain = content.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
if (plain.length <= max) return plain
return `${plain.slice(0, max)}`
}
async function fetchWritingActivity(userId: string, since: Date) {
const rows = await prisma.$queryRaw<Array<{ date: string; count: number }>>`
SELECT to_char("updatedAt" AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS date,
COUNT(*)::int AS count
FROM "Note"
WHERE "userId" = ${userId}
AND "trashedAt" IS NULL
AND "updatedAt" >= ${since}
GROUP BY 1
ORDER BY 1 ASC
`
return rows.map(r => ({ date: r.date, count: Number(r.count) }))
}
/**
* GET /api/briefing — agrégat rapide pour le tableau de bord.
* Pas de génération IA synchrone ; les pistes sont dans GET /api/briefing/paths.
*/
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 now = new Date()
const ninetyDaysAgo = new Date(now)
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)
const [aiConsent, aiSettings, systemConfig] = await Promise.all([
hasUserAiConsent(),
prisma.userAISettings.findUnique({
where: { userId },
select: { memoryEcho: true },
}),
getSystemConfig(),
])
const aiProviderReady = !!getChatProvider(systemConfig)
const memoryEchoEnabled = aiSettings?.memoryEcho ?? true
const aiActive = aiConsent && aiProviderReady && memoryEchoEnabled
const [
recentNotes,
inboxCount,
dueFlashcards,
upcomingReminders,
unviewedInsights,
recentAgentActions,
pinnedNotes,
writingActivity,
gmailStatus,
agentSuggestions,
bridgeSuggestions,
] = await Promise.all([
prisma.note.findMany({
where: { userId, trashedAt: null, isArchived: false },
select: {
id: true, title: true, content: true, color: true,
notebookId: true, updatedAt: true, createdAt: true,
},
orderBy: { updatedAt: 'desc' },
take: 6,
}),
prisma.note.count({
where: { userId, notebookId: null, isArchived: false, trashedAt: null },
}),
prisma.flashcard.count({
where: { deck: { userId }, nextReviewAt: { lte: now } },
}),
prisma.note.findMany({
where: {
userId, reminder: { gte: now }, isReminderDone: false, trashedAt: null,
},
select: { id: true, title: true, reminder: true, notebookId: true },
orderBy: { reminder: 'asc' },
take: 5,
}),
prisma.memoryEchoInsight.findMany({
where: { userId, viewed: false, dismissed: false },
select: {
id: true, insight: true, similarityScore: true,
insightDate: true, viewed: true,
note1: { select: { id: true, title: true, content: true } },
note2: { select: { id: true, title: true, content: true } },
},
orderBy: { insightDate: 'desc' },
take: 5,
}).catch(() => []),
prisma.agentAction.findMany({
where: {
agent: { userId },
status: 'completed',
createdAt: { gte: new Date(now.getTime() - 48 * 60 * 60 * 1000) },
},
select: {
id: true, result: true, createdAt: true,
agent: { select: { id: true, name: true, type: true } },
},
orderBy: { createdAt: 'desc' },
take: 3,
}).catch(() => []),
prisma.note.findMany({
where: { userId, isPinned: true, trashedAt: null, isArchived: false },
select: { id: true, title: true, notebookId: true, updatedAt: true },
orderBy: { updatedAt: 'desc' },
take: 5,
}),
fetchWritingActivity(userId, ninetyDaysAgo).catch(() => []),
gmailScannerService.getStatus(userId).catch(() => ({
connected: false,
recentCaptures: 0,
})),
agentSuggestionService.getPending(userId, 3).catch(() => []),
bridgeNotesService.getBridgeSuggestions(userId).catch(() => []),
])
let insightRows = unviewedInsights
if (insightRows.length < 5) {
const viewedRecent = await prisma.memoryEchoInsight.findMany({
where: {
userId,
dismissed: false,
viewed: true,
id: { notIn: insightRows.map(i => i.id) },
},
select: {
id: true, insight: true, similarityScore: true,
insightDate: true, viewed: true,
note1: { select: { id: true, title: true, content: true } },
note2: { select: { id: true, title: true, content: true } },
},
orderBy: { insightDate: 'desc' },
take: 5 - insightRows.length,
}).catch(() => [])
insightRows = [...insightRows, ...viewedRecent]
}
const notebookIds = [...new Set(recentNotes.map(n => n.notebookId).filter(Boolean))] as string[]
const notebooks = notebookIds.length > 0
? await prisma.notebook.findMany({
where: { id: { in: notebookIds } },
select: { id: true, name: true, color: true, icon: true },
})
: []
const notebookMap = new Map(notebooks.map(n => [n.id, n]))
return NextResponse.json({
recentNotes: recentNotes.map(n => ({
...n,
notebook: n.notebookId ? notebookMap.get(n.notebookId) || null : null,
})),
inboxCount,
dueFlashcards,
upcomingReminders: upcomingReminders.map(r => ({
id: r.id,
title: r.title,
reminder: r.reminder?.toISOString() || null,
notebookId: r.notebookId,
})),
insights: insightRows.map(i => ({
id: i.id,
insight: i.insight,
score: i.similarityScore,
date: i.insightDate.toISOString(),
viewed: i.viewed,
note1: { id: i.note1.id, title: i.note1.title },
note2: { id: i.note2.id, title: i.note2.title },
note1Excerpt: excerptNoteContent(i.note1.content),
note2Excerpt: excerptNoteContent(i.note2.content),
})),
bridgeSuggestions: bridgeSuggestions.slice(0, 3).map(s => ({
clusterAId: s.clusterAId,
clusterBId: s.clusterBId,
clusterAName: s.clusterAName,
clusterBName: s.clusterBName,
suggestedTitle: s.suggestedTitle,
suggestedContent: s.suggestedContent,
justification: s.justification,
})),
ai: {
consent: aiConsent,
memoryEchoEnabled,
providerReady: aiProviderReady,
active: aiActive,
},
agentActions: recentAgentActions.map(a => ({
id: a.id,
agentName: a.agent?.name || 'Agent',
agentType: a.agent?.type || 'custom',
result: a.result ? String(a.result).slice(0, 200) : null,
createdAt: a.createdAt.toISOString(),
})),
gmail: gmailStatus,
pinnedNotes: pinnedNotes.map(n => ({
id: n.id,
title: n.title,
notebookId: n.notebookId,
updatedAt: n.updatedAt.toISOString(),
})),
writingActivity,
agentSuggestions: agentSuggestions.map(s => ({
id: s.id,
topic: s.topic,
reason: s.reason,
suggestedType: s.suggestedType,
suggestedFrequency: s.suggestedFrequency,
relatedNoteCount: (() => {
try { return JSON.parse(s.relatedNoteIds).length } catch { return 0 }
})(),
clusterId: s.clusterId,
})),
})
}