import { useState, useEffect } from 'react'; import type { KeyboardEvent } from 'react'; import { useAiConfig, AiConfig, CouncilGenerator } from '@/hooks/useAiConfig'; import { useAuth, PERMISSION_KEYS } from '@/contexts/AuthContext'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Skeleton } from '@/components/ui/skeleton'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Save, Eye, EyeOff, AlertTriangle, Bot, Sparkles, RefreshCw, Plus, Trash2, Package, Cherry, Shield, X } from 'lucide-react'; import { toast } from 'sonner'; import { format } from 'date-fns'; import { ModelCombobox } from '@/components/ModelCombobox'; type ListField = 'heuristic_whitelist' | 'heuristic_keyword_triggers'; const parseListValue = (value?: string | null): string[] => { if (!value) return []; return value .split(/[\n,]+/) .map((item) => item.trim()) .filter(Boolean); }; const appendUniqueItem = (items: string[], value: string): string[] => { const trimmed = value.trim(); if (!trimmed) return items; const existing = new Set(items.map((item) => item.toLowerCase())); if (existing.has(trimmed.toLowerCase())) return items; return [...items, trimmed]; }; const removeItem = (items: string[], value: string): string[] => { const target = value.toLowerCase(); return items.filter((item) => item.toLowerCase() !== target); }; export function AiSettingsPage() { const { hasPermission, isSuperAdmin } = useAuth(); const { config, loading, error, updateConfig, refetch } = useAiConfig(); // Local form state const [formData, setFormData] = useState>({}); const [showApiKey, setShowApiKey] = useState(false); const [isSaving, setIsSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [whitelistInput, setWhitelistInput] = useState(''); const [keywordInput, setKeywordInput] = useState(''); // Permission check - only super admins or those with MANAGE_AI_CONFIG can access const canManage = isSuperAdmin || hasPermission(PERMISSION_KEYS.MANAGE_AI_CONFIG); // Initialize form data when config loads useEffect(() => { if (config) { // Handle generators array - default to one generator with the legacy model or default const generators: CouncilGenerator[] = config.council_generators && config.council_generators.length > 0 ? config.council_generators : [{ model: config.council_generator_model || 'anthropic/claude-3.5-sonnet' }]; setFormData({ openrouter_api_key: config.openrouter_api_key || '', default_model: config.default_model || '', default_temperature: config.default_temperature, model_generate: config.model_generate || '', temperature_generate: config.temperature_generate, model_fix: config.model_fix || '', temperature_fix: config.temperature_fix, model_polish: config.model_polish || '', temperature_polish: config.temperature_polish, council_enabled: config.council_enabled || false, council_generator_model: config.council_generator_model || '', council_generators: generators, council_reviewer_model: config.council_reviewer_model || '', council_reviewer_temperature: config.council_reviewer_temperature, council_selection_mode: config.council_selection_mode || 'whole_set', cherry_pick_ensure_intensity_distribution: config.cherry_pick_ensure_intensity_distribution ?? true, classifier_enabled: config.classifier_enabled ?? true, classifier_model: config.classifier_model || 'openai/gpt-4o', classifier_temperature: config.classifier_temperature, classifier_prompt: config.classifier_prompt || '', heuristics_enabled: config.heuristics_enabled ?? false, heuristic_min_text_length: config.heuristic_min_text_length ?? 12, heuristic_whitelist_max_length: config.heuristic_whitelist_max_length ?? 30, heuristic_skip_if_no_alnum: config.heuristic_skip_if_no_alnum ?? true, heuristic_skip_media_without_text: config.heuristic_skip_media_without_text ?? false, heuristic_record_reason: config.heuristic_record_reason ?? false, heuristic_use_default_whitelist: config.heuristic_use_default_whitelist ?? true, heuristic_use_default_keywords: config.heuristic_use_default_keywords ?? true, heuristic_whitelist: config.heuristic_whitelist || '', heuristic_keyword_triggers: config.heuristic_keyword_triggers || '', }); setHasChanges(false); } }, [config]); const handleChange = (field: K, value: AiConfig[K]) => { setFormData(prev => ({ ...prev, [field]: value })); setHasChanges(true); }; const whitelistItems = parseListValue(formData.heuristic_whitelist ?? ''); const keywordItems = parseListValue(formData.heuristic_keyword_triggers ?? ''); const updateListField = (field: ListField, items: string[]) => { handleChange(field, items.length > 0 ? items.join('\n') : ''); }; const handleAddListItem = ( field: ListField, value: string, clearValue: (next: string) => void ) => { const current = field === 'heuristic_whitelist' ? whitelistItems : keywordItems; const nextItems = appendUniqueItem(current, value); if (nextItems !== current) { updateListField(field, nextItems); } if (value.trim()) { clearValue(''); } }; const handleRemoveListItem = (field: ListField, value: string) => { const current = field === 'heuristic_whitelist' ? whitelistItems : keywordItems; const nextItems = removeItem(current, value); updateListField(field, nextItems); }; const handleListKeyDown = ( event: KeyboardEvent, field: ListField, value: string, clearValue: (next: string) => void ) => { if (event.key === 'Enter') { event.preventDefault(); handleAddListItem(field, value, clearValue); } }; const handleSave = async () => { setIsSaving(true); try { const { error: saveError } = await updateConfig(formData); if (saveError) { toast.error(saveError); } else { toast.success('AI settings saved successfully'); setHasChanges(false); } } catch { toast.error('Failed to save settings'); } finally { setIsSaving(false); } }; const handleReset = () => { if (config) { const generators: CouncilGenerator[] = config.council_generators && config.council_generators.length > 0 ? config.council_generators : [{ model: config.council_generator_model || 'anthropic/claude-3.5-sonnet' }]; setFormData({ openrouter_api_key: config.openrouter_api_key || '', default_model: config.default_model || '', default_temperature: config.default_temperature, model_generate: config.model_generate || '', temperature_generate: config.temperature_generate, model_fix: config.model_fix || '', temperature_fix: config.temperature_fix, model_polish: config.model_polish || '', temperature_polish: config.temperature_polish, council_enabled: config.council_enabled || false, council_generator_model: config.council_generator_model || '', council_generators: generators, council_reviewer_model: config.council_reviewer_model || '', council_reviewer_temperature: config.council_reviewer_temperature, council_selection_mode: config.council_selection_mode || 'whole_set', cherry_pick_ensure_intensity_distribution: config.cherry_pick_ensure_intensity_distribution ?? true, classifier_enabled: config.classifier_enabled ?? true, classifier_model: config.classifier_model || 'openai/gpt-4o', classifier_temperature: config.classifier_temperature, classifier_prompt: config.classifier_prompt || '', heuristics_enabled: config.heuristics_enabled ?? false, heuristic_min_text_length: config.heuristic_min_text_length ?? 12, heuristic_whitelist_max_length: config.heuristic_whitelist_max_length ?? 30, heuristic_skip_if_no_alnum: config.heuristic_skip_if_no_alnum ?? true, heuristic_skip_media_without_text: config.heuristic_skip_media_without_text ?? false, heuristic_record_reason: config.heuristic_record_reason ?? false, heuristic_use_default_whitelist: config.heuristic_use_default_whitelist ?? true, heuristic_use_default_keywords: config.heuristic_use_default_keywords ?? true, heuristic_whitelist: config.heuristic_whitelist || '', heuristic_keyword_triggers: config.heuristic_keyword_triggers || '', }); setHasChanges(false); } }; // Helper functions for managing generators array const generators = (formData.council_generators as CouncilGenerator[]) || [{ model: 'anthropic/claude-3.5-sonnet' }]; const addGenerator = () => { const newGenerators = [...generators, { model: '' }]; handleChange('council_generators', newGenerators); }; const removeGenerator = (index: number) => { if (generators.length <= 1) { toast.error('You must have at least one generator'); return; } const newGenerators = generators.filter((_, i) => i !== index); handleChange('council_generators', newGenerators); }; const updateGenerator = (index: number, updates: Partial) => { const newGenerators = generators.map((g, i) => i === index ? { ...g, ...updates } : g); handleChange('council_generators', newGenerators); }; if (!canManage) { return (
Access Denied You don't have permission to manage AI settings. Contact a super admin for access.
); } if (loading) { return (

AI Settings

Configure AI models and API settings

); } if (error) { return (

AI Settings

Configure AI models and API settings

Error loading settings {error}
); } return (
{/* Header */}

AI Settings

Configure AI models and API settings for content generation

{hasChanges && ( )}
{config?.updated_at && (

Last updated: {format(new Date(config.updated_at), 'MMM d, yyyy HH:mm')}

)} {/* API Configuration */} API Configuration Configure your OpenRouter API key for AI model access
handleChange('openrouter_api_key', e.target.value)} placeholder="sk-or-v1-..." className="pr-10 font-mono" />

Get your API key from{' '} openrouter.ai/keys

{/* Moderation Settings */} Moderation Settings Configure automatic content moderation for messages

Automatically flag illegal/dangerous content

handleChange('classifier_enabled', checked)} />
handleChange('classifier_model', value)} placeholder="openai/gpt-4o" />

Model used for classification (must support vision if image moderation is needed)

handleChange('classifier_temperature', e.target.value === '' ? null : Number(e.target.value))} placeholder="1.0" />