fix(ui): thème, textes et lisibilité restants de la revue
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:
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user