feat: migrate semantic search to pgvector + full-text search
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m12s
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:
325
architectural-grid10/src/components/AgentsView.tsx
Normal file
325
architectural-grid10/src/components/AgentsView.tsx
Normal 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 d’un 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 l’avis 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user