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>
298 lines
11 KiB
TypeScript
298 lines
11 KiB
TypeScript
export const DASHBOARD_LAYOUT_VERSION = 5 as const
|
|
|
|
/** Widgets visibles dans la mise en page par défaut (réf. prototype / capture utilisateur). */
|
|
export const CANONICAL_VISIBLE_WIDGET_IDS: readonly DashboardWidgetId[] = [
|
|
'capture',
|
|
'next-paths',
|
|
'intelligence',
|
|
'resume',
|
|
'mind-map',
|
|
'sentiment',
|
|
'inbox',
|
|
'revision',
|
|
'stats',
|
|
'reminders',
|
|
'flashcards-progress',
|
|
'agents',
|
|
] as const
|
|
|
|
export type DashboardWidgetId =
|
|
| 'capture'
|
|
| 'next-paths'
|
|
| 'resume'
|
|
| 'intelligence'
|
|
| 'reminders'
|
|
| 'mind-map'
|
|
| 'agents'
|
|
| 'sentiment'
|
|
| 'inbox'
|
|
| 'revision'
|
|
| 'stats'
|
|
| 'agent-activity'
|
|
| 'gmail'
|
|
| 'activity'
|
|
| 'pinned'
|
|
| 'usage'
|
|
| 'daily-review'
|
|
| 'open-loops'
|
|
| 'daily-note'
|
|
| 'link-suggestions'
|
|
| 'bridges'
|
|
| 'flashcards-progress'
|
|
|
|
export type DashboardWidgetZone = 'full' | 'main' | 'side'
|
|
|
|
export type DashboardWidgetCategory =
|
|
| 'essential'
|
|
| 'paths'
|
|
| 'cognition'
|
|
| 'productivity'
|
|
| 'agents'
|
|
|
|
export interface DashboardWidgetPlacement {
|
|
id: DashboardWidgetId
|
|
visible: boolean
|
|
order: number
|
|
zone: DashboardWidgetZone
|
|
}
|
|
|
|
export interface DashboardLayout {
|
|
version: typeof DASHBOARD_LAYOUT_VERSION
|
|
widgets: DashboardWidgetPlacement[]
|
|
}
|
|
|
|
export const DASHBOARD_WIDGET_META: Record<DashboardWidgetId, {
|
|
required: boolean
|
|
zone: DashboardWidgetZone
|
|
category: DashboardWidgetCategory
|
|
defaultVisible: boolean
|
|
}> = {
|
|
capture: { required: true, zone: 'full', category: 'essential', defaultVisible: true },
|
|
'next-paths': { required: false, zone: 'main', category: 'paths', defaultVisible: true },
|
|
resume: { required: true, zone: 'main', category: 'essential', defaultVisible: true },
|
|
intelligence: { required: true, zone: 'main', category: 'essential', defaultVisible: true },
|
|
reminders: { required: false, zone: 'side', category: 'productivity', defaultVisible: true },
|
|
'mind-map': { required: false, zone: 'main', category: 'cognition', defaultVisible: true },
|
|
agents: { required: false, zone: 'side', category: 'agents', defaultVisible: true },
|
|
sentiment: { required: false, zone: 'side', category: 'agents', defaultVisible: true },
|
|
inbox: { required: false, zone: 'side', category: 'productivity', defaultVisible: false },
|
|
revision: { required: false, zone: 'side', category: 'productivity', defaultVisible: false },
|
|
stats: { required: false, zone: 'side', category: 'cognition', defaultVisible: false },
|
|
'agent-activity': { required: false, zone: 'side', category: 'agents', defaultVisible: false },
|
|
gmail: { required: false, zone: 'side', category: 'productivity', defaultVisible: false },
|
|
activity: { required: false, zone: 'main', category: 'cognition', defaultVisible: false },
|
|
pinned: { required: false, zone: 'side', category: 'cognition', defaultVisible: false },
|
|
usage: { required: false, zone: 'side', category: 'agents', defaultVisible: false },
|
|
'daily-review': { required: false, zone: 'side', category: 'paths', defaultVisible: false },
|
|
'open-loops': { required: false, zone: 'side', category: 'paths', defaultVisible: false },
|
|
'daily-note': { required: false, zone: 'side', category: 'paths', defaultVisible: false },
|
|
'link-suggestions': { required: false, zone: 'main', category: 'paths', defaultVisible: false },
|
|
bridges: { required: false, zone: 'main', category: 'cognition', defaultVisible: false },
|
|
'flashcards-progress': { required: false, zone: 'side', category: 'productivity', defaultVisible: false },
|
|
}
|
|
|
|
export const DASHBOARD_CATEGORY_ORDER: DashboardWidgetCategory[] = [
|
|
'essential',
|
|
'paths',
|
|
'cognition',
|
|
'productivity',
|
|
'agents',
|
|
]
|
|
|
|
export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
|
|
version: DASHBOARD_LAYOUT_VERSION,
|
|
widgets: [
|
|
// Pleine largeur
|
|
{ id: 'capture', visible: true, order: 0, zone: 'full' },
|
|
// Colonne principale (gauche) — ordre vertical
|
|
{ id: 'next-paths', visible: true, order: 1, zone: 'main' },
|
|
{ id: 'intelligence', visible: true, order: 2, zone: 'main' },
|
|
{ id: 'resume', visible: true, order: 3, zone: 'main' },
|
|
{ id: 'mind-map', visible: true, order: 4, zone: 'main' },
|
|
// Colonne latérale (droite) — cartes compactes + widgets IA
|
|
{ id: 'sentiment', visible: true, order: 5, zone: 'side' },
|
|
{ id: 'inbox', visible: true, order: 6, zone: 'side' },
|
|
{ id: 'revision', visible: true, order: 7, zone: 'side' },
|
|
{ id: 'stats', visible: true, order: 8, zone: 'side' },
|
|
{ id: 'reminders', visible: true, order: 9, zone: 'side' },
|
|
{ id: 'flashcards-progress', visible: true, order: 10, zone: 'side' },
|
|
{ id: 'agents', visible: true, order: 11, zone: 'side' },
|
|
// Catalogue — masqués par défaut, ajoutables via « Personnaliser »
|
|
{ id: 'daily-review', visible: false, order: 12, zone: 'side' },
|
|
{ id: 'agent-activity', visible: false, order: 13, zone: 'side' },
|
|
{ id: 'gmail', visible: false, order: 14, zone: 'side' },
|
|
{ id: 'activity', visible: false, order: 15, zone: 'main' },
|
|
{ id: 'pinned', visible: false, order: 16, zone: 'side' },
|
|
{ id: 'usage', visible: false, order: 17, zone: 'side' },
|
|
{ id: 'open-loops', visible: false, order: 18, zone: 'side' },
|
|
{ id: 'daily-note', visible: false, order: 19, zone: 'side' },
|
|
{ id: 'link-suggestions', visible: false, order: 20, zone: 'main' },
|
|
{ id: 'bridges', visible: false, order: 21, zone: 'main' },
|
|
],
|
|
}
|
|
|
|
/** Clone frais — évite de muter le constant partagé via useState. */
|
|
export function getDefaultDashboardLayout(): DashboardLayout {
|
|
return structuredClone(DEFAULT_DASHBOARD_LAYOUT)
|
|
}
|
|
|
|
export function resetDashboardLayout(): DashboardLayout {
|
|
return getDefaultDashboardLayout()
|
|
}
|
|
|
|
const ALL_WIDGET_IDS = Object.keys(DASHBOARD_WIDGET_META) as DashboardWidgetId[]
|
|
|
|
const LEGACY_SPAN_ZONE: Record<string, DashboardWidgetZone> = {
|
|
full: 'full',
|
|
large: 'main',
|
|
medium: 'main',
|
|
small: 'side',
|
|
compact: 'side',
|
|
}
|
|
|
|
function isValidZone(zone: unknown): zone is DashboardWidgetZone {
|
|
return zone === 'full' || zone === 'main' || zone === 'side'
|
|
}
|
|
|
|
function legacyZoneFromItem(item: Record<string, unknown>, id: DashboardWidgetId): DashboardWidgetZone {
|
|
if (isValidZone(item.zone)) return item.zone
|
|
const span = item.span
|
|
if (typeof span === 'string' && LEGACY_SPAN_ZONE[span]) return LEGACY_SPAN_ZONE[span]
|
|
return DASHBOARD_WIDGET_META[id].zone
|
|
}
|
|
|
|
export function normalizeDashboardLayout(raw: unknown): DashboardLayout {
|
|
if (!raw || typeof raw !== 'object') return getDefaultDashboardLayout()
|
|
|
|
const data = raw as Partial<DashboardLayout> & { version?: number }
|
|
if (!Array.isArray(data.widgets) || data.widgets.length === 0) return getDefaultDashboardLayout()
|
|
|
|
// Layout périmé ou vide → preset canonique
|
|
const incomingVersion = typeof data.version === 'number' ? data.version : 0
|
|
if (incomingVersion < DASHBOARD_LAYOUT_VERSION) {
|
|
return getDefaultDashboardLayout()
|
|
}
|
|
|
|
const byId = new Map<DashboardWidgetId, DashboardWidgetPlacement>()
|
|
|
|
for (const item of data.widgets) {
|
|
if (!item || typeof item !== 'object') continue
|
|
const id = (item as DashboardWidgetPlacement).id
|
|
if (!ALL_WIDGET_IDS.includes(id)) continue
|
|
const meta = DASHBOARD_WIDGET_META[id]
|
|
byId.set(id, {
|
|
id,
|
|
visible: meta.required ? true : Boolean((item as DashboardWidgetPlacement).visible),
|
|
order: Number.isFinite((item as DashboardWidgetPlacement).order)
|
|
? Number((item as DashboardWidgetPlacement).order)
|
|
: 0,
|
|
zone: legacyZoneFromItem(item as unknown as Record<string, unknown>, id),
|
|
})
|
|
}
|
|
|
|
for (const id of ALL_WIDGET_IDS) {
|
|
if (!byId.has(id)) {
|
|
const meta = DASHBOARD_WIDGET_META[id]
|
|
const fallback = DEFAULT_DASHBOARD_LAYOUT.widgets.find(w => w.id === id)
|
|
byId.set(id, {
|
|
id,
|
|
visible: meta.defaultVisible,
|
|
order: fallback?.order ?? 99,
|
|
zone: meta.zone,
|
|
})
|
|
}
|
|
}
|
|
|
|
const widgets = [...byId.values()]
|
|
.sort((a, b) => a.order - b.order)
|
|
.map((w, index) => ({ ...w, order: index }))
|
|
|
|
const layout = { version: DASHBOARD_LAYOUT_VERSION, widgets }
|
|
const visibleCount = layout.widgets.filter(w => w.visible).length
|
|
if (visibleCount === 0) return getDefaultDashboardLayout()
|
|
|
|
return layout
|
|
}
|
|
|
|
export function reorderWidgets(
|
|
layout: DashboardLayout,
|
|
activeId: DashboardWidgetId,
|
|
overId: DashboardWidgetId,
|
|
): DashboardLayout {
|
|
const activeMeta = DASHBOARD_WIDGET_META[activeId]
|
|
const overMeta = DASHBOARD_WIDGET_META[overId]
|
|
if (activeMeta.zone !== overMeta.zone) return layout
|
|
|
|
const visible = layout.widgets
|
|
.filter(w => w.visible && w.zone === activeMeta.zone)
|
|
.sort((a, b) => a.order - b.order)
|
|
const activeIdx = visible.findIndex(w => w.id === activeId)
|
|
const overIdx = visible.findIndex(w => w.id === overId)
|
|
if (activeIdx === -1 || overIdx === -1 || activeIdx === overIdx) return layout
|
|
|
|
const next = [...visible]
|
|
const [moved] = next.splice(activeIdx, 1)
|
|
next.splice(overIdx, 0, moved)
|
|
|
|
const orderMap = new Map(next.map((w, i) => [w.id, i]))
|
|
return {
|
|
...layout,
|
|
widgets: layout.widgets.map(w => {
|
|
if (w.zone !== activeMeta.zone || !w.visible) return w
|
|
return { ...w, order: orderMap.has(w.id) ? orderMap.get(w.id)! : w.order + next.length }
|
|
}),
|
|
}
|
|
}
|
|
|
|
export function setWidgetVisibility(
|
|
layout: DashboardLayout,
|
|
id: DashboardWidgetId,
|
|
visible: boolean,
|
|
): DashboardLayout {
|
|
if (DASHBOARD_WIDGET_META[id].required && !visible) return layout
|
|
return {
|
|
...layout,
|
|
widgets: layout.widgets.map(w => w.id === id ? { ...w, visible } : w),
|
|
}
|
|
}
|
|
|
|
export function visibleWidgets(layout: DashboardLayout): DashboardWidgetPlacement[] {
|
|
return layout.widgets.filter(w => w.visible).sort((a, b) => a.order - b.order)
|
|
}
|
|
|
|
export function visibleWidgetsInZone(
|
|
layout: DashboardLayout,
|
|
zone: DashboardWidgetZone,
|
|
): DashboardWidgetPlacement[] {
|
|
return visibleWidgets(layout).filter(w => w.zone === zone)
|
|
}
|
|
|
|
export function hiddenWidgetIds(layout: DashboardLayout): DashboardWidgetId[] {
|
|
return layout.widgets.filter(w => !w.visible).map(w => w.id)
|
|
}
|
|
|
|
export function catalogByCategory(layout: DashboardLayout): Record<DashboardWidgetCategory, DashboardWidgetId[]> {
|
|
const result = Object.fromEntries(
|
|
DASHBOARD_CATEGORY_ORDER.map(c => [c, [] as DashboardWidgetId[]]),
|
|
) as Record<DashboardWidgetCategory, DashboardWidgetId[]>
|
|
|
|
for (const id of ALL_WIDGET_IDS) {
|
|
result[DASHBOARD_WIDGET_META[id].category].push(id)
|
|
}
|
|
|
|
for (const cat of DASHBOARD_CATEGORY_ORDER) {
|
|
result[cat].sort((a, b) => {
|
|
const aVis = layout.widgets.find(w => w.id === a)?.visible ? 0 : 1
|
|
const bVis = layout.widgets.find(w => w.id === b)?.visible ? 0 : 1
|
|
if (aVis !== bVis) return aVis - bVis
|
|
return a.localeCompare(b)
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export function isWidgetVisible(layout: DashboardLayout, id: DashboardWidgetId): boolean {
|
|
return layout.widgets.find(w => w.id === id)?.visible ?? DASHBOARD_WIDGET_META[id].defaultVisible
|
|
}
|