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>
205 lines
6.3 KiB
TypeScript
205 lines
6.3 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 { buildDashboardPaths, buildOpenLoops } from '@/lib/dashboard/paths.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)}…`
|
|
}
|
|
|
|
interface PathsBriefingSnapshot {
|
|
recentNote?: {
|
|
id: string
|
|
title: string | null
|
|
content: string
|
|
notebookId: string | null
|
|
} | null
|
|
inboxCount?: number
|
|
dueFlashcards?: number
|
|
insights?: Array<{
|
|
id: string
|
|
insight: string
|
|
score: number
|
|
viewed: boolean
|
|
note1: { id: string; title: string | null }
|
|
note2: { id: string; title: string | null }
|
|
note1Excerpt?: string
|
|
note2Excerpt?: string
|
|
}>
|
|
bridgeSuggestions?: Array<{
|
|
clusterAId: number
|
|
clusterBId: number
|
|
clusterAName: string
|
|
clusterBName: string
|
|
suggestedTitle: string
|
|
suggestedContent: string
|
|
justification: string
|
|
}>
|
|
agentSuggestions?: Array<{
|
|
id: string
|
|
topic: string
|
|
reason: string
|
|
clusterId: number | null
|
|
}>
|
|
includeOpenLoops?: boolean
|
|
}
|
|
|
|
async function resolvePathsPayload(userId: string, snapshot?: PathsBriefingSnapshot | null) {
|
|
const now = new Date()
|
|
const hasSnapshot = !!snapshot?.recentNote
|
|
|
|
const needsAgents = !snapshot?.agentSuggestions
|
|
const needsBridges = !snapshot?.bridgeSuggestions
|
|
const needsInsights = !snapshot?.insights
|
|
const needsCounts = snapshot?.inboxCount === undefined || snapshot?.dueFlashcards === undefined
|
|
const needsFocusNote = !snapshot?.recentNote
|
|
|
|
const [
|
|
aiConsent,
|
|
aiSettings,
|
|
systemConfig,
|
|
recentNotes,
|
|
inboxCount,
|
|
dueFlashcards,
|
|
unviewedInsights,
|
|
agentSuggestions,
|
|
bridgeSuggestions,
|
|
] = await Promise.all([
|
|
hasUserAiConsent(),
|
|
prisma.userAISettings.findUnique({
|
|
where: { userId },
|
|
select: { memoryEcho: true },
|
|
}),
|
|
getSystemConfig(),
|
|
needsFocusNote
|
|
? prisma.note.findMany({
|
|
where: { userId, trashedAt: null, isArchived: false },
|
|
select: { id: true, title: true, content: true, notebookId: true, updatedAt: true },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 1,
|
|
})
|
|
: Promise.resolve([]),
|
|
needsCounts
|
|
? prisma.note.count({
|
|
where: { userId, notebookId: null, isArchived: false, trashedAt: null },
|
|
})
|
|
: Promise.resolve(snapshot!.inboxCount!),
|
|
needsCounts
|
|
? prisma.flashcard.count({
|
|
where: { deck: { userId }, nextReviewAt: { lte: now } },
|
|
})
|
|
: Promise.resolve(snapshot!.dueFlashcards!),
|
|
needsInsights
|
|
? prisma.memoryEchoInsight.findMany({
|
|
where: { userId, dismissed: false },
|
|
select: {
|
|
id: true, insight: true, similarityScore: 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(() => [])
|
|
: Promise.resolve([]),
|
|
needsAgents
|
|
? agentSuggestionService.getPending(userId, 3).catch(() => [])
|
|
: Promise.resolve([]),
|
|
needsBridges
|
|
? bridgeNotesService.getBridgeSuggestions(userId).catch(() => [])
|
|
: Promise.resolve([]),
|
|
])
|
|
|
|
const aiProviderReady = !!getChatProvider(systemConfig)
|
|
const memoryEchoEnabled = aiSettings?.memoryEcho ?? true
|
|
const aiActive = aiConsent && aiProviderReady && memoryEchoEnabled
|
|
|
|
const focusNotes = hasSnapshot && snapshot?.recentNote
|
|
? [{
|
|
...snapshot.recentNote,
|
|
updatedAt: now,
|
|
}]
|
|
: recentNotes
|
|
|
|
const insights = snapshot?.insights ?? unviewedInsights.map(i => ({
|
|
id: i.id,
|
|
insight: i.insight,
|
|
score: i.similarityScore,
|
|
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),
|
|
}))
|
|
|
|
const resolvedBridges = snapshot?.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,
|
|
}))
|
|
|
|
const resolvedAgents = snapshot?.agentSuggestions
|
|
?? agentSuggestions.map(s => ({
|
|
id: s.id,
|
|
topic: s.topic,
|
|
reason: s.reason,
|
|
clusterId: s.clusterId,
|
|
}))
|
|
|
|
const paths = await buildDashboardPaths({
|
|
userId,
|
|
aiActive,
|
|
recentNotes: focusNotes,
|
|
inboxCount: needsCounts ? inboxCount : snapshot!.inboxCount!,
|
|
dueFlashcards: needsCounts ? dueFlashcards : snapshot!.dueFlashcards!,
|
|
insights,
|
|
bridgeSuggestions: resolvedBridges,
|
|
agentSuggestions: resolvedAgents,
|
|
}).catch(() => [])
|
|
|
|
const openLoops = snapshot?.includeOpenLoops === false
|
|
? []
|
|
: await buildOpenLoops(userId).catch(() => [])
|
|
|
|
return { paths, openLoops }
|
|
}
|
|
|
|
/**
|
|
* GET /api/briefing/paths — pistes enrichies (fallback sans snapshot client).
|
|
*/
|
|
export async function GET() {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const payload = await resolvePathsPayload(session.user.id)
|
|
return NextResponse.json(payload)
|
|
}
|
|
|
|
/**
|
|
* POST /api/briefing/paths — enrichissement avec snapshot briefing (évite re-fetch agents/ponts).
|
|
*/
|
|
export async function POST(request: Request) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const snapshot = await request.json().catch(() => null) as PathsBriefingSnapshot | null
|
|
const payload = await resolvePathsPayload(session.user.id, snapshot)
|
|
return NextResponse.json(payload)
|
|
}
|