Files
Momento/memento-note/components/intelligence-hub.tsx
Antigravity 30da592ba2
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m3s
CI / Deploy production (on server) (push) Successful in 23s
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>
2026-07-14 16:50:53 +00:00

730 lines
30 KiB
TypeScript

'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import { motion, AnimatePresence } from 'motion/react'
import {
Sparkles, Zap, Lightbulb, Bot, ChevronLeft, ChevronRight,
ExternalLink, GitCompare, X, RefreshCw, Loader2, Link2, Brain, ArrowRight,
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { DashboardWidgetTitleRow } from '@/components/dashboard-widget-title-row'
import { MEMORY_ECHO_LEGACY_EN_FALLBACKS } from '@/lib/ai/memory-echo-i18n'
// ─── Types ─────────────────────────────────────────────
export interface IntelBriefingInsight {
id: string
insight: string
score: number
date: string
viewed: boolean
note1: { id: string; title: string | null }
note2: { id: string; title: string | null }
note1Excerpt?: string
note2Excerpt?: string
}
export interface IntelBridgeNote {
noteId: string
bridgeScore: number
clusterNames?: string[]
note?: { id: string; title: string | null; content?: string }
}
export interface IntelBridgeSuggestion {
clusterAId: number
clusterBId: number
clusterAName: string
clusterBName: string
suggestedTitle: string
suggestedContent: string
justification: string
}
export interface IntelAgentAction {
id: string
agentName: string
result: string | null
createdAt: string
}
type IntelFilter = 'all' | 'connections' | 'bridges' | 'ideas' | 'agents'
type IntelItem =
| { kind: 'insight'; id: string; priority: number; insight: IntelBriefingInsight }
| { kind: 'bridge'; id: string; priority: number; bridge: IntelBridgeNote }
| { kind: 'suggestion'; id: string; priority: number; suggestion: IntelBridgeSuggestion }
| { kind: 'agent'; id: string; priority: number; agent: IntelAgentAction }
const CLUSTER_COLORS = ['#F87171', '#60A5FA', '#34D399', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF']
const FILTER_KINDS: Record<IntelFilter, IntelItem['kind'][] | null> = {
all: null,
connections: ['insight'],
bridges: ['bridge'],
ideas: ['suggestion'],
agents: ['agent'],
}
function stripHtml(html: string): string {
return html.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
}
function formatRelativeTime(
dateStr: string,
t: (key: string, params?: Record<string, string | number>) => string,
): string {
const diff = Date.now() - new Date(dateStr).getTime()
const mins = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (mins < 1) return t('time.justNow')
if (mins < 60) return t('time.minutesAgo', { count: mins })
if (hours < 24) return t('time.hoursAgo', { count: hours })
return t('time.daysAgo', { count: days })
}
// ─── Visual: lien entre deux notes ─────────────────────
function ConnectionDiagram({
note1Title,
note2Title,
score,
color = '#6366F1',
}: {
note1Title: string
note2Title: string
score: number
color?: string
}) {
return (
<div className="flex items-center gap-2 py-3">
<div
className="flex-1 min-w-0 p-2.5 rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/60 text-start"
style={{ borderColor: `${color}30` }}
>
<p className="text-[10px] font-semibold text-ink dark:text-dark-ink truncate leading-tight">
{note1Title}
</p>
</div>
<div className="shrink-0 flex flex-col items-center gap-0.5 px-1">
<div className="w-8 h-px" style={{ background: `linear-gradient(90deg, transparent, ${color}, transparent)` }} />
<span
className="text-[9px] font-mono font-bold px-2 py-0.5 rounded-full"
style={{ color, backgroundColor: `${color}14`, border: `1px solid ${color}25` }}
>
{Math.round(score * 100)}%
</span>
<div className="w-8 h-px" style={{ background: `linear-gradient(90deg, transparent, ${color}, transparent)` }} />
</div>
<div
className="flex-1 min-w-0 p-2.5 rounded-xl border border-border/30 bg-white/80 dark:bg-zinc-900/60 text-start"
style={{ borderColor: `${color}30` }}
>
<p className="text-[10px] font-semibold text-ink dark:text-dark-ink truncate leading-tight">
{note2Title}
</p>
</div>
</div>
)
}
// ─── Props ─────────────────────────────────────────────
export interface IntelligenceHubProps {
loading: boolean
aiActive: boolean
hasAiConsent: boolean
providerReady: boolean
memoryEchoEnabled: boolean
insights: IntelBriefingInsight[]
bridgeNotes: IntelBridgeNote[]
bridgeSuggestions: IntelBridgeSuggestion[]
agentActions: IntelAgentAction[]
onNoteSelect: (noteId: string) => void
onRefreshEcho: () => void
onEnableAi: () => void
echoRefreshing: boolean
onDismissInsight: (insight: IntelBriefingInsight) => void
onDismissBridgeSuggestion: (s: IntelBridgeSuggestion) => void
onCreateBridgeSuggestion: (s: IntelBridgeSuggestion) => void
onOpenInsightNote: (insight: IntelBriefingInsight, noteId: string) => void
dismissingInsightId: string | null
actingBridgeSuggestionKey: string | null
prefersReducedMotion: boolean
}
// ─── Component ─────────────────────────────────────────
export function IntelligenceHub({
loading,
aiActive,
hasAiConsent,
providerReady,
memoryEchoEnabled,
insights,
bridgeNotes,
bridgeSuggestions,
agentActions,
onNoteSelect,
onRefreshEcho,
onEnableAi,
echoRefreshing,
onDismissInsight,
onDismissBridgeSuggestion,
onCreateBridgeSuggestion,
onOpenInsightNote,
dismissingInsightId,
actingBridgeSuggestionKey,
prefersReducedMotion,
}: IntelligenceHubProps) {
const router = useRouter()
const { t } = useLanguage()
const [filter, setFilter] = useState<IntelFilter>('all')
const [activeIndex, setActiveIndex] = useState(0)
const localizeInsightText = useCallback((insight: IntelBriefingInsight) => {
const text = insight.insight.trim()
if (MEMORY_ECHO_LEGACY_EN_FALLBACKS.has(text)) {
if (insight.note1Excerpt && insight.note2Excerpt) {
return t('homeDashboard.connectionBetween', {
note1: insight.note1.title || insight.note1Excerpt.slice(0, 50),
note2: insight.note2.title || insight.note2Excerpt.slice(0, 50),
})
}
return t('memoryEcho.defaultInsight')
}
return text
}, [t])
const allItems = useMemo(() => {
const insightNoteIds = new Set<string>()
for (const i of insights) {
insightNoteIds.add(i.note1.id)
insightNoteIds.add(i.note2.id)
}
const items: IntelItem[] = []
for (const insight of insights) {
items.push({
kind: 'insight',
id: `insight-${insight.id}`,
priority: (insight.viewed ? 40 : 100) + insight.score * 10,
insight,
})
}
for (const bridge of bridgeNotes) {
if (insightNoteIds.has(bridge.noteId)) continue
items.push({
kind: 'bridge',
id: `bridge-${bridge.noteId}`,
priority: 60 + bridge.bridgeScore * 10,
bridge,
})
}
for (const suggestion of bridgeSuggestions) {
items.push({
kind: 'suggestion',
id: `suggestion-${suggestion.clusterAId}-${suggestion.clusterBId}`,
priority: 80,
suggestion,
})
}
for (const agent of agentActions) {
items.push({
kind: 'agent',
id: `agent-${agent.id}`,
priority: 30,
agent,
})
}
return items.sort((a, b) => b.priority - a.priority).slice(0, 10)
}, [insights, bridgeNotes, bridgeSuggestions, agentActions])
const counts = useMemo(() => ({
all: allItems.length,
connections: allItems.filter(i => i.kind === 'insight').length,
bridges: allItems.filter(i => i.kind === 'bridge').length,
ideas: allItems.filter(i => i.kind === 'suggestion').length,
agents: allItems.filter(i => i.kind === 'agent').length,
}), [allItems])
const filteredItems = useMemo(() => {
const kinds = FILTER_KINDS[filter]
if (!kinds) return allItems
return allItems.filter(i => kinds.includes(i.kind))
}, [allItems, filter])
useEffect(() => {
setActiveIndex(0)
}, [filter])
useEffect(() => {
if (activeIndex >= filteredItems.length && filteredItems.length > 0) {
setActiveIndex(filteredItems.length - 1)
}
}, [activeIndex, filteredItems.length])
const activeItem = filteredItems[activeIndex] ?? null
const newCount = insights.filter(i => !i.viewed).length
const goPrev = () => setActiveIndex(i => Math.max(0, i - 1))
const goNext = () => setActiveIndex(i => Math.min(filteredItems.length - 1, i + 1))
const filters: { key: IntelFilter; label: string }[] = [
{ key: 'all', label: t('homeDashboard.intelFilterAll') },
{ key: 'connections', label: t('homeDashboard.intelFilterConnections') },
{ key: 'bridges', label: t('homeDashboard.intelFilterBridges') },
{ key: 'ideas', label: t('homeDashboard.intelFilterIdeas') },
{ key: 'agents', label: t('homeDashboard.intelFilterAgents') },
]
const slideVariants = prefersReducedMotion
? { initial: {}, animate: {}, exit: {} }
: {
initial: { opacity: 0, x: 24, scale: 0.98 },
animate: { opacity: 1, x: 0, scale: 1 },
exit: { opacity: 0, x: -24, scale: 0.98 },
}
const renderSpotlight = (item: IntelItem) => {
switch (item.kind) {
case 'insight': {
const { insight } = item
const text = localizeInsightText(insight)
const excerpt = insight.note1Excerpt || insight.note2Excerpt
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 mb-3">
<Sparkles size={12} className="text-indigo-500" />
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
{t('homeDashboard.semanticConnection')}
</span>
{!insight.viewed && (
<span className="w-1.5 h-1.5 rounded-full bg-ochre animate-pulse" />
)}
</div>
<ConnectionDiagram
note1Title={insight.note1.title || t('homeDashboard.untitled')}
note2Title={insight.note2.title || t('homeDashboard.untitled')}
score={insight.score}
/>
<p className="text-[11px] text-ink/75 dark:text-dark-ink/75 font-serif italic leading-relaxed line-clamp-3 px-1">
« {text} »
</p>
{excerpt && (
<p className="text-[9px] text-concrete/80 line-clamp-2 mt-2 px-1 border-s-2 border-indigo-500/20 ps-2">
{excerpt}
</p>
)}
<div className="flex items-center gap-1.5 mt-auto pt-3 flex-wrap">
<button
type="button"
onClick={() => onOpenInsightNote(insight, insight.note1.id)}
className="inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition-colors"
>
<ExternalLink size={9} />
{t('homeDashboard.intelOpenNote')}
</button>
<button
type="button"
onClick={() => onOpenInsightNote(insight, insight.note2.id)}
className="inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg border border-border/40 hover:border-indigo-400/40 transition-colors"
>
<GitCompare size={9} />
{t('homeDashboard.intelCompare')}
</button>
<button
type="button"
disabled={dismissingInsightId === insight.id}
onClick={() => onDismissInsight(insight)}
className="p-1.5 rounded-lg border border-border/30 text-concrete hover:text-rose-500 ms-auto disabled:opacity-40"
title={t('homeDashboard.dismissConnection')}
>
{dismissingInsightId === insight.id
? <Loader2 size={10} className="animate-spin" />
: <X size={10} />}
</button>
</div>
</div>
)
}
case 'bridge': {
const { bridge } = item
const title = bridge.note?.title || t('homeDashboard.untitled')
const excerpt = bridge.note?.content ? stripHtml(bridge.note.content).slice(0, 160) : ''
const names = bridge.clusterNames ?? []
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex items-center gap-2">
<Zap size={12} className="text-ochre" />
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-ochre">
{t('homeDashboard.bridgeNote')}
</span>
</div>
<span className="text-[9px] font-mono font-bold text-ochre bg-ochre/10 px-2 py-0.5 rounded-full">
{Math.round(bridge.bridgeScore * 100)}%
</span>
</div>
<button
type="button"
onClick={() => onNoteSelect(bridge.noteId)}
className="text-start group flex-1"
>
<p className="text-sm font-semibold text-ink dark:text-dark-ink group-hover:text-ochre transition-colors mb-2 leading-snug">
{title}
</p>
{excerpt && (
<p className="text-[10px] text-concrete leading-relaxed line-clamp-3 mb-3">{excerpt}</p>
)}
{names.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{names.map((name, i) => (
<span
key={`${bridge.noteId}-${name}`}
className="inline-flex items-center gap-1 px-2 py-1 rounded-full border border-border/25 bg-white/70 dark:bg-zinc-900/50"
>
<span
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: CLUSTER_COLORS[i % CLUSTER_COLORS.length] }}
/>
<span className="text-[8px] font-mono uppercase text-concrete">{name}</span>
</span>
))}
</div>
)}
</button>
<button
type="button"
onClick={() => onNoteSelect(bridge.noteId)}
className="mt-3 inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg bg-ochre/90 text-white hover:bg-ochre transition-colors w-fit"
>
<ExternalLink size={9} />
{t('homeDashboard.intelOpenNote')}
</button>
</div>
)
}
case 'suggestion': {
const { suggestion } = item
const key = `${suggestion.clusterAId}-${suggestion.clusterBId}`
const busy = actingBridgeSuggestionKey === key
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 mb-3">
<Lightbulb size={12} className="text-violet-500" />
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-violet-600 dark:text-violet-400 truncate">
{t('homeDashboard.intelMissingLink')}
</span>
</div>
<div className="flex items-center gap-2 mb-3">
<span className="px-2 py-1 rounded-lg bg-violet-500/10 text-[9px] font-mono font-bold text-violet-700 dark:text-violet-300 truncate max-w-[45%]">
{suggestion.clusterAName}
</span>
<div className="flex-1 h-px bg-gradient-to-r from-violet-400/40 via-ochre/60 to-violet-400/40" />
<span className="px-2 py-1 rounded-lg bg-ochre/10 text-[9px] font-mono font-bold text-ochre truncate max-w-[45%]">
{suggestion.clusterBName}
</span>
</div>
<p className="text-sm font-semibold text-ink dark:text-dark-ink mb-1.5">{suggestion.suggestedTitle}</p>
<p className="text-[10px] text-concrete leading-relaxed line-clamp-2 flex-1">{suggestion.suggestedContent}</p>
<div className="flex items-center gap-1.5 mt-3 pt-3 border-t border-border/15">
<button
type="button"
disabled={busy}
onClick={() => onCreateBridgeSuggestion(suggestion)}
className="inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg bg-violet-600 text-white hover:bg-violet-700 transition-colors disabled:opacity-40"
>
{busy ? <Loader2 size={9} className="animate-spin" /> : <Link2 size={9} />}
{t('homeDashboard.createBridgeNote')}
</button>
<button
type="button"
disabled={busy}
onClick={() => onDismissBridgeSuggestion(suggestion)}
className="text-[8.5px] font-mono uppercase px-2 py-1.5 rounded-lg border border-border/30 text-concrete hover:text-rose-500 disabled:opacity-40"
>
{t('homeDashboard.dismiss')}
</button>
</div>
</div>
)
}
case 'agent': {
const { agent } = item
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-2 mb-3">
<Bot size={12} className="text-ochre" />
<span className="text-[9px] font-mono font-bold uppercase tracking-wider text-ochre">
{agent.agentName}
</span>
<span className="text-[8px] font-mono text-concrete/60 ms-auto">
{formatRelativeTime(agent.createdAt, t)}
</span>
</div>
{agent.result ? (
<p className="text-[11px] text-concrete leading-relaxed line-clamp-4 flex-1">
{agent.result}
</p>
) : (
<p className="text-[11px] text-concrete/60 italic flex-1">{t('homeDashboard.intelAgentNoResult')}</p>
)}
<button
type="button"
onClick={() => router.push('/agents')}
className="mt-3 inline-flex items-center gap-1 text-[8.5px] font-mono uppercase font-bold px-2.5 py-1.5 rounded-lg border border-ochre/30 text-ochre hover:bg-ochre/10 transition-colors w-fit"
>
{t('homeDashboard.intelViewAgent')}
<ArrowRight size={9} />
</button>
</div>
)
}
}
}
const spotlightAccent = (item: IntelItem | null) => {
if (!item) return 'from-stone-100/50 to-transparent border-border/30'
switch (item.kind) {
case 'insight': return 'from-indigo-500/[0.06] via-transparent to-transparent border-indigo-400/25'
case 'bridge': return 'from-ochre/[0.06] via-transparent to-transparent border-ochre/25'
case 'suggestion': return 'from-violet-500/[0.06] via-transparent to-transparent border-violet-400/25'
case 'agent': return 'from-ochre/[0.04] via-transparent to-transparent border-border/30'
}
}
return (
<section className="p-5 rounded-2xl bg-white dark:bg-zinc-900 border border-border/30 flex flex-col">
{/* Header */}
<DashboardWidgetTitleRow
widgetId="intelligence"
icon={(
<div className="w-5 h-5 rounded bg-brand-accent/10 flex items-center justify-center text-brand-accent shrink-0">
<Sparkles size={12} />
</div>
)}
title={t('homeDashboard.aiFound')}
actions={(
<>
{newCount > 0 && (
<span className="text-[8px] font-mono font-bold text-brand-accent bg-brand-accent/10 px-2 py-0.5 rounded uppercase">
{newCount} {t('homeDashboard.new')}
</span>
)}
{aiActive && (
<button
type="button"
onClick={onRefreshEcho}
disabled={echoRefreshing}
title={t('homeDashboard.analyzeNotes')}
className="p-1.5 rounded-lg border border-border/30 hover:border-brand-accent/40 hover:bg-brand-accent/5 transition-all disabled:opacity-40"
>
{echoRefreshing
? <Loader2 size={11} className="animate-spin text-brand-accent" />
: <RefreshCw size={11} className="text-concrete hover:text-brand-accent" />}
</button>
)}
</>
)}
/>
{loading ? (
<div className="h-[220px] rounded-2xl bg-stone-50 dark:bg-zinc-950/30 animate-pulse" />
) : !aiActive ? (
<div className="p-5 rounded-2xl border border-dashed border-border/40 bg-stone-50/50 dark:bg-zinc-950/30 text-center space-y-3 min-h-[180px] flex flex-col justify-center">
<p className="text-xs text-concrete leading-relaxed">
{!hasAiConsent
? t('homeDashboard.aiConsentRequired')
: !providerReady
? t('homeDashboard.aiProviderUnavailable')
: t('homeDashboard.memoryEchoDisabled')}
</p>
{!hasAiConsent && (
<button
type="button"
onClick={onEnableAi}
className="text-[9px] font-mono uppercase font-bold px-3 py-1.5 rounded-lg bg-ink text-white dark:bg-white dark:text-black hover:opacity-90 transition-opacity mx-auto"
>
{t('homeDashboard.enableAi')}
</button>
)}
</div>
) : allItems.length === 0 ? (
<div className="p-5 rounded-2xl border border-dashed border-border/40 bg-stone-50/50 dark:bg-zinc-950/30 text-center space-y-3 min-h-[180px] flex flex-col justify-center">
<Brain size={22} className="mx-auto text-concrete/40" strokeWidth={1.25} />
<p className="text-xs text-concrete leading-relaxed">{t('homeDashboard.noConnections')}</p>
<button
type="button"
onClick={onRefreshEcho}
disabled={echoRefreshing}
className="inline-flex items-center gap-1.5 text-[9px] font-mono uppercase font-bold px-3 py-1.5 rounded-lg border border-ochre/30 text-ochre hover:bg-ochre/10 transition-all disabled:opacity-40 mx-auto"
>
{echoRefreshing ? <Loader2 size={11} className="animate-spin" /> : <Sparkles size={11} />}
{t('homeDashboard.analyzeNotes')}
</button>
</div>
) : (
<>
{/* Filtres */}
<div className="flex gap-1 overflow-x-auto custom-scrollbar pb-1 -mx-0.5 px-0.5 mb-3">
{filters.map(f => {
const count = counts[f.key]
if (f.key !== 'all' && count === 0) return null
const active = filter === f.key
return (
<button
key={f.key}
type="button"
onClick={() => setFilter(f.key)}
className={`shrink-0 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[8.5px] font-mono font-bold uppercase tracking-wider transition-all ${
active
? 'bg-ink text-white dark:bg-white dark:text-black shadow-sm'
: 'bg-stone-100 dark:bg-zinc-800 text-concrete hover:text-ink dark:hover:text-dark-ink'
}`}
>
{f.label}
{count > 0 && (
<span className={`text-[7px] px-1 py-px rounded-full ${active ? 'bg-white/20' : 'bg-black/5 dark:bg-white/10'}`}>
{count}
</span>
)}
</button>
)
})}
</div>
{filteredItems.length === 0 ? (
<p className="text-xs text-concrete italic text-center py-8">{t('homeDashboard.intelFilterEmpty')}</p>
) : (
<>
{/* Spotlight carousel */}
<div className="relative">
{filteredItems.length > 1 && (
<>
<button
type="button"
onClick={goPrev}
disabled={activeIndex === 0}
className="absolute start-0 top-1/2 -translate-y-1/2 -translate-x-1 z-10 p-1 rounded-full border border-border/40 bg-white/90 dark:bg-zinc-900/90 shadow-sm disabled:opacity-25 hover:border-ochre/40 transition-all"
aria-label={t('homeDashboard.intelPrev')}
>
<ChevronLeft size={14} />
</button>
<button
type="button"
onClick={goNext}
disabled={activeIndex >= filteredItems.length - 1}
className="absolute end-0 top-1/2 -translate-y-1/2 translate-x-1 z-10 p-1 rounded-full border border-border/40 bg-white/90 dark:bg-zinc-900/90 shadow-sm disabled:opacity-25 hover:border-ochre/40 transition-all"
aria-label={t('homeDashboard.intelNext')}
>
<ChevronRight size={14} />
</button>
</>
)}
<div
className={`min-h-[200px] mx-5 p-4 rounded-2xl border bg-gradient-to-br ${spotlightAccent(activeItem)} transition-colors`}
>
<AnimatePresence mode="wait">
{activeItem && (
<motion.div
key={activeItem.id}
initial={slideVariants.initial}
animate={slideVariants.animate}
exit={slideVariants.exit}
transition={{ duration: prefersReducedMotion ? 0 : 0.22, ease: 'easeOut' }}
className="min-h-[180px] flex flex-col"
>
{renderSpotlight(activeItem)}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* Navigation dots + position */}
{filteredItems.length > 1 && (
<div className="flex items-center justify-center gap-3 mt-3">
<div className="flex items-center gap-1.5">
{filteredItems.map((item, idx) => (
<button
key={item.id}
type="button"
onClick={() => setActiveIndex(idx)}
className={`rounded-full transition-all ${
idx === activeIndex
? 'w-5 h-1.5 bg-ochre'
: 'w-1.5 h-1.5 bg-concrete/25 hover:bg-concrete/50'
}`}
aria-label={t('homeDashboard.intelGoTo', { index: idx + 1 })}
/>
))}
</div>
<span className="text-[8px] font-mono text-concrete/60 uppercase">
{t('homeDashboard.intelPosition', { current: activeIndex + 1, total: filteredItems.length })}
</span>
</div>
)}
{/* File d'attente compacte */}
{filteredItems.length > 1 && (
<div className="flex gap-1.5 mt-3 overflow-x-auto custom-scrollbar pt-1">
{filteredItems.map((item, idx) => {
const isActive = idx === activeIndex
let label = ''
let Icon = Sparkles
let accent = 'text-indigo-500'
if (item.kind === 'insight') {
label = item.insight.note1.title?.slice(0, 28) || t('homeDashboard.untitled')
Icon = Sparkles
accent = 'text-indigo-500'
} else if (item.kind === 'bridge') {
label = item.bridge.note?.title?.slice(0, 28) || t('homeDashboard.untitled')
Icon = Zap
accent = 'text-ochre'
} else if (item.kind === 'suggestion') {
label = item.suggestion.suggestedTitle.slice(0, 28)
Icon = Lightbulb
accent = 'text-violet-500'
} else {
label = item.agent.agentName
Icon = Bot
accent = 'text-ochre'
}
return (
<button
key={item.id}
type="button"
onClick={() => setActiveIndex(idx)}
className={`shrink-0 flex items-center gap-1.5 px-2 py-1.5 rounded-lg border text-start max-w-[130px] transition-all ${
isActive
? 'border-ochre/40 bg-ochre/5 shadow-sm'
: 'border-border/25 bg-stone-50/50 dark:bg-zinc-950/30 hover:border-border/50'
}`}
>
<Icon size={9} className={`shrink-0 ${accent}`} />
<span className="text-[8px] font-medium text-ink dark:text-dark-ink truncate">{label}</span>
</button>
)
})}
</div>
)}
</>
)}
</>
)}
</section>
)
}