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>
326 lines
19 KiB
TypeScript
326 lines
19 KiB
TypeScript
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>
|
||
);
|
||
};
|