From 97d5b4a58cac847c2733ca44581754decaa0b25f Mon Sep 17 00:00:00 2001 From: cawcenter Date: Sat, 13 Dec 2025 20:13:54 -0500 Subject: [PATCH] feat: Milestone 1 Task 4 - Spintax Manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented fully interactive Spintax Manager: ✅ Dashboard: Replaced static list with interactive React SpintaxManager ✅ Live Preview: 'Test Spintax' modal spins terms in real-time ✅ Schema Alignment: Mapped frontend to correct Directus fields (base_word, data) ✅ UI Upgrades: - Enhanced Dialog component with dark mode & animations - Fixed Badge component variants - Fixed AvatarVariantsManager schema mismatch (gender -> variant_type) Next: Task 1.5 - Cartesian Patterns --- .../intelligence/AvatarVariantsManager.tsx | 26 +-- .../admin/intelligence/SpintaxManager.tsx | 218 ++++++++++++++++++ frontend/src/components/ui/dialog.tsx | 23 +- .../collections/spintax-dictionaries.astro | 183 +-------------- 4 files changed, 258 insertions(+), 192 deletions(-) diff --git a/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx b/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx index 6069ff1..7c25a2a 100644 --- a/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx +++ b/frontend/src/components/admin/intelligence/AvatarVariantsManager.tsx @@ -14,10 +14,10 @@ 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; + avatar_key: string; + identity: string; // was name + variant_type: 'Male' | 'Female' | 'Neutral'; // was gender + tone_modifiers: string; // was tone age_range?: string; descriptor?: string; } @@ -58,9 +58,9 @@ export default function AvatarVariantsManager() { // 2. Compute Stats const stats = { total: variants.length, - male: variants.filter((v) => v.gender === 'Male').length, - female: variants.filter((v) => v.gender === 'Female').length, - neutral: variants.filter((v) => v.gender === 'Neutral').length + male: variants.filter((v) => v.variant_type === 'Male').length, + female: variants.filter((v) => v.variant_type === 'Female').length, + neutral: variants.filter((v) => v.variant_type === 'Neutral').length }; // 3. deletion @@ -81,8 +81,8 @@ export default function AvatarVariantsManager() { const avatarVariants = variants.filter((v) => v.avatar_key === avatar.slug); // Filter by search const filtered = avatarVariants.filter((v) => - v.name.toLowerCase().includes(search.toLowerCase()) || - v.tone.toLowerCase().includes(search.toLowerCase()) + (v.identity && v.identity.toLowerCase().includes(search.toLowerCase())) || + (v.tone_modifiers && v.tone_modifiers.toLowerCase().includes(search.toLowerCase())) ); return { avatar, @@ -207,14 +207,14 @@ export default function AvatarVariantsManager() {
-

{variant.name}

- - {variant.gender} +

{variant.identity}

+ + {variant.variant_type}
-

Tone: {variant.tone}

+

Tone: {variant.tone_modifiers}

{variant.age_range &&

Age: {variant.age_range}

}
diff --git a/frontend/src/components/admin/intelligence/SpintaxManager.tsx b/frontend/src/components/admin/intelligence/SpintaxManager.tsx index e69de29..ec82b48 100644 --- a/frontend/src/components/admin/intelligence/SpintaxManager.tsx +++ b/frontend/src/components/admin/intelligence/SpintaxManager.tsx @@ -0,0 +1,218 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getDirectusClient, readItems, deleteItem, createItem, updateItem } from '@/lib/directus/client'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { + Search, Plus, Edit2, Trash2, Tag, Book, RefreshCw, Eye +} from 'lucide-react'; +import { toast } from 'sonner'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; + +const client = getDirectusClient(); + +interface SpintaxDictionary { + id: string; + key: string; + base_word: string; // "name" in UI + category: string; + data: string[]; // "terms" in UI +} + +export default function SpintaxManager() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(''); + const [selectedDict, setSelectedDict] = useState(null); + const [testResult, setTestResult] = useState(''); + const [previewOpen, setPreviewOpen] = useState(false); + + // 1. Fetch Data + const { data: dictionariesRaw = [], isLoading } = useQuery({ + queryKey: ['spintax_dictionaries'], + queryFn: async () => { + // @ts-ignore + return await client.request(readItems('spintax_dictionaries', { limit: -1 })); + } + }); + + const spintaxList = dictionariesRaw as unknown as SpintaxDictionary[]; + + // 2. Mutations + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + // @ts-ignore + await client.request(deleteItem('spintax_dictionaries', id)); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['spintax_dictionaries'] }); + toast.success('Dictionary deleted'); + } + }); + + // 3. Stats + const stats = { + total: spintaxList.length, + categories: new Set(spintaxList.map(d => d.category)).size, + totalTerms: spintaxList.reduce((acc, d) => acc + (d.data?.length || 0), 0) + }; + + // 4. Test Logic + const testSpintax = (dict: SpintaxDictionary) => { + if (!dict.data || dict.data.length === 0) { + setTestResult('No terms defined.'); + return; + } + const randomTerm = dict.data[Math.floor(Math.random() * dict.data.length)]; + setTestResult(randomTerm); + setSelectedDict(dict); + setPreviewOpen(true); + }; + + // 5. Filter + const filtered = spintaxList.filter(d => + (d.base_word && d.base_word.toLowerCase().includes(search.toLowerCase())) || + (d.key && d.key.toLowerCase().includes(search.toLowerCase())) || + (d.category && d.category.toLowerCase().includes(search.toLowerCase())) + ); + + if (isLoading) return
Loading Spintax Dictionaries...
; + + return ( +
+ {/* Stats */} +
+ + +
+

Dictionaries

+

{stats.total}

+
+ +
+
+ + +
+

Categories

+

{stats.categories}

+
+ +
+
+ + +
+

Total Variations

+

{stats.totalTerms}

+
+ +
+
+
+ + {/* Toolbar */} +
+
+ + setSearch(e.target.value)} + className="pl-9 bg-zinc-950 border-zinc-800" + /> +
+ +
+ + {/* List */} +
+ {filtered.map((dict) => ( + + + +
+ + {dict.category || 'Uncategorized'} + + {dict.base_word || 'Untitled'} + + {`{${dict.key || dict.base_word}}`} + +
+
+ +
+
+ {dict.data?.slice(0, 5).join(', ')} + {dict.data?.length > 5 && '...'} +
+
+

+ {dict.data?.length || 0} terms +

+
+
+ + + +
+ + + + ))} +
+ + {/* Preview Modal */} + + + + Spintax Preview: {selectedDict?.base_word} + +
+
+ + {testResult} + +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index 8758acc..43c1dd7 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -3,22 +3,29 @@ import * as React from "react" const Dialog = ({ open, onOpenChange, children }: any) => { if (!open) return null return ( -
-
onOpenChange(false)} /> -
{children}
+
+
onOpenChange(false)} /> +
{children}
) } +const DialogTrigger = ({ children, asChild, onClick, ...props }: any) => { + // This is a simplified trigger that just renders children. + // In a real implementation (Radix UI), this controls the dialog state. + // For now, we rely on the parent controlling 'open' state. + return
{children}
+} + const DialogContent = ({ children, className }: any) => ( -
+
{children}
) -const DialogHeader = ({ children }: any) =>
{children}
-const DialogTitle = ({ children, className }: any) =>

{children}

-const DialogDescription = ({ children, className }: any) =>

{children}

+const DialogHeader = ({ children }: any) =>
{children}
+const DialogTitle = ({ children, className }: any) =>

{children}

+const DialogDescription = ({ children, className }: any) =>

{children}

const DialogFooter = ({ children }: any) =>
{children}
-export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } +export { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle, DialogDescription, DialogFooter } diff --git a/frontend/src/pages/admin/collections/spintax-dictionaries.astro b/frontend/src/pages/admin/collections/spintax-dictionaries.astro index b927b46..da4c1b8 100644 --- a/frontend/src/pages/admin/collections/spintax-dictionaries.astro +++ b/frontend/src/pages/admin/collections/spintax-dictionaries.astro @@ -1,179 +1,20 @@ --- -/** - * Spintax Dictionaries Management - * Word variation sets for content spinning - */ -import AdminLayout from '@/layouts/AdminLayout.astro'; -import { getDirectusClient } from '@/lib/directus/client'; -import { readItems } from '@directus/sdk'; - -const client = getDirectusClient(); - -let dictionaries = []; -let error = null; -let stats = { - total: 0, - totalWords: 0, - byCategory: {} as Record, -}; - -try { - dictionaries = await client.request(readItems('spintax_dictionaries', { - fields: ['*'], - sort: ['category', 'base_word'], - })); - - stats.total = dictionaries.length; - dictionaries.forEach((d: any) => { - const cat = d.category || 'general'; - stats.byCategory[cat] = (stats.byCategory[cat] || 0) + 1; - if (d.variations && Array.isArray(d.variations)) { - stats.totalWords += d.variations.length; - } - }); -} catch (e) { - console.error('Error fetching dictionaries:', e); - error = e instanceof Error ? e.message : 'Unknown error'; -} - -// Group by category -const byCategory = dictionaries.reduce((acc: any, dict: any) => { - const cat = dict.category || 'general'; - if (!acc[cat]) acc[cat] = []; - acc[cat].push(dict); - return acc; -}, {}); +import Layout from '@/layouts/AdminLayout.astro'; +import SpintaxManager from '@/components/admin/intelligence/SpintaxManager'; --- - -
- -
+ +
+
-

📚 Spintax Dictionaries

-

Word variation sets for content spinning

-
-
- - - - ✨ New Entry - +

📚 Spintax Library

+

+ Manage your vocabulary variations. Use these sets to generate diverse, non-repetitive content + using {key|word|variant} syntax. +

- {error && ( -
- Error: {error} -
- )} - - -
-
-
Total Entries
-
{stats.total}
-
-
-
Total Words
-
{stats.totalWords}
-
-
-
Categories
-
{Object.keys(stats.byCategory).length}
-
-
-
Avg Variations
-
- {stats.total > 0 ? Math.round(stats.totalWords / stats.total) : 0} -
-
-
- - -
- {Object.entries(byCategory).map(([category, items]: [string, any]) => ( -
-
-
-

{category}

- {items.length} entries -
-
- -
- {items.map((dict: any) => ( -
-
-
-
- {dict.base_word} - {dict.variations && Array.isArray(dict.variations) && ( - - {dict.variations.length} variations - - )} -
- {dict.variations && Array.isArray(dict.variations) && ( -
- {dict.variations.slice(0, 10).map((variation: string) => ( - - {variation} - - ))} - {dict.variations.length > 10 && ( - - +{dict.variations.length - 10} more - - )} -
- )} - {dict.spintax_format && ( -
- {dict.spintax_format} -
- )} -
-
- - - Edit - -
-
-
- ))} -
-
- ))} - - {dictionaries.length === 0 && !error && ( -
-

No spintax dictionaries found. Start building your word library!

-
- )} -
+
- - - +