fix: Remove Intelligence Library components causing build failure

- Remove incomplete CRUD components that require Shadcn UI
- Keep Jumpstart fix and frontend plugin upgrades
- Diagnostic test confirms all API connections working
- Build now succeeds and ready for deployment

Note: Intelligence Library UI components will be added in next phase
after Shadcn UI components are properly set up.
This commit is contained in:
cawcenter
2025-12-13 18:38:21 -05:00
parent 903c7193a9
commit df8a5b2372
13 changed files with 342 additions and 1620 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,369 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ColumnDef } from '@tanstack/react-table';
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
import { DataTable } from '../shared/DataTable';
import { CRUDModal } from '../shared/CRUDModal';
import { DeleteConfirm } from '../shared/DeleteConfirm';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
// Validation schema
const avatarVariantSchema = z.object({
avatar_key: z.string().min(1, 'Avatar key is required'),
variant_type: z.enum(['male', 'female', 'neutral']),
pronoun: z.string().min(1, 'Pronoun is required'),
identity: z.string().min(1, 'Identity is required'),
tone_modifiers: z.string().optional(),
});
type AvatarVariantFormData = z.infer<typeof avatarVariantSchema>;
interface AvatarVariant {
id: string;
avatar_key: string;
variant_type: 'male' | 'female' | 'neutral';
pronoun: string;
identity: string;
tone_modifiers?: string;
}
export default function AvatarVariantManager() {
const [variants, setVariants] = useState<AvatarVariant[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [editingVariant, setEditingVariant] = useState<AvatarVariant | null>(null);
const [deletingVariant, setDeletingVariant] = useState<AvatarVariant | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const {
register,
handleSubmit,
reset,
setValue,
watch,
formState: { errors },
} = useForm<AvatarVariantFormData>({
resolver: zodResolver(avatarVariantSchema),
});
const variantType = watch('variant_type');
// Load data
const loadVariants = async () => {
setIsLoading(true);
try {
const client = getDirectusClient();
const data = await client.request(
readItems('avatar_variants', {
fields: ['*'],
sort: ['avatar_key', 'variant_type'],
})
);
setVariants(data as AvatarVariant[]);
} catch (error) {
console.error('Error loading avatar variants:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadVariants();
}, []);
// Handle create/edit
const onSubmit = async (data: AvatarVariantFormData) => {
setIsSubmitting(true);
try {
const client = getDirectusClient();
if (editingVariant) {
await client.request(
updateItem('avatar_variants', editingVariant.id, data)
);
} else {
await client.request(createItem('avatar_variants', data));
}
await loadVariants();
setIsModalOpen(false);
reset();
setEditingVariant(null);
} catch (error) {
console.error('Error saving variant:', error);
alert('Failed to save variant');
} finally {
setIsSubmitting(false);
}
};
// Handle delete
const handleDelete = async () => {
if (!deletingVariant) return;
setIsSubmitting(true);
try {
const client = getDirectusClient();
await client.request(deleteItem('avatar_variants', deletingVariant.id));
await loadVariants();
setIsDeleteOpen(false);
setDeletingVariant(null);
} catch (error) {
console.error('Error deleting variant:', error);
alert('Failed to delete variant');
} finally {
setIsSubmitting(false);
}
};
// Handle edit click
const handleEdit = (variant: AvatarVariant) => {
setEditingVariant(variant);
setValue('avatar_key', variant.avatar_key);
setValue('variant_type', variant.variant_type);
setValue('pronoun', variant.pronoun);
setValue('identity', variant.identity);
setValue('tone_modifiers', variant.tone_modifiers || '');
setIsModalOpen(true);
};
// Handle add click
const handleAdd = () => {
setEditingVariant(null);
reset();
setIsModalOpen(true);
};
// Handle delete click
const handleDeleteClick = (variant: AvatarVariant) => {
setDeletingVariant(variant);
setIsDeleteOpen(true);
};
// Export data
const handleExport = () => {
const dataStr = JSON.stringify(variants, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `avatar-variants-${new Date().toISOString().split('T')[0]}.json`;
link.click();
};
// Table columns
const columns: ColumnDef<AvatarVariant>[] = [
{
accessorKey: 'avatar_key',
header: 'Avatar',
cell: ({ row }) => (
<span className="font-medium text-white">{row.original.avatar_key}</span>
),
},
{
accessorKey: 'variant_type',
header: 'Type',
cell: ({ row }) => {
const type = row.original.variant_type;
const colors = {
male: 'bg-blue-500/20 text-blue-400',
female: 'bg-pink-500/20 text-pink-400',
neutral: 'bg-purple-500/20 text-purple-400',
};
return (
<Badge className={colors[type]}>
{type}
</Badge>
);
},
},
{
accessorKey: 'pronoun',
header: 'Pronoun',
},
{
accessorKey: 'identity',
header: 'Identity',
},
{
accessorKey: 'tone_modifiers',
header: 'Tone Modifiers',
cell: ({ row }) => row.original.tone_modifiers || '—',
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(row.original)}
className="text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
>
Edit
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteClick(row.original)}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
Delete
</Button>
</div>
),
},
];
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-4 gap-4">
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Total Variants</div>
<div className="text-3xl font-bold text-white">{variants.length}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Male</div>
<div className="text-3xl font-bold text-blue-400">
{variants.filter((v) => v.variant_type === 'male').length}
</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Female</div>
<div className="text-3xl font-bold text-pink-400">
{variants.filter((v) => v.variant_type === 'female').length}
</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Neutral</div>
<div className="text-3xl font-bold text-purple-400">
{variants.filter((v) => v.variant_type === 'neutral').length}
</div>
</div>
</div>
{/* Data Table */}
<DataTable
data={variants}
columns={columns}
onAdd={handleAdd}
onExport={handleExport}
searchPlaceholder="Search variants..."
addButtonText="Add Variant"
isLoading={isLoading}
/>
{/* Create/Edit Modal */}
<CRUDModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setEditingVariant(null);
reset();
}}
title={editingVariant ? 'Edit Avatar Variant' : 'Create Avatar Variant'}
description="Configure gender and tone variations for an avatar"
onSubmit={handleSubmit(onSubmit)}
isSubmitting={isSubmitting}
>
<form className="space-y-4">
<div>
<Label htmlFor="avatar_key">Avatar Key</Label>
<Input
id="avatar_key"
{...register('avatar_key')}
placeholder="e.g., dr_smith"
className="bg-slate-900 border-slate-700"
/>
{errors.avatar_key && (
<p className="text-red-400 text-sm mt-1">{errors.avatar_key.message}</p>
)}
</div>
<div>
<Label htmlFor="variant_type">Variant Type</Label>
<Select
value={variantType}
onValueChange={(value) => setValue('variant_type', value as any)}
>
<SelectTrigger className="bg-slate-900 border-slate-700">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="neutral">Neutral</SelectItem>
</SelectContent>
</Select>
{errors.variant_type && (
<p className="text-red-400 text-sm mt-1">{errors.variant_type.message}</p>
)}
</div>
<div>
<Label htmlFor="pronoun">Pronoun</Label>
<Input
id="pronoun"
{...register('pronoun')}
placeholder="e.g., he/him, she/her, they/them"
className="bg-slate-900 border-slate-700"
/>
{errors.pronoun && (
<p className="text-red-400 text-sm mt-1">{errors.pronoun.message}</p>
)}
</div>
<div>
<Label htmlFor="identity">Identity</Label>
<Input
id="identity"
{...register('identity')}
placeholder="e.g., Dr. Sarah Smith"
className="bg-slate-900 border-slate-700"
/>
{errors.identity && (
<p className="text-red-400 text-sm mt-1">{errors.identity.message}</p>
)}
</div>
<div>
<Label htmlFor="tone_modifiers">Tone Modifiers (Optional)</Label>
<Input
id="tone_modifiers"
{...register('tone_modifiers')}
placeholder="e.g., professional, friendly"
className="bg-slate-900 border-slate-700"
/>
</div>
</form>
</CRUDModal>
{/* Delete Confirmation */}
<DeleteConfirm
isOpen={isDeleteOpen}
onClose={() => {
setIsDeleteOpen(false);
setDeletingVariant(null);
}}
onConfirm={handleDelete}
itemName={deletingVariant?.identity}
isDeleting={isSubmitting}
/>
</div>
);
}

View File

@@ -1,155 +0,0 @@
import React, { useState, useEffect } from 'react';
import { getDirectusClient, readItems } from '@/lib/directus/client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
interface GeoLocation {
id: string;
city: string;
state: string;
zip_focus?: string;
neighborhood?: string;
cluster: number;
}
interface GeoCluster {
id: number;
cluster_name: string;
locations?: GeoLocation[];
}
export default function GeoIntelligenceManager() {
const [clusters, setClusters] = useState<GeoCluster[]>([]);
const [locations, setLocations] = useState<GeoLocation[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Load data
const loadData = async () => {
setIsLoading(true);
try {
const client = getDirectusClient();
// Load clusters
const clustersData = await client.request(
readItems('geo_clusters', {
fields: ['*'],
sort: ['cluster_name'],
})
);
// Load locations
const locationsData = await client.request(
readItems('geo_locations', {
fields: ['*'],
sort: ['state', 'city'],
})
);
setClusters(clustersData as GeoCluster[]);
setLocations(locationsData as GeoLocation[]);
} catch (error) {
console.error('Error loading geo intelligence:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
// Group locations by cluster
const locationsByCluster = locations.reduce((acc, loc) => {
if (!acc[loc.cluster]) acc[loc.cluster] = [];
acc[loc.cluster].push(loc);
return acc;
}, {} as Record<number, GeoLocation[]>);
const totalCities = locations.length;
const totalStates = new Set(locations.map(l => l.state)).size;
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Total Clusters</div>
<div className="text-3xl font-bold text-white">{clusters.length}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Total Cities</div>
<div className="text-3xl font-bold text-blue-400">{totalCities}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">States Covered</div>
<div className="text-3xl font-bold text-green-400">{totalStates}</div>
</div>
</div>
{/* Clusters */}
<div className="space-y-4">
{clusters.map((cluster) => {
const clusterLocations = locationsByCluster[cluster.id] || [];
return (
<Card key={cluster.id} className="bg-slate-800 border-slate-700">
<CardHeader className="pb-3">
<CardTitle className="text-white flex justify-between items-center">
<span className="flex items-center gap-3">
🗺 {cluster.cluster_name}
</span>
<Badge className="bg-blue-600">
{clusterLocations.length} Cities
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{clusterLocations.map((loc) => (
<div
key={loc.id}
className="bg-slate-900 border border-slate-700 rounded-lg p-4 hover:border-blue-500/50 transition-colors"
>
<div className="font-medium text-white mb-1">
{loc.city}, {loc.state}
</div>
{loc.neighborhood && (
<div className="text-xs text-slate-400 mb-1">
📍 {loc.neighborhood}
</div>
)}
{loc.zip_focus && (
<div className="text-xs text-slate-500">
ZIP: {loc.zip_focus}
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
);
})}
</div>
{clusters.length === 0 && (
<Card className="bg-slate-800 border-slate-700">
<CardContent className="p-12 text-center">
<p className="text-slate-400 mb-4">No geographic clusters found.</p>
<p className="text-sm text-slate-500">
Run the schema initialization script to import geo intelligence data.
</p>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -1,354 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ColumnDef } from '@tanstack/react-table';
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
import { DataTable } from '../shared/DataTable';
import { CRUDModal } from '../shared/CRUDModal';
import { DeleteConfirm } from '../shared/DeleteConfirm';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
// Validation schema
const cartesianSchema = z.object({
pattern_key: z.string().min(1, 'Pattern key is required'),
pattern_type: z.string().min(1, 'Pattern type is required'),
formula: z.string().min(1, 'Formula is required'),
example_output: z.string().optional(),
description: z.string().optional(),
});
type CartesianFormData = z.infer<typeof cartesianSchema>;
interface CartesianPattern {
id: string;
pattern_key: string;
pattern_type: string;
formula: string;
example_output?: string;
description?: string;
}
export default function CartesianManager() {
const [patterns, setPatterns] = useState<CartesianPattern[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [editingPattern, setEditingPattern] = useState<CartesianPattern | null>(null);
const [deletingPattern, setDeletingPattern] = useState<CartesianPattern | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const {
register,
handleSubmit,
reset,
setValue,
formState: { errors },
} = useForm<CartesianFormData>({
resolver: zodResolver(cartesianSchema),
});
// Load data
const loadPatterns = async () => {
setIsLoading(true);
try {
const client = getDirectusClient();
const data = await client.request(
readItems('cartesian_patterns', {
fields: ['*'],
sort: ['pattern_type', 'pattern_key'],
})
);
setPatterns(data as CartesianPattern[]);
} catch (error) {
console.error('Error loading cartesian patterns:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadPatterns();
}, []);
// Handle create/edit
const onSubmit = async (data: CartesianFormData) => {
setIsSubmitting(true);
try {
const client = getDirectusClient();
if (editingPattern) {
await client.request(
updateItem('cartesian_patterns', editingPattern.id, data)
);
} else {
await client.request(createItem('cartesian_patterns', data));
}
await loadPatterns();
setIsModalOpen(false);
reset();
setEditingPattern(null);
} catch (error) {
console.error('Error saving pattern:', error);
alert('Failed to save pattern');
} finally {
setIsSubmitting(false);
}
};
// Handle delete
const handleDelete = async () => {
if (!deletingPattern) return;
setIsSubmitting(true);
try {
const client = getDirectusClient();
await client.request(deleteItem('cartesian_patterns', deletingPattern.id));
await loadPatterns();
setIsDeleteOpen(false);
setDeletingPattern(null);
} catch (error) {
console.error('Error deleting pattern:', error);
alert('Failed to delete pattern');
} finally {
setIsSubmitting(false);
}
};
// Handle edit click
const handleEdit = (pattern: CartesianPattern) => {
setEditingPattern(pattern);
Object.keys(pattern).forEach((key) => {
setValue(key as any, (pattern as any)[key]);
});
setIsModalOpen(true);
};
// Handle add click
const handleAdd = () => {
setEditingPattern(null);
reset();
setIsModalOpen(true);
};
// Handle delete click
const handleDeleteClick = (pattern: CartesianPattern) => {
setDeletingPattern(pattern);
setIsDeleteOpen(true);
};
// Export data
const handleExport = () => {
const dataStr = JSON.stringify(patterns, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `cartesian-patterns-${new Date().toISOString().split('T')[0]}.json`;
link.click();
};
// Table columns
const columns: ColumnDef<CartesianPattern>[] = [
{
accessorKey: 'pattern_key',
header: 'Pattern Key',
cell: ({ row }) => (
<span className="font-medium text-white font-mono">{row.original.pattern_key}</span>
),
},
{
accessorKey: 'pattern_type',
header: 'Type',
cell: ({ row }) => (
<Badge className="bg-purple-600">
{row.original.pattern_type}
</Badge>
),
},
{
accessorKey: 'formula',
header: 'Formula',
cell: ({ row }) => (
<code className="text-xs text-green-400 bg-slate-900 px-2 py-1 rounded">
{row.original.formula.length > 50
? row.original.formula.substring(0, 50) + '...'
: row.original.formula}
</code>
),
},
{
accessorKey: 'example_output',
header: 'Example',
cell: ({ row }) => (
<span className="text-sm italic text-slate-400">
{row.original.example_output
? (row.original.example_output.length > 40
? row.original.example_output.substring(0, 40) + '...'
: row.original.example_output)
: '—'}
</span>
),
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(row.original)}
className="text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
>
Edit
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteClick(row.original)}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
Delete
</Button>
</div>
),
},
];
const patternTypes = new Set(patterns.map(p => p.pattern_type));
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Total Patterns</div>
<div className="text-3xl font-bold text-white">{patterns.length}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Pattern Types</div>
<div className="text-3xl font-bold text-purple-400">{patternTypes.size}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Avg Formula Length</div>
<div className="text-3xl font-bold text-green-400">
{patterns.length > 0
? Math.round(patterns.reduce((sum, p) => sum + p.formula.length, 0) / patterns.length)
: 0}
</div>
</div>
</div>
{/* Data Table */}
<DataTable
data={patterns}
columns={columns}
onAdd={handleAdd}
onExport={handleExport}
searchPlaceholder="Search patterns..."
addButtonText="Add Pattern"
isLoading={isLoading}
/>
{/* Create/Edit Modal */}
<CRUDModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setEditingPattern(null);
reset();
}}
title={editingPattern ? 'Edit Cartesian Pattern' : 'Create Cartesian Pattern'}
description="Define content generation patterns using Cartesian product logic"
onSubmit={handleSubmit(onSubmit)}
isSubmitting={isSubmitting}
>
<form className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="pattern_key">Pattern Key</Label>
<Input
id="pattern_key"
{...register('pattern_key')}
placeholder="e.g., headline_template_1"
className="bg-slate-900 border-slate-700"
/>
{errors.pattern_key && (
<p className="text-red-400 text-sm mt-1">{errors.pattern_key.message}</p>
)}
</div>
<div>
<Label htmlFor="pattern_type">Pattern Type</Label>
<Input
id="pattern_type"
{...register('pattern_type')}
placeholder="e.g., headline, intro, cta"
className="bg-slate-900 border-slate-700"
/>
{errors.pattern_type && (
<p className="text-red-400 text-sm mt-1">{errors.pattern_type.message}</p>
)}
</div>
</div>
<div>
<Label htmlFor="formula">Formula</Label>
<Textarea
id="formula"
{...register('formula')}
placeholder="e.g., {adjective} {noun} in {location}"
className="bg-slate-900 border-slate-700 font-mono text-sm"
rows={3}
/>
{errors.formula && (
<p className="text-red-400 text-sm mt-1">{errors.formula.message}</p>
)}
<p className="text-xs text-slate-500 mt-1">
Use {'{'}curly braces{'}'} for variables that will be replaced
</p>
</div>
<div>
<Label htmlFor="example_output">Example Output (Optional)</Label>
<Textarea
id="example_output"
{...register('example_output')}
placeholder="e.g., Amazing Services in Austin"
className="bg-slate-900 border-slate-700"
rows={2}
/>
</div>
<div>
<Label htmlFor="description">Description (Optional)</Label>
<Input
id="description"
{...register('description')}
placeholder="e.g., Main headline template for service pages"
className="bg-slate-900 border-slate-700"
/>
</div>
</form>
</CRUDModal>
{/* Delete Confirmation */}
<DeleteConfirm
isOpen={isDeleteOpen}
onClose={() => {
setIsDeleteOpen(false);
setDeletingPattern(null);
}}
onConfirm={handleDelete}
itemName={deletingPattern?.pattern_key}
isDeleting={isSubmitting}
/>
</div>
);
}

View File

@@ -1,333 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ColumnDef } from '@tanstack/react-table';
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
import { DataTable } from '../shared/DataTable';
import { CRUDModal } from '../shared/CRUDModal';
import { DeleteConfirm } from '../shared/DeleteConfirm';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
// Validation schema
const spintaxSchema = z.object({
category: z.string().min(1, 'Category is required'),
terms: z.string().min(1, 'At least one term is required'),
description: z.string().optional(),
});
type SpintaxFormData = z.infer<typeof spintaxSchema>;
interface SpintaxDictionary {
id: string;
category: string;
data: string[];
description?: string;
}
export default function SpintaxManager() {
const [dictionaries, setDictionaries] = useState<SpintaxDictionary[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [editingDict, setEditingDict] = useState<SpintaxDictionary | null>(null);
const [deletingDict, setDeletingDict] = useState<SpintaxDictionary | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const {
register,
handleSubmit,
reset,
setValue,
formState: { errors },
} = useForm<SpintaxFormData>({
resolver: zodResolver(spintaxSchema),
});
// Load data
const loadDictionaries = async () => {
setIsLoading(true);
try {
const client = getDirectusClient();
const data = await client.request(
readItems('spintax_dictionaries', {
fields: ['*'],
sort: ['category'],
})
);
setDictionaries(data as SpintaxDictionary[]);
} catch (error) {
console.error('Error loading spintax dictionaries:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadDictionaries();
}, []);
// Handle create/edit
const onSubmit = async (formData: SpintaxFormData) => {
setIsSubmitting(true);
try {
const client = getDirectusClient();
// Convert comma-separated terms to array
const termsArray = formData.terms
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
const data = {
category: formData.category,
data: termsArray,
description: formData.description,
};
if (editingDict) {
await client.request(
updateItem('spintax_dictionaries', editingDict.id, data)
);
} else {
await client.request(createItem('spintax_dictionaries', data));
}
await loadDictionaries();
setIsModalOpen(false);
reset();
setEditingDict(null);
} catch (error) {
console.error('Error saving dictionary:', error);
alert('Failed to save dictionary');
} finally {
setIsSubmitting(false);
}
};
// Handle delete
const handleDelete = async () => {
if (!deletingDict) return;
setIsSubmitting(true);
try {
const client = getDirectusClient();
await client.request(deleteItem('spintax_dictionaries', deletingDict.id));
await loadDictionaries();
setIsDeleteOpen(false);
setDeletingDict(null);
} catch (error) {
console.error('Error deleting dictionary:', error);
alert('Failed to delete dictionary');
} finally {
setIsSubmitting(false);
}
};
// Handle edit click
const handleEdit = (dict: SpintaxDictionary) => {
setEditingDict(dict);
setValue('category', dict.category);
setValue('terms', (dict.data || []).join(', '));
setValue('description', dict.description || '');
setIsModalOpen(true);
};
// Handle add click
const handleAdd = () => {
setEditingDict(null);
reset();
setIsModalOpen(true);
};
// Handle delete click
const handleDeleteClick = (dict: SpintaxDictionary) => {
setDeletingDict(dict);
setIsDeleteOpen(true);
};
// Export data
const handleExport = () => {
const dataStr = JSON.stringify(dictionaries, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `spintax-dictionaries-${new Date().toISOString().split('T')[0]}.json`;
link.click();
};
// Table columns
const columns: ColumnDef<SpintaxDictionary>[] = [
{
accessorKey: 'category',
header: 'Category',
cell: ({ row }) => (
<span className="font-medium text-white">{row.original.category}</span>
),
},
{
accessorKey: 'data',
header: 'Terms',
cell: ({ row }) => (
<div className="flex flex-wrap gap-1 max-w-md">
{(row.original.data || []).slice(0, 5).map((term, i) => (
<Badge key={i} variant="outline" className="text-xs">
{term}
</Badge>
))}
{(row.original.data || []).length > 5 && (
<Badge variant="outline" className="text-xs text-slate-500">
+{(row.original.data || []).length - 5} more
</Badge>
)}
</div>
),
},
{
accessorKey: 'count',
header: 'Count',
cell: ({ row }) => (
<Badge className="bg-blue-600">
{(row.original.data || []).length}
</Badge>
),
},
{
accessorKey: 'description',
header: 'Description',
cell: ({ row }) => row.original.description || '—',
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(row.original)}
className="text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
>
Edit
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteClick(row.original)}
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
>
Delete
</Button>
</div>
),
},
];
const totalTerms = dictionaries.reduce((sum, d) => sum + (d.data || []).length, 0);
return (
<div className="space-y-6">
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Total Dictionaries</div>
<div className="text-3xl font-bold text-white">{dictionaries.length}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Total Terms</div>
<div className="text-3xl font-bold text-blue-400">{totalTerms}</div>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div className="text-sm text-slate-400 mb-2">Avg Terms/Dict</div>
<div className="text-3xl font-bold text-green-400">
{dictionaries.length > 0 ? Math.round(totalTerms / dictionaries.length) : 0}
</div>
</div>
</div>
{/* Data Table */}
<DataTable
data={dictionaries}
columns={columns}
onAdd={handleAdd}
onExport={handleExport}
searchPlaceholder="Search dictionaries..."
addButtonText="Add Dictionary"
isLoading={isLoading}
/>
{/* Create/Edit Modal */}
<CRUDModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setEditingDict(null);
reset();
}}
title={editingDict ? 'Edit Spintax Dictionary' : 'Create Spintax Dictionary'}
description="Manage synonym variations for content generation"
onSubmit={handleSubmit(onSubmit)}
isSubmitting={isSubmitting}
>
<form className="space-y-4">
<div>
<Label htmlFor="category">Category</Label>
<Input
id="category"
{...register('category')}
placeholder="e.g., adjectives, verbs, locations"
className="bg-slate-900 border-slate-700"
/>
{errors.category && (
<p className="text-red-400 text-sm mt-1">{errors.category.message}</p>
)}
</div>
<div>
<Label htmlFor="terms">Terms (comma-separated)</Label>
<Textarea
id="terms"
{...register('terms')}
placeholder="e.g., great, excellent, amazing, fantastic"
className="bg-slate-900 border-slate-700"
rows={4}
/>
{errors.terms && (
<p className="text-red-400 text-sm mt-1">{errors.terms.message}</p>
)}
<p className="text-xs text-slate-500 mt-1">
Separate terms with commas. Each term will be a variation.
</p>
</div>
<div>
<Label htmlFor="description">Description (Optional)</Label>
<Input
id="description"
{...register('description')}
placeholder="e.g., Positive adjectives for product descriptions"
className="bg-slate-900 border-slate-700"
/>
</div>
</form>
</CRUDModal>
{/* Delete Confirmation */}
<DeleteConfirm
isOpen={isDeleteOpen}
onClose={() => {
setIsDeleteOpen(false);
setDeletingDict(null);
}}
onConfirm={handleDelete}
itemName={deletingDict?.category}
isDeleting={isSubmitting}
/>
</div>
);
}

View File

@@ -1,76 +0,0 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
interface CRUDModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
description?: string;
onSubmit: () => void;
isSubmitting?: boolean;
children: React.ReactNode;
submitText?: string;
cancelText?: string;
}
export function CRUDModal({
isOpen,
onClose,
title,
description,
onSubmit,
isSubmitting = false,
children,
submitText = 'Save',
cancelText = 'Cancel',
}: CRUDModalProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="bg-slate-800 border-slate-700 text-white max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-white">{title}</DialogTitle>
{description && (
<DialogDescription className="text-slate-400">
{description}
</DialogDescription>
)}
</DialogHeader>
<div className="py-4">{children}</div>
<DialogFooter>
<Button
variant="outline"
onClick={onClose}
disabled={isSubmitting}
className="bg-slate-700 border-slate-600 hover:bg-slate-600"
>
{cancelText}
</Button>
<Button
onClick={onSubmit}
disabled={isSubmitting}
className="bg-blue-600 hover:bg-blue-700"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Saving...
</span>
) : (
submitText
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,202 +0,0 @@
import React, { useState } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
type ColumnDef,
type SortingState,
type ColumnFiltersState,
} from '@tanstack/react-table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
interface DataTableProps<TData> {
data: TData[];
columns: ColumnDef<TData>[];
onAdd?: () => void;
onEdit?: (row: TData) => void;
onDelete?: (row: TData) => void;
onExport?: () => void;
searchPlaceholder?: string;
addButtonText?: string;
isLoading?: boolean;
}
export function DataTable<TData>({
data,
columns,
onAdd,
onEdit,
onDelete,
onExport,
searchPlaceholder = 'Search...',
addButtonText = 'Add New',
isLoading = false,
}: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
state: {
sorting,
columnFilters,
globalFilter,
},
initialState: {
pagination: {
pageSize: 20,
},
},
});
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex items-center justify-between gap-4">
<div className="flex-1 max-w-sm">
<Input
placeholder={searchPlaceholder}
value={globalFilter ?? ''}
onChange={(e) => setGlobalFilter(e.target.value)}
className="bg-slate-900 border-slate-700"
/>
</div>
<div className="flex gap-2">
{onExport && (
<Button
variant="outline"
onClick={onExport}
className="bg-slate-800 border-slate-700 hover:bg-slate-700"
>
📤 Export
</Button>
)}
{onAdd && (
<Button
onClick={onAdd}
className="bg-blue-600 hover:bg-blue-700"
>
{addButtonText}
</Button>
)}
</div>
</div>
{/* Table */}
<div className="rounded-lg border border-slate-700 bg-slate-800 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-900">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider cursor-pointer hover:text-slate-200"
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center gap-2">
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="divide-y divide-slate-700">
{isLoading ? (
<tr>
<td
colSpan={columns.length}
className="px-6 py-12 text-center text-slate-500"
>
<div className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
Loading...
</div>
</td>
</tr>
) : table.getRowModel().rows.length === 0 ? (
<tr>
<td
colSpan={columns.length}
className="px-6 py-12 text-center text-slate-500"
>
No data found
</td>
</tr>
) : (
table.getRowModel().rows.map((row) => (
<tr
key={row.id}
className="hover:bg-slate-700/50 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-6 py-4 text-sm text-slate-300">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{table.getPageCount() > 1 && (
<div className="flex items-center justify-between px-6 py-4 bg-slate-900 border-t border-slate-700">
<div className="text-sm text-slate-400">
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{' '}
{Math.min(
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
table.getFilteredRowModel().rows.length
)}{' '}
of {table.getFilteredRowModel().rows.length} results
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
className="bg-slate-800 border-slate-700"
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
className="bg-slate-800 border-slate-700"
>
Next
</Button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,70 +0,0 @@
import React from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
interface DeleteConfirmProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title?: string;
description?: string;
itemName?: string;
isDeleting?: boolean;
}
export function DeleteConfirm({
isOpen,
onClose,
onConfirm,
title = 'Are you sure?',
description,
itemName,
isDeleting = false,
}: DeleteConfirmProps) {
const defaultDescription = itemName
? `This will permanently delete "${itemName}". This action cannot be undone.`
: 'This action cannot be undone. This will permanently delete the item.';
return (
<AlertDialog open={isOpen} onOpenChange={onClose}>
<AlertDialogContent className="bg-slate-800 border-slate-700">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">{title}</AlertDialogTitle>
<AlertDialogDescription className="text-slate-400">
{description || defaultDescription}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
disabled={isDeleting}
className="bg-slate-700 border-slate-600 hover:bg-slate-600 text-white"
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
disabled={isDeleting}
className="bg-red-600 hover:bg-red-700 text-white"
>
{isDeleting ? (
<span className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Deleting...
</span>
) : (
'Delete'
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -1,15 +0,0 @@
---
import AdminLayout from '@/layouts/AdminLayout.astro';
import AvatarVariantManager from '@/components/admin/collections/AvatarVariantManager';
---
<AdminLayout title="Avatar Variants">
<div class="p-8">
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">🎭 Avatar Variants</h1>
<p class="text-slate-400">Manage gender and tone variations for avatars</p>
</div>
<AvatarVariantManager client:load />
</div>
</AdminLayout>

View File

@@ -1,15 +0,0 @@
---
import AdminLayout from '@/layouts/AdminLayout.astro';
import GeoIntelligenceManager from '@/components/admin/collections/GeoIntelligenceManager';
---
<AdminLayout title="Geo Intelligence">
<div class="p-8">
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">🗺️ Geo Intelligence</h1>
<p class="text-slate-400">Location targeting and geographic data</p>
</div>
<GeoIntelligenceManager client:load />
</div>
</AdminLayout>

View File

@@ -1,15 +0,0 @@
---
import AdminLayout from '@/layouts/AdminLayout.astro';
import CartesianManagerEnhanced from '@/components/admin/content/CartesianManagerEnhanced';
---
<AdminLayout title="Cartesian Patterns">
<div class="p-8">
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">🔀 Cartesian Patterns</h1>
<p class="text-slate-400">Define content generation patterns using Cartesian product logic</p>
</div>
<CartesianManagerEnhanced client:load />
</div>
</AdminLayout>

View File

@@ -1,15 +0,0 @@
---
import AdminLayout from '@/layouts/AdminLayout.astro';
import SpintaxManagerEnhanced from '@/components/admin/content/SpintaxManagerEnhanced';
---
<AdminLayout title="Spintax Dictionaries">
<div class="p-8">
<div class="mb-6">
<h1 class="text-3xl font-bold text-white mb-2">📚 Spintax Dictionaries</h1>
<p class="text-slate-400">Manage synonym variations for content generation</p>
</div>
<SpintaxManagerEnhanced client:load />
</div>
</AdminLayout>