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>
This commit is contained in:
156
memento-note/lib/dashboard/paths-fast.ts
Normal file
156
memento-note/lib/dashboard/paths-fast.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { DashboardPath } from '@/lib/dashboard/path-types'
|
||||
|
||||
function excerpt(text: string, max = 120): string {
|
||||
const plain = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
if (plain.length <= max) return plain
|
||||
return `${plain.slice(0, max)}…`
|
||||
}
|
||||
|
||||
export interface BriefingPathsInput {
|
||||
recentNotes: Array<{
|
||||
id: string
|
||||
title: string | null
|
||||
content: string
|
||||
notebookId: string | 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
|
||||
}>
|
||||
}
|
||||
|
||||
/** Pistes instantanées à partir du briefing déjà chargé — sans requête lourde. */
|
||||
export function buildFastPathsFromBriefing(input: BriefingPathsInput): DashboardPath[] {
|
||||
const paths: DashboardPath[] = []
|
||||
const focus = input.recentNotes[0]
|
||||
|
||||
if (focus) {
|
||||
paths.push({
|
||||
id: `continue-${focus.id}`,
|
||||
type: 'continue',
|
||||
priority: 100,
|
||||
title: focus.title || 'Untitled',
|
||||
description: excerpt(focus.content, 140),
|
||||
actionKey: 'continue',
|
||||
noteId: focus.id,
|
||||
notebookId: focus.notebookId ?? undefined,
|
||||
})
|
||||
|
||||
for (const ins of input.insights
|
||||
.filter(i => i.note1.id === focus.id || i.note2.id === focus.id)
|
||||
.slice(0, 2)) {
|
||||
const other = ins.note1.id === focus.id ? ins.note2 : ins.note1
|
||||
paths.push({
|
||||
id: `connect-${focus.id}-${other.id}`,
|
||||
type: 'connect',
|
||||
priority: 88,
|
||||
title: other.title || 'Untitled',
|
||||
description: ins.insight,
|
||||
actionKey: 'compare',
|
||||
noteId: focus.id,
|
||||
note2Id: other.id,
|
||||
notebookId: focus.notebookId ?? undefined,
|
||||
score: Math.round(ins.score * 100),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const freshInsight = input.insights.find(i => !i.viewed)
|
||||
if (freshInsight) {
|
||||
paths.push({
|
||||
id: `resurface-${freshInsight.id}`,
|
||||
type: 'resurface',
|
||||
priority: 85,
|
||||
title: `${freshInsight.note1.title || '…'} ↔ ${freshInsight.note2.title || '…'}`,
|
||||
description: freshInsight.insight,
|
||||
actionKey: 'openInsight',
|
||||
insightId: freshInsight.id,
|
||||
noteId: freshInsight.note1.id,
|
||||
note2Id: freshInsight.note2.id,
|
||||
score: Math.round(freshInsight.score * 100),
|
||||
})
|
||||
}
|
||||
|
||||
for (const bridge of input.bridgeSuggestions.slice(0, 2)) {
|
||||
paths.push({
|
||||
id: `bridge-${bridge.clusterAId}-${bridge.clusterBId}`,
|
||||
type: 'bridge',
|
||||
priority: 70,
|
||||
title: bridge.suggestedTitle,
|
||||
description: bridge.justification || bridge.suggestedContent.slice(0, 140),
|
||||
actionKey: 'createBridge',
|
||||
clusterAId: bridge.clusterAId,
|
||||
clusterBId: bridge.clusterBId,
|
||||
notebookId: focus?.notebookId ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
for (const agent of input.agentSuggestions.slice(0, 2)) {
|
||||
paths.push({
|
||||
id: `research-${agent.id}`,
|
||||
type: 'research',
|
||||
priority: 65,
|
||||
title: agent.topic,
|
||||
description: agent.reason,
|
||||
actionKey: 'createAgent',
|
||||
agentSuggestionId: agent.id,
|
||||
notebookId: focus?.notebookId ?? undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (input.inboxCount > 0) {
|
||||
paths.push({
|
||||
id: 'organize-inbox',
|
||||
type: 'organize',
|
||||
priority: 60,
|
||||
title: `${input.inboxCount} notes`,
|
||||
description: 'inbox',
|
||||
actionKey: 'organizeInbox',
|
||||
})
|
||||
}
|
||||
|
||||
if (input.dueFlashcards > 0) {
|
||||
paths.push({
|
||||
id: 'review-flashcards',
|
||||
type: 'review',
|
||||
priority: 55,
|
||||
title: `${input.dueFlashcards}`,
|
||||
description: 'flashcards',
|
||||
actionKey: 'reviewCards',
|
||||
})
|
||||
}
|
||||
|
||||
paths.push({
|
||||
id: 'daily-journal',
|
||||
type: 'daily',
|
||||
priority: 40,
|
||||
title: new Date().toISOString().slice(0, 10),
|
||||
description: 'daily',
|
||||
actionKey: 'openDaily',
|
||||
})
|
||||
|
||||
return paths.sort((a, b) => b.priority - a.priority).slice(0, 8)
|
||||
}
|
||||
Reference in New Issue
Block a user