Publication IA: - 4 templates (magazine, brief, essay, simple) avec CSS riche - Rewrite IA (article/exercises/tutorial/reference/mixed) - Modération avec timeout 12s + fallback safe - Quotas publish_enhance par tier (basic=2, pro=15, business=100) - Détection contenu stale (hash) - Migration DB publishedContent/publishedTemplate/publishedSourceHash Fixes: - cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast - _isShared ajouté au type Note (champ virtuel serveur) - callout colors PDF export: extraction fonction pure testable - admin/published: guard note.userId null - Cmd+S fonctionne en mode dialog (pas seulement fullPage) i18n: - 23 clés publish* traduites dans les 15 locales - Extension Web Clipper: 13 locales mise à jour Tests: - callout-colors.test.ts (6 tests) - note-visible-in-view.test.ts (5 tests) - entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs - 199/199 tests passent Tracker: user-stories.md sync avec sprint-status.yaml
397 lines
22 KiB
TypeScript
397 lines
22 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
Plus,
|
||
ArrowLeft,
|
||
Clock,
|
||
Activity,
|
||
Trash2,
|
||
Edit3,
|
||
Play,
|
||
Eye,
|
||
Microscope,
|
||
Globe,
|
||
Layers,
|
||
Zap,
|
||
BookOpen,
|
||
Sparkles,
|
||
ChevronDown,
|
||
Info,
|
||
Check,
|
||
ClipboardCheck,
|
||
ListTodo,
|
||
ArrowRight,
|
||
Loader2,
|
||
Menu
|
||
} from 'lucide-react';
|
||
import { motion, AnimatePresence } from 'motion/react';
|
||
import { Carnet, Note } from '../types';
|
||
import { HierarchicalCarnetSelector } from './HierarchicalCarnetSelector';
|
||
import { extractActionItems } from '../services/geminiService';
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
|
||
interface AgentsViewProps {
|
||
selectedAgentId: string | null;
|
||
setSelectedAgentId: (id: string | null) => void;
|
||
carnets: Carnet[];
|
||
notes: Note[];
|
||
onAddNote?: (note: Note) => void;
|
||
onOpenSidebar?: () => void;
|
||
}
|
||
|
||
export const AgentsView: React.FC<AgentsViewProps> = ({
|
||
selectedAgentId,
|
||
setSelectedAgentId,
|
||
carnets,
|
||
notes,
|
||
onAddNote,
|
||
onOpenSidebar
|
||
}) => {
|
||
const [selectedCarnetForAgent, setSelectedCarnetForAgent] = React.useState<string | null>('4');
|
||
const [agentType, setAgentType] = React.useState<'Surveillant' | 'Personnalisé' | 'Slides' | 'Diagramme' | 'Tasks'>('Tasks');
|
||
const [isRunningAgent, setIsRunningAgent] = React.useState(false);
|
||
const [agentResult, setAgentResult] = React.useState<string | null>(null);
|
||
|
||
const handleRunTaskAgent = async () => {
|
||
setIsRunningAgent(true);
|
||
setAgentResult(null);
|
||
|
||
// Get notes from carnet
|
||
const filteredNotes = notes.filter(n => n.carnetId === selectedCarnetForAgent && !n.isDeleted);
|
||
|
||
try {
|
||
const result = await extractActionItems(filteredNotes);
|
||
setAgentResult(result);
|
||
} catch (e) {
|
||
console.error(e);
|
||
} finally {
|
||
setIsRunningAgent(false);
|
||
}
|
||
};
|
||
|
||
const handleSaveResult = () => {
|
||
if (!agentResult || !onAddNote) return;
|
||
|
||
const carnetName = carnets.find(c => c.id === selectedCarnetForAgent)?.name || 'General';
|
||
|
||
const newNote: Note = {
|
||
id: uuidv4(),
|
||
carnetId: selectedCarnetForAgent || '1',
|
||
title: `Consolidated Actions: ${carnetName}`,
|
||
date: new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date()),
|
||
content: agentResult,
|
||
imageUrl: 'https://images.unsplash.com/photo-1484480974693-6ca0a78fb36b?auto=format&fit=crop&q=80&w=800&h=600',
|
||
tags: [{ id: 't-tasks', label: 'Action Items', type: 'ai' }]
|
||
};
|
||
|
||
onAddNote(newNote);
|
||
alert('Note de tâches créée avec succès !');
|
||
setSelectedAgentId(null);
|
||
};
|
||
|
||
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-6 sm: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 lg:items-end gap-4">
|
||
<div className="flex items-center gap-4">
|
||
<button
|
||
onClick={onOpenSidebar}
|
||
className="lg:hidden p-2 -ml-2 text-ink hover:bg-black/5 rounded-lg transition-colors"
|
||
>
|
||
<Menu size={20} />
|
||
</button>
|
||
<div className="space-y-1">
|
||
<h1 className="text-3xl sm: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>
|
||
</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é.' },
|
||
{ id: 'a4', icon: <ClipboardCheck size={20} className="text-rose-500" />, title: 'Action Miner', status: 'Inactif', type: 'TASKS', meta: 'À la demande', desc: 'Scan vos notes pour extraire automatiquement les tâches, assignés et deadlines.' },
|
||
].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: 'Tasks', icon: <ListTodo size={18} />, label: 'Action Items', desc: 'Extrait les tâches et deadlines' },
|
||
{ 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-accent bg-white shadow-xl shadow-accent/10' : 'border-border bg-white/50 hover:bg-white'}`}
|
||
>
|
||
<div className={`p-3 rounded-xl transition-all ${agentType === type.id ? 'bg-accent 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-accent' : 'border-border opacity-20'}`}>
|
||
{agentType === type.id && <div className="w-2 h-2 bg-accent 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-accent/5 focus:border-accent/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 Memento'
|
||
].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-accent border-accent text-white' : 'bg-white border-border group-hover:border-accent/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="pt-8 border-t border-border flex flex-col items-center gap-6">
|
||
{agentResult ? (
|
||
<div className="w-full space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h5 className="text-[10px] font-bold uppercase tracking-widest text-concrete">Résultat de l'extraction</h5>
|
||
<button onClick={handleSaveResult} className="text-accent hover:underline text-[10px] font-bold uppercase tracking-widest flex items-center gap-1">
|
||
<ArrowRight size={12} /> Sauvegarder dans une note
|
||
</button>
|
||
</div>
|
||
<div className="bg-slate-50 dark:bg-black/20 p-6 rounded-2xl border border-accent/20 font-serif text-sm text-ink leading-relaxed whitespace-pre-wrap max-h-96 overflow-y-auto">
|
||
{agentResult}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={handleRunTaskAgent}
|
||
disabled={isRunningAgent}
|
||
className="px-12 py-4 bg-accent text-white rounded-2xl text-sm font-bold uppercase tracking-[0.2em] hover:opacity-90 transition-all shadow-xl shadow-accent/20 flex items-center gap-4 disabled:opacity-50"
|
||
>
|
||
{isRunningAgent ? <Loader2 size={18} className="animate-spin" /> : <Play size={18} fill="currentColor" />}
|
||
Lancer l'extraction d'actions
|
||
</button>
|
||
)}
|
||
<p className="text-[10px] text-concrete/60 italic">Cet agent analysera toutes les notes du carnet sélectionné.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|