feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
816
architectural-grid/src/components/AISidebar.tsx
Normal file
816
architectural-grid/src/components/AISidebar.tsx
Normal file
@@ -0,0 +1,816 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Sparkles,
|
||||
ChevronRight,
|
||||
MessageSquare,
|
||||
FileCode,
|
||||
Globe,
|
||||
Send,
|
||||
Scissors,
|
||||
Zap,
|
||||
Languages,
|
||||
Layout,
|
||||
ArrowRightLeft,
|
||||
BookOpen,
|
||||
History,
|
||||
Target,
|
||||
Network,
|
||||
Clock,
|
||||
AlertCircle
|
||||
} 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[];
|
||||
notes?: Note[];
|
||||
onOpenNote?: (noteId: string) => void;
|
||||
onUpdateNote?: (note: Note) => void;
|
||||
}
|
||||
|
||||
export const AISidebar: React.FC<AISidebarProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
activeNote,
|
||||
aiTab,
|
||||
setAiTab,
|
||||
selectedTone,
|
||||
setSelectedTone,
|
||||
carnets,
|
||||
notes = [],
|
||||
onOpenNote = (_noteId: string) => {},
|
||||
onUpdateNote
|
||||
}) => {
|
||||
const [selectedContextId, setSelectedContextId] = React.useState<string | null>(null);
|
||||
const [hoveredOrbitNode, setHoveredOrbitNode] = React.useState<any | null>(null);
|
||||
|
||||
const explicitWikiLinks = React.useMemo(() => [
|
||||
{ source: 'n1', target: 'n1-b' },
|
||||
{ source: 'n3', target: 'n3-b' },
|
||||
{ source: 'bridge-1', target: 'n1' },
|
||||
{ source: 'bridge-1', target: 'n2' },
|
||||
], []);
|
||||
|
||||
const CARNET_COLOR_PALETTE: { [key: string]: string } = {
|
||||
'1': '#D97706', // Daily Notes - Warm Amber
|
||||
'2': '#059669', // Project: Neo - Soft Emerald
|
||||
'3': '#4F46E5', // Shared Docs - Rich Indigo
|
||||
'4': '#0891B2', // Architecture Research - Clean Cyan
|
||||
'5': '#EA580C', // History of Architecture - Deep Orange
|
||||
'6': '#DB2777', // Modernism - Vibrant Rose
|
||||
'7': '#65A30D', // Sustainable Design - Cool Lime
|
||||
};
|
||||
|
||||
const DEFAULT_CARNET_COLOR = '#71717A';
|
||||
|
||||
const backlinks = React.useMemo(() => {
|
||||
if (!activeNote || !notes) return [];
|
||||
return notes.filter(n => {
|
||||
if (n.id === activeNote.id || n.isDeleted) return false;
|
||||
const isExplicit = explicitWikiLinks.some(link =>
|
||||
(link.source === n.id && link.target === activeNote.id)
|
||||
);
|
||||
const isContentLink = n.content.toLowerCase().includes(`[[${activeNote.title.toLowerCase()}]]`);
|
||||
return isExplicit || isContentLink;
|
||||
});
|
||||
}, [activeNote, notes, explicitWikiLinks]);
|
||||
|
||||
const outboundLinks = React.useMemo(() => {
|
||||
if (!activeNote || !notes) return [];
|
||||
return notes.filter(n => {
|
||||
if (n.id === activeNote.id || n.isDeleted) return false;
|
||||
const isExplicit = explicitWikiLinks.some(link =>
|
||||
(link.source === activeNote.id && link.target === n.id)
|
||||
);
|
||||
const isContentLink = activeNote.content.toLowerCase().includes(`[[${n.title.toLowerCase()}]]`);
|
||||
return isExplicit || isContentLink;
|
||||
});
|
||||
}, [activeNote, notes, explicitWikiLinks]);
|
||||
|
||||
const unlinkedMentions = React.useMemo(() => {
|
||||
if (!activeNote || !notes) return [];
|
||||
return notes.filter(n => {
|
||||
if (n.id === activeNote.id || n.isDeleted) return false;
|
||||
const isLinked = [...backlinks, ...outboundLinks].some(link => link.id === n.id);
|
||||
if (isLinked) return false;
|
||||
return n.content.toLowerCase().includes(activeNote.title.toLowerCase());
|
||||
});
|
||||
}, [activeNote, notes, backlinks, outboundLinks]);
|
||||
|
||||
const orbitNodes = React.useMemo(() => {
|
||||
const list: { id: string; title: string; color: string; carnetName: string; relationship: 'backlink' | 'outbound' | 'mention' }[] = [];
|
||||
|
||||
backlinks.forEach(n => {
|
||||
const carnet = carnets.find(c => c.id === n.carnetId);
|
||||
list.push({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
|
||||
carnetName: carnet?.name || 'Carnet',
|
||||
relationship: 'backlink'
|
||||
});
|
||||
});
|
||||
|
||||
outboundLinks.forEach(n => {
|
||||
const carnet = carnets.find(c => c.id === n.carnetId);
|
||||
list.push({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
|
||||
carnetName: carnet?.name || 'Carnet',
|
||||
relationship: 'outbound'
|
||||
});
|
||||
});
|
||||
|
||||
unlinkedMentions.forEach(n => {
|
||||
const carnet = carnets.find(c => c.id === n.carnetId);
|
||||
list.push({
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
color: CARNET_COLOR_PALETTE[n.carnetId] || DEFAULT_CARNET_COLOR,
|
||||
carnetName: carnet?.name || 'Carnet',
|
||||
relationship: 'mention'
|
||||
});
|
||||
});
|
||||
|
||||
return list.slice(0, 8);
|
||||
}, [backlinks, outboundLinks, unlinkedMentions, carnets]);
|
||||
|
||||
const getSnippetWithHighlight = (content: string, term: string) => {
|
||||
const index = content.toLowerCase().indexOf(term.toLowerCase());
|
||||
if (index === -1) {
|
||||
return <span>{content.substring(0, 80)}...</span>;
|
||||
}
|
||||
const start = Math.max(0, index - 40);
|
||||
const end = Math.min(content.length, index + term.length + 40);
|
||||
const before = content.substring(start, index);
|
||||
const match = content.substring(index, index + term.length);
|
||||
const after = content.substring(index + term.length, end);
|
||||
return (
|
||||
<span>
|
||||
{start > 0 && "..."}
|
||||
{before}
|
||||
<mark className="bg-ochre/20 dark:bg-ochre/40 text-ochre px-1 py-0.5 rounded font-bold">{match}</mark>
|
||||
{after}
|
||||
{end < content.length && "..."}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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-[#FDFCFB] dark:bg-[#0D0D0D] shadow-2xl flex flex-col z-50 shrink-0 relative"
|
||||
>
|
||||
<div className="p-6 border-b border-border/60 space-y-1.5 bg-white/50 dark:bg-black/20 backdrop-blur-md">
|
||||
<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', 'relations'] as AITab[]).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setAiTab(tab)}
|
||||
className={`flex-1 py-3 text-[9px] uppercase tracking-wider font-bold transition-all relative
|
||||
${aiTab === tab ? 'text-manganese' : 'text-muted-ink hover:text-ink/60'}`}
|
||||
>
|
||||
{tab === 'relations' ? 'réseau' : 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="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted-ink">Contexte</label>
|
||||
<div className="flex items-center gap-1">
|
||||
{(['Professional', 'Creative', 'Academic', 'Casual'] as AITone[]).map((tone) => (
|
||||
<button
|
||||
key={tone}
|
||||
onClick={() => setSelectedTone(tone)}
|
||||
className={`w-8 h-8 rounded-lg flex items-center justify-center text-[9px] font-bold transition-all border
|
||||
${selectedTone === tone
|
||||
? 'bg-accent text-white border-accent shadow-sm'
|
||||
: 'bg-glass border-border/40 text-muted-ink hover:border-accent/40'}`}
|
||||
title={tone}
|
||||
>
|
||||
{tone.substring(0, 2)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="w-full px-4 py-2.5 bg-white/60 dark:bg-black/40 border border-border rounded-xl text-xs flex items-center justify-between cursor-default group transition-all hover:border-accent/30">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<FileCode size={14} className="text-accent/60" />
|
||||
<span className="font-medium text-ink">Note Active</span>
|
||||
</div>
|
||||
<div className="text-[8px] bg-accent/5 text-accent/60 px-1.5 py-0.5 rounded-full uppercase font-bold tracking-tighter">Auto</div>
|
||||
</div>
|
||||
|
||||
<HierarchicalCarnetSelector
|
||||
carnets={carnets}
|
||||
selectedId={selectedContextId}
|
||||
onSelect={setSelectedContextId}
|
||||
placeholder="Context supplémentaire..."
|
||||
className="w-full text-[11px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-48 flex flex-col items-center justify-center text-center space-y-3 text-muted-ink/30">
|
||||
<div className="w-12 h-12 rounded-full border border-dashed border-muted-ink/10 flex items-center justify-center">
|
||||
<MessageSquare size={18} />
|
||||
</div>
|
||||
<p className="text-[11px] italic leading-relaxed px-12">Conversation prête. Posez votre question ci-dessous.</p>
|
||||
</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-accent/30 transition-all duration-500 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Layout size={80} className="text-accent" />
|
||||
</div>
|
||||
|
||||
<div className="relative space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-slate-50 rounded-lg text-accent">
|
||||
<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-accent/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-accent/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-accent text-paper rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-accent/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 === 'relations' && (
|
||||
<motion.div
|
||||
key="relations"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="space-y-6 animate-fadeIn"
|
||||
>
|
||||
<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">Vue Graphe Locale</h4>
|
||||
<div className="h-px flex-1 bg-border/40" />
|
||||
</div>
|
||||
|
||||
{activeNote ? (
|
||||
<>
|
||||
{/* Interactive local graph SVG container */}
|
||||
<div className="relative p-2 bg-slate-50/50 dark:bg-black/30 border border-border/60 rounded-2xl overflow-hidden shadow-inner flex flex-col items-center">
|
||||
<svg width="100%" height="220" viewBox="0 0 320 220" className="select-none font-sans">
|
||||
<defs>
|
||||
<filter id="glow-panel-sidebar-three" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Dotted circle boundary helper */}
|
||||
<circle cx="160" cy="110" r="70" fill="none" stroke="#E2E8F0" strokeWidth="1" strokeDasharray="3,6" className="dark:stroke-neutral-800" />
|
||||
|
||||
{/* Connections */}
|
||||
{orbitNodes.map((node, i) => {
|
||||
const angle = i * (orbitNodes.length > 0 ? (2 * Math.PI) / orbitNodes.length : 0);
|
||||
const nx = 160 + 70 * Math.cos(angle);
|
||||
const ny = 110 + 62 * Math.sin(angle);
|
||||
return (
|
||||
<g key={node.id}>
|
||||
<line
|
||||
x1="160"
|
||||
y1="110"
|
||||
x2={nx}
|
||||
y2={ny}
|
||||
stroke={node.relationship === 'mention' ? '#94A3B8' : '#A47148'}
|
||||
strokeWidth={node.relationship === 'mention' ? 1.2 : 2}
|
||||
strokeDasharray={node.relationship === 'mention' ? '3,3' : 'none'}
|
||||
className="opacity-50 transition-all hover:opacity-100"
|
||||
/>
|
||||
{node.relationship === 'outbound' && (
|
||||
<polygon
|
||||
points={`${160 + (nx - 160) * 0.75},${110 + (ny - 110) * 0.75} ${160 + (nx - 160) * 0.75 - 4},${110 + (ny - 110) * 0.75 - 4} ${160 + (nx - 160) * 0.75 - 4},${110 + (ny - 110) * 0.75 + 4}`}
|
||||
transform={`rotate(${(angle * 180) / Math.PI}, ${160 + (nx - 160) * 0.75}, ${110 + (ny - 110) * 0.75})`}
|
||||
fill="#A47148"
|
||||
className="opacity-70"
|
||||
/>
|
||||
)}
|
||||
{node.relationship === 'backlink' && (
|
||||
<polygon
|
||||
points={`${160 + (nx - 160) * 0.3},${110 + (ny - 110) * 0.3} ${160 + (nx - 160) * 0.3 - 4},${110 + (ny - 110) * 0.3 - 4} ${160 + (nx - 160) * 0.3 - 4},${110 + (ny - 110) * 0.3 + 4}`}
|
||||
transform={`rotate(${((angle + Math.PI) * 180) / Math.PI}, ${160 + (nx - 160) * 0.3}, ${110 + (ny - 110) * 0.3})`}
|
||||
fill="#A47148"
|
||||
className="opacity-70"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Center node (Active Note) */}
|
||||
<g>
|
||||
<circle
|
||||
cx="160"
|
||||
cy="110"
|
||||
r="15"
|
||||
fill="#A47148"
|
||||
className="stroke-white dark:stroke-black stroke-[3px] shadow transition-transform duration-300 hover:scale-110 active:scale-95 cursor-pointer"
|
||||
/>
|
||||
<circle cx="160" cy="110" r="5" fill="#FFFFFF" />
|
||||
</g>
|
||||
|
||||
{/* Orbit nodes */}
|
||||
{orbitNodes.map((node, i) => {
|
||||
const angle = i * (orbitNodes.length > 0 ? (2 * Math.PI) / orbitNodes.length : 0);
|
||||
const nx = 160 + 70 * Math.cos(angle);
|
||||
const ny = 110 + 62 * Math.sin(angle);
|
||||
const isHovered = hoveredOrbitNode?.id === node.id;
|
||||
return (
|
||||
<g
|
||||
key={node.id}
|
||||
className="cursor-pointer group"
|
||||
onClick={() => onOpenNote(node.id)}
|
||||
onMouseEnter={() => setHoveredOrbitNode(node)}
|
||||
onMouseLeave={() => setHoveredOrbitNode(null)}
|
||||
>
|
||||
<circle
|
||||
cx={nx}
|
||||
cy={ny}
|
||||
r={isHovered ? 11 : 8}
|
||||
fill={node.color}
|
||||
stroke={isHovered ? '#000000' : '#FFFFFF'}
|
||||
strokeWidth={1.5}
|
||||
className="transition-all duration-200 group-hover:shadow"
|
||||
/>
|
||||
<text
|
||||
x={nx}
|
||||
y={ny + 15}
|
||||
textAnchor="middle"
|
||||
className="text-[7.5px] font-sans font-bold select-none pointer-events-none fill-ink/70 dark:fill-white/70"
|
||||
>
|
||||
{node.title.length > 10 ? node.title.substring(0, 8) + '...' : node.title}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Interactive local tooltip card info */}
|
||||
<div className="w-full mt-2 bg-white dark:bg-black/40 border border-border/80 rounded-xl p-3 text-xs leading-normal font-sans">
|
||||
{hoveredOrbitNode ? (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-[8px] font-bold uppercase tracking-wide text-muted-ink">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: hoveredOrbitNode.color }} />
|
||||
{hoveredOrbitNode.carnetName}
|
||||
</span>
|
||||
<span className="text-ochre">
|
||||
{hoveredOrbitNode.relationship === 'backlink' ? 'Lien Entrant' : hoveredOrbitNode.relationship === 'outbound' ? 'Lien Sortant' : 'Mention Simple'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-bold text-ink dark:text-white truncate">{hoveredOrbitNode.title}</p>
|
||||
<p className="text-[9px] text-muted-ink italic">Cliquez pour ouvrir la note</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-1 text-muted-ink/60 text-[10px] font-medium leading-normal flex items-center justify-center gap-1.5">
|
||||
<Network size={12} className="text-muted-ink/40" />
|
||||
Survolez un nœud, cliquez pour ouvrir
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lists of backlinks & unlinked mentions */}
|
||||
<div className="space-y-4 pt-2 font-sans">
|
||||
{/* 1. Backlinks */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-[10px] uppercase tracking-[0.18em] font-bold text-muted-ink flex items-center justify-between">
|
||||
<span>Liens Entrans ({backlinks.length})</span>
|
||||
</h5>
|
||||
{backlinks.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[160px] overflow-y-auto custom-scrollbar pr-1">
|
||||
{backlinks.map(n => (
|
||||
<div
|
||||
key={n.id}
|
||||
onClick={() => onOpenNote(n.id)}
|
||||
className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between text-muted-ink font-sans">
|
||||
<span className="text-[9px] font-bold uppercase tracking-wider text-ink dark:text-white truncate max-w-[180px]">{n.title}</span>
|
||||
<span className="text-[8px] bg-accent/5 text-accent/80 px-1.5 py-0.5 rounded-full font-bold uppercase tracking-tight">Réf</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-ink/70 dark:text-white/70 italic leading-snug">
|
||||
{getSnippetWithHighlight(n.content, activeNote.title)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-ink leading-normal italic bg-slate-50 dark:bg-zinc-900/40 p-3 rounded-xl border border-border/40">Aucun lien entrant explicite pointant vers cette note.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 2. Outbound Links */}
|
||||
<div className="space-y-2 text-sans">
|
||||
<h5 className="text-[10px] uppercase tracking-[0.18em] font-bold text-muted-ink flex items-center justify-between font-sans">
|
||||
<span>Liens Sortants ({outboundLinks.length})</span>
|
||||
</h5>
|
||||
{outboundLinks.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[160px] overflow-y-auto custom-scrollbar pr-1">
|
||||
{outboundLinks.map(n => (
|
||||
<div
|
||||
key={n.id}
|
||||
onClick={() => onOpenNote(n.id)}
|
||||
className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm animate-fadeIn"
|
||||
>
|
||||
<div className="flex items-center justify-between text-muted-ink font-sans font-medium">
|
||||
<span className="text-[9px] font-bold uppercase tracking-wider text-ink dark:text-white truncate max-w-[180px]">{n.title}</span>
|
||||
<span className="text-[8px] bg-indigo-500/10 text-indigo-500 px-1.5 py-0.5 rounded-full font-bold uppercase tracking-tight font-sans">Cible</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-ink/70 dark:text-white/70 italic leading-snug font-sans">
|
||||
{getSnippetWithHighlight(activeNote.content, n.title)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-ink leading-normal italic bg-slate-50 dark:bg-zinc-900/40 p-3 rounded-xl border border-border/40">Cette note ne pointe vers aucun lien sortant explicite.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 3. Unlinked Mentions */}
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-[10px] uppercase tracking-[0.18em] font-bold text-muted-ink flex items-center justify-between">
|
||||
<span>Mentions Simples ({unlinkedMentions.length})</span>
|
||||
</h5>
|
||||
{unlinkedMentions.length > 0 ? (
|
||||
<div className="space-y-2 max-h-[160px] overflow-y-auto custom-scrollbar pr-1 font-sans">
|
||||
{unlinkedMentions.map(n => (
|
||||
<div
|
||||
key={n.id}
|
||||
onClick={() => onOpenNote(n.id)}
|
||||
className="p-3 bg-white dark:bg-zinc-950 border border-border hover:border-accent/40 rounded-xl cursor-pointer transition-all space-y-1.5 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between text-muted-ink">
|
||||
<span className="text-[9px] font-bold uppercase tracking-wider text-ink dark:text-white truncate max-w-[150px]">{n.title}</span>
|
||||
<span className="text-[8px] bg-neutral-100 dark:bg-neutral-800 text-muted-ink px-1.5 py-0.5 rounded-full font-bold uppercase tracking-tight">Mention</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-ink/70 dark:text-white/70 italic leading-snug font-sans">
|
||||
{getSnippetWithHighlight(n.content, activeNote.title)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-ink leading-normal italic bg-slate-50 dark:bg-zinc-900/40 p-3 rounded-xl border border-border/40">Aucune mention textuelle non-liée trouvée dans vos autres notes.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-ink/40">
|
||||
<Network size={36} className="mx-auto mb-3 opacity-30" />
|
||||
<p className="text-xs font-serif italic">Veuillez sélectionner une note pour explorer son graphe relationnel.</p>
|
||||
</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-accent 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-accent 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-accent text-white rounded-xl text-sm font-bold flex items-center justify-center gap-3 hover:opacity-90 transition-opacity shadow-lg shadow-accent/20">
|
||||
<Sparkles size={18} />
|
||||
Générer l'aperçu
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{aiTab === 'discussion' && (
|
||||
<div className="p-6 bg-white/40 dark:bg-black/20 border-t border-border backdrop-blur-xl">
|
||||
<div className="relative group/chat">
|
||||
<textarea
|
||||
rows={5}
|
||||
placeholder="Tapez votre demande ici..."
|
||||
className="w-full bg-white/80 dark:bg-white/5 border border-border rounded-[24px] p-5 pr-14 text-sm outline-none focus:border-accent focus:ring-4 ring-accent/5 transition-all resize-none leading-relaxed font-light shadow-inner"
|
||||
/>
|
||||
<div className="absolute right-4 bottom-4 flex flex-col gap-2">
|
||||
<button className="p-2.5 bg-accent text-white rounded-xl transition-all hover:scale-110 active:scale-95 shadow-lg shadow-accent/20">
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute left-6 bottom-4 flex gap-3 text-muted-ink/40">
|
||||
<button className="hover:text-accent transition-colors"><Globe size={14} /></button>
|
||||
<button className="hover:text-accent transition-colors"><Network size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<p className="text-[9px] text-muted-ink/40 uppercase tracking-[0.3em] font-bold">Shift+Enter for new line</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user