feat: migrate semantic search to pgvector + full-text search
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m12s

Replace JSON-string embeddings with native pgvector(1536) storage and
add PostgreSQL full-text search (tsvector/GIN) with Reciprocal Rank Fusion
for hybrid keyword + semantic ranking.

Changes:
- NoteEmbedding.embedding: String → vector(1536) via pgvector
- NoteEmbedding: added updatedAt for reindex tracking
- Note: added tsv (tsvector) with auto-update trigger for FTS
- semantic-search.service: hybrid FTS + vector search with RRF fusion
- embedding.service: toVectorString() for pgvector SQL literals
- Removed JS-side cosine similarity loops (now DB-side via <=>)
- Added HNSW index on NoteEmbedding.embedding (cosine distance)
- Added GIN index on Note.tsv for FTS queries

Schema migration in: prisma/migrations/20260512120000_pgvector_and_fts_search/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antigravity
2026-05-12 07:03:56 +00:00
parent 92c3a6f307
commit 03e6a62b80
43 changed files with 4024 additions and 786 deletions

View File

@@ -0,0 +1,379 @@
import React from 'react';
import {
Sparkles,
ChevronRight,
MessageSquare,
FileCode,
Globe,
Send,
Scissors,
Zap,
Languages,
Layout,
ArrowRightLeft,
BookOpen,
History,
Target
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { AITab, AITone, Note, Carnet } from '../types';
import { HierarchicalCarnetSelector } from './HierarchicalCarnetSelector';
interface AISidebarProps {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
activeNote: Note | undefined;
aiTab: AITab;
setAiTab: (tab: AITab) => void;
selectedTone: AITone;
setSelectedTone: (tone: AITone) => void;
carnets: Carnet[];
}
export const AISidebar: React.FC<AISidebarProps> = ({
isOpen,
setIsOpen,
activeNote,
aiTab,
setAiTab,
selectedTone,
setSelectedTone,
carnets
}) => {
const [selectedContextId, setSelectedContextId] = React.useState<string | null>(null);
return (
<AnimatePresence>
{isOpen && (
<motion.aside
initial={{ x: 400, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 400, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="w-[400px] border-l border-border bg-white shadow-2xl flex flex-col z-50 shrink-0 relative"
>
<div className="p-6 border-b border-border space-y-2">
<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-ochre" />
IA Assistant
</h3>
<button
onClick={() => setIsOpen(false)}
className="p-1 hover:bg-slate-100 rounded-full transition-colors text-muted-ink"
>
<ChevronRight size={20} />
</button>
</div>
<p className="text-[11px] text-muted-ink uppercase tracking-wider font-medium opacity-60 truncate">
"{activeNote?.title}"
</p>
</div>
<div className="flex border-b border-border px-2">
{(['discussion', 'actions', 'resources'] as AITab[]).map((tab) => (
<button
key={tab}
onClick={() => setAiTab(tab)}
className={`flex-1 py-3 text-[10px] uppercase tracking-[0.2em] font-bold transition-all relative
${aiTab === tab ? 'text-manganese' : 'text-muted-ink hover:text-ink/60'}`}
>
{tab}
{aiTab === tab && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-ochre"
/>
)}
</button>
))}
</div>
<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-8"
>
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4 text-muted-ink/40">
<div className="w-16 h-16 rounded-full border border-dashed border-muted-ink/20 flex items-center justify-center">
<MessageSquare size={24} />
</div>
<p className="text-xs font-serif italic leading-relaxed px-8">Posez une question à l'Assistant pour commencer.</p>
</div>
<div className="space-y-4">
<div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-ink">Source du Contexte</label>
<div className="space-y-3">
<div className="w-full p-3 bg-glass border border-border rounded-lg text-xs flex items-center justify-between cursor-default backdrop-blur-sm">
<div className="flex items-center gap-2">
<FileCode size={14} className="text-blueprint" />
<span className="font-medium text-ink">Note Active</span>
</div>
<div className="text-[8px] bg-blueprint/10 text-blueprint px-1.5 py-0.5 rounded uppercase font-bold tracking-tighter italic">Auto</div>
</div>
<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>
<HierarchicalCarnetSelector
carnets={carnets}
selectedId={selectedContextId}
onSelect={setSelectedContextId}
placeholder="Inclure un carnet..."
className="w-full"
/>
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-ink">Ton d'écriture</label>
<div className="grid grid-cols-2 gap-2">
{(['Professional', 'Creative', 'Academic', 'Casual'] as AITone[]).map((tone) => (
<button
key={tone}
onClick={() => setSelectedTone(tone)}
className={`p-3 rounded-xl border text-[11px] font-medium transition-all
${selectedTone === tone ? 'bg-manganese text-paper border-manganese shadow-lg shadow-manganese/10' : 'bg-glass border-border text-muted-ink hover:border-ink/20'}`}
>
{tone.toUpperCase().substring(0, 3)}
</button>
))}
</div>
</div>
</div>
</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="space-y-8">
<div>
<div className="flex items-center gap-2 mb-4">
<div className="h-px flex-1 bg-border/40" />
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-muted-ink 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: 'Clarifier', color: 'ochre' },
{ icon: <Scissors size={14} />, label: 'Raccourcir', color: 'rust' },
{ icon: <Zap size={14} />, label: 'Améliorer', color: 'sage' },
{ icon: <Languages size={14} />, label: 'Traduire', color: 'slate' },
].map((action, i) => (
<button
key={i}
className="flex flex-col items-center gap-3 p-4 bg-glass 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-manganese group-hover:text-paper shadow-sm text-ink/60`}>
{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-glass border border-border rounded-xl text-[10px] font-bold text-ink/80 hover:bg-slate-50 dark:hover:bg-white/10 transition-colors hover:border-ink/20 uppercase tracking-widest">
<FileCode size={14} className="text-muted-ink" />
Convertir en Markdown
</button>
</div>
</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-muted-ink 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-blueprint/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-blueprint" />
</div>
<div className="relative space-y-5">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-50 rounded-lg text-blueprint">
<Layout size={18} />
</div>
<div className="space-y-0.5">
<h5 className="text-sm font-bold text-ink leading-none">Présentation</h5>
<p className="text-[10px] text-muted-ink uppercase tracking-tight">Convertir en slides interactives</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<span className="text-[9px] uppercase tracking-[0.2em] font-bold text-muted-ink/60 px-1">Thème</span>
<select className="w-full bg-glass dark:bg-black/20 border border-border rounded-lg px-2 py-2 text-xs outline-none focus:ring-1 ring-blueprint/10 transition-all cursor-pointer">
<option>Architectural Mono</option>
<option>Vibrant Tech</option>
<option>Minimal Silk</option>
</select>
</div>
<div className="space-y-1.5">
<span className="text-[9px] uppercase tracking-[0.2em] font-bold text-muted-ink/60 px-1">Style</span>
<select className="w-full bg-glass dark:bg-black/20 border border-border rounded-lg px-2 py-2 text-xs outline-none focus:ring-1 ring-blueprint/10 transition-all cursor-pointer">
<option>Professional</option>
<option>Creative</option>
<option>Brutalist</option>
</select>
</div>
</div>
<button className="w-full py-3.5 bg-blueprint text-paper rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-blueprint/20 uppercase tracking-[0.2em]">
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-sage/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-sage" />
</div>
<div className="relative space-y-5">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-50 rounded-lg text-sage">
<BookOpen size={18} />
</div>
<div className="space-y-0.5">
<h5 className="text-sm font-bold text-ink leading-none">Diagramme</h5>
<p className="text-[10px] text-muted-ink uppercase tracking-tight">Visualisation de structure</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<span className="text-[9px] uppercase tracking-[0.2em] font-bold text-muted-ink/60 px-1">Type</span>
<select className="w-full bg-glass dark:bg-black/20 border border-border rounded-lg px-2 py-2 text-xs outline-none focus:ring-1 ring-sage/10 transition-all cursor-pointer">
<option>Logic Flow</option>
<option>Mind Map</option>
<option>Hierarchy</option>
</select>
</div>
<div className="space-y-1.5">
<span className="text-[9px] uppercase tracking-[0.2em] font-bold text-muted-ink/60 px-1">Style</span>
<select className="w-full bg-glass dark:bg-black/20 border border-border rounded-lg px-2 py-2 text-xs outline-none focus:ring-1 ring-sage/10 transition-all cursor-pointer">
<option>Draft</option>
<option>Polished</option>
<option>Handwritten</option>
</select>
</div>
</div>
<button className="w-full py-3.5 bg-sage text-paper rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-sage/20 uppercase tracking-[0.2em]">
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>
</div>
</motion.div>
)}
{aiTab === 'resources' && (
<motion.div
key="resources"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-8"
>
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-ink">URL (Optionnel)</label>
<div className="relative">
<input type="text" placeholder="https://..." className="w-full bg-glass border border-border rounded-lg pl-3 pr-10 py-3 text-xs outline-none focus:border-blueprint transition-colors" />
<Globe size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-ink/40" />
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-ink">Texte de la ressource</label>
<textarea
rows={8}
placeholder="Collez votre texte ici (markdown, HTML, texte brut...)"
className="w-full bg-glass border border-border rounded-lg p-4 text-xs outline-none focus:border-blueprint transition-colors resize-none leading-relaxed"
/>
</div>
<div className="space-y-3">
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-ink">Mode d'intégration</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'replace', label: 'Remplacer', sub: 'Direct, sans IA' },
{ id: 'append', label: 'Compléter', sub: 'Ajoute sans réécrire' },
{ id: 'merge', label: 'Fusionner', sub: 'Réécrit et intègre' },
].map((mode) => (
<button key={mode.id} className={`flex flex-col items-center justify-center p-3 rounded-lg border transition-all text-center ${mode.id === 'append' ? 'bg-sage/10 border-sage/50 ring-1 ring-sage/10' : 'bg-white border-border hover:bg-slate-50'}`}>
<span className={`text-[11px] font-bold ${mode.id === 'append' ? 'text-sage' : 'text-ink'}`}>{mode.label}</span>
<span className="text-[8px] text-muted-ink opacity-60 leading-tight mt-1 font-medium">{mode.sub}</span>
</button>
))}
</div>
</div>
<button className="w-full py-4 bg-blueprint text-white rounded-xl text-sm font-bold flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-blueprint/20">
<Sparkles size={18} />
Générer l'aperçu
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{aiTab === 'discussion' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="p-6 bg-white border-t border-border"
>
<div className="relative">
<textarea
rows={3}
placeholder="Posez une question sur cette note..."
className="w-full bg-glass backdrop-blur-sm border border-border rounded-2xl p-4 pr-12 text-sm outline-none focus:border-blueprint transition-colors resize-none leading-relaxed font-light"
/>
<div className="absolute right-3 bottom-3 flex gap-2">
<button className="p-2 text-muted-ink hover:text-ink rounded-lg transition-colors">
<Globe size={16} />
</button>
<button className="p-2 bg-blueprint text-white rounded-lg transition-transform hover:scale-105 active:scale-95 shadow-lg shadow-blueprint/10">
<Send size={16} />
</button>
</div>
</div>
<p className="text-[9px] text-muted-ink text-center mt-3 uppercase tracking-widest font-bold opacity-30 italic">Maj+Entrée = nouvelle ligne</p>
</motion.div>
)}
</AnimatePresence>
</motion.aside>
)}
</AnimatePresence>
);
};

View File

@@ -0,0 +1,325 @@
import React from 'react';
import {
Plus,
ArrowLeft,
Clock,
Activity,
Trash2,
Edit3,
Play,
Eye,
Microscope,
Globe,
Layers,
Zap,
BookOpen,
Sparkles,
ChevronDown,
Info,
Check
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { Carnet, Note } from '../types';
import { HierarchicalCarnetSelector } from './HierarchicalCarnetSelector';
interface AgentsViewProps {
selectedAgentId: string | null;
setSelectedAgentId: (id: string | null) => void;
carnets: Carnet[];
}
export const AgentsView: React.FC<AgentsViewProps> = ({
selectedAgentId,
setSelectedAgentId,
carnets
}) => {
const [selectedCarnetForAgent, setSelectedCarnetForAgent] = React.useState<string | null>('4');
const [agentType, setAgentType] = React.useState<'Surveillant' | 'Personnalisé' | 'Slides' | 'Diagramme'>('Diagramme');
return (
<div className="h-full flex flex-col overflow-y-auto custom-scrollbar bg-[#F9F8F6] dark:bg-dark-paper space-y-12">
{!selectedAgentId ? (
<>
<header className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-paper/80 backdrop-blur-md z-30">
<div className="flex justify-between items-end">
<div className="space-y-1">
<h1 className="text-4xl font-serif font-medium tracking-tight text-ink">Mes Agents</h1>
<p className="text-sm text-muted-ink font-light">Automatisez vos tâches de veille et de recherche.</p>
</div>
<button className="px-6 py-2.5 bg-ink text-paper text-sm font-medium rounded-xl hover:opacity-90 transition-all flex items-center gap-3 shadow-lg shadow-ink/10">
<Plus size={18} />
Nouvel Agent
</button>
</div>
<div className="flex items-center gap-8 border-b border-ink/5 pt-4">
{['Tous', 'Veilleur', 'Chercheur', 'Surveillant', 'Personnalisé'].map((tag, i) => (
<button key={i} className={`pb-4 text-xs font-bold uppercase tracking-widest transition-all relative ${i === 0 ? 'text-ink' : 'text-muted-ink hover:text-ink/60'}`}>
{tag}
{i === 0 && <motion.div layoutId="activeAgentTag" className="absolute bottom-0 left-0 right-0 h-0.5 bg-ink" />}
</button>
))}
</div>
</header>
<div className="px-12 flex-1 pb-20 space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ id: 'a1', icon: <Eye size={20} className="text-amber-600" />, title: 'Surveillant de Notes', status: 'Réussi', type: 'SURVEILLANT', meta: 'Hebdomadaire • 6 exéc.', desc: 'Analyse les notes récentes dun carnet et suggère des compléments, références et liens.' },
{ id: 'a2', icon: <Microscope size={20} className="text-indigo-600" />, title: 'Chercheur de Sujet', status: 'Réussi', type: 'CHERCHEUR', meta: 'Hebdomadaire • 14 exéc.', desc: 'Recherche des informations approfondies sur les derniers modèles de Deepseek et voir lavis des utilisateurs.' },
{ id: 'a3', icon: <Globe size={20} className="text-emerald-600" />, title: 'Veille IA', status: 'Réussi', type: 'VEILLEUR', meta: 'Quotidien • 20 exéc.', desc: 'Scrape les flux RSS de 6 sites IA (The Verge, TechCrunch...) et génère un résumé.' },
].map((agent, i) => (
<div
key={i}
onClick={() => setSelectedAgentId(agent.id)}
className="bg-white dark:bg-white/5 border border-border rounded-2xl p-6 space-y-6 hover:border-ink/20 transition-all group cursor-pointer shadow-sm relative overflow-hidden"
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="p-3 bg-slate-50 dark:bg-white/10 rounded-xl group-hover:bg-ink group-hover:text-paper transition-all">
{agent.icon}
</div>
<div className="space-y-1">
<h4 className="text-[13px] font-bold text-ink">{agent.title}</h4>
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-ink opacity-60">{agent.type}</p>
</div>
</div>
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-8 h-4 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-3 after:w-3 after:transition-all peer-checked:bg-emerald-500"></div>
</label>
</div>
</div>
<p className="text-xs text-muted-ink leading-relaxed line-clamp-3">
{agent.desc}
</p>
<div className="space-y-3">
<div className="flex items-center justify-between text-[10px] text-muted-ink font-medium">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1"><Clock size={10} /> {agent.meta.split('•')[0]}</span>
<span>{agent.meta.split('•')[1]}</span>
</div>
</div>
<div className="flex items-center justify-between text-[10px] text-muted-ink font-medium">
<div className="flex items-center gap-2">
<span className="uppercase tracking-tight">Prochaine exécution</span>
<span className="text-ink">Hebdomadaire</span>
</div>
<div className="flex items-center gap-2">
<span className="uppercase tracking-tight">Dernier statut</span>
<span className="text-emerald-600 flex items-center gap-1"><Activity size={8} /> {agent.status}</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-2 border-t border-border pt-4">
<button className="py-2 border border-border rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 flex items-center justify-center transition-colors text-muted-ink hover:text-ink"><Edit3 size={14} /> <span className="ml-2 text-[10px] font-bold uppercase">Modifier</span></button>
<button
onClick={(e) => { e.stopPropagation(); }}
className="py-2 border border-border rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 flex items-center justify-center transition-colors text-muted-ink hover:text-ink"
>
<Play size={14} className="fill-current" />
</button>
<button
onClick={(e) => { e.stopPropagation(); }}
className="py-2 border border-border rounded-lg hover:bg-rose-50 hover:text-rose-600 hover:border-rose-100 flex items-center justify-center transition-colors text-muted-ink"
>
<Trash2 size={14} />
</button>
</div>
</div>
))}
</div>
<div className="space-y-8">
<div className="flex items-center gap-4">
<h5 className="text-[10px] font-bold uppercase tracking-[0.3em] text-muted-ink whitespace-nowrap">Modèles</h5>
<div className="h-px w-full bg-border/40" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ title: 'Veille IA', desc: 'Scrape les flux RSS de 6 sites IA et génère un résumé hebdomadaire.', icon: <Globe size={18} /> },
{ title: 'Veille Tech', desc: 'Crée un résumé quotidien des news Hacker News et Product Hunt.', icon: <Zap size={18} /> },
{ title: 'Veille Dev', desc: 'Surveille les repos GitHub pour détecter les nouvelles releases.', icon: <Layers size={18} /> },
].map((model, i) => (
<div key={i} className="bg-white/40 dark:bg-white/5 border border-dashed border-border rounded-2xl p-6 group cursor-pointer hover:bg-white dark:hover:bg-white/10 hover:border-ink/20 transition-all">
<div className="w-8 h-8 rounded-lg bg-slate-50 dark:bg-white/10 flex items-center justify-center text-muted-ink group-hover:bg-ink group-hover:text-paper mb-4 transition-all">
{model.icon}
</div>
<h4 className="text-[13px] font-bold text-ink mb-2">{model.title}</h4>
<p className="text-xs text-muted-ink leading-relaxed mb-4">{model.desc}</p>
<button className="text-[11px] font-bold uppercase tracking-widest text-ink hover:opacity-60 transition-opacity flex items-center gap-2">
<Plus size={14} /> Installer
</button>
</div>
))}
</div>
</div>
</div>
</>
) : (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex-1 flex flex-col"
>
<header className="px-12 py-10 border-b border-border bg-white dark:bg-paper backdrop-blur-md sticky top-0 z-30">
<div className="flex items-center justify-between max-w-5xl mx-auto">
<button
onClick={() => setSelectedAgentId(null)}
className="flex items-center gap-3 text-xs font-bold uppercase tracking-widest text-muted-ink hover:text-ink transition-colors"
>
<ArrowLeft size={16} />
Retour
</button>
<div className="flex items-center gap-4">
<button className="px-5 py-2 text-xs font-bold uppercase tracking-widest border border-border rounded-xl hover:bg-slate-50 dark:hover:bg-white/5 transition-all">
Logs
</button>
<button className="px-6 py-2 bg-ink text-paper text-xs font-bold uppercase tracking-widest rounded-xl hover:opacity-90 transition-all shadow-lg shadow-ink/10">
Enregistrer
</button>
</div>
</div>
</header>
<div className="flex-1 px-12 py-16 max-w-5xl mx-auto w-full space-y-24">
<section className="space-y-12">
<div className="text-center space-y-4">
<p className="text-[10px] font-bold uppercase tracking-[0.4em] text-concrete">Sélectionnez le type d'agent</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto">
{[
{ id: 'Surveillant', icon: <Eye size={18} />, label: 'Surveillant', desc: 'Surveille un carnet et analyse les notes' },
{ id: 'Personnalisé', icon: <Layers size={18} />, label: 'Personnalisé', desc: 'Agent libre avec votre propre prompt' },
{ id: 'Slides', icon: <Layers size={18} />, label: 'Slides', desc: 'Crée une présentation PowerPoint à partir de notes' },
{ id: 'Diagramme', icon: <Zap size={18} />, label: 'Diagramme', desc: 'Crée un diagramme Excalidraw à partir de notes' },
].map((type) => (
<button
key={type.id}
onClick={() => setAgentType(type.id as any)}
className={`p-6 rounded-2xl border-2 transition-all flex flex-col items-center gap-3 text-center group relative
${agentType === type.id ? 'border-blueprint bg-white shadow-xl shadow-blueprint/10' : 'border-border bg-white/50 hover:bg-white'}`}
>
<div className={`p-3 rounded-xl transition-all ${agentType === type.id ? 'bg-blueprint text-white' : 'bg-slate-50 text-concrete group-hover:text-ink'}`}>
{type.icon}
</div>
<div className="space-y-1">
<p className="text-[13px] font-bold text-ink">{type.label}</p>
<p className="text-[10px] text-muted-ink leading-tight">{type.desc}</p>
</div>
<div className={`absolute top-4 right-4 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all
${agentType === type.id ? 'border-blueprint' : 'border-border opacity-20'}`}>
{agentType === type.id && <div className="w-2 h-2 bg-blueprint rounded-full" />}
</div>
</button>
))}
</div>
</div>
</section>
<section className="space-y-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-[0.3em] text-concrete">
CONFIGURATION <Info size={12} className="opacity-40" />
</div>
<button className="flex items-center gap-2 px-6 py-2 border-2 border-rose-100 bg-rose-50 rounded-xl text-rose-500 text-[11px] font-bold uppercase tracking-widest hover:bg-rose-100 transition-colors">
<Trash2 size={14} /> Supprimer
</button>
</div>
<div className="bg-white dark:bg-white/5 border border-border/60 rounded-[32px] p-12 space-y-12 shadow-sm">
<div className="space-y-6">
<div className="flex items-center gap-2">
<label className="text-[11px] font-bold uppercase tracking-widest text-concrete">DESCRIPTION (OPTIONEL)</label>
<Info size={12} className="text-concrete/40" />
</div>
<textarea
className="w-full bg-slate-50 dark:bg-black/20 border border-border/40 rounded-2xl p-6 text-sm outline-none focus:ring-4 ring-blueprint/5 focus:border-blueprint/40 transition-all font-light leading-relaxed resize-none text-ink"
placeholder="Décrivez brièvement le rôle de cet agent..."
defaultValue="Lit une note et génère un diagramme visuel dans le Lab Excalidraw."
/>
</div>
<div className="space-y-6">
<div className="flex items-center gap-2">
<label className="text-[11px] font-bold uppercase tracking-widest text-concrete">CARNET À SURVEILLER</label>
<Info size={12} className="text-concrete/40" />
</div>
<HierarchicalCarnetSelector
carnets={carnets}
selectedId={selectedCarnetForAgent}
onSelect={setSelectedCarnetForAgent}
/>
</div>
<div className="space-y-6">
<div className="flex items-center gap-2">
<label className="text-[11px] font-bold uppercase tracking-widest text-concrete">NOTES À ANALYSER</label>
<Info size={12} className="text-concrete/40" />
</div>
<div className="bg-slate-50 dark:bg-black/20 border border-border/40 rounded-2xl overflow-hidden divide-y divide-border/20">
{[
'Résumé du conteneur LXC devSandbox',
'Connexion SSH sans mot de passe à devSandbox',
'Gateway token (blank to generate)',
'Procédure d\'accès à openclaw',
'Derniers commits du repo Momento'
].map((note, i) => (
<label key={i} className="flex items-center gap-4 px-6 py-4 cursor-pointer hover:bg-white/50 transition-colors group">
<div className={`w-5 h-5 rounded border transition-all flex items-center justify-center
${i === 0 ? 'bg-blueprint border-blueprint text-white' : 'bg-white border-border group-hover:border-blueprint/40'}`}>
{i === 0 && <Check size={12} />}
</div>
<input type="checkbox" className="hidden" defaultChecked={i === 0} />
<span className={`text-[13px] transition-colors ${i === 0 ? 'font-medium text-ink' : 'text-muted-ink'}`}>{note}</span>
</label>
))}
</div>
<p className="text-[10px] text-concrete/60 italic font-medium">{1} note(s) sélectionnée(s)</p>
</div>
<div className="space-y-8">
<div className="flex items-center gap-2">
<label className="text-[11px] font-bold uppercase tracking-widest text-concrete">TYPE DE DIAGRAMME</label>
</div>
<div className="grid grid-cols-2 md:grid-cols-2 gap-3">
{[
'Auto (détection métier)', 'Flowchart (processus)',
'Mindmap (idées)', 'Organigramme (équipes)',
'Timeline / roadmap', 'Process map (opérations)',
'Architecture cloud (zones/RG)'
].map((type, i) => (
<button key={i} className={`px-6 py-4 rounded-xl border text-[13px] text-left transition-all
${i === 0 ? 'border-ink bg-slate-50 font-bold text-ink ring-2 ring-ink/5' : 'border-border text-concrete hover:border-concrete/40 hover:bg-slate-50/50'}`}>
{type}
</button>
))}
</div>
</div>
<div className="space-y-8">
<div className="flex items-center gap-2">
<label className="text-[11px] font-bold uppercase tracking-widest text-concrete">STYLE DU DIAGRAMME EXCALIDRAW</label>
</div>
<div className="flex flex-wrap gap-4">
{[
'Coloré (Excalidraw)', 'Sketch+ (Excalidraw accentué)', 'Austère (sobre)'
].map((style, i) => (
<button key={i} className={`px-6 py-4 rounded-xl border text-[13px] transition-all
${i === 1 ? 'border-ink bg-white font-bold text-ink ring-2 ring-ink/5 shadow-lg' : 'border-border text-concrete hover:bg-slate-50'}`}>
{style}
</button>
))}
</div>
</div>
</div>
</section>
</div>
</motion.div>
)}
</div>
);
};

View File

@@ -0,0 +1,208 @@
import React, { useState, useMemo } from 'react';
import {
ChevronRight,
ChevronDown,
Folder,
FolderOpen,
Check,
Search
} from 'lucide-react';
import { Carnet } from '../types';
import { motion, AnimatePresence } from 'motion/react';
interface HierarchicalCarnetSelectorProps {
carnets: Carnet[];
selectedId: string | null;
onSelect: (id: string) => void;
className?: string;
placeholder?: string;
}
export const HierarchicalCarnetSelector: React.FC<HierarchicalCarnetSelectorProps> = ({
carnets,
selectedId,
onSelect,
className = "",
placeholder = "Sélectionner un carnet..."
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(['1', '4'])); // Default expand some
const selectedCarnet = carnets.find(c => c.id === selectedId);
// Derive the path for display
const path = useMemo(() => {
if (!selectedCarnet) return [];
const trail: Carnet[] = [];
let current = selectedCarnet;
while (current) {
trail.unshift(current);
if (!current.parentId) break;
const parent = carnets.find(c => c.id === current.parentId);
if (!parent) break;
current = parent;
}
return trail;
}, [selectedCarnet, carnets]);
const toggleExpand = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
const newExpanded = new Set(expandedIds);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedIds(newExpanded);
};
const filteredCarnets = useMemo(() => {
if (!searchQuery) return carnets;
return carnets.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()));
}, [carnets, searchQuery]);
const renderTree = (parentId?: string, level = 0) => {
const children = carnets.filter(c => c.parentId === parentId);
if (children.length === 0) return null;
return (
<div className={level > 0 ? "ml-4 border-l border-border/40 pl-2" : ""}>
{children.map(carnet => {
const isExpanded = expandedIds.has(carnet.id) || searchQuery.length > 0;
const hasChildren = carnets.some(c => c.parentId === carnet.id);
const isSelected = selectedId === carnet.id;
// If searching and this carnet doesn't match AND none of its children match, skip it
if (searchQuery && !carnet.name.toLowerCase().includes(searchQuery.toLowerCase())) {
const hasMatchingChild = (id: string): boolean => {
const childrenOfId = carnets.filter(c => c.parentId === id);
return childrenOfId.some(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()) || hasMatchingChild(c.id));
};
if (!hasMatchingChild(carnet.id)) return null;
}
return (
<div key={carnet.id} className="select-none">
<div
onClick={() => {
onSelect(carnet.id);
if (!searchQuery) setIsOpen(false);
}}
className={`flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer transition-all group
${isSelected ? 'bg-blueprint/10 text-blueprint font-bold' : 'hover:bg-slate-50 dark:hover:bg-white/5 text-ink'}`}
>
<div className="w-4 flex items-center justify-center">
{hasChildren ? (
<button
onClick={(e) => toggleExpand(e, carnet.id)}
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded transition-colors"
>
{isExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</button>
) : null}
</div>
<div className={`p-1 rounded ${isSelected ? 'bg-blueprint/20' : 'bg-slate-100 dark:bg-white/5 group-hover:bg-white/40'}`}>
{isExpanded && hasChildren ? <FolderOpen size={13} /> : <Folder size={13} />}
</div>
<span className="text-[13px] truncate flex-1">{carnet.name}</span>
{isSelected && <Check size={14} className="opacity-60" />}
</div>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
{renderTree(carnet.id, level + 1)}
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
);
};
return (
<div className={`relative ${className}`}>
<div
onClick={() => setIsOpen(!isOpen)}
className="w-full bg-slate-50 dark:bg-black/20 border border-border/80 rounded-xl px-4 py-4 text-sm outline-none focus:ring-4 ring-blueprint/5 focus:border-blueprint/40 transition-all cursor-pointer text-ink flex items-center gap-3"
>
<Folder size={16} className="text-blueprint/60 shrink-0" />
<div className="flex-1 flex items-center gap-1 min-w-0">
{path.length > 0 ? (
<div className="flex items-center gap-1.5 truncate">
{path.map((item, i) => (
<React.Fragment key={item.id}>
{i > 0 && <span className="text-concrete/40 text-[10px]">/</span>}
<span className={`truncate ${i === path.length - 1 ? 'font-bold' : 'text-concrete'}`}>
{item.name}
</span>
</React.Fragment>
))}
</div>
) : (
<span className="text-concrete italic">{placeholder}</span>
)}
</div>
<ChevronDown size={14} className={`transition-transform duration-300 text-concrete shrink-0 ${isOpen ? 'rotate-180' : ''}`} />
</div>
<AnimatePresence>
{isOpen && (
<>
<div
className="fixed inset-0 z-[60]"
onClick={() => setIsOpen(false)}
/>
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.98 }}
className="absolute z-[70] mt-2 w-full bg-white dark:bg-dark-paper border border-border shadow-2xl rounded-2xl overflow-hidden flex flex-col min-w-[280px]"
>
<div className="p-3 border-b border-border/40 bg-slate-50/50">
<div className="relative">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-concrete" />
<input
autoFocus
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Filtrer les carnets..."
className="w-full bg-white border border-border rounded-lg pl-9 pr-4 py-2 text-xs outline-none focus:border-blueprint transition-colors"
/>
</div>
</div>
<div className="max-h-72 overflow-y-auto custom-scrollbar p-2">
{renderTree(undefined)}
</div>
<div className="p-2 border-t border-border/40 bg-slate-50/30 flex justify-between items-center px-4">
<span className="text-[9px] font-bold text-concrete uppercase tracking-widest">
Structure des carnets
</span>
<button
onClick={() => setIsOpen(false)}
className="text-[10px] font-bold text-blueprint hover:underline"
>
Fermer
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -0,0 +1,469 @@
import React from 'react';
import {
Plus,
Search,
Share2,
Pin,
ChevronRight,
ArrowLeft,
MoreVertical,
Sparkles,
Tag as TagIcon,
X,
BookOpen,
Edit3,
Eye,
Trash2
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { Note, Carnet, Tag } from '../types';
import { SlashMenu } from './SlashMenu';
interface NotebooksViewProps {
activeNoteId: string | null;
activeCarnet: Carnet | undefined;
filteredNotes: Note[];
activeNote: Note | undefined;
setActiveNoteId: (id: string | null) => void;
togglePin: (id: string) => void;
setShowNewNoteModal: (show: boolean) => void;
isAISidebarOpen: boolean;
setIsAISidebarOpen: (open: boolean) => void;
selectedTagIds: string[];
setSelectedTagIds: (ids: string[]) => void;
allNotes: Note[];
activeCarnetId: string;
setShowNewCarnetModal: (show: boolean, parentId?: string, isRenaming?: boolean, carnetId?: string) => void;
onDeleteNote: (id: string) => void;
}
export const NotebooksView: React.FC<NotebooksViewProps> = ({
activeNoteId,
activeCarnet,
filteredNotes,
activeNote,
setActiveNoteId,
togglePin,
setShowNewNoteModal,
isAISidebarOpen,
setIsAISidebarOpen,
selectedTagIds,
setSelectedTagIds,
allNotes,
activeCarnetId,
setShowNewCarnetModal,
onDeleteNote
}) => {
const [isTagsExpanded, setIsTagsExpanded] = React.useState(false);
const [tagSearchQuery, setTagSearchQuery] = React.useState('');
const [isEditing, setIsEditing] = React.useState(false);
const [slashMenu, setSlashMenu] = React.useState<{ isOpen: boolean; top: number; left: number } | null>(null);
const handleEditorKeyDown = (e: React.KeyboardEvent) => {
if (e.key === '/') {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
setSlashMenu({
isOpen: true,
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX
});
}
}
};
const insertCommand = (type: string) => {
console.log(`Command selected: ${type}`);
setSlashMenu(null);
};
const availableTags = React.useMemo(() => {
const carnetNotes = allNotes.filter(n => n.carnetId === activeCarnetId);
const tagsMap = new Map<string, Tag>();
carnetNotes.forEach(note => {
note.tags?.forEach(tag => {
tagsMap.set(tag.id, tag);
});
});
return Array.from(tagsMap.values()).sort((a, b) => {
// AI tags first, then alphabetical
if (a.type === 'ai' && b.type !== 'ai') return -1;
if (a.type !== 'ai' && b.type === 'ai') return 1;
return a.label.localeCompare(b.label);
});
}, [allNotes, activeCarnetId]);
const visibleTags = React.useMemo(() => {
let filtered = availableTags;
if (tagSearchQuery) {
filtered = availableTags.filter(t =>
t.label.toLowerCase().includes(tagSearchQuery.toLowerCase())
);
} else if (!isTagsExpanded) {
filtered = availableTags.slice(0, 10);
// Ensure selected tags are always visible even if not in the first 10
selectedTagIds.forEach(id => {
if (!filtered.find(t => t.id === id)) {
const tag = availableTags.find(t => t.id === id);
if (tag) filtered.push(tag);
}
});
}
return filtered;
}, [availableTags, isTagsExpanded, tagSearchQuery, selectedTagIds]);
const toggleTag = (tagId: string) => {
if (selectedTagIds.includes(tagId)) {
setSelectedTagIds(selectedTagIds.filter(id => id !== tagId));
} else {
setSelectedTagIds([...selectedTagIds, tagId]);
}
};
if (!activeNoteId) {
return (
<div className="h-full flex flex-col overflow-y-auto">
<header className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-paper/80 backdrop-blur-md z-30">
<div className="flex justify-between items-start">
<h1 className="text-4xl font-serif font-medium tracking-tight text-ink leading-tight pr-12">
{activeCarnet?.name} {filteredNotes[0]?.date || 'Oct 26'}
</h1>
</div>
<div className="flex items-center justify-between border-b border-ink/5 pb-4">
<div className="flex items-center gap-6">
<button
onClick={() => setShowNewNoteModal(true)}
className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity"
>
<Plus size={16} />
<span>Add Note</span>
</button>
<button
onClick={() => setShowNewCarnetModal(true, activeCarnetId)}
className="flex items-center gap-2 text-[13px] text-concrete font-medium hover:text-ink transition-all"
>
<BookOpen size={16} />
<span>New Sub-Carnet</span>
</button>
<button className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity">
<Search size={16} />
<span>Search</span>
</button>
</div>
<button className="flex items-center gap-2 text-[13px] text-ink font-medium hover:opacity-70 transition-opacity">
<Share2 size={16} />
<span>Share</span>
</button>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-[0.2em] text-concrete">
<TagIcon size={12} />
<span>Filter by Tags</span>
{selectedTagIds.length > 0 && (
<span className="bg-blueprint/10 text-blueprint px-2 py-0.5 rounded-full text-[9px] lowercase tracking-normal">
{selectedTagIds.length} active
</span>
)}
</div>
{availableTags.length > 10 && (
<div className="relative group">
<input
type="text"
placeholder="Search tags..."
className="bg-transparent border-b border-border/40 text-[10px] outline-none focus:border-blueprint/40 py-1 px-2 w-32 transition-all focus:w-48 placeholder:text-concrete/40"
onChange={(e) => setTagSearchQuery(e.target.value)}
/>
</div>
)}
</div>
<div className="flex flex-wrap gap-2 items-center min-h-[32px]">
<AnimatePresence mode="popLayout">
{visibleTags.map(tag => {
const isActive = selectedTagIds.includes(tag.id);
return (
<motion.button
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
key={tag.id}
onClick={() => toggleTag(tag.id)}
className={`px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all border flex items-center gap-2
${isActive
? 'bg-ink text-paper border-ink shadow-lg shadow-ink/10'
: 'bg-white/40 border-border text-concrete hover:border-concrete/40 hover:bg-white/60'}`}
>
{tag.type === 'ai' && (
<Sparkles
size={10}
className={isActive ? 'text-blueprint' : 'text-blueprint/60'}
/>
)}
{tag.label}
{isActive && <X size={10} />}
</motion.button>
);
})}
</AnimatePresence>
{availableTags.length > 10 && !tagSearchQuery && (
<button
onClick={() => setIsTagsExpanded(!isTagsExpanded)}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-concrete/60 hover:text-ink transition-colors border border-dashed border-border rounded-full"
>
{isTagsExpanded ? 'Show less' : `+ ${availableTags.length - 10} more`}
</button>
)}
{selectedTagIds.length > 0 && (
<button
onClick={() => setSelectedTagIds([])}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-rust hover:underline ml-auto"
>
Clear all
</button>
)}
</div>
</div>
</header>
<div className="px-12 flex-1 pb-20">
<div className="max-w-3xl space-y-16">
{filteredNotes.map((note, index) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * index, duration: 0.8 }}
key={note.id}
className="space-y-4 group cursor-pointer relative"
onClick={() => setActiveNoteId(note.id)}
>
<h2 className="text-2xl font-serif font-medium text-ink flex items-center justify-between">
<span className="flex items-center gap-3">
{note.isPinned && <Pin size={18} className="text-amber-500 fill-amber-500" />}
{note.title}
</span>
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
togglePin(note.id);
}}
className={`p-2 rounded-full transition-all ${note.isPinned ? 'text-amber-600 bg-amber-50' : 'opacity-0 group-hover:opacity-60 hover:bg-slate-100 text-ink'}`}
>
<Pin size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDeleteNote(note.id);
}}
className="p-2 rounded-full opacity-0 group-hover:opacity-60 hover:opacity-100 hover:bg-rose-50 text-rose-500 transition-all"
>
<Trash2 size={16} />
</button>
<button className="opacity-0 group-hover:opacity-40 transition-opacity">
<ChevronRight size={20} />
</button>
</div>
</h2>
<div className="flex flex-col md:flex-row gap-8 items-start">
<div className="w-full md:w-56 aspect-[4/3] bg-white/50 dark:bg-white/5 border border-border overflow-hidden rounded shadow-sm flex-shrink-0">
<img
src={note.imageUrl}
alt={note.title}
className="w-full h-full object-cover mix-blend-multiply opacity-80 grayscale contrast-125 hover:grayscale-0 hover:opacity-100 transition-all duration-500"
referrerPolicy="no-referrer"
/>
</div>
<div className="space-y-3">
<div className="flex flex-wrap gap-2 mb-2">
{note.tags?.map(tag => (
<div
key={tag.id}
className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider border flex items-center gap-1.5
${tag.type === 'ai'
? 'bg-blueprint/5 border-blueprint/20 text-blueprint'
: 'bg-concrete/5 border-border text-concrete'}`}
>
{tag.type === 'ai' && <Sparkles size={8} />}
{tag.label}
</div>
))}
</div>
<p className="text-[14px] leading-relaxed text-ink/80 font-light max-w-lg line-clamp-4">
{note.content}
</p>
<span className="text-[11px] text-muted-ink uppercase tracking-widest font-medium">Read more</span>
</div>
</div>
</motion.div>
))}
{filteredNotes.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<p className="font-serif text-xl italic text-muted-ink">This notebook is waiting for its first vision.</p>
<button
onClick={() => setShowNewNoteModal(true)}
className="px-6 py-2 border border-ink text-[13px] uppercase tracking-[0.2em] hover:bg-ink hover:text-paper transition-all"
>
Begin Drawing
</button>
</div>
)}
</div>
</div>
<footer className="px-12 py-6 border-t border-ink/5 text-center mt-auto">
<p className="text-[11px] text-muted-ink uppercase tracking-[0.2em] font-medium">
&copy; 2024 Architectural Grid. All rights reserved.
</p>
</footer>
</div>
);
}
return (
<div className="h-full flex overflow-hidden transition-all duration-500">
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-paper">
<div className="px-12 py-8 flex items-center justify-between sticky top-0 bg-white/90 dark:bg-paper/90 backdrop-blur-sm z-40 border-b border-border">
<button
onClick={() => setActiveNoteId(null)}
className="flex items-center gap-2 text-ink hover:opacity-60 transition-opacity"
>
<ArrowLeft size={18} />
<span className="text-sm font-medium">Back to collection</span>
</button>
<div className="flex items-center gap-4">
<button
onClick={() => setIsEditing(!isEditing)}
className={`flex items-center gap-2 px-4 py-1.5 rounded-lg border transition-all duration-300
${isEditing ? 'bg-blueprint text-white border-blueprint shadow-lg shadow-blueprint/20' : 'border-border text-ink hover:bg-slate-50'}`}
>
{isEditing ? <Eye size={16} /> : <Edit3 size={16} />}
<span className="text-xs font-bold uppercase tracking-widest">{isEditing ? 'Visualiser' : 'Modifier'}</span>
</button>
<button
onClick={() => togglePin(activeNoteId!)}
className={`p-2 rounded-full transition-all ${activeNote?.isPinned ? 'text-amber-600 bg-amber-50 dark:bg-ochre/10' : 'text-muted-ink hover:text-ink'}`}
title={activeNote?.isPinned ? "Unpin note" : "Pin note"}
>
<Pin size={18} className={activeNote?.isPinned ? 'fill-amber-600' : ''} />
</button>
<button
onClick={() => setIsAISidebarOpen(!isAISidebarOpen)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300
${isAISidebarOpen ? 'bg-ink text-paper border-ink' : 'border-border text-ink hover:bg-white/50 dark:hover:bg-white/5'}`}
>
<Sparkles size={16} />
<span className="text-xs font-medium">AI Assistant</span>
</button>
<button className="p-2 text-muted-ink hover:text-red-500 transition-colors">
<Trash2 size={18} />
</button>
<button className="p-2 text-muted-ink hover:text-ink transition-colors">
<MoreVertical size={18} />
</button>
</div>
</div>
<div className="max-w-4xl mx-auto w-full px-12 py-16 space-y-12 relative">
<AnimatePresence>
{slashMenu?.isOpen && (
<SlashMenu
position={{ top: slashMenu.top, left: slashMenu.left }}
onSelect={(type) => insertCommand(type)}
onClose={() => setSlashMenu(null)}
/>
)}
</AnimatePresence>
<div className="space-y-4">
<div className="flex items-center gap-3 text-[12px] text-muted-ink uppercase tracking-[.25em] font-bold">
<span className="text-blueprint">{activeCarnet?.name}</span>
<ChevronRight size={10} className="text-concrete" />
<span className="text-concrete">{activeNote?.date}</span>
</div>
{isEditing ? (
<input
type="text"
defaultValue={activeNote?.title}
className="w-full text-5xl md:text-6xl font-serif font-bold text-ink leading-tight bg-transparent border-none outline-none focus:ring-0 placeholder:text-concrete/20"
placeholder="Titre de la note..."
/>
) : (
<h1 className="text-5xl md:text-6xl font-serif font-bold text-ink leading-tight">
{activeNote?.title}
</h1>
)}
<div className="flex flex-wrap gap-2 pt-2">
{activeNote?.tags?.map(tag => (
<div
key={tag.id}
className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border flex items-center gap-2
${tag.type === 'ai'
? 'bg-blueprint/5 border-blueprint/20 text-blueprint'
: 'bg-paper border-border text-concrete'}`}
>
{tag.type === 'ai' && <Sparkles size={12} />}
{tag.label}
{tag.type === 'ai' && (
<div className="w-1.5 h-1.5 rounded-full bg-blueprint animate-pulse" />
)}
</div>
))}
</div>
</div>
<div className="aspect-[16/9] w-full bg-slate-100 dark:bg-white/5 rounded-xl overflow-hidden shadow-2xl relative group/img">
<img
src={activeNote?.imageUrl}
alt={activeNote?.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover/img:scale-105"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-0 bg-gradient-to-t from-ink/20 to-transparent pointer-events-none" />
</div>
<div className="max-w-2xl mx-auto w-full space-y-8 pb-40">
{isEditing ? (
<textarea
defaultValue={activeNote?.content}
onKeyDown={handleEditorKeyDown}
className="w-full min-h-[500px] text-lg leading-relaxed text-ink/90 font-serif bg-transparent border-none outline-none focus:ring-0 resize-none placeholder:text-concrete/20"
placeholder="Commencez à écrire... Tapez '/' pour les commandes."
/>
) : (
<div className="space-y-8">
<p className="text-xl md:text-2xl font-serif leading-relaxed text-ink italic">
{activeNote?.content.split('.')[0]}.
</p>
<div className="h-px bg-border w-32" />
<div className="space-y-6">
{activeNote?.content.split('\n').map((line, i) => (
<p key={i} className="text-lg leading-relaxed text-ink/80 font-light text-justify selection:bg-blueprint/20">
{line}
</p>
))}
{activeNote?.id.startsWith('n-') && (
<p className="text-lg leading-relaxed text-ink/80 font-light text-justify border-l-2 border-blueprint/20 pl-6 italic">
Architectural grids serve as the invisible scaffolding upon which spatial experiences are constructed. Beyond mere structural repetition, they facilitate a rhythmic dialogue between materiality and void. In this exploration, we examine how light fractures these rigid boundaries, creating a dynamic interplay that evolves with the passage of time.
</p>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { SettingsTab } from '../types';
import { SettingsHeader } from './settings/SettingsHeader';
import { GeneralTab } from './settings/GeneralTab';
import { AITab } from './settings/AITab';
import { AppearanceTab } from './settings/AppearanceTab';
interface SettingsViewProps {
activeSettingsTab: SettingsTab;
setActiveSettingsTab: (tab: SettingsTab) => void;
}
export const SettingsView: React.FC<SettingsViewProps> = ({
activeSettingsTab,
setActiveSettingsTab
}) => {
return (
<div className="h-full flex flex-col bg-paper dark:bg-dark-paper overflow-y-auto custom-scrollbar relative font-sans">
<div className="absolute inset-0 opacity-[0.04] pointer-events-none grainy-bg mix-blend-multiply dark:mix-blend-overlay" />
<div className="relative z-10 flex flex-col min-h-full">
<SettingsHeader
activeTab={activeSettingsTab}
setActiveTab={setActiveSettingsTab}
/>
<div className="flex-1 px-12 pb-24 h-full">
<div className="max-w-6xl mx-auto">
<AnimatePresence mode="wait">
{activeSettingsTab === 'general' && (
<GeneralTab key="general" />
)}
{activeSettingsTab === 'ai' && (
<AITab key="ai" />
)}
{activeSettingsTab === 'appearance' && (
<AppearanceTab key="appearance" />
)}
{['profile', 'data', 'mcp', 'about'].includes(activeSettingsTab) && (
<motion.div
key="placeholder"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="h-[50vh] flex flex-col items-center justify-center border border-dashed border-border rounded-[32px] space-y-6 bg-white/20 dark:bg-white/5"
>
<div className="w-16 h-16 rounded-3xl border border-dashed border-concrete/20 flex items-center justify-center text-concrete/40 bg-paper/50">
<span className="text-2xl font-serif italic text-concrete">?</span>
</div>
<div className="text-center space-y-1">
<p className="text-ink font-bold text-sm tracking-tight">Section en développement</p>
<p className="text-concrete italic text-[11px] font-light">Le module {activeSettingsTab} sera disponible prochainement.</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,450 @@
import React from 'react';
import {
Plus,
Archive,
Settings,
ChevronRight,
BookOpen,
Bot,
Microscope,
Activity,
Pin,
Moon,
Sun,
Bell,
Lock,
Edit3,
Trash2,
Users,
Clock
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { NavigationView, Carnet, Note } from '../types';
interface NoteLinkProps {
note: Note;
isActive: boolean;
onClick: () => void;
}
const NoteLink: React.FC<NoteLinkProps> = ({ note, isActive, onClick }) => (
<motion.button
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
onClick={onClick}
className={`w-full flex items-center gap-2 pl-12 pr-4 py-2 text-[12px] transition-colors rounded-lg
${isActive ? 'bg-white/50 dark:bg-white/10 text-ink font-medium' : 'text-muted-ink hover:text-ink hover:bg-white/30'}`}
>
<div className="flex items-center gap-2 flex-1 truncate">
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${isActive ? 'bg-ink' : 'bg-transparent border border-muted-ink/30'}`} />
<span className="truncate">{note.title}</span>
</div>
{note.isPinned && <Pin size={10} className="text-amber-500 shrink-0" />}
</motion.button>
);
interface SidebarItemProps {
carnet: Carnet;
isActive: boolean;
notes: Note[];
activeNoteId: string | null;
onCarnetClick: () => void;
onNoteClick: (noteId: string) => void;
onAddSubCarnet: () => void;
onRename: () => void;
onDelete: () => void;
children?: React.ReactNode;
level: number;
isExpanded: boolean;
toggleExpand: () => void;
}
const SidebarItem: React.FC<SidebarItemProps> = ({
carnet,
isActive,
notes,
activeNoteId,
onCarnetClick,
onNoteClick,
onAddSubCarnet,
onRename,
onDelete,
children,
level,
isExpanded,
toggleExpand
}) => {
const hasChildren = React.Children.count(children) > 0;
return (
<div className="space-y-0.5">
<div
className="flex items-center group relative h-10"
style={{ paddingLeft: `${level * 12}px` }}
>
{/* Hierarchy Guide Line */}
{level > 0 && (
<div className="absolute left-[8px] top-[-10px] bottom-1/2 w-px bg-border/40" />
)}
{level > 0 && (
<div className="absolute left-[8px] top-1/2 w-[8px] h-px bg-border/40" />
)}
<div className="flex-1 flex items-center gap-1">
{hasChildren ? (
<button
onClick={(e) => {
e.stopPropagation();
toggleExpand();
}}
className="p-1 hover:bg-ink/5 dark:hover:bg-white/5 rounded-md transition-colors text-muted-ink"
>
<motion.div animate={{ rotate: isExpanded ? 90 : 0 }} transition={{ duration: 0.2 }}>
<ChevronRight size={14} />
</motion.div>
</button>
) : (
<div className="w-6" /> // Spacer for alignment
)}
{/* Hierarchy Connector Line */}
{hasChildren && level > 0 && (
<div className="absolute left-[-16px] top-[14px] w-3 h-[1px] bg-border/60" />
)}
<motion.div
whileHover={{ x: 2 }}
className={`flex-1 flex items-center gap-2.5 px-2 py-1.5 rounded-lg transition-all duration-300 group/item cursor-pointer relative
${isActive ? 'bg-white shadow-sm border border-border/40 dark:bg-white/10' : 'hover:bg-white/40 dark:hover:bg-white/5'}`}
onClick={onCarnetClick}
>
{/* active indicator dot */}
{isActive && (
<motion.div
layoutId="active-indicator"
className="absolute -left-1 w-1 h-4 bg-blueprint rounded-full"
/>
)}
<div className={`w-6 h-6 rounded-md flex items-center justify-center text-[10px] font-bold border transition-all
${isActive ? 'bg-blueprint text-white border-blueprint' : 'bg-paper dark:bg-white/10 text-concrete border-border dark:border-white/10'}`}>
{carnet.initial}
</div>
<div className="flex-1 text-left flex items-center gap-2 min-w-0">
<span className={`text-[12px] font-medium transition-colors truncate ${isActive ? 'text-ink' : 'text-muted-ink group-hover:text-ink'}`}>
{carnet.name}
</span>
{carnet.isPrivate && <Lock size={10} className="text-concrete/60 shrink-0" />}
</div>
<div className="flex items-center gap-1 opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
onAddSubCarnet();
}}
className="p-1 hover:bg-ink/10 dark:hover:bg-white/10 rounded-md transition-all text-concrete hover:text-ink"
title="Add sub-carnet"
>
<Plus size={10} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onRename();
}}
className="p-1 hover:bg-ink/10 dark:hover:bg-white/10 rounded-md transition-all text-concrete hover:text-ink"
title="Rename"
>
<Edit3 size={10} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="p-1 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-all text-concrete hover:text-red-500"
title="Delete"
>
<Trash2 size={10} />
</button>
{notes.length > 0 && (
<span className="text-[9px] font-bold text-concrete/40 px-1.5 border border-border/40 rounded-full group-hover:text-concrete transition-colors">
{notes.length}
</span>
)}
</div>
</motion.div>
</div>
</div>
<AnimatePresence initial={false}>
{(isExpanded || (isActive && !hasChildren)) && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
className="overflow-hidden"
>
<div className="relative" style={{ marginLeft: `${(level + 1) * 12 + 10}px` }}>
{/* Vertical line for nested content */}
<div className="absolute left-[-6px] top-0 bottom-4 w-px bg-border/30" />
<div className="space-y-1 py-1">
{children}
{isActive && !hasChildren && notes.map(note => (
<NoteLink
key={note.id}
note={note}
isActive={activeNoteId === note.id}
onClick={() => onNoteClick(note.id)}
/>
))}
{isActive && !hasChildren && notes.length === 0 && (
<p className="pl-8 py-2 text-[10px] italic text-concrete/40 font-light">
No notes found
</p>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
interface SidebarProps {
activeView: NavigationView;
isDarkMode: boolean;
setIsDarkMode: (val: boolean) => void;
setActiveView: (view: NavigationView) => void;
carnets: Carnet[];
notes: Note[];
activeCarnetId: string;
activeNoteId: string | null;
setActiveCarnetId: (id: string) => void;
setActiveNoteId: (id: string | null) => void;
setShowNewCarnetModal: (show: boolean, parentId?: string, isRenaming?: boolean, carnetId?: string) => void;
onDeleteCarnet: (id: string) => void;
}
export const Sidebar: React.FC<SidebarProps> = ({
activeView,
isDarkMode,
setIsDarkMode,
setActiveView,
carnets,
notes,
activeCarnetId,
activeNoteId,
setActiveCarnetId,
setActiveNoteId,
setShowNewCarnetModal,
onDeleteCarnet
}) => {
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(new Set(['4'])); // Default expand Research
const toggleExpand = (id: string) => {
const newSet = new Set(expandedIds);
if (newSet.has(id)) newSet.delete(id);
else newSet.add(id);
setExpandedIds(newSet);
};
const renderCarnetTree = (parentId: string | undefined = undefined, level: number = 0) => {
return carnets
.filter(c => c.parentId === parentId && !c.isDeleted)
.map(carnet => (
<SidebarItem
key={carnet.id}
carnet={carnet}
isActive={activeCarnetId === carnet.id}
notes={notes.filter(n => n.carnetId === carnet.id && !n.isDeleted)}
activeNoteId={activeNoteId}
level={level}
isExpanded={expandedIds.has(carnet.id)}
toggleExpand={() => toggleExpand(carnet.id)}
onAddSubCarnet={() => {
if (!expandedIds.has(carnet.id)) toggleExpand(carnet.id);
setShowNewCarnetModal(true, carnet.id);
}}
onRename={() => {
setShowNewCarnetModal(true, undefined, true, carnet.id);
}}
onDelete={() => {
onDeleteCarnet(carnet.id);
}}
onCarnetClick={() => {
setActiveCarnetId(carnet.id);
setActiveNoteId(null);
// Auto expand when clicking
if (!expandedIds.has(carnet.id)) toggleExpand(carnet.id);
}}
onNoteClick={(id) => {
setActiveCarnetId(carnet.id);
setActiveNoteId(id);
}}
>
{renderCarnetTree(carnet.id, level + 1)}
</SidebarItem>
));
};
return (
<aside className="w-80 bg-white/30 dark:bg-[#151515] backdrop-blur-md border-r border-border p-6 flex flex-col z-20 shrink-0 transition-colors duration-500">
<div className="mb-10 flex items-center justify-between">
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-white/10 border border-border flex items-center justify-center text-ink font-serif text-lg shadow-sm">
A
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsDarkMode(!isDarkMode)}
className="p-2 text-muted-ink hover:text-ink transition-all bg-white/50 dark:bg-white/10 rounded-full border border-border dark:border-white/10"
>
{isDarkMode ? <Sun size={14} /> : <Moon size={14} />}
</button>
<button className="p-2 text-muted-ink hover:text-ink transition-all relative group bg-white/50 dark:bg-white/10 rounded-full border border-border dark:border-white/10">
<Bell size={14} />
<span className="absolute -top-1 -right-1 w-4 h-4 bg-rose-500 text-white text-[9px] font-bold flex items-center justify-center rounded-full border border-white shadow-sm">
3
</span>
</button>
<div className="flex bg-white/50 dark:bg-white/10 p-1 rounded-full border border-border dark:border-white/10 transition-all">
<button
onClick={() => setActiveView('notebooks')}
className={`p-1.5 rounded-full transition-all ${activeView === 'notebooks' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink'}`}
title="Carnets"
>
<BookOpen size={14} />
</button>
<button
onClick={() => setActiveView('reminders')}
className={`p-1.5 rounded-full transition-all ${activeView === 'reminders' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink'}`}
title="Rappels"
>
<Clock size={14} />
</button>
<button
onClick={() => setActiveView('agents')}
className={`p-1.5 rounded-full transition-all ${activeView === 'agents' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink'}`}
title="Agents"
>
<Bot size={14} />
</button>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto space-y-8 -mx-2 px-2 py-4 custom-scrollbar">
{activeView === 'notebooks' ? (
<div className="space-y-6">
<div className="flex items-center justify-between px-4">
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase">
Architecture Grid
</p>
<button
onClick={() => setShowNewCarnetModal(true)}
className="p-1 hover:bg-paper dark:hover:bg-white/5 rounded-md text-concrete hover:text-ink transition-colors"
title="New Carnet"
>
<Plus size={14} />
</button>
</div>
<nav className="space-y-0.5">
{renderCarnetTree()}
</nav>
</div>
) : activeView === 'shared' ? (
<div className="space-y-6">
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase px-4">
Partagé avec moi
</p>
<div className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30">
<Users size={24} className="mx-auto text-concrete/40 mb-3" />
<p className="text-[11px] text-concrete italic">Aucun document partagé pour le moment.</p>
</div>
</div>
) : activeView === 'reminders' ? (
<div className="space-y-6">
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase px-4">
Rappels programmés
</p>
<div className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30">
<Clock size={24} className="mx-auto text-concrete/40 mb-3" />
<p className="text-[11px] text-concrete italic">Aucun rappel actif.</p>
</div>
</div>
) : activeView === 'agents' ? (
<div>
<p className="text-[10px] font-bold text-muted-ink tracking-widest uppercase mb-4 px-4">
Intelligence OS
</p>
<div className="space-y-1">
{[
{ id: 'a1', name: 'Mes Agents', icon: <Bot size={16} /> },
{ id: 'a2', name: 'Le Lab AI', icon: <Microscope size={16} /> },
{ id: 'a3', name: 'Activités', icon: <Activity size={16} /> },
].map(item => (
<button
key={item.id}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 group
${item.id === 'a1' ? 'active-nav-item' : 'text-muted-ink hover:bg-white/40 dark:hover:bg-white/5 hover:text-ink'}`}
>
<div className={`w-8 h-8 rounded-full flex items-center justify-center border transition-colors
${item.id === 'a1' ? 'bg-ink text-paper border-ink' : 'bg-white/60 dark:bg-white/10 border-border dark:border-white/10 group-hover:border-ink/20'}`}>
{item.icon}
</div>
<span className="text-[13px] font-medium">{item.name}</span>
</button>
))}
</div>
</div>
) : null}
</div>
<div className="pt-4 border-t border-border/40 mt-auto pb-4">
<div className="px-2 space-y-0.5">
<button
onClick={() => setActiveView('shared')}
className={`w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl ${activeView === 'shared' ? 'bg-blueprint/5 text-blueprint' : 'text-muted-ink hover:text-ink hover:bg-black/5'}`}
>
<Users size={14} className={activeView === 'shared' ? 'text-blueprint' : 'text-muted-ink group-hover:text-ink'} />
<span className="flex-1 text-left">Partagé</span>
</button>
<button className="w-full flex items-center gap-3 px-3 py-2 text-[12px] text-muted-ink hover:text-ink hover:bg-black/5 transition-all font-medium group rounded-xl">
<Archive size={14} className="text-muted-ink group-hover:text-ink" />
<span className="flex-1 text-left">Archives</span>
</button>
<button
onClick={() => setActiveView('trash')}
className={`w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl ${activeView === 'trash' ? 'bg-rose-50 text-rose-500' : 'text-muted-ink hover:text-rose-500 hover:bg-rose-50/50'}`}
>
<Trash2 size={14} className={activeView === 'trash' ? 'text-rose-500' : 'text-muted-ink group-hover:text-rose-500'} />
<span className="flex-1 text-left">Corbeille</span>
{notes.some(n => n.isDeleted) && (
<div className="w-1.5 h-1.5 rounded-full bg-rose-400" />
)}
</button>
<div className="my-2 h-px bg-border/20 mx-2" />
<button
onClick={() => setActiveView('settings')}
className={`w-full flex items-center gap-3 px-3 py-2 text-[12px] transition-all font-medium group rounded-xl ${activeView === 'settings' ? 'bg-ink text-paper shadow-sm' : 'text-muted-ink hover:text-ink hover:bg-black/5'}`}
>
<Settings size={14} className={activeView === 'settings' ? 'text-paper' : 'text-muted-ink group-hover:text-ink'} />
<span className="flex-1 text-left">Paramètres</span>
</button>
</div>
</div>
</aside>
);
};

View File

@@ -0,0 +1,65 @@
import React from 'react';
import {
Heading1,
Heading2,
List,
Quote,
Code,
Image as ImageIcon,
Type,
Sparkles
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
interface SlashMenuProps {
position: { top: number; left: number };
onSelect: (type: string) => void;
onClose: () => void;
}
export const SlashMenu: React.FC<SlashMenuProps> = ({ position, onSelect, onClose }) => {
const commands = [
{ id: 'h1', label: 'Titre Principal', icon: <Heading1 size={14} />, desc: 'Grand titre de section' },
{ id: 'h2', label: 'Sous-titre', icon: <Heading2 size={14} />, desc: 'Titre de niveau 2' },
{ id: 'bullet', label: 'Liste à puces', icon: <List size={14} />, desc: 'Liste simple' },
{ id: 'quote', label: 'Citation', icon: <Quote size={14} />, desc: 'Bloc de texte mis en avant' },
{ id: 'code', label: 'Bloc de Code', icon: <Code size={14} />, desc: 'Code ou texte technique' },
{ id: 'image', label: 'Image', icon: <ImageIcon size={14} />, desc: 'Insérer un visuel' },
{ id: 'ai-summary', label: 'Résumé IA', icon: <Sparkles size={14} />, desc: 'Générer un résumé court', special: true },
];
return (
<>
<div className="fixed inset-0 z-[60]" onClick={onClose} />
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
className="fixed z-[70] w-64 bg-white dark:bg-[#1A1A1A] border border-border shadow-2xl rounded-xl overflow-hidden py-2"
style={{ top: position.top, left: position.left }}
>
<div className="px-3 py-2 text-[10px] font-bold text-concrete uppercase tracking-widest border-b border-border/40 mb-1">
Commandes rapides
</div>
<div className="max-h-80 overflow-y-auto custom-scrollbar">
{commands.map((cmd) => (
<button
key={cmd.id}
onClick={() => onSelect(cmd.id)}
className="w-full flex items-start gap-3 px-3 py-2 hover:bg-paper dark:hover:bg-white/5 transition-colors group text-left"
>
<div className={`p-2 rounded-lg border border-border transition-colors group-hover:border-ink/20
${cmd.special ? 'bg-blueprint/10 text-blueprint border-blueprint/20' : 'bg-white/50 dark:bg-white/5 text-ink'}`}>
{cmd.icon}
</div>
<div className="space-y-0.5">
<p className="text-xs font-bold text-ink">{cmd.label}</p>
<p className="text-[10px] text-muted-ink leading-tight">{cmd.desc}</p>
</div>
</button>
))}
</div>
</motion.div>
</>
);
};

View File

@@ -0,0 +1,218 @@
import React from 'react';
import {
Trash2,
RotateCcw,
X,
FileText,
Folder,
Search,
ChevronRight,
Clock,
AlertCircle
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { Note, Carnet } from '../types';
interface TrashViewProps {
deletedNotes: Note[];
deletedCarnets: Carnet[];
onRestoreNote: (id: string) => void;
onRestoreCarnet: (id: string) => void;
onPermanentDeleteNote: (id: string) => void;
onPermanentDeleteCarnet: (id: string) => void;
onEmptyTrash: () => void;
}
export const TrashView: React.FC<TrashViewProps> = ({
deletedNotes,
deletedCarnets,
onRestoreNote,
onRestoreCarnet,
onPermanentDeleteNote,
onPermanentDeleteCarnet,
onEmptyTrash
}) => {
const [searchQuery, setSearchQuery] = React.useState('');
const [filterType, setFilterType] = React.useState<'all' | 'notes' | 'carnets'>('all');
const getDaysRemaining = (dateString?: string) => {
if (!dateString) return 30;
const deletedDate = new Date(dateString);
const now = new Date();
const diffTime = now.getTime() - deletedDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return Math.max(0, 30 - diffDays);
};
const filteredItems = React.useMemo(() => {
const items = [
...deletedNotes.map(n => ({ ...n, itemType: 'note' as const })),
...deletedCarnets.map(c => ({ ...c, itemType: 'carnet' as const }))
];
return items
.filter(item => {
const matchesSearch = ('title' in item ? item.title : item.name).toLowerCase().includes(searchQuery.toLowerCase());
const matchesType = filterType === 'all' || (filterType === 'notes' && item.itemType === 'note') || (filterType === 'carnets' && item.itemType === 'carnet');
return matchesSearch && matchesType;
})
.sort((a, b) => {
const dateA = a.deletedAt ? new Date(a.deletedAt).getTime() : 0;
const dateB = b.deletedAt ? new Date(b.deletedAt).getTime() : 0;
return dateB - dateA;
});
}, [deletedNotes, deletedCarnets, searchQuery, filterType]);
return (
<div className="h-full flex flex-col bg-[#F9F8F6] dark:bg-dark-paper">
<header className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-[#F9F8F6]/80 backdrop-blur-md z-30 border-b border-border/20">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h1 className="text-4xl font-serif font-medium text-ink flex items-center gap-4">
Corbeille <Trash2 size={28} className="text-rose-400 opacity-40" />
</h1>
<p className="text-[10px] text-concrete font-bold uppercase tracking-[0.3em] opacity-60">
Auto-suppression après 30 jours
</p>
</div>
{filteredItems.length > 0 && (
<button
onClick={() => {
if (window.confirm('Vider la corbeille ? Cette action est irréversible.')) {
onEmptyTrash();
}
}}
className="px-6 py-3 bg-paper border border-border text-rose-500 rounded-2xl text-[10px] font-bold uppercase tracking-widest hover:bg-rose-50 hover:border-rose-100 transition-all shadow-sm"
>
Vider tout
</button>
)}
</div>
<div className="flex items-center gap-6">
<div className="group relative flex-1 max-w-xl">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-concrete group-focus-within:text-ink transition-colors" size={16} />
<input
type="text"
placeholder="Rechercher..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white dark:bg-white/5 border border-border/40 rounded-2xl pl-12 pr-6 py-4 text-sm outline-none focus:ring-4 ring-ink/5 transition-all shadow-sm"
/>
</div>
<div className="flex bg-white dark:bg-white/5 p-1 rounded-2xl border border-border shadow-sm">
{(['all', 'notes', 'carnets'] as const).map((type) => (
<button
key={type}
onClick={() => setFilterType(type)}
className={`px-6 py-3 rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all
${filterType === type ? 'bg-ink text-paper shadow-lg' : 'text-concrete hover:text-ink'}`}
>
{type === 'all' ? 'Tous' : type === 'notes' ? 'Notes' : 'Carnets'}
</button>
))}
</div>
</div>
</header>
<main className="flex-1 px-12 py-12 overflow-y-auto custom-scrollbar">
{filteredItems.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<AnimatePresence mode="popLayout">
{filteredItems.map((item) => {
const daysLeft = getDaysRemaining(item.deletedAt);
return (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white dark:bg-white/5 border border-border/60 rounded-[32px] p-8 group hover:shadow-2xl hover:border-blueprint/20 transition-all relative overflow-hidden flex flex-col"
>
{/* Countdown Progress Bar */}
<div className="absolute top-0 left-0 w-full h-1 bg-slate-100 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(daysLeft / 30) * 100}%` }}
className={`h-full ${daysLeft < 5 ? 'bg-rose-500' : 'bg-blueprint'}`}
/>
</div>
<div className="flex justify-between items-start mb-6">
<div className={`p-3 rounded-2xl ${item.itemType === 'note' ? 'bg-blueprint/10 text-blueprint' : 'bg-concrete/10 text-concrete'}`}>
{item.itemType === 'note' ? <FileText size={20} /> : <Folder size={20} />}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => item.itemType === 'note' ? onRestoreNote(item.id) : onRestoreCarnet(item.id)}
className="flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-xl text-[10px] font-bold uppercase tracking-widest hover:bg-emerald-100 transition-colors"
>
<RotateCcw size={12} /> Restaurer
</button>
<button
onClick={() => item.itemType === 'note' ? onPermanentDeleteNote(item.id) : onPermanentDeleteCarnet(item.id)}
className="p-2 hover:bg-rose-50 text-rose-500 rounded-xl transition-colors"
title="Supprimer définitivement"
>
<X size={16} />
</button>
</div>
</div>
<div className="space-y-2 mb-8 flex-1">
<h3 className="text-base font-serif font-medium text-ink leading-tight">
{'title' in item ? item.title : item.name}
</h3>
<div className="flex items-center gap-3">
<div className={`text-[9px] font-bold uppercase tracking-widest px-2 py-0.5 rounded border ${daysLeft < 5 ? 'border-rose-200 text-rose-500 bg-rose-50' : 'border-blueprint/20 text-blueprint bg-blueprint/5'}`}>
{daysLeft} JOURS RESTANTS
</div>
<span className="text-[10px] text-concrete font-medium uppercase tracking-tight flex items-center gap-1">
<Clock size={10} /> {('deletedAt' in item && item.deletedAt) ? new Date(item.deletedAt).toLocaleDateString() : ''}
</span>
</div>
</div>
{item.itemType === 'note' && 'content' in item ? (
<div className="text-[12px] text-concrete line-clamp-3 leading-relaxed opacity-60 font-light border-t border-border/40 pt-4">
{item.content.replace(/[#*`]/g, '')}
</div>
) : (
<div className="border-t border-border/40 pt-4">
<div className="text-[9px] font-bold text-concrete/40 uppercase tracking-widest">
Contenu du dossier préservé
</div>
</div>
)}
</motion.div>
);
})}
</AnimatePresence>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-center space-y-6 opacity-40">
<div className="p-8 rounded-full bg-slate-100 border-2 border-dashed border-border flex items-center justify-center">
<Trash2 size={64} className="text-concrete" />
</div>
<div className="space-y-2">
<h2 className="text-2xl font-serif text-ink italic">Corbeille vide</h2>
<p className="text-sm text-concrete max-w-xs">
Les éléments que vous supprimez apparaîtront ici. Ils seront conservés pendant 30 jours avant suppression définitive.
</p>
</div>
</div>
)}
</main>
<footer className="px-12 py-6 bg-white/50 border-t border-border flex items-center gap-4">
<AlertCircle size={14} className="text-concrete" />
<p className="text-[10px] text-concrete font-medium uppercase tracking-widest">
Conseil : La restauration d'un carnet restaurera également toutes les notes à l'intérieur.
</p>
</footer>
</div>
);
};

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { Sparkles, Edit3, MessageCircle, Languages, Tag, History, FlaskConical } from 'lucide-react';
import { motion } from 'motion/react';
const AISettingCard = ({ icon, title, description, defaultChecked = false }: any) => (
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-6 flex items-center justify-between group hover:shadow-xl hover:shadow-blueprint/5 transition-all duration-300">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-blueprint/10 rounded-2xl text-blueprint group-hover:bg-blueprint group-hover:text-white group-hover:scale-110 transition-all duration-300 border border-blueprint/20">
{icon}
</div>
<div className="space-y-1">
<h4 className="text-[13px] font-bold text-ink">{title}</h4>
<p className="text-[10px] text-muted-ink leading-relaxed pr-4 line-clamp-2">{description}</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0 ml-4">
<input type="checkbox" className="sr-only peer" defaultChecked={defaultChecked} />
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-blueprint"></div>
</label>
</div>
);
export const AITab: React.FC = () => {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-16 pb-20"
>
<div className="space-y-10">
<h3 className="text-[10px] font-bold uppercase tracking-[0.4em] text-muted-ink opacity-60">Configurez vos fonctionnalités IA et préférences</h3>
<div className="space-y-6">
<h4 className="text-sm font-bold text-ink border-b border-border/40 pb-4">Fonctionnalités IA</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<AISettingCard
icon={<Edit3 size={18} />}
title="Suggestions de titre"
description="Suggérer des titres pour les notes sans titre après 50+ mots"
defaultChecked
/>
<AISettingCard
icon={<Sparkles size={18} />}
title="IA Note"
description="Active le bouton de chat IA et les outils d'amélioration du texte"
defaultChecked
/>
<AISettingCard
icon={<MessageCircle size={18} />}
title="💡 J'ai remarqué quelque chose..."
description="Aperçu quotidien de vos notes"
defaultChecked
/>
<AISettingCard
icon={<Languages size={18} />}
title="Détection de langue"
description="Détecte automatiquement la langue de vos notes"
defaultChecked
/>
<AISettingCard
icon={<Tag size={18} />}
title="Suggestion des labels"
description="Suggère et applique des étiquettes automatiquement à vos notes"
defaultChecked
/>
<AISettingCard
icon={<History size={18} />}
title="Historique des notes"
description="Active les snapshots de versions et la restauration depuis History"
defaultChecked
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-6">
{/* Fréquence */}
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-8 space-y-10">
<div className="space-y-1.5 text-left text-blueprint">
<h4 className="text-sm font-bold">Fréquence</h4>
<p className="text-[10px] opacity-60 uppercase tracking-wider font-semibold">Fréquence d'analyse des connexions</p>
</div>
<div className="space-y-6">
<label className="flex items-center gap-4 cursor-pointer group">
<input type="radio" name="freq" className="sr-only peer" defaultChecked />
<div className="w-5 h-5 rounded-full border-2 border-border peer-checked:border-blueprint flex items-center justify-center p-0.5 transition-all">
<div className="w-full h-full rounded-full bg-transparent peer-checked:bg-blueprint" />
</div>
<span className="text-sm font-medium text-ink group-hover:opacity-70 transition-opacity">Quotidienne</span>
</label>
<label className="flex items-center gap-4 cursor-pointer group">
<input type="radio" name="freq" className="sr-only peer" />
<div className="w-5 h-5 rounded-full border-2 border-border peer-checked:border-blueprint flex items-center justify-center p-0.5 transition-all">
<div className="w-full h-full rounded-full bg-transparent peer-checked:bg-blueprint" />
</div>
<span className="text-sm font-medium text-ink group-hover:opacity-70 transition-opacity">Hebdomadaire</span>
</label>
</div>
</div>
{/* Mode d'historique */}
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-8 space-y-10">
<div className="space-y-1.5 text-left text-blueprint">
<h4 className="text-sm font-bold">Mode d'historique</h4>
<p className="text-[10px] opacity-60 uppercase tracking-wider font-semibold">Gestion des snapshots</p>
</div>
<div className="space-y-6">
<label className="flex items-center gap-4 cursor-pointer group">
<input type="radio" name="hist" className="sr-only peer" defaultChecked />
<div className="w-5 h-5 rounded-full border-2 border-border peer-checked:border-blueprint flex items-center justify-center p-0.5 transition-all">
<div className="w-full h-full rounded-full bg-transparent peer-checked:bg-blueprint" />
</div>
<div className="space-y-0.5">
<p className="text-sm font-bold text-ink">Manuel (bouton commit)</p>
<p className="text-[10px] text-muted-ink">Créer des snapshots manuellement</p>
</div>
</label>
<label className="flex items-center gap-4 cursor-pointer group">
<input type="radio" name="hist" className="sr-only peer" />
<div className="w-5 h-5 rounded-full border-2 border-border peer-checked:border-blueprint flex items-center justify-center p-0.5 transition-all">
<div className="w-full h-full rounded-full bg-transparent peer-checked:bg-blueprint" />
</div>
<div className="space-y-0.5">
<p className="text-sm font-bold text-ink">Automatique (intelligent)</p>
<p className="text-[10px] text-muted-ink">Snapshots automatiques avec détection</p>
</div>
</label>
</div>
</div>
</div>
{/* Mode Démo */}
<div className="bg-ochre/5 dark:bg-ochre/10 border border-ochre/20 rounded-2xl p-8 flex items-center justify-between group transition-all duration-300 hover:bg-ochre/10">
<div className="flex items-center gap-6">
<div className="p-3 bg-paper dark:bg-ochre/20 rounded-2xl text-ochre border border-ochre/30">
<FlaskConical size={20} />
</div>
<div className="space-y-1.5 text-left">
<h4 className="text-sm font-bold text-ink flex items-center gap-3">
🧪 Mode Démo
</h4>
<p className="text-[11px] text-muted-ink leading-relaxed font-medium">Accélère Memory Echo pour les tests. Les connexions apparaissent instantanément.</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer shrink-0 ml-4">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-ochre"></div>
</label>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { Palette, Type, LayoutGrid, Maximize } from 'lucide-react';
import { motion } from 'motion/react';
const AppearanceSelect = ({ icon, title, description, options, defaultValue }: any) => (
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-2xl p-8 space-y-8 group transition-all duration-300 hover:shadow-xl hover:shadow-slate/5">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-slate border border-border group-hover:scale-110 transition-transform duration-300">
{icon}
</div>
<div className="space-y-0.5 text-left">
<h4 className="text-base font-bold text-ink">{title}</h4>
<p className="text-[11px] text-concrete leading-tight">{description}</p>
</div>
</div>
<div className="relative group/select">
<select
defaultValue={defaultValue}
className="w-full bg-white/50 dark:bg-black/40 border border-border rounded-xl px-5 py-4 text-sm outline-none focus:ring-1 ring-slate/20 appearance-none cursor-pointer text-ink font-bold transition-all hover:bg-white dark:hover:bg-black/60"
>
{options.map((opt: string) => (
<option key={opt}>{opt}</option>
))}
</select>
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-concrete group-hover/select:text-slate transition-colors">
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</div>
</div>
);
export const AppearanceTab: React.FC = () => {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-16 pb-20"
>
<div className="space-y-10">
<h3 className="text-[10px] font-bold uppercase tracking-[0.4em] text-concrete opacity-60">Personnaliser l'apparence de l'application</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<AppearanceSelect
icon={<Palette size={20} />}
title="Thème"
description="Sélectionner le mode visuel"
options={['Clair', 'Sombre', 'Système']}
defaultValue="Clair"
/>
<AppearanceSelect
icon={<Type size={20} />}
title="Taille de la police"
description="Ajustez la lisibilité globale de l'interface"
options={['Petite', 'Moyenne', 'Grande']}
defaultValue="Moyenne"
/>
<AppearanceSelect
icon={<Type size={20} />}
title="Famille de polices"
description="La typographie définit l'âme de l'application"
options={['Inter', 'JetBrains Mono', 'Public Sans', 'Outfit']}
defaultValue="JetBrains Mono"
/>
<AppearanceSelect
icon={<LayoutGrid size={20} />}
title="Affichage des notes"
description="Gestion visuelle de la grille de composition"
options={['Cartes (grille)', 'Liste', 'Tableau']}
defaultValue="Cartes (grille)"
/>
<AppearanceSelect
icon={<Maximize size={20} />}
title="Taille des notes"
description="Structure de la mise en page des éléments"
options={['Taille uniforme', 'Variable (Masonry)']}
defaultValue="Taille uniforme"
/>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { Globe, Bell } from 'lucide-react';
import { motion } from 'motion/react';
export const GeneralTab: React.FC = () => {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-12"
>
<div className="space-y-4">
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-concrete">Paramètres généraux de l'application</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Langue */}
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-slate border border-border">
<Globe size={18} />
</div>
<div className="space-y-0.5">
<h4 className="text-base font-bold text-ink">Langue</h4>
<p className="text-[11px] text-concrete">Sélectionner une langue</p>
</div>
</div>
<div className="relative group">
<select className="w-full bg-white/50 dark:bg-black/40 border border-border rounded-xl px-5 py-3.5 text-sm outline-none focus:ring-1 ring-blueprint/20 appearance-none cursor-pointer transition-all hover:bg-white dark:hover:bg-black/60 text-ink font-medium">
<option>Français</option>
<option>English</option>
<option>Español</option>
</select>
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none opacity-40 text-concrete">
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</div>
</div>
{/* Notifications */}
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-slate border border-border">
<Bell size={18} />
</div>
<div className="space-y-0.5">
<h4 className="text-base font-bold text-ink">Notifications</h4>
<p className="text-[11px] text-concrete">Gérez vos préférences de notifications</p>
</div>
</div>
<div className="space-y-6 divide-y divide-border/40 text-left">
<div className="flex items-center justify-between pt-0">
<div className="space-y-1">
<p className="text-xs font-bold text-ink">Notifications par email</p>
<p className="text-[10px] text-concrete leading-relaxed">Recevoir des notifications importantes par email</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-slate"></div>
</label>
</div>
<div className="flex items-center justify-between pt-6">
<div className="space-y-1">
<p className="text-xs font-bold text-ink">Notifications bureau</p>
<p className="text-[10px] text-concrete leading-relaxed">Recevoir des notifications dans votre navigateur</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-slate"></div>
</label>
</div>
</div>
</div>
</div>
</div>
</motion.div>
);
};

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { Settings, Sparkles, Palette, User, Database, Code, Info } from 'lucide-react';
import { motion } from 'motion/react';
import { SettingsTab } from '../../types';
interface SettingsHeaderProps {
activeTab: SettingsTab;
setActiveTab: (tab: SettingsTab) => void;
}
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({ activeTab, setActiveTab }) => {
const tabs = [
{ id: 'general', label: 'Paramètres généraux', icon: <Settings size={14} /> },
{ id: 'ai', label: 'Paramètres IA', icon: <Sparkles size={14} /> },
{ id: 'appearance', label: 'Apparence', icon: <Palette size={14} /> },
{ id: 'profile', label: 'Profil', icon: <User size={14} /> },
{ id: 'data', label: 'Gestion des données', icon: <Database size={14} /> },
{ id: 'mcp', label: 'Paramètres MCP', icon: <Code size={14} /> },
{ id: 'about', label: 'À propos', icon: <Info size={14} /> },
];
return (
<header className="px-12 pt-20 pb-16 space-y-12">
<div className="space-y-4">
<h1 className="text-[64px] font-serif text-ink tracking-tight leading-none italic font-medium">Paramètres</h1>
<p className="text-[10px] font-bold uppercase tracking-[0.4em] text-concrete opacity-60">Configuration & Préférences</p>
</div>
<nav className="flex items-center gap-1 border-b border-border/40 pb-px">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as SettingsTab)}
className={`flex items-center gap-2.5 px-6 py-5 text-[10px] font-bold uppercase tracking-[0.18em] transition-all relative whitespace-nowrap
${activeTab === tab.id ? 'text-ink' : 'text-concrete hover:text-ink/60'}`}
>
<span className={activeTab === tab.id ? 'text-ink' : 'text-concrete'}>{tab.icon}</span>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="activeSettingsTabLine"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-ink"
transition={{ type: 'spring', bounce: 0.1, duration: 0.8 }}
/>
)}
</button>
))}
</nav>
</header>
);
};