feat(ui): enhance archetype and template inspectors with customization
- Add getArchetypeDisplayChar() and getTemplateDisplayChar() utils - Add getTemplateColor() utility function - ArchetypeInspector: Add color palette (30 presets), emoji selector (40 emojis), capability autocomplete, live preview, clone functionality - TemplateInspector: Same enhancements plus team composition editor
This commit is contained in:
parent
adcceb68bb
commit
7b27f673fe
3 changed files with 550 additions and 116 deletions
|
|
@ -1,17 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, ShieldAlert, Trash2, Plus } from 'lucide-react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { X, Save, ShieldAlert, Trash2, Plus, Copy, Palette, Smile } from 'lucide-react';
|
||||
import type { AgentArchetype } from '../../lib/types-swarm';
|
||||
|
||||
const COLOR_PRESETS = [
|
||||
'#3b82f6', '#2563eb', '#1d4ed8', '#0ea5e9', '#06b6d4',
|
||||
'#10b981', '#059669', '#22c55e', '#84cc16', '#a3e635',
|
||||
'#8b5cf6', '#7c3aed', '#a855f7', '#c084fc', '#e879f9',
|
||||
'#ef4444', '#dc2626', '#f97316', '#fb923c', '#fbbf24',
|
||||
'#ec4899', '#db2777', '#f472b6', '#f9a8d4', '#fda4af',
|
||||
'#6366f1', '#64748b', '#78716c', '#57534e', '#1e293b',
|
||||
];
|
||||
|
||||
const EMOJI_PRESETS = [
|
||||
'🏗️', '⚙️', '🔍', '🧪', '🚀', '🤖', '👨💻', '👩💻', '🧙♂️', '🧙♀️',
|
||||
'🔧', '📝', '🎯', '⚡', '🛡️', '📊', '🗂️', '💡', '🔮', '🧩',
|
||||
'⭐', '🔥', '💎', '🚦', '🎪', '🎨', '🎭', '🃏', '👑', '🏆',
|
||||
'🦅', '🐺', '🦁', '🐻', '🦊', '🐙', '🐝', '🦋', '🌿', '🌊',
|
||||
];
|
||||
|
||||
const SUGGESTED_CAPABILITIES = [
|
||||
'coding', 'testing', 'debugging', 'refactoring', 'documentation',
|
||||
'code_review', 'system_design', 'architecture', 'planning', 'analysis',
|
||||
'research', 'investigation', 'deployment', 'ci_cd', 'monitoring',
|
||||
'security', 'performance', 'optimization', 'integration', 'migration',
|
||||
'data_analysis', 'automation', 'scripting', 'api_design', 'database',
|
||||
'frontend', 'backend', 'devops', 'qa', 'mentoring',
|
||||
];
|
||||
|
||||
interface ArchetypeInspectorProps {
|
||||
archetype?: AgentArchetype;
|
||||
onClose: () => void;
|
||||
onSave: (data: Partial<AgentArchetype>) => Promise<void>;
|
||||
onDelete?: (id: string) => Promise<void>;
|
||||
onClone?: (archetype: AgentArchetype) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: ArchetypeInspectorProps) {
|
||||
export function ArchetypeInspector({ archetype, onClose, onSave, onDelete, onClone }: ArchetypeInspectorProps) {
|
||||
const isNew = !archetype;
|
||||
|
||||
const [name, setName] = useState(archetype?.name || '');
|
||||
|
|
@ -19,10 +45,16 @@ export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: Arc
|
|||
const [systemPrompt, setSystemPrompt] = useState(archetype?.systemPrompt || '');
|
||||
const [capabilities, setCapabilities] = useState<string[]>(archetype?.capabilities || []);
|
||||
const [color, setColor] = useState(archetype?.color || '#3b82f6');
|
||||
const [icon, setIcon] = useState(archetype?.icon || '');
|
||||
const [newCapability, setNewCapability] = useState('');
|
||||
const [capabilityFilter, setCapabilityFilter] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
const [showCapabilityDropdown, setShowCapabilityDropdown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (archetype) {
|
||||
|
|
@ -31,13 +63,25 @@ export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: Arc
|
|||
setSystemPrompt(archetype.systemPrompt);
|
||||
setCapabilities(archetype.capabilities);
|
||||
setColor(archetype.color);
|
||||
setIcon(archetype.icon || '');
|
||||
}
|
||||
}, [archetype]);
|
||||
|
||||
const handleAddCapability = () => {
|
||||
if (newCapability.trim()) {
|
||||
setCapabilities([...capabilities, newCapability.trim().toLowerCase()]);
|
||||
const filteredSuggestions = useMemo(() => {
|
||||
return SUGGESTED_CAPABILITIES.filter(
|
||||
cap =>
|
||||
cap.includes(capabilityFilter.toLowerCase()) &&
|
||||
!capabilities.includes(cap)
|
||||
).slice(0, 6);
|
||||
}, [capabilityFilter, capabilities]);
|
||||
|
||||
const handleAddCapability = (cap?: string) => {
|
||||
const toAdd = cap || newCapability.trim();
|
||||
if (toAdd && !capabilities.includes(toAdd.toLowerCase())) {
|
||||
setCapabilities([...capabilities, toAdd.toLowerCase()]);
|
||||
setNewCapability('');
|
||||
setCapabilityFilter('');
|
||||
setShowCapabilityDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -62,6 +106,7 @@ export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: Arc
|
|||
systemPrompt: systemPrompt.trim(),
|
||||
capabilities,
|
||||
color,
|
||||
icon: icon || undefined,
|
||||
isBuiltIn: archetype?.isBuiltIn
|
||||
});
|
||||
onClose();
|
||||
|
|
@ -90,16 +135,38 @@ export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: Arc
|
|||
}
|
||||
};
|
||||
|
||||
const handleClone = async () => {
|
||||
if (!archetype || !onClone) return;
|
||||
|
||||
setIsCloning(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await onClone(archetype);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to clone');
|
||||
} finally {
|
||||
setIsCloning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayChar = icon || name.charAt(0) || '?';
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
<div className="flex flex-col h-[85vh] w-full max-w-2xl overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
|
||||
<div className="flex flex-col h-[90vh] w-full max-w-3xl overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="h-10 w-10 rounded-lg flex items-center justify-center font-bold text-lg border"
|
||||
style={{ backgroundColor: `${color}15`, color: color, borderColor: `${color}30` }}
|
||||
className="h-12 w-12 rounded-xl flex items-center justify-center text-xl font-bold border-2 transition-all duration-200"
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color: color,
|
||||
borderColor: `${color}50`
|
||||
}}
|
||||
>
|
||||
{name.charAt(0) || '?'}
|
||||
{displayChar}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-[var(--ui-text-primary)] leading-tight">
|
||||
|
|
@ -116,145 +183,298 @@ export function ArchetypeInspector({ archetype, onClose, onSave, onDelete }: Arc
|
|||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mx-5 mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg text-rose-400 text-sm">
|
||||
<div className="mx-5 mt-4 p-3 bg-rose-500/10 border border-rose-500/20 rounded-lg text-rose-400 text-sm flex items-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4 flex-shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{archetype?.isBuiltIn && (
|
||||
<div className="mx-5 mt-4 flex items-start gap-3 bg-[var(--ui-accent-warning)]/10 border border-[var(--ui-accent-warning)]/20 p-3 rounded-lg text-[var(--ui-accent-warning)]">
|
||||
<ShieldAlert className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<span className="font-semibold">Built-in Archetype.</span> This is a core system role. You cannot delete it.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-5 space-y-6 custom-scrollbar">
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g., System Architect"
|
||||
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Brief description of this archetype's role"
|
||||
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Color</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="w-10 h-10 rounded cursor-pointer border border-[var(--ui-border-soft)]"
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="flex-1 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
placeholder="e.g., Code Reviewer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
placeholder="Brief description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">System Prompt *</label>
|
||||
<label className="block text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">System Prompt *</label>
|
||||
<textarea
|
||||
value={systemPrompt}
|
||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||
placeholder="You are an expert software engineer..."
|
||||
rows={6}
|
||||
className="w-full bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] font-mono resize-y focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)] custom-scrollbar"
|
||||
className="w-full px-3 py-2 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] focus:outline-none focus:ring-2 focus:ring-blue-500/50 font-mono text-sm resize-none"
|
||||
placeholder="You are a helpful assistant that..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider mb-1.5 block">Capabilities</label>
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newCapability}
|
||||
onChange={(e) => setNewCapability(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddCapability())}
|
||||
placeholder="e.g., execute_code"
|
||||
className="flex-1 bg-[#0a111a] border border-[var(--ui-border-soft)] rounded-md px-3 py-2 text-sm text-[var(--ui-text-primary)] focus:outline-none focus:border-[var(--ui-accent-info)] focus:ring-1 focus:ring-[var(--ui-accent-info)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCapability}
|
||||
disabled={!newCapability.trim()}
|
||||
className="px-3 py-2 bg-[var(--ui-border-soft)] hover:bg-[var(--ui-border-hover)] text-white rounded-md transition-colors disabled:opacity-50 flex items-center gap-1 text-sm font-medium"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Add
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">
|
||||
<Palette className="w-4 h-4" />
|
||||
Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg border-2 border-white/20"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowColorPicker(!showColorPicker)}
|
||||
className="px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-secondary)] hover:bg-white/5 text-sm"
|
||||
>
|
||||
{showColorPicker ? 'Hide' : 'Pick'}
|
||||
</button>
|
||||
</div>
|
||||
{showColorPicker && (
|
||||
<div className="grid grid-cols-10 gap-1.5 p-2 bg-[var(--ui-bg-soft)] rounded-lg border border-[var(--ui-border-soft)]">
|
||||
{COLOR_PRESETS.map((presetColor) => (
|
||||
<button
|
||||
key={presetColor}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setColor(presetColor);
|
||||
setShowColorPicker(false);
|
||||
}}
|
||||
className={`h-6 w-6 rounded-md border-2 transition-all hover:scale-110 ${color === presetColor ? 'border-white ring-2 ring-white/30' : 'border-transparent'}`}
|
||||
style={{ backgroundColor: presetColor }}
|
||||
title={presetColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{capabilities.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{capabilities.map((cap, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 bg-[#14202e] border border-[var(--ui-border-soft)] px-2.5 py-1 rounded-full text-xs text-[var(--ui-text-primary)] isolate">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: color }}></div>
|
||||
<span>{cap}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveCapability(i)}
|
||||
className="ml-1 text-[var(--ui-text-muted)] hover:text-rose-400 transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">
|
||||
<Smile className="w-4 h-4" />
|
||||
Icon / Emoji
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg flex items-center justify-center text-lg border border-[var(--ui-border-soft)] bg-[var(--ui-bg-soft)]"
|
||||
style={{ color }}
|
||||
>
|
||||
{icon || '?'}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={icon}
|
||||
onChange={(e) => setIcon(e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
placeholder="Emoji or leave empty"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
className="px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-secondary)] hover:bg-white/5 text-sm"
|
||||
>
|
||||
{showEmojiPicker ? 'Hide' : 'Pick'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-[var(--ui-text-muted)] italic py-2">No specific capabilities defined.</div>
|
||||
)}
|
||||
{showEmojiPicker && (
|
||||
<div className="grid grid-cols-10 gap-1.5 p-2 bg-[var(--ui-bg-soft)] rounded-lg border border-[var(--ui-border-soft)]">
|
||||
{EMOJI_PRESETS.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIcon(emoji);
|
||||
setShowEmojiPicker(false);
|
||||
}}
|
||||
className={`h-6 w-6 rounded-md flex items-center justify-center text-base transition-all hover:scale-110 hover:bg-white/10 ${icon === emoji ? 'bg-white/20 ring-2 ring-white/30' : ''}`}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">Capabilities</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{capabilities.map((cap, index) => (
|
||||
<span
|
||||
key={cap}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-secondary)]"
|
||||
>
|
||||
{cap}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveCapability(index)}
|
||||
className="text-[var(--ui-text-muted)] hover:text-rose-400 transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newCapability}
|
||||
onChange={(e) => {
|
||||
setNewCapability(e.target.value);
|
||||
setCapabilityFilter(e.target.value);
|
||||
setShowCapabilityDropdown(true);
|
||||
}}
|
||||
onFocus={() => setShowCapabilityDropdown(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddCapability();
|
||||
}
|
||||
}}
|
||||
className="flex-1 px-3 py-2 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] placeholder:text-[var(--ui-text-muted)] focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-sm"
|
||||
placeholder="Add capability..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddCapability()}
|
||||
disabled={!newCapability.trim()}
|
||||
className="px-3 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{showCapabilityDropdown && filteredSuggestions.length > 0 && (
|
||||
<div className="absolute z-10 mt-1 w-full bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] rounded-lg shadow-lg overflow-hidden">
|
||||
{filteredSuggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
onClick={() => handleAddCapability(suggestion)}
|
||||
className="w-full px-3 py-2 text-left text-sm text-[var(--ui-text-primary)] hover:bg-white/5 transition-colors"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--ui-border-soft)] pt-4">
|
||||
<h3 className="text-sm font-medium text-[var(--ui-text-secondary)] mb-3">Live Preview</h3>
|
||||
<div className="p-4 rounded-xl bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)]">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="h-10 w-10 rounded-xl flex items-center justify-center text-lg font-bold border-2"
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color: color,
|
||||
borderColor: `${color}50`
|
||||
}}
|
||||
>
|
||||
{displayChar}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-[var(--ui-text-primary)]">
|
||||
{name || 'Archetype Name'}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--ui-text-muted)]">
|
||||
{description || 'No description'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{capabilities.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{capabilities.slice(0, 5).map((cap) => (
|
||||
<span
|
||||
key={cap}
|
||||
className="px-2 py-0.5 rounded-full text-[10px] font-medium"
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color: color
|
||||
}}
|
||||
>
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
{capabilities.length > 5 && (
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-medium text-[var(--ui-text-muted)]">
|
||||
+{capabilities.length - 5} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{systemPrompt && (
|
||||
<div className="mt-3 p-2 rounded-lg bg-black/20 text-xs text-[var(--ui-text-muted)] font-mono line-clamp-2">
|
||||
{systemPrompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--ui-border-soft)] bg-[#0A111A] p-4 flex items-center justify-between flex-shrink-0 rounded-b-xl">
|
||||
<div>
|
||||
{!isNew && !archetype?.isBuiltIn && (
|
||||
<div className="flex items-center justify-between border-t border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
|
||||
<div className="flex items-center gap-2">
|
||||
{!isNew && onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="px-4 py-2 border border-rose-500/20 text-rose-400 hover:bg-rose-500/10 rounded-md text-sm font-medium transition-colors flex items-center gap-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={isDeleting || archetype?.isBuiltIn}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-rose-400 hover:bg-rose-500/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
)}
|
||||
{!isNew && onClone && (
|
||||
<button
|
||||
onClick={handleClone}
|
||||
disabled={isCloning}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-[var(--ui-text-secondary)] hover:bg-white/5 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
{isCloning ? 'Cloning...' : 'Clone'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{archetype?.isBuiltIn && (
|
||||
<span className="text-xs text-amber-400 bg-amber-500/10 px-2 py-1 rounded">
|
||||
Built-in archetype
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-[var(--ui-text-muted)] hover:text-white transition-colors cursor-pointer"
|
||||
className="px-4 py-2 rounded-lg text-[var(--ui-text-secondary)] hover:bg-white/5 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="px-5 py-2 bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md text-sm font-medium transition-colors flex items-center gap-2 shadow-lg shadow-blue-500/20 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={isSaving || !name.trim() || !systemPrompt.trim()}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{isSaving ? 'Saving...' : (isNew ? 'Create Archetype' : 'Save Changes')}
|
||||
{isSaving ? 'Saving...' : (isNew ? 'Create' : 'Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,27 +1,49 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, Save, Trash2, Plus, Network, ShieldAlert } from 'lucide-react';
|
||||
import { X, Save, Trash2, Plus, Network, ShieldAlert, Palette, Smile, Copy } from 'lucide-react';
|
||||
import type { SwarmTemplate, AgentArchetype } from '../../lib/types-swarm';
|
||||
|
||||
const COLOR_PRESETS = [
|
||||
'#3b82f6', '#2563eb', '#1d4ed8', '#0ea5e9', '#06b6d4',
|
||||
'#10b981', '#059669', '#22c55e', '#84cc16', '#a3e635',
|
||||
'#8b5cf6', '#7c3aed', '#a855f7', '#c084fc', '#e879f9',
|
||||
'#ef4444', '#dc2626', '#f97316', '#fb923c', '#fbbf24',
|
||||
'#ec4899', '#db2777', '#f472b6', '#f9a8d4', '#fda4af',
|
||||
'#6366f1', '#64748b', '#78716c', '#57534e', '#1e293b',
|
||||
];
|
||||
|
||||
const EMOJI_PRESETS = [
|
||||
'🏗️', '⚙️', '🔍', '🧪', '🚀', '🤖', '👨💻', '👩💻', '🧙♂️', '🧙♀️',
|
||||
'🔧', '📝', '🎯', '⚡', '🛡️', '📊', '🗂️', '💡', '🔮', '🧩',
|
||||
'⭐', '🔥', '💎', '🚦', '🎪', '🎨', '🎭', '🃏', '👑', '🏆',
|
||||
'🦅', '🐺', '🦁', '🐻', '🦊', '🐙', '🐝', '🦋', '🌿', '🌊',
|
||||
];
|
||||
|
||||
interface TemplateInspectorProps {
|
||||
template?: SwarmTemplate;
|
||||
archetypes: AgentArchetype[];
|
||||
onClose: () => void;
|
||||
onSave: (data: Partial<SwarmTemplate>) => Promise<void>;
|
||||
onDelete?: (id: string) => Promise<void>;
|
||||
onClone?: (template: SwarmTemplate) => Promise<void>;
|
||||
}
|
||||
|
||||
export function TemplateInspector({ template, archetypes, onClose, onSave, onDelete }: TemplateInspectorProps) {
|
||||
export function TemplateInspector({ template, archetypes, onClose, onSave, onDelete, onClone }: TemplateInspectorProps) {
|
||||
const isNew = !template;
|
||||
|
||||
const [name, setName] = useState(template?.name || '');
|
||||
const [description, setDescription] = useState(template?.description || '');
|
||||
const [team, setTeam] = useState<{ archetypeId: string; count: number }[]>(template?.team || []);
|
||||
const [protoFormula, setProtoFormula] = useState(template?.protoFormula || '');
|
||||
const [color, setColor] = useState(template?.color || '#f59e0b');
|
||||
const [icon, setIcon] = useState(template?.icon || '');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (template) {
|
||||
|
|
@ -29,6 +51,8 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
setDescription(template.description);
|
||||
setTeam(template.team);
|
||||
setProtoFormula(template.protoFormula || '');
|
||||
setColor(template.color || '#f59e0b');
|
||||
setIcon(template.icon || '');
|
||||
}
|
||||
}, [template]);
|
||||
|
||||
|
|
@ -71,6 +95,8 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
description: description.trim(),
|
||||
team,
|
||||
protoFormula: protoFormula.trim() || undefined,
|
||||
color,
|
||||
icon: icon || undefined,
|
||||
isBuiltIn: template?.isBuiltIn
|
||||
});
|
||||
onClose();
|
||||
|
|
@ -99,7 +125,32 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
}
|
||||
};
|
||||
|
||||
const handleClone = async () => {
|
||||
if (!template || !onClone) return;
|
||||
|
||||
setIsCloning(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await onClone(template);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to clone');
|
||||
} finally {
|
||||
setIsCloning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const totalAgents = team.reduce((acc, curr) => acc + curr.count, 0);
|
||||
const displayChar = icon || totalAgents;
|
||||
|
||||
const getArchetypeName = (id: string) => {
|
||||
return archetypes.find(a => a.id === id)?.name || id;
|
||||
};
|
||||
|
||||
const getArchetypeColor = (id: string) => {
|
||||
return archetypes.find(a => a.id === id)?.color || '#64748b';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
|
|
@ -107,8 +158,11 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
|
||||
<div className="flex items-center justify-between border-b border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-10 w-10 flex-shrink-0 rounded-full bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-500 font-bold text-lg">
|
||||
{totalAgents}
|
||||
<div
|
||||
className="h-12 w-12 rounded-xl flex items-center justify-center text-xl font-bold border-2"
|
||||
style={{ backgroundColor: `${color}20`, color: color, borderColor: `${color}50` }}
|
||||
>
|
||||
{displayChar}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -167,6 +221,97 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">
|
||||
<Palette className="w-4 h-4" />
|
||||
Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg border-2 border-white/20"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowColorPicker(!showColorPicker)}
|
||||
className="px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-secondary)] hover:bg-white/5 text-sm"
|
||||
>
|
||||
{showColorPicker ? 'Hide' : 'Pick'}
|
||||
</button>
|
||||
</div>
|
||||
{showColorPicker && (
|
||||
<div className="grid grid-cols-10 gap-1.5 p-2 bg-[var(--ui-bg-soft)] rounded-lg border border-[var(--ui-border-soft)]">
|
||||
{COLOR_PRESETS.map((presetColor) => (
|
||||
<button
|
||||
key={presetColor}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setColor(presetColor);
|
||||
setShowColorPicker(false);
|
||||
}}
|
||||
className={`h-6 w-6 rounded-md border-2 transition-all hover:scale-110 ${color === presetColor ? 'border-white ring-2 ring-white/30' : 'border-transparent'}`}
|
||||
style={{ backgroundColor: presetColor }}
|
||||
title={presetColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-[var(--ui-text-secondary)] mb-1.5">
|
||||
<Smile className="w-4 h-4" />
|
||||
Icon / Emoji
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="h-8 w-8 rounded-lg flex items-center justify-center text-lg border border-[var(--ui-border-soft)] bg-[var(--ui-bg-soft)]"
|
||||
style={{ color }}
|
||||
>
|
||||
{icon || '?'}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={icon}
|
||||
onChange={(e) => setIcon(e.target.value)}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
placeholder="Emoji or leave empty"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
className="px-3 py-1.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-secondary)] hover:bg-white/5 text-sm"
|
||||
>
|
||||
{showEmojiPicker ? 'Hide' : 'Pick'}
|
||||
</button>
|
||||
</div>
|
||||
{showEmojiPicker && (
|
||||
<div className="grid grid-cols-10 gap-1.5 p-2 bg-[var(--ui-bg-soft)] rounded-lg border border-[var(--ui-border-soft)]">
|
||||
{EMOJI_PRESETS.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIcon(emoji);
|
||||
setShowEmojiPicker(false);
|
||||
}}
|
||||
className={`h-6 w-6 rounded-md flex items-center justify-center text-base transition-all hover:scale-110 hover:bg-white/10 ${icon === emoji ? 'bg-white/20 ring-2 ring-white/30' : ''}`}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--ui-border-soft)] pt-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<label className="text-xs font-semibold text-[var(--ui-text-muted)] uppercase tracking-wider block">Team Composition *</label>
|
||||
|
|
@ -228,12 +373,58 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--ui-border-soft)] pt-4">
|
||||
<h3 className="text-sm font-medium text-[var(--ui-text-secondary)] mb-3">Live Preview</h3>
|
||||
<div className="p-4 rounded-xl bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)]">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="h-10 w-10 rounded-xl flex items-center justify-center text-lg font-bold border-2"
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color: color,
|
||||
borderColor: `${color}50`
|
||||
}}
|
||||
>
|
||||
{displayChar}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-[var(--ui-text-primary)]">
|
||||
{name || 'Template Name'}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--ui-text-muted)]">
|
||||
{description || 'No description'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{team.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{team.map((member, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 rounded-full text-[10px] font-medium"
|
||||
style={{
|
||||
backgroundColor: `${getArchetypeColor(member.archetypeId)}20`,
|
||||
color: getArchetypeColor(member.archetypeId)
|
||||
}}
|
||||
>
|
||||
{getArchetypeName(member.archetypeId)} ×{member.count}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{protoFormula && (
|
||||
<div className="mt-3 p-2 rounded-lg bg-black/20 text-xs text-[var(--ui-text-muted)] font-mono line-clamp-2">
|
||||
{protoFormula}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer Controls */}
|
||||
<div className="border-t border-[var(--ui-border-soft)] bg-[#0A111A] p-4 flex items-center justify-between flex-shrink-0 rounded-b-xl">
|
||||
<div>
|
||||
{!isNew && !template?.isBuiltIn && (
|
||||
<div className="flex items-center gap-2">
|
||||
{!isNew && !template?.isBuiltIn && onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
|
|
@ -244,6 +435,17 @@ export function TemplateInspector({ template, archetypes, onClose, onSave, onDel
|
|||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
)}
|
||||
{!isNew && onClone && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClone}
|
||||
disabled={isCloning}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-[var(--ui-text-secondary)] hover:bg-white/5 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
{isCloning ? 'Cloning...' : 'Clone'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
|
|
|
|||
|
|
@ -4,3 +4,15 @@ import { twMerge } from "tailwind-merge";
|
|||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function getArchetypeDisplayChar(archetype: { icon?: string; name: string }): string {
|
||||
return archetype.icon || archetype.name.charAt(0) || '?';
|
||||
}
|
||||
|
||||
export function getTemplateDisplayChar(template: { icon?: string; name: string }): string {
|
||||
return template.icon || template.name.charAt(0) || '?';
|
||||
}
|
||||
|
||||
export function getTemplateColor(template: { color?: string }): string {
|
||||
return template.color || '#f59e0b';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue