feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s

- Sidebar: dynamic brand-accent colors, brainstorm section restyled
- AI chat general: popup panel with expand/collapse, hides when contextual AI open
- AI chat contextual: tabs reordered (Actions first), X close button, height fix
- Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.)
- Global color cleanup: emerald/orange hardcoded → brand-accent dynamic
- Brainstorm page: orange → brand-accent throughout
- PageEntry animation component added to key pages
- Floating AI button: bg-brand-accent instead of hardcoded black
- i18n: all 15 locales updated with new AI/billing keys
- Billing: freemium quota tracking, BYOK, stripe subscription scaffolding
- Admin: integrated into new design
- AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
Antigravity
2026-05-16 12:59:30 +00:00
parent 1fcea6ed7d
commit bd495be965
2284 changed files with 395285 additions and 2327 deletions

View File

@@ -1,12 +1,16 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import { useState, useRef, useEffect, useCallback } from 'react'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import type { UIMessage } from 'ai'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { X, Bot, Sparkles, History, Send, Globe, Briefcase, Palette, GraduationCap, Coffee, Loader2, BookOpen, Layers, Square, Plus } from 'lucide-react'
import {
X, Bot, Sparkles, History, Send, Globe, Briefcase, Palette, GraduationCap, Coffee,
Loader2, Layers, Square, Plus, ChevronRight, MessageSquare, FileCode,
Zap, Network, Clock, Scissors, Languages, Layout, ArrowRightLeft, BookOpen,
Maximize2, Minimize2
} from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { MarkdownContent } from '@/components/markdown-content'
import { useWebSearchAvailable } from '@/hooks/use-web-search-available'
@@ -14,6 +18,7 @@ import { useNotebooks } from '@/context/notebooks-context'
import { HierarchicalNotebookSelector } from '@/components/hierarchical-notebook-selector'
import { toast } from 'sonner'
import { createConversation } from '@/app/actions/chat-actions'
import { motion, AnimatePresence } from 'motion/react'
function getTextContent(msg: UIMessage): string {
if (msg.parts && Array.isArray(msg.parts)) {
@@ -23,36 +28,37 @@ function getTextContent(msg: UIMessage): string {
return ''
}
const TONES = [
{ id: 'professional', label: 'Professional', icon: Briefcase },
{ id: 'creative', label: 'Creative', icon: Palette },
{ id: 'academic', label: 'Academic', icon: GraduationCap },
{ id: 'casual', label: 'Casual', icon: Coffee },
]
const TONE_IDS = [
{ id: 'professional', icon: Briefcase },
{ id: 'creative', icon: Palette },
{ id: 'academic', icon: GraduationCap },
{ id: 'casual', icon: Coffee },
] as const
type AITab = 'discussion' | 'actions' | 'explore' | 'resources'
export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: boolean } = {}) {
const { t, language } = useLanguage()
const webSearchAvailable = useWebSearchAvailable()
const { notebooks } = useNotebooks()
const [isOpen, setIsOpen] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const [activeTab, setActiveTab] = useState('chat')
const [isContextualAIVisible, setIsContextualAIVisible] = useState(false)
const [aiTab, setAiTab] = useState<AITab>('discussion')
const [selectedTone, setSelectedTone] = useState('professional')
const [webSearch, setWebSearch] = useState(false)
const [chatScope, setChatScope] = useState<'all' | string>('all')
const [input, setInput] = useState('')
const [conversationId, setConversationId] = useState<string | undefined>()
const [isContextualAIVisible, setIsContextualAIVisible] = useState(false)
const [history, setHistory] = useState<any[]>([])
const [historyLoading, setHistoryLoading] = useState(false)
const [insights, setInsights] = useState<string>('')
const [insightsLoading, setInsightsLoading] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const transport = useRef(new DefaultChatTransport({ api: '/api/chat' })).current
const { messages, setMessages, sendMessage, status, stop } = useChat({
@@ -69,8 +75,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
const text = input.trim()
if (!text || isLoading) return
setInput('')
// Create conversation upfront so we have the ID for continuity
let convId = conversationId
if (!convId) {
try {
@@ -82,7 +86,6 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
return
}
}
try {
await sendMessage(
{ text },
@@ -125,9 +128,10 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
}
useEffect(() => {
if (activeTab === 'history') fetchHistory()
if (activeTab === 'insights' && !insights) fetchInsights()
}, [activeTab])
if (aiTab === 'discussion') {
// history is loaded on demand via insights tab
}
}, [aiTab])
useEffect(() => {
if (messagesEndRef.current) {
@@ -135,331 +139,456 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
}
}, [messages])
const handleToggle = useCallback(() => setIsOpen(v => !v), [])
const handleVisibility = useCallback((e: any) => setIsContextualAIVisible(e.detail), [])
useEffect(() => {
const handleToggle = () => setIsOpen(v => !v)
const handleVisibility = (e: any) => setIsContextualAIVisible(e.detail)
window.addEventListener('toggle-ai-chat', handleToggle)
window.addEventListener('contextual-ai-visibility', handleVisibility)
return () => {
window.removeEventListener('toggle-ai-chat', handleToggle)
window.removeEventListener('contextual-ai-visibility', handleVisibility)
}
}, [])
}, [handleToggle, handleVisibility])
// Floating trigger when closed
if (!isOpen) {
if (isContextualAIVisible) return null
if (!showFloatingTrigger) return null
if (!showFloatingTrigger || isContextualAIVisible) return null
return (
<Button
<motion.button
onClick={() => setIsOpen(true)}
className="fixed bottom-6 end-6 h-12 w-12 rounded-full shadow-xl z-40 transition-transform hover:scale-105 bg-muted text-foreground hover:bg-muted/80 border border-border"
size="icon"
className="fixed bottom-6 end-6 h-12 w-12 rounded-2xl shadow-xl z-40 bg-brand-accent text-white hover:scale-105 border border-brand-accent/20"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
title={t('ai.openAssistant')}
>
<Sparkles className="h-5 w-5 text-memento-accent" />
</Button>
<Sparkles className="h-5 w-5 mx-auto" />
</motion.button>
)
}
return (
<aside className={cn(
"fixed bottom-20 end-6 border border-border/40 bg-memento-paper dark:bg-background flex flex-col z-40 shadow-2xl rounded-2xl overflow-hidden transition-all duration-300",
isExpanded ? "w-[80vw] h-[85vh] max-w-[1200px]" : "h-[700px] max-h-[85vh] w-[360px]"
)}>
{/* Header */}
<div className="px-5 py-4 border-b border-border/40 flex items-center justify-between">
<div>
<h2 className="font-heading text-lg font-semibold text-foreground tracking-tight">{t('ai.assistantTitle')}</h2>
<p className="text-xs text-muted-foreground font-medium">{t('ai.poweredByMomento')}</p>
<motion.aside
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className={cn(
"fixed bottom-20 end-6 border border-border/60 bg-[#FDFCFB] dark:bg-[#0D0D0D] shadow-2xl flex flex-col z-50 rounded-2xl overflow-hidden transition-all duration-300",
isExpanded ? "w-[80vw] h-[85vh] max-w-[1200px]" : "w-[420px] h-[85vh] max-h-[800px]"
)}
>
<div className="flex flex-col h-full">
{/* Header */}
<div className="p-6 border-b border-border/60 space-y-1.5 bg-white/50 dark:bg-black/20 backdrop-blur-md shrink-0">
<div className="flex items-center justify-between">
<h3 className="flex items-center gap-2 font-serif text-xl font-medium text-ink">
<Sparkles size={18} className="text-brand-accent" />
{t('ai.assistantTitle')}
</h3>
<div className="flex items-center gap-1">
<button
onClick={() => setIsExpanded(e => !e)}
className="p-1.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg transition-colors text-concrete"
title={isExpanded ? t('ai.shrinkPanel') : t('ai.expandPanel')}
>
{isExpanded ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
</button>
<button
onClick={() => { setMessages([]); setConversationId(undefined) }}
className="p-1.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg transition-colors text-concrete"
title={t('ai.newDiscussion')}
>
<Plus size={16} />
</button>
<button
onClick={() => setIsOpen(false)}
className="p-1.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-lg transition-colors text-concrete"
>
<X size={18} />
</button>
</div>
</div>
<p className="text-[11px] text-concrete uppercase tracking-wider font-medium opacity-60">
{t('ai.poweredByMomento')}
</p>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" onClick={() => { setMessages([]); setConversationId(undefined) }} className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted" title={t('ai.newDiscussion')}>
<Plus className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => setIsExpanded(!isExpanded)} className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
{isExpanded ? (
<>
<polyline points="4 14 10 14 10 20"></polyline>
<polyline points="20 10 14 10 14 4"></polyline>
<line x1="14" y1="10" x2="21" y2="3"></line>
<line x1="3" y1="21" x2="10" y2="14"></line>
</>
) : (
<>
<polyline points="15 3 21 3 21 9"></polyline>
<polyline points="9 21 3 21 3 15"></polyline>
<line x1="21" y1="3" x2="14" y2="10"></line>
<line x1="3" y1="21" x2="10" y2="14"></line>
</>
)}
</svg>
</Button>
<Button variant="ghost" size="icon" onClick={() => setIsOpen(false)} className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted">
<X className="h-4 w-4" />
</Button>
{/* Tabs */}
<div className="flex border-b border-border px-2 shrink-0">
{(['discussion', 'actions', 'explore', 'resources'] as AITab[]).map((tab) => {
const labels: Record<AITab, string> = {
discussion: t('ai.chatTab') || 'Discussion',
actions: t('ai.actionsTab') || 'Actions',
explore: t('ai.exploreTab') || 'Explorer',
resources: t('ai.resourcesTab') || 'Ressources',
}
return (
<button
key={tab}
onClick={() => setAiTab(tab)}
className={cn(
"flex-1 py-3 text-[10px] uppercase tracking-[0.2em] font-bold transition-all relative",
aiTab === tab ? 'text-brand-accent' : 'text-concrete hover:text-ink/60'
)}
>
{labels[tab]}
{aiTab === tab && (
<motion.div
layoutId="activeAiTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-accent"
/>
)}
</button>
)
})}
</div>
</div>
{/* Navigation Tabs */}
<div className="flex px-4 pt-4 border-b border-border/40 shrink-0">
<button
onClick={() => setActiveTab('chat')}
className={cn(
"flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all",
activeTab === 'chat' ? "border-memento-blue text-memento-blue" : "border-transparent text-muted-foreground hover:text-foreground"
)}
>
<Bot className="h-4 w-4" /> {t('ai.chatTab')}
</button>
<button
onClick={() => setActiveTab('insights')}
className={cn(
"flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all",
activeTab === 'insights' ? "border-memento-blue text-memento-blue" : "border-transparent text-muted-foreground hover:text-foreground"
)}
>
<Sparkles className="h-4 w-4" /> {t('ai.insightsTab')}
</button>
<button
onClick={() => setActiveTab('history')}
className={cn(
"flex-1 pb-3 border-b-2 text-sm font-semibold flex items-center justify-center gap-2 transition-all",
activeTab === 'history' ? "border-memento-blue text-memento-blue" : "border-transparent text-muted-foreground hover:text-foreground"
)}
>
<History className="h-4 w-4" /> {t('ai.historyTab')}
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
<AnimatePresence mode="wait">
{aiTab === 'discussion' && (
<motion.div key="discussion" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-6">
{/* Context selector */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.writingTone')}</label>
<div className="flex items-center gap-1">
{TONE_IDS.map((tone) => {
const Icon = tone.icon
return (
<button
key={tone.id}
onClick={() => setSelectedTone(tone.id)}
title={t(`ai.tones.${tone.id}`)}
className={cn(
'w-8 h-8 rounded-lg flex items-center justify-center text-[9px] font-bold transition-all border',
selectedTone === tone.id
? 'bg-brand-accent text-white border-brand-accent shadow-sm'
: 'bg-white/50 dark:bg-white/5 border-border/40 text-concrete hover:border-brand-accent/40'
)}
>
<Icon className="h-3.5 w-3.5" />
</button>
)
})}
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto p-5 space-y-6">
{activeTab === 'chat' && (
<>
{/* AI Welcome Message */}
{messages.length === 0 && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-memento-blue/20 text-memento-blue flex items-center justify-center flex-shrink-0 border border-memento-blue/30 shadow-sm">
<Bot className="h-4 w-4" />
<button
onClick={() => setChatScope('all')}
className={cn(
'w-full p-2.5 border rounded-xl text-[11px] flex items-center justify-between transition-all',
chatScope === 'all' ? 'bg-brand-accent/5 border-brand-accent/30' : 'bg-white/50 dark:bg-white/5 border-border/40 hover:border-ink/20'
)}
>
<div className="flex items-center gap-2.5">
<FileCode size={14} className="text-brand-accent/60" />
<span className={cn('font-medium', chatScope === 'all' ? 'text-brand-accent' : 'text-concrete')}>
{t('ai.allMyNotes') || 'Toutes mes notes'}
</span>
</div>
{chatScope === 'all' && (
<span className="text-[8px] bg-brand-accent/10 text-brand-accent px-1.5 py-0.5 rounded-full uppercase font-bold">Auto</span>
)}
</button>
<div className="flex items-center gap-2 px-2">
<div className="h-px flex-1 bg-border/40" />
<span className="text-[9px] font-bold text-concrete uppercase tracking-widest">+ Carnet</span>
<div className="h-px flex-1 bg-border/40" />
</div>
<HierarchicalNotebookSelector
notebooks={notebooks.filter(nb => !nb.trashedAt)}
selectedId={chatScope !== 'all' ? chatScope : null}
onSelect={(id) => setChatScope(id)}
placeholder={t('ai.selectNotebook') || 'Inclure un carnet...'}
dropUp
/>
</div>
<div className="bg-memento-paper dark:bg-background border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm">
<p className="text-sm text-foreground leading-relaxed">
{t('ai.welcomeMsg')}
{/* Messages */}
{messages.length === 0 && (
<div className="h-48 flex flex-col items-center justify-center text-center space-y-3 text-concrete/30">
<div className="w-12 h-12 rounded-full border border-dashed border-concrete/10 flex items-center justify-center">
<MessageSquare size={18} />
</div>
<p className="text-[11px] italic leading-relaxed px-12">{t('ai.welcomeMsg')}</p>
</div>
)}
{messages.map((msg: UIMessage) => {
const text = getTextContent(msg)
if (msg.role === 'assistant' && !text) return null
return (
<div key={msg.id} className={cn('flex gap-3', msg.role === 'user' && 'flex-row-reverse')}>
<div className={cn(
'w-8 h-8 rounded-xl flex items-center justify-center flex-shrink-0',
msg.role === 'user' ? 'bg-ink text-paper' : 'bg-brand-accent/10 text-brand-accent',
)}>
{msg.role === 'user' ? 'U' : <Bot className="h-4 w-4" />}
</div>
<div className={cn(
'max-w-[85%] p-3.5 rounded-2xl text-sm leading-relaxed',
msg.role === 'user'
? 'bg-ink text-paper rounded-tr-sm'
: 'bg-white/60 dark:bg-white/5 border border-border rounded-tl-sm text-ink',
)}>
{msg.role === 'assistant' ? <MarkdownContent content={text} /> : <p>{text}</p>}
</div>
</div>
)
})}
{isLoading && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-xl bg-brand-accent/10 text-brand-accent flex items-center justify-center flex-shrink-0">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
<div className="bg-white/60 dark:bg-white/5 border border-border p-3.5 rounded-2xl rounded-tl-sm">
<Loader2 className="h-4 w-4 animate-spin text-concrete" />
</div>
</div>
)}
<div ref={messagesEndRef} />
</motion.div>
)}
{aiTab === 'actions' && (
<motion.div key="actions" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-8">
<div className="flex items-center gap-2 mb-2">
<div className="h-px flex-1 bg-border/40" />
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">Transformations</h4>
<div className="h-px flex-1 bg-border/40" />
</div>
<div className="grid grid-cols-2 gap-2">
{[
{ icon: <Sparkles size={14} />, label: t('ai.actionClarify') || 'Clarifier' },
{ icon: <Scissors size={14} />, label: t('ai.actionShorten') || 'Raccourcir' },
{ icon: <Zap size={14} />, label: t('ai.actionImprove') || 'Améliorer' },
{ icon: <Languages size={14} />, label: t('ai.actionTranslate') || 'Traduire' },
].map((action, i) => (
<button key={i} className="flex flex-col items-center gap-3 p-4 bg-white/50 dark:bg-white/5 border border-border rounded-xl transition-all group hover:border-ink/20">
<div className="p-2 rounded-lg bg-slate-50 dark:bg-white/10 transition-colors group-hover:bg-brand-accent group-hover:text-white shadow-sm text-concrete">
{action.icon}
</div>
<span className="text-[10px] font-bold text-ink/80 uppercase tracking-widest">{action.label}</span>
</button>
))}
<button className="col-span-2 flex items-center justify-center gap-3 py-3 px-4 bg-white/50 dark:bg-white/5 border border-border rounded-xl text-[10px] font-bold text-ink/80 hover:bg-white dark:hover:bg-white/10 transition-colors hover:border-ink/20 uppercase tracking-widest">
<FileCode size={14} className="text-concrete" />
{t('ai.actionMarkdown') || 'Convertir en Markdown'}
</button>
</div>
<div className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<div className="h-px flex-1 bg-border/40" />
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">Generation Tools</h4>
<div className="h-px flex-1 bg-border/40" />
</div>
<div className="group relative p-6 rounded-2xl bg-white border border-border hover:border-brand-accent/30 transition-all duration-500 overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<Layout size={80} className="text-brand-accent" />
</div>
<div className="relative space-y-5">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-50 rounded-lg text-brand-accent"><Layout size={18} /></div>
<div className="space-y-0.5">
<h5 className="text-sm font-bold text-ink leading-none">{t('ai.slides') || 'Présentation'}</h5>
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.slidesDesc') || 'Convertir en slides interactives'}</p>
</div>
</div>
<button className="w-full py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-brand-accent/20 uppercase tracking-[0.2em]">
{t('ai.generate') || 'Générer'} <ArrowRightLeft size={14} className="opacity-60" />
</button>
</div>
</div>
<div className="group relative p-6 rounded-2xl bg-white border border-border hover:border-emerald-500/30 transition-all duration-500 overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<BookOpen size={80} className="text-emerald-500" />
</div>
<div className="relative space-y-5">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-50 rounded-lg text-emerald-500"><BookOpen size={18} /></div>
<div className="space-y-0.5">
<h5 className="text-sm font-bold text-ink leading-none">{t('ai.diagram') || 'Diagramme'}</h5>
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.diagramDesc') || 'Visualisation de structure'}</p>
</div>
</div>
<button className="w-full py-3.5 bg-emerald-600 text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-emerald-600/20 uppercase tracking-[0.2em]">
{t('ai.trace') || 'Tracer'} <ArrowRightLeft size={14} className="opacity-60" />
</button>
</div>
</div>
</div>
<div className="flex flex-col items-center gap-2 opacity-20 py-4">
<History size={16} />
<span className="text-[10px] font-bold uppercase tracking-widest whitespace-nowrap">Auto-Save Enabled</span>
</div>
</motion.div>
)}
{aiTab === 'explore' && (
<motion.div key="explore" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-6">
<div className="flex items-center gap-2 mb-2">
<div className="h-px flex-1 bg-border/40" />
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-concrete whitespace-nowrap">Intelligence Modules</h4>
<div className="h-px flex-1 bg-border/40" />
</div>
<div className="space-y-3">
<button className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-brand-accent/30 transition-all text-left overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<Zap size={60} className="text-brand-accent" />
</div>
<div className="relative flex items-center gap-4">
<div className="p-3 bg-brand-accent/10 rounded-xl text-brand-accent group-hover:bg-brand-accent group-hover:text-white transition-colors">
<Zap size={20} />
</div>
<div>
<h5 className="font-bold text-ink text-sm">{t('ai.brainstormWave') || 'Brainstorm Wave'}</h5>
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.brainstormWaveDesc') || 'Unfold dimensions of thought'}</p>
</div>
</div>
</button>
<button className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-indigo-500/30 transition-all text-left overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<Network size={60} className="text-indigo-500" />
</div>
<div className="relative flex items-center gap-4">
<div className="p-3 bg-indigo-500/10 rounded-xl text-indigo-500 group-hover:bg-indigo-500 group-hover:text-white transition-colors">
<Network size={20} />
</div>
<div>
<h5 className="font-bold text-ink text-sm">{t('ai.semanticNetwork') || 'Réseau Sémantique'}</h5>
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.semanticNetworkDesc') || 'Detect clusters and bridges'}</p>
</div>
</div>
</button>
<button className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-rose-500/30 transition-all text-left overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
<Clock size={60} className="text-rose-500" />
</div>
<div className="relative flex items-center gap-4">
<div className="p-3 bg-rose-500/10 rounded-xl text-rose-500 group-hover:bg-rose-500 group-hover:text-white transition-colors">
<Clock size={20} />
</div>
<div>
<h5 className="font-bold text-ink text-sm">{t('ai.temporalForecast') || 'Prévision Temporelle'}</h5>
<p className="text-[10px] text-concrete uppercase tracking-tight">{t('ai.temporalForecastDesc') || 'Predict relevance recurrence'}</p>
</div>
</div>
</button>
</div>
<div className="p-6 rounded-2xl bg-slate-50 dark:bg-white/5 border border-dashed border-border">
<p className="text-[10px] text-concrete leading-relaxed font-medium italic text-center">
{t('ai.modulesHint') || 'Ces modules utilisent les embeddings pour analyser vos pensées.'}
</p>
</div>
</div>
</motion.div>
)}
{/* Messages */}
{messages.map((msg: UIMessage) => {
const text = getTextContent(msg)
// Skip empty assistant messages (thinking/reasoning phase)
if (msg.role === 'assistant' && !text) return null
return (
<div key={msg.id} className={cn('flex gap-3', msg.role === 'user' && 'flex-row-reverse')}>
<div className={cn(
'w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 border text-[10px] font-bold',
msg.role === 'user'
? 'bg-muted border-border text-muted-foreground'
: 'bg-memento-blue/20 text-memento-blue border-memento-blue/30 shadow-sm',
)}>
{msg.role === 'user' ? 'U' : <Bot className="h-4 w-4" />}
</div>
<div className={cn(
'max-w-[85%] p-3.5 rounded-2xl text-sm leading-relaxed shadow-sm',
msg.role === 'user'
? 'bg-memento-blue text-white rounded-tr-sm'
: 'bg-memento-paper dark:bg-background border border-border/50 rounded-tl-sm text-foreground',
)}>
{msg.role === 'assistant'
? <MarkdownContent content={text} />
: <p>{text}</p>}
{aiTab === 'resources' && (
<motion.div key="resources" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resourceUrl') || 'URL (Optionnel)'}</label>
<div className="relative">
<input type="text" placeholder="https://..." className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl pl-4 pr-10 py-3 text-xs outline-none focus:border-brand-accent transition-colors text-ink" />
<Globe size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-concrete/40" />
</div>
</div>
)
})}
{isLoading && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-memento-blue/10 text-memento-blue flex items-center justify-center flex-shrink-0 border border-memento-blue/20">
<Bot className="h-4 w-4" />
<div className="space-y-2">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.resourceText') || 'Texte de la ressource'}</label>
<textarea
rows={8}
placeholder={t('ai.resourcePlaceholder') || 'Collez votre texte ici...'}
className="w-full bg-white/50 dark:bg-white/5 border border-border rounded-xl p-4 text-xs outline-none focus:border-brand-accent transition-colors resize-none leading-relaxed text-ink"
/>
</div>
<div className="bg-memento-paper dark:bg-background border border-border/50 p-3.5 rounded-2xl rounded-tl-sm shadow-sm">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
<div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-concrete">{t('ai.integrationMode') || "Mode d'intégration"}</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'replace', label: t('ai.modeReplace') || 'Remplacer', sub: 'Direct' },
{ id: 'append', label: t('ai.modeAppend') || 'Compléter', sub: 'Ajoute' },
{ id: 'merge', label: t('ai.modeMerge') || 'Fusionner', sub: 'Intègre' },
].map((mode) => (
<button key={mode.id} className="flex flex-col items-center justify-center p-3 rounded-xl border transition-all text-center bg-white border-border hover:bg-slate-50 dark:hover:bg-white/5">
<span className="text-[10px] font-bold text-ink">{mode.label}</span>
<span className="text-[8px] text-concrete opacity-60 leading-tight mt-1 font-medium">{mode.sub}</span>
</button>
))}
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</>
)}
{activeTab === 'insights' && (
<div className="h-full">
<h3 className="text-sm font-semibold mb-4 flex items-center gap-2"><Sparkles className="h-4 w-4 text-memento-accent" /> {t('ai.summaryLast5')}</h3>
{insightsLoading ? (
<div className="flex flex-col items-center justify-center py-10 opacity-60">
<Loader2 className="h-8 w-8 animate-spin mb-4 text-muted-foreground" />
<p className="text-xs text-muted-foreground">{t('ai.analyzingProgress')}</p>
</div>
) : insights ? (
<div className="prose prose-sm dark:prose-invert">
<MarkdownContent content={insights} />
</div>
) : (
<div className="flex justify-center mt-6">
<Button onClick={fetchInsights} variant="outline" size="sm">{t('ai.generateInsightsBtn')}</Button>
</div>
)}
</div>
)}
{activeTab === 'history' && (
<div className="space-y-3">
{historyLoading ? (
<div className="flex justify-center py-10 opacity-60">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : history.length > 0 ? (
history.map(conv => (
<button
key={conv.id}
className="w-full text-start p-3 rounded-xl border border-border/50 hover:bg-muted/50 hover:border-memento-blue/30 transition-all flex flex-col gap-1"
onClick={() => {
setConversationId(conv.id)
setMessages(conv.messages.map((m: any) => ({
id: m.id,
role: m.role,
content: m.content,
parts: [{ type: 'text', text: m.content }]
})))
setActiveTab('chat')
}}
>
<span className="text-sm font-medium line-clamp-1">{conv.title || conv.messages[0]?.content || t('ai.newDiscussion')}</span>
<span className="text-[10px] text-muted-foreground">{new Date(conv.updatedAt).toLocaleString()}</span>
<button className="w-full py-4 bg-brand-accent text-white rounded-xl text-[11px] font-bold uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-brand-accent/20">
<Sparkles size={18} />
{t('ai.generatePreview') || "Générer l'aperçu"}
</button>
))
) : (
<div className="flex flex-col items-center justify-center py-10 opacity-60 text-center space-y-2">
<History className="h-8 w-8 text-muted-foreground" />
<p className="text-sm">{t('ai.noRecentConversations')}</p>
</div>
</motion.div>
)}
</div>
)}
</div>
{/* Input Area & Tone Controls (Only in Chat tab) */}
<div className={cn("p-4 border-t border-border/40 bg-memento-paper dark:bg-background shrink-0", activeTab !== 'chat' && "hidden")}>
{/* Context Scope */}
<div className="mb-3 space-y-2">
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block ms-1">Source du Contexte</span>
<button
onClick={() => setChatScope('all')}
className={cn(
'w-full p-2.5 border rounded-lg text-xs flex items-center justify-between transition-all',
chatScope === 'all' ? 'bg-memento-blue/15 border-memento-blue/40 shadow-inner' : 'bg-card border-border hover:border-foreground/20'
)}
>
<div className="flex items-center gap-2">
<Layers className="h-3.5 w-3.5 text-memento-blue/70" />
<span className={cn('font-bold', chatScope === 'all' ? 'text-memento-blue' : 'text-foreground/60')}>
{t('ai.allMyNotes') || 'Toutes mes notes'}
</span>
</div>
{chatScope === 'all' && (
<span className="text-[8px] bg-memento-blue/20 text-memento-blue px-1.5 py-0.5 rounded uppercase font-bold border border-memento-blue/30">Auto</span>
)}
</button>
<div className="flex items-center gap-2 px-2">
<div className="h-px flex-1 bg-border/40" />
<span className="text-[9px] font-bold text-muted-foreground uppercase tracking-widest">+ Carnet</span>
<div className="h-px flex-1 bg-border/40" />
</div>
<HierarchicalNotebookSelector
notebooks={notebooks.filter(nb => !nb.trashedAt)}
selectedId={chatScope !== 'all' ? chatScope : null}
onSelect={(id) => setChatScope(id)}
placeholder={t('ai.selectNotebook') || 'Inclure un carnet...'}
dropUp
/>
</AnimatePresence>
</div>
{/* Tone Selection */}
<div className="mb-3">
<span className="text-[9px] font-bold uppercase tracking-widest text-muted-foreground block mb-1.5 ms-1">{t('ai.writingTone')}</span>
<div className="grid grid-cols-4 gap-1">
{TONES.map(tone => {
const Icon = tone.icon
const isSelected = selectedTone === tone.id
return (
<button
key={tone.id}
onClick={() => setSelectedTone(tone.id)}
title={tone.label}
className={cn(
"py-1 rounded-md border text-[10px] font-medium transition-all flex flex-col items-center justify-center gap-0.5",
isSelected
? "border-memento-blue bg-memento-blue/15 text-memento-blue shadow-sm font-bold"
: "border-border/60 bg-memento-paper dark:bg-background text-muted-foreground hover:bg-muted hover:border-border"
)}
>
<Icon className="h-3 w-3" />
<span className="hidden sm:inline text-[9px]">{tone.label.slice(0, 4)}.</span>
</button>
)
})}
</div>
</div>
{/* Text Input */}
<div className="relative bg-memento-paper dark:bg-background border border-border/60 rounded-xl p-1 focus-within:border-memento-blue focus-within:ring-1 focus-within:ring-memento-blue/20 transition-all shadow-sm">
<textarea
className="w-full bg-transparent border-none focus:ring-0 resize-none text-sm text-foreground placeholder:text-muted-foreground/70 p-2 min-h-[60px] max-h-[120px]"
placeholder={t('ai.chatPlaceholder')}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() }
}}
disabled={isLoading}
/>
<div className="flex justify-between items-center px-1 pb-1">
<Button
type="button"
variant="ghost"
size="sm"
className={cn("h-7 px-2 text-[10px] gap-1", webSearch ? "bg-emerald-50 text-emerald-600 hover:bg-emerald-100 hover:text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400" : "text-muted-foreground hover:text-foreground")}
onClick={() => webSearchAvailable && setWebSearch(!webSearch)}
disabled={!webSearchAvailable}
title={webSearchAvailable ? t('ai.webSearchLabel') : t('ai.webSearchNotConfigured')}
{/* Textarea — only on discussion tab */}
<AnimatePresence>
{aiTab === 'discussion' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="p-6 bg-white/40 dark:bg-black/20 border-t border-border backdrop-blur-xl shrink-0"
>
<Globe className="h-3.5 w-3.5" />
Web{webSearchAvailable ? '' : ' ⚠'}
</Button>
{isLoading ? (
<Button
size="icon"
className="h-8 w-8 rounded-lg bg-red-500 text-white shadow-sm hover:bg-red-600 transition-all"
onClick={() => stop()}
>
<Square className="h-4 w-4" />
</Button>
) : (
<Button
size="icon"
className="h-8 w-8 rounded-lg bg-memento-blue text-white shadow-sm hover:shadow-md transition-all"
onClick={handleSend}
disabled={!input.trim()}
>
<Send className="h-4 w-4 ms-0.5" />
</Button>
)}
</div>
</div>
<div className="relative group/chat">
<textarea
rows={4}
placeholder={t('ai.chatPlaceholder')}
className="w-full bg-white/80 dark:bg-white/5 border border-border rounded-[24px] p-5 pr-14 text-sm outline-none focus:border-brand-accent focus:ring-4 ring-brand-accent/5 transition-all resize-none leading-relaxed font-light shadow-inner text-ink"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() }
}}
disabled={isLoading}
/>
<div className="absolute right-4 bottom-4 flex flex-col gap-2">
{isLoading ? (
<button onClick={() => stop()} className="p-2.5 bg-rose-500 text-white rounded-xl transition-all hover:scale-110 active:scale-95 shadow-lg shadow-rose-500/20">
<Square size={16} />
</button>
) : (
<button onClick={handleSend} disabled={!input.trim()} className="p-2.5 bg-brand-accent text-white rounded-xl transition-all hover:scale-110 active:scale-95 shadow-lg shadow-brand-accent/20 disabled:opacity-40 disabled:pointer-events-none">
<Send size={16} />
</button>
)}
</div>
<div className="absolute left-6 bottom-4 flex gap-3 text-concrete/40">
<button
onClick={() => webSearchAvailable && setWebSearch(!webSearch)}
className={cn("hover:text-brand-accent transition-colors", webSearch && "text-brand-accent")}
>
<Globe size={14} />
</button>
<button className="hover:text-brand-accent transition-colors"><Network size={14} /></button>
</div>
</div>
<div className="flex justify-center mt-4">
<p className="text-[9px] text-concrete/40 uppercase tracking-[0.3em] font-bold">Shift+Enter for new line</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</aside>
</motion.aside>
)
}