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:
zenchantlive 2026-02-26 10:22:11 -08:00
parent adcceb68bb
commit 7b27f673fe
3 changed files with 550 additions and 116 deletions

View file

@ -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>

View file

@ -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">

View file

@ -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';
}