fix(ui): thème, textes et lisibilité restants de la revue
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 6m57s
CI / Deploy production (on server) (push) Successful in 24s

Les boutons suivent la couleur d’apparence, les libellés trop petits ou trop techniques sont clarifiés, et le catalogue des fournisseurs se met à jour tout seul.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-08-30 21:13:07 +00:00
parent ebf6f16fde
commit afbb0dfc2d
77 changed files with 2842 additions and 1546 deletions

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback, useMemo, type ReactNode } from 'react'
import { useRouter } from 'next/navigation'
import { useReducedMotion } from 'motion/react'
import { Inbox, Send, Bell, Mail, Loader2 } from 'lucide-react'
import { Send, Bell, Loader2, PenLine } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import { redirectToAiConsentSettings } from '@/lib/consent/ai-consent-redirect'
@@ -30,6 +30,7 @@ import {
} from '@/components/dashboard-path-widgets'
import type { DashboardPath } from '@/lib/dashboard/path-types'
import { buildFastPathsFromBriefing } from '@/lib/dashboard/paths-fast'
import { pathNoteTitle, pickFocusNote } from '@/lib/dashboard/path-title'
import { emitAiUsageChanged } from '@/lib/ai-usage-sync'
import {
DashboardInboxWidget,
@@ -50,8 +51,10 @@ import type { LucideIcon } from 'lucide-react'
interface BriefingPinnedNote {
id: string
title: string | null
excerpt?: string
notebookId: string | null
updatedAt: string
notebook?: { id: string; name: string; color: string | null; icon: string | null } | null
}
interface ActivityDay {
@@ -137,6 +140,7 @@ interface SentimentData {
emotions?: Record<string, number>
summary?: string
topTopic?: string
relatedNotes?: Array<{ id: string; title: string | null; notebookId: string | null }>
}
interface MindMapData {
@@ -217,7 +221,13 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const [data, setData] = useState<{
recentNotes: BriefingNote[]
inboxCount: number
inboxPreview?: Array<{ id: string; title: string | null; notebookId: string | null }>
inboxPreview?: Array<{
id: string
title: string | null
excerpt?: string
notebookId: string | null
updatedAt?: string
}>
dueFlashcards: number
upcomingReminders: BriefingReminder[]
insights: BriefingInsight[]
@@ -245,6 +255,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const [capturing, setCapturing] = useState(false)
const [inboxPulse, setInboxPulse] = useState(0)
const [actingSuggestionId, setActingSuggestionId] = useState<string | null>(null)
const [createdAgent, setCreatedAgent] = useState<{ id: string | null; topic: string } | null>(null)
const [echoRefreshing, setEchoRefreshing] = useState(false)
const [dismissingInsightId, setDismissingInsightId] = useState<string | null>(null)
const [actingBridgeSuggestionKey, setActingBridgeSuggestionKey] = useState<string | null>(null)
@@ -267,7 +278,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const loadPaths = useCallback(async (briefing: NonNullable<typeof data>) => {
setPathsEnriching(true)
try {
const focus = briefing.recentNotes[0]
const focus = pickFocusNote(briefing.recentNotes)
const res = await fetch('/api/briefing/paths', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -418,12 +429,22 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const gmail = data?.gmail
const pinnedNotes = data?.pinnedNotes ?? []
const writingActivity = data?.writingActivity ?? []
const pathsList = paths
const recentNotes = data?.recentNotes ?? []
const pathsList = useMemo(
() => paths.filter(p => p.type !== 'organize' && p.type !== 'review'),
[paths],
)
const pathBridgeKeys = useMemo(
() => pathsList
.filter(p => p.type === 'bridge' && p.clusterAId != null && p.clusterBId != null)
.map(p => `${p.clusterAId}-${p.clusterBId}`),
[pathsList],
)
const focusNote = useMemo(() => pickFocusNote(recentNotes), [recentNotes])
const openLoopsList = openLoops
const briefingLoading = data === null
const pathsLoading = data === null
const pathsDetailLoading = pathsEnriching
const recentNotes = data?.recentNotes ?? []
const topBridgeNotes = useMemo(() => (mindMap?.bridgeNotes ?? []).slice(0, 3), [mindMap?.bridgeNotes])
const dateLocale = localeForLanguage(language)
@@ -434,25 +455,19 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const themeCount = mindMap?.clusters.length ?? 0
const resumeNotes = useMemo(() => recentNotes.map(n => ({
id: n.id,
title: n.title,
excerpt: stripHtml(n.content).slice(0, 180),
notebookName: n.notebook?.name || t('homeDashboard.inbox'),
notebookColor: n.notebook?.color || '#8B5CF6',
updatedAt: n.updatedAt,
notebookId: n.notebookId,
})), [recentNotes, t])
const briefingSubtitle = useMemo(() => {
if (briefingLoading) return ''
const parts: string[] = []
if (inboxCount > 0) parts.push(t('homeDashboard.pulseInbox', { count: inboxCount }))
if (dueFlashcards > 0) parts.push(t('homeDashboard.pulseReview', { count: dueFlashcards }))
if (discoveryCount > 0) parts.push(t('homeDashboard.pulseDiscoveries', { count: discoveryCount }))
if (parts.length === 0) return t('homeDashboard.pulseClear')
return parts.join(' · ')
}, [briefingLoading, inboxCount, dueFlashcards, discoveryCount, t])
const resumeNotes = useMemo(() => recentNotes.flatMap(n => {
const displayTitle = pathNoteTitle(n.title, n.content)
if (!displayTitle) return []
return [{
id: n.id,
title: displayTitle,
excerpt: stripHtml(n.content).slice(0, 180),
notebookName: n.notebook?.name || t('homeDashboard.inbox'),
notebookColor: n.notebook?.color || '#8B5CF6',
updatedAt: n.updatedAt,
notebookId: n.notebookId,
}]
}), [recentNotes, t])
const handleCapture = useCallback(async () => {
const text = captureText.trim()
@@ -609,19 +624,20 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
const handleAcceptSuggestion = useCallback(async (id: string) => {
setActingSuggestionId(id)
const topic = data?.agentSuggestions?.find(s => s.id === id)?.topic ?? ''
try {
const res = await fetch(`/api/agents/suggestions/${id}/accept`, { method: 'POST' })
const json = await res.json()
if (!res.ok) throw new Error(json.error)
setData(prev => prev ? { ...prev, agentSuggestions: prev.agentSuggestions?.filter(s => s.id !== id) ?? [] } : prev)
setCreatedAgent({ id: json.agentId ?? null, topic })
toast.success(t('homeDashboard.agentCreated'))
if (json.agentId) router.push(`/agents?id=${json.agentId}`)
} catch {
toast.error(t('homeDashboard.agentFailed'))
} finally {
setActingSuggestionId(null)
}
}, [t, router])
}, [t, data?.agentSuggestions])
const handleDismissSuggestion = useCallback(async (id: string) => {
setActingSuggestionId(id)
@@ -752,12 +768,17 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
case 'capture':
return wrap(
<div className="relative rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm shadow-sm h-full">
<div className="flex items-center justify-between gap-2 px-3.5 pt-2.5">
<div className="flex items-center justify-between gap-2 px-3 pt-2">
<div className="flex items-center gap-2 min-w-0">
<Inbox size={11} className="text-brand-accent shrink-0" />
<span className="text-[8px] font-mono font-bold uppercase tracking-widest text-concrete">
{t('homeDashboard.quickCapture')}
</span>
<PenLine size={11} className="text-brand-accent shrink-0" />
<div className="min-w-0">
<span className="text-[8px] font-mono font-bold uppercase tracking-widest text-concrete block">
{t('homeDashboard.quickCapture')}
</span>
<span className="text-[9px] text-concrete leading-tight">
{t('homeDashboard.captureGoesToFile')}
</span>
</div>
</div>
<DashboardWidgetHelp widgetId="capture" />
</div>
@@ -767,14 +788,15 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
onKeyDown={handleCaptureKeyDown}
placeholder={t('homeDashboard.quickCapturePlaceholder')}
rows={2}
className="w-full text-sm px-3.5 pb-3 pt-1.5 pe-12 bg-transparent outline-none text-ink dark:text-dark-ink resize-none leading-relaxed placeholder:text-concrete/45"
className="w-full text-sm px-3 pb-2 pt-1 pe-12 bg-transparent outline-none text-ink dark:text-dark-ink resize-none leading-snug h-[2.75rem] placeholder:text-concrete/45"
/>
<button
type="button"
onClick={handleCapture}
disabled={!captureText.trim() || capturing}
className="absolute bottom-2.5 end-2.5 p-2 bg-ink text-white dark:bg-white dark:text-black rounded-lg disabled:opacity-25 hover:scale-105 active:scale-95 transition-all shadow-sm"
className="absolute bottom-1.5 end-2 p-1.5 bg-brand-accent text-white rounded-lg disabled:opacity-25 hover:bg-brand-accent/90 hover:scale-105 active:scale-95 transition-all shadow-sm"
aria-busy={capturing}
aria-label={t('homeDashboard.captureSend')}
>
{capturing
? <Loader2 size={12} className="animate-spin" />
@@ -788,7 +810,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
paths={pathsList}
loading={pathsLoading}
enriching={pathsEnriching}
focusNoteTitle={recentNotes[0]?.title}
focusNoteTitle={focusNote ? pathNoteTitle(focusNote.title, focusNote.content) : null}
onAction={handlePathAction}
prefersReducedMotion={!!prefersReducedMotion}
/>,
@@ -887,6 +909,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
dismissingInsightId={dismissingInsightId}
actingBridgeSuggestionKey={actingBridgeSuggestionKey}
prefersReducedMotion={!!prefersReducedMotion}
excludeBridgeSuggestionKeys={pathBridgeKeys}
/>,
)
case 'reminders':
@@ -903,7 +926,18 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
<div className="h-10 rounded-lg bg-stone-50 dark:bg-zinc-950/40 animate-pulse" />
</div>
) : reminders.length === 0 ? (
<p className="text-[11px] text-concrete italic py-1">{t('homeDashboard.allCaughtUp')}</p>
<div className="rounded-xl border border-dashed border-border/35 bg-stone-50/50 dark:bg-zinc-950/30 p-3">
<p className="text-[11px] text-concrete leading-relaxed mb-2">
{t('homeDashboard.remindersEmpty')}
</p>
<button
type="button"
onClick={() => router.push('/home?reminders=1&forceList=1')}
className="text-[9px] font-mono font-bold uppercase text-brand-accent hover:underline"
>
{t('homeDashboard.remindersOpenAll')}
</button>
</div>
) : (
<div className="space-y-1.5">
{reminders.slice(0, 4).map(r => (
@@ -921,33 +955,15 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
</span>
</button>
))}
<button
type="button"
onClick={() => router.push('/home?reminders=1&forceList=1')}
className="w-full text-start px-1 pt-1 text-[9px] font-mono uppercase font-bold text-brand-accent hover:underline"
>
{t('homeDashboard.remindersOpenAll')}
</button>
</div>
)}
{!briefingLoading && (
gmail?.connected ? (
<button
type="button"
onClick={() => router.push('/settings/integrations')}
className="w-full flex items-center justify-between gap-2 p-2.5 rounded-xl border border-border/20 hover:border-brand-accent/25 transition-all text-start mt-2"
>
<div className="flex items-center gap-2 min-w-0">
<Mail size={11} className="text-concrete" />
<span className="text-[10px] text-ink dark:text-dark-ink truncate">{t('homeDashboard.gmailCaptures')}</span>
</div>
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-1.5 py-0.5 rounded">
{t('homeDashboard.gmailRecent', { count: gmail.recentCaptures })}
</span>
</button>
) : (
<button
type="button"
onClick={() => router.push('/settings/integrations')}
className="w-full text-[9px] font-mono uppercase tracking-wider text-concrete hover:text-brand-accent transition-colors text-start py-2 mt-2"
>
{t('homeDashboard.gmailConnect')}
</button>
)
)}
</div>,
)
case 'mind-map':
@@ -971,6 +987,9 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
formatFrequency={formatAgentFrequency}
onAccept={handleAcceptSuggestion}
onDismiss={handleDismissSuggestion}
createdAgent={createdAgent}
onOpenCreated={() => router.push(createdAgent?.id ? `/agents?id=${createdAgent.id}` : '/agents')}
onClearCreated={() => setCreatedAgent(null)}
prefersReducedMotion={!!prefersReducedMotion}
/>,
)
@@ -983,6 +1002,8 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
summary={sentiment?.summary}
emotions={sentiment?.emotions}
emotionMeta={EMOTION_META}
relatedNotes={sentiment?.relatedNotes}
onSelectNote={onNoteSelect}
/>,
)
case 'inbox':
@@ -993,6 +1014,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
loading={briefingLoading}
onOpen={() => router.push('/home?forceList=1')}
onSelect={onNoteSelect}
formatRelativeTime={relTime}
/>,
)
case 'revision':
@@ -1043,6 +1065,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
notes={pinnedNotes}
loading={briefingLoading}
onSelect={onNoteSelect}
formatRelativeTime={relTime}
/>,
)
case 'usage':
@@ -1058,11 +1081,11 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
aiStatus, insights, topBridgeNotes, bridgeSuggestions, agentActions, handleRefreshEcho,
handleEnableAi, echoRefreshing, handleDismissInsight, handleDismissBridgeSuggestion,
handleCreateBridgeSuggestion, handleOpenFromInsight, dismissingInsightId,
actingBridgeSuggestionKey, reminders, gmail, dateLocale, router, mindMap,
agentSuggestions, actingSuggestionId, formatAgentFrequency, handleAcceptSuggestion,
actingBridgeSuggestionKey, pathBridgeKeys, reminders, gmail, dateLocale, router, mindMap,
agentSuggestions, actingSuggestionId, createdAgent, formatAgentFrequency, handleAcceptSuggestion,
handleDismissSuggestion, sentiment, pinnedNotes, writingActivity, themeCount,
pathsList, openLoopsList, handlePathAction, dailyReviewItems, linkSuggestionPaths,
handleOpenDailyNote, flashcardStats,
handleOpenDailyNote, flashcardStats, focusNote,
])
return (
@@ -1078,7 +1101,7 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
<button
type="button"
onClick={() => { void reloadBriefingAndPaths() }}
className="shrink-0 text-[10px] font-mono font-bold uppercase tracking-wider px-3 py-2 rounded-lg bg-ink text-white dark:bg-white dark:text-black"
className="shrink-0 text-[10px] font-mono font-bold uppercase tracking-wider px-3 py-2 rounded-lg bg-brand-accent text-white hover:bg-brand-accent/90"
>
{t('homeDashboard.briefingRetry')}
</button>
@@ -1086,20 +1109,13 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
)}
{/* ── En-tête : orientation en 2 secondes ── */}
<header className="mb-5">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-2 pb-4 border-b border-border/20">
<div>
<h1 className="font-serif text-2xl sm:text-3xl font-medium text-ink dark:text-dark-ink tracking-tight">
{t('homeDashboard.title')}
</h1>
<p className="text-[10px] font-mono uppercase tracking-[0.2em] text-concrete font-bold mt-1">
{new Date().toLocaleDateString(dateLocale, { weekday: 'long', day: 'numeric', month: 'long' })}
</p>
</div>
{!briefingLoading && briefingSubtitle && (
<p className="text-[11px] text-concrete leading-relaxed max-w-md sm:text-end">
{briefingSubtitle}
</p>
)}
<div className="pb-4 border-b border-border/20">
<h1 className="font-serif text-2xl sm:text-3xl font-medium text-ink dark:text-dark-ink tracking-tight">
{t('homeDashboard.title')}
</h1>
<p className="text-[10px] font-mono uppercase tracking-[0.2em] text-concrete font-bold mt-1">
{new Date().toLocaleDateString(dateLocale, { weekday: 'long', day: 'numeric', month: 'long' })}
</p>
</div>
{/* Pulse : file d'attente cognitive en un coup d'œil */}
@@ -1121,15 +1137,12 @@ export function DashboardView({ onNoteSelect }: DashboardViewProps) {
</div>
</header>
<div className="pb-24">
<div className="pb-8">
<DashboardWidgetGrid
renderWidget={renderWidget}
isWidgetEmpty={(id) => {
if (briefingLoading) return false
if (id === 'sentiment') return !sentimentLoading && (!sentiment?.available || !sentiment?.dominantEmotion)
if (id === 'reminders') return reminders.length === 0
if (id === 'revision') return dueFlashcards === 0
if (id === 'inbox') return inboxCount === 0
return false
}}
/>