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>
241 lines
7.6 KiB
TypeScript
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(/ /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,
|
|
})),
|
|
})
|
|
}
|