diff --git a/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx b/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx index e69de29..305b708 100644 --- a/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx +++ b/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx @@ -0,0 +1,265 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getDirectusClient, readItems, deleteItem, createItem } from '@/lib/directus/client'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { + Users, Plus, Search, Edit2, Trash2, Copy, Play, + ChevronDown, ChevronRight, User, Sparkles +} from 'lucide-react'; +import { toast } from 'sonner'; +import { motion, AnimatePresence } from 'framer-motion'; + +interface Variant { + id: string; + avatar_key: string; // The slug of the parent avatar + name: string; // e.g. "Aggressive closer" + gender: 'Male' | 'Female' | 'Neutral'; + tone: string; + age_range?: string; + descriptor?: string; +} + +interface Avatar { + id: string; + slug: string; + base_name: string; +} + +export default function AvatarVariantsManager() { + const queryClient = useQueryClient(); + const client = getDirectusClient(); + const [search, setSearch] = useState(''); + const [expandedGroups, setExpandedGroups] = useState>({}); + + // 1. Fetch Data + const { data: variants = [], isLoading: isLoadingVariants } = useQuery({ + queryKey: ['avatar_variants'], + queryFn: async () => { + // @ts-ignore + return await client.request(readItems('avatar_variants', { limit: -1 })); + } + }); + + const { data: avatars = [] } = useQuery({ + queryKey: ['avatar_intelligence'], + queryFn: async () => { + // @ts-ignore + return await client.request(readItems('avatar_intelligence', { sort: ['base_name'], limit: -1 })); + } + }); + + // 2. Compute Stats + const stats = { + total: variants.length, + male: variants.filter((v: Variant) => v.gender === 'Male').length, + female: variants.filter((v: Variant) => v.gender === 'Female').length, + neutral: variants.filter((v: Variant) => v.gender === 'Neutral').length + }; + + // 3. deletion + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + // @ts-ignore + await client.request(deleteItem('avatar_variants', id)); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['avatar_variants'] }); + toast.success('Variant deleted'); + }, + onError: (err: any) => toast.error(err.message) + }); + + // 4. Grouping Logic + const groupedVariants = avatars.map((avatar: Avatar) => { + const avatarVariants = variants.filter((v: Variant) => v.avatar_key === avatar.slug); + // Filter by search + const filtered = avatarVariants.filter((v: Variant) => + v.name.toLowerCase().includes(search.toLowerCase()) || + v.tone.toLowerCase().includes(search.toLowerCase()) + ); + return { + avatar, + variants: filtered, + count: avatarVariants.length + }; + }).filter((group: any) => group.variants.length > 0 || search === ''); // Hide empty groups only if searching + + const toggleGroup = (slug: string) => { + setExpandedGroups(prev => ({ ...prev, [slug]: !prev[slug] })); + }; + + // Helper for gender colors + const getGenderColor = (gender: string) => { + switch (gender) { + case 'Male': return 'bg-blue-500/10 text-blue-400 border-blue-500/20'; + case 'Female': return 'bg-pink-500/10 text-pink-400 border-pink-500/20'; + default: return 'bg-purple-500/10 text-purple-400 border-purple-500/20'; + } + }; + + if (isLoadingVariants) return
Loading variants...
; + + return ( +
+ {/* Stats Cards */} +
+ + + Total Variants +
+ {stats.total} + +
+
+
+ + + Male +
+ {stats.male} + +
+
+
+ + + Female +
+ {stats.female} + +
+
+
+ + + Neutral +
+ {stats.neutral} + +
+
+
+
+ + {/* Toolbar */} +
+
+ + setSearch(e.target.value)} + className="pl-9 bg-zinc-950 border-zinc-800 text-white focus:border-purple-500/50" + /> +
+
+ +
+
+ + {/* Grouped List */} +
+ {groupedVariants.map((group: any) => { + const isExpanded = expandedGroups[group.avatar.slug] ?? true; // Default open + + return ( + +
toggleGroup(group.avatar.slug)} + > +
+ {isExpanded ? : } +

{group.avatar.base_name}

+ + {group.variants.length} base + +
+ +
+ + + {isExpanded && ( + +
+ {group.variants.map((variant: Variant) => ( + + +
+

{variant.name}

+ + {variant.gender} + +
+ +
+

Tone: {variant.tone}

+ {variant.age_range &&

Age: {variant.age_range}

} +
+ +
+ + + + +
+
+
+ ))} + + {group.variants.length === 0 && ( +
+

No variants for this avatar yet.

+ +
+ )} +
+
+ )} +
+
+ ); + })} + + {groupedVariants.length === 0 && ( +
+ No avatars or variants found matching your search. +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/admin/content/avatar-variants.astro b/frontend/src/pages/admin/content/avatar-variants.astro new file mode 100644 index 0000000..3241372 --- /dev/null +++ b/frontend/src/pages/admin/content/avatar-variants.astro @@ -0,0 +1,20 @@ +--- +import Layout from '@/layouts/AdminLayout.astro'; +import AvatarVariantsManager from '@/components/admin/intelligence/AvatarVariantsManager'; +--- + + +
+
+
+

🧬 Variant Laboratory

+

+ Fine-tune specific persona variations. Create "Aggressive" sales clones or "Empathetic" support agents + derived from your base Avatars. +

+
+
+ + +
+