All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export - Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG) - Add document Q&A overlay with streaming chat and PDF preview - Add note attachments UI with status polling, grid layout, and auto-scroll - Add task extraction AI tool and agent executor improvements - Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings - Fix brainstorm 'Create Note' button: add success toast and redirect to created note - Fix memory echo notification infinite polling - Fix chat route to always include document_search tool - Add brainstorm i18n keys across all 14 locales - Add socket server for real-time brainstorm collaboration - Add hierarchical notebook selector and organize notebook dialog improvements - Add sidebar brainstorm section with session management - Update prisma schema with brainstorm tables, attachments, and document chunks
467 lines
25 KiB
TypeScript
467 lines
25 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Sparkles,
|
|
ChevronRight,
|
|
MessageSquare,
|
|
FileCode,
|
|
Globe,
|
|
Send,
|
|
Scissors,
|
|
Zap,
|
|
Languages,
|
|
Layout,
|
|
ArrowRightLeft,
|
|
BookOpen,
|
|
History,
|
|
Target,
|
|
Network,
|
|
Clock
|
|
} 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', 'explore', '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 === 'explore' && (
|
|
<motion.div
|
|
key="explore"
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -10 }}
|
|
className="space-y-6"
|
|
>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div className="h-px flex-1 bg-border/40" />
|
|
<h4 className="text-[10px] uppercase tracking-[0.25em] font-bold text-muted-ink whitespace-nowrap">Intelligence Modules</h4>
|
|
<div className="h-px flex-1 bg-border/40" />
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<button
|
|
onClick={() => {
|
|
// These will be handled in App.tsx by observing activeView
|
|
window.dispatchEvent(new CustomEvent('switch-view', { detail: 'brainstorm' }));
|
|
}}
|
|
className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-ochre/30 transition-all text-left overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
|
<Zap size={60} className="text-ochre" />
|
|
</div>
|
|
<div className="relative flex items-center gap-4">
|
|
<div className="p-3 bg-ochre/10 rounded-xl text-ochre group-hover:bg-ochre group-hover:text-white transition-colors">
|
|
<Zap size={20} fill="currentColor" />
|
|
</div>
|
|
<div>
|
|
<h5 className="font-bold text-ink text-sm">Brainstorm Wave</h5>
|
|
<p className="text-[10px] text-muted-ink uppercase tracking-tight">Unfold dimensions of thought</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
window.dispatchEvent(new CustomEvent('switch-view', { detail: 'insights' }));
|
|
}}
|
|
className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-indigo-500/30 transition-all text-left overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
|
<Network size={60} className="text-indigo-500" />
|
|
</div>
|
|
<div className="relative flex items-center gap-4">
|
|
<div className="p-3 bg-indigo-500/10 rounded-xl text-indigo-500 group-hover:bg-indigo-500 group-hover:text-white transition-colors">
|
|
<Network size={20} />
|
|
</div>
|
|
<div>
|
|
<h5 className="font-bold text-ink text-sm">Semantic Network</h5>
|
|
<p className="text-[10px] text-muted-ink uppercase tracking-tight">Detect clusters and bridges</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
window.dispatchEvent(new CustomEvent('switch-view', { detail: 'temporal' }));
|
|
}}
|
|
className="w-full group relative p-5 rounded-2xl bg-white border border-border hover:border-rose-500/30 transition-all text-left overflow-hidden"
|
|
>
|
|
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
|
<Clock size={60} className="text-rose-500" />
|
|
</div>
|
|
<div className="relative flex items-center gap-4">
|
|
<div className="p-3 bg-rose-500/10 rounded-xl text-rose-500 group-hover:bg-rose-500 group-hover:text-white transition-colors">
|
|
<Clock size={20} />
|
|
</div>
|
|
<div>
|
|
<h5 className="font-bold text-ink text-sm">Temporal Forecast</h5>
|
|
<p className="text-[10px] text-muted-ink uppercase tracking-tight">Predict relevance recurrence</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-6 rounded-2xl bg-slate-50 dark:bg-white/5 border border-dashed border-border mt-6">
|
|
<p className="text-[10px] text-muted-ink leading-relaxed font-medium italic text-center">
|
|
Ces modules utilisent les embeddings du modèle Gemini pour analyser graphiquement vos pensées.
|
|
</p>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{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>
|
|
);
|
|
};
|