feat(dashboard): tableau de bord Second Brain configurable avec chargement progressif
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m3s
CI / Deploy production (on server) (push) Successful in 23s

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:
Antigravity
2026-07-14 16:50:53 +00:00
parent d38a99586b
commit 30da592ba2
62 changed files with 7741 additions and 335 deletions

View File

@@ -10,6 +10,12 @@ import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types'
import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types'
import '@excalidraw/excalidraw/index.css'
import type { PresentationSpec } from '@/lib/types/presentation'
import { useLanguage } from '@/lib/i18n'
function SlidesLoadingFallback() {
const { t } = useLanguage()
return <div className="absolute inset-0 flex items-center justify-center bg-zinc-950 text-white/40 text-sm">{t('lab.loadingSlides')}</div>
}
const Excalidraw = dynamic(
async () => (await import('@excalidraw/excalidraw')).Excalidraw,
@@ -18,7 +24,7 @@ const Excalidraw = dynamic(
const SlidesRenderer = dynamic(
() => import('./slides-renderer').then(m => ({ default: m.SlidesRenderer })),
{ ssr: false, loading: () => <div className="absolute inset-0 flex items-center justify-center bg-zinc-950 text-white/40 text-sm">Chargement des slides</div> }
{ ssr: false, loading: () => <SlidesLoadingFallback /> }
)
interface CanvasBoardProps {
@@ -62,6 +68,7 @@ function parseCanvasScene(initialData?: string): {
}
function PptxViewer({ data, name }: { data: PptxPayload; name: string }) {
const { t } = useLanguage()
const handleDownload = () => {
try {
const byteCharacters = atob(data.base64)
@@ -83,7 +90,7 @@ function PptxViewer({ data, name }: { data: PptxPayload; name: string }) {
URL.revokeObjectURL(url)
} catch (e) {
console.error('[PptxViewer] Download failed:', e)
toast.error('Failed to download presentation')
toast.error(t('lab.downloadFailed'))
}
}
@@ -106,7 +113,7 @@ function PptxViewer({ data, name }: { data: PptxPayload; name: string }) {
className="flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-colors font-medium shadow-sm"
>
<Download className="w-5 h-5" />
Download .pptx
{t('lab.exportPpt')}
</button>
<p className="text-xs text-muted-foreground max-w-sm text-center">
This file can be opened in Microsoft PowerPoint, Google Slides, or Keynote.
@@ -117,6 +124,7 @@ function PptxViewer({ data, name }: { data: PptxPayload; name: string }) {
}
function SlidesViewer({ data, name, canvasId }: { data: SlidesPayload; name: string; canvasId?: string | null }) {
const { t } = useLanguage()
const iframeRef = useRef<HTMLIFrameElement>(null)
const [currentSlide, setCurrentSlide] = useState(1)
const [isLoaded, setIsLoaded] = useState(!!data.spec) // spec → React renderer, no iframe loading
@@ -194,27 +202,27 @@ function SlidesViewer({ data, name, canvasId }: { data: SlidesPayload; name: str
href={`/api/canvas/slides/pptx?id=${canvasId}`}
download
className="flex items-center gap-1.5 px-3 py-1.5 bg-white/10 hover:bg-white/20 rounded-lg text-white/70 hover:text-white text-xs font-medium transition-all"
title="Exporter en PowerPoint"
title={t('lab.exportPpt')}
>
<Download className="w-3.5 h-3.5" />
Export PPTX
{t('lab.exportPpt')}
</a>
)}
<button
onClick={downloadHtml}
className="flex items-center gap-1.5 px-3 py-1.5 bg-white/10 hover:bg-white/20 rounded-lg text-white/70 hover:text-white text-xs font-medium transition-all"
title="Télécharger le HTML"
title={t('lab.downloadHtml')}
>
<Download className="w-3.5 h-3.5" />
Export HTML
{t('lab.downloadHtml')}
</button>
<button
onClick={openFullscreen}
className="flex items-center gap-1.5 px-3 py-1.5 bg-white/10 hover:bg-white/20 rounded-lg text-white/70 hover:text-white text-xs font-medium transition-all"
title="Ouvrir en plein écran"
title={t('lab.openFullscreen')}
>
<Maximize2 className="w-3.5 h-3.5" />
Plein écran
{t('lab.openFullscreen')}
</button>
</div>
</div>
@@ -228,7 +236,7 @@ function SlidesViewer({ data, name, canvasId }: { data: SlidesPayload; name: str
{!isLoaded && (
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-zinc-950 gap-3">
<div className="w-8 h-8 rounded-full border-2 border-white/10 border-t-white/60 animate-spin" />
<span className="text-xs text-white/30">Chargement de la présentation</span>
<span className="text-xs text-white/30">{t('lab.loadingPresentation')}</span>
</div>
)}
<iframe
@@ -244,7 +252,7 @@ function SlidesViewer({ data, name, canvasId }: { data: SlidesPayload; name: str
<button
onClick={() => navigate(-1)}
className="absolute left-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 flex items-center justify-center rounded-full bg-black/40 hover:bg-black/70 backdrop-blur-sm border border-white/10 text-white/40 hover:text-white transition-all opacity-0 group-hover:opacity-100"
aria-label="Slide précédent"
aria-label={t('lab.previousSlide')}
>
<ChevronLeft className="w-5 h-5" />
</button>
@@ -252,7 +260,7 @@ function SlidesViewer({ data, name, canvasId }: { data: SlidesPayload; name: str
<button
onClick={() => navigate(1)}
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 flex items-center justify-center rounded-full bg-black/40 hover:bg-black/70 backdrop-blur-sm border border-white/10 text-white/40 hover:text-white transition-all opacity-0 group-hover:opacity-100"
aria-label="Slide suivant"
aria-label={t('lab.nextSlide')}
>
<ChevronRight className="w-5 h-5" />
</button>

View File

@@ -11,10 +11,16 @@ import {
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
FunnelChart, Funnel, LabelList,
} from 'recharts'
import { useLanguage } from '@/lib/i18n'
function MermaidLoadingFallback() {
const { t } = useLanguage()
return <div style={{ color: 'rgba(255,255,255,0.3)', padding: 24 }}>{t('common.loading')}</div>
}
const MermaidDiagram = dynamic(
() => import('./mermaid-diagram').then(m => ({ default: m.MermaidDiagram })),
{ ssr: false, loading: () => <div style={{ color: 'rgba(255,255,255,0.3)', padding: 24 }}>Chargement</div> }
{ ssr: false, loading: () => <MermaidLoadingFallback /> }
)
// ── Constants ──────────────────────────────────────────────────────────────────
@@ -688,6 +694,7 @@ export interface SlidesRendererProps {
}
export function SlidesRenderer({ spec }: SlidesRendererProps) {
const { t } = useLanguage()
const [current, setCurrent] = useState(0)
const [showNotes, setShowNotes] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
@@ -795,12 +802,12 @@ export function SlidesRenderer({ spec }: SlidesRendererProps) {
{/* Navigation buttons */}
{current > 0 && (
<button style={navBtn(true)} onClick={prev} aria-label="Précédent">
<button style={navBtn(true)} onClick={prev} aria-label={t('lab.previous')}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6" /></svg>
</button>
)}
{current < total - 1 && (
<button style={navBtn(false)} onClick={next} aria-label="Suivant">
<button style={navBtn(false)} onClick={next} aria-label={t('lab.next')}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 6 15 12 9 18" /></svg>
</button>
)}
@@ -830,12 +837,12 @@ export function SlidesRenderer({ spec }: SlidesRendererProps) {
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: palette.accent }}>
Notes de présentation
{t('lab.notes')}
</span>
<button
onClick={() => setShowNotes(false)}
style={{ background: 'none', border: 'none', color: isDark ? '#94a3b8' : '#64748b', cursor: 'pointer', fontSize: 13, padding: 4 }}
title="Masquer les notes"
title={t('lab.hideNotes')}
>
</button>
@@ -860,10 +867,10 @@ export function SlidesRenderer({ spec }: SlidesRendererProps) {
display: 'flex', alignItems: 'center', gap: 6, transition: 'all 0.2s',
backdropFilter: 'blur(4px)',
}}
title="Bascule des notes de présentation (Raccourci: N)"
title={t('lab.toggleNotes')}
>
<span>📝</span>
<span>Notes</span>
<span>{t('lab.notes')}</span>
</button>
)}
<div style={{ fontSize: 12, fontWeight: 600, color: isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.35)', background: isDark ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.8)', padding: '3px 10px', borderRadius: 100, backdropFilter: 'blur(4px)', border: `1px solid ${isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}` }}>