// Force rebuild - Frontend updated with edit prompt functionality import React, { useState, useEffect, useCallback } from 'react'; import { useAdmin } from '../../hooks/useAdmin'; import { PageContainer, Heading, Button, Container, TextInput, SelectInput, Spinner } from '@ifrc-go/ui'; import styles from './AdminPage.module.css'; import uploadStyles from '../UploadPage/UploadPage.module.css'; const SELECTED_MODEL_KEY = 'selectedVlmModel'; interface PromptData { p_code: string; label: string; metadata_instructions?: string; image_type: string; is_active: boolean; } interface ModelData { m_code: string; label: string; model_type: string; provider?: string; model_id?: string; config?: { provider?: string; model_id?: string; model?: string; stub?: boolean; }; is_available: boolean; is_fallback: boolean; } interface ImageTypeData { image_type: string; label: string; } interface SchemaData { schema_id: string; title: string; version: string; created_at?: string; schema: any; } export default function AdminPage() { const { isAuthenticated, isLoading, login, logout } = useAdmin(); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [isLoggingIn, setIsLoggingIn] = useState(false); const [availableModels, setAvailableModels] = useState([]); const [selectedModel, setSelectedModel] = useState(''); const [selectedFallbackModel, setSelectedFallbackModel] = useState(''); // Prompts state const [availablePrompts, setAvailablePrompts] = useState([]); const [imageTypes, setImageTypes] = useState([]); // Schema management state const [availableSchemas, setAvailableSchemas] = useState([]); const [showEditSchemaForm, setShowEditSchemaForm] = useState(false); const [editingSchema, setEditingSchema] = useState(null); const [newSchemaData, setNewSchemaData] = useState({ schema_id: '', title: '', version: '', schema: {} }); // Prompt management state const [showEditPromptForm, setShowEditPromptForm] = useState(false); const [showAddPromptForm, setShowAddPromptForm] = useState(false); const [addingPromptType, setAddingPromptType] = useState<'crisis_map' | 'drone_image' | null>(null); const [editingPrompt, setEditingPrompt] = useState(null); const [newPromptData, setNewPromptData] = useState({ p_code: '', label: '', metadata_instructions: '', image_type: 'crisis_map', is_active: false }); // Model management state const [showAddModelForm, setShowAddModelForm] = useState(false); const [showEditModelForm, setShowEditModelForm] = useState(false); const [editingModel, setEditingModel] = useState(null); const [newModelData, setNewModelData] = useState({ m_code: '', label: '', model_type: 'custom', provider: 'huggingface', model_id: '', is_available: false, is_fallback: false }); // Modal states const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [showSetupInstructionsModal, setShowSetupInstructionsModal] = useState(false); const [showTestResultsModal, setShowTestResultsModal] = useState(false); const [modelToDelete, setModelToDelete] = useState(''); const [setupInstructions, setSetupInstructions] = useState(''); // VLM Testing states const [testResults, setTestResults] = useState(''); const [testResultsTitle, setTestResultsTitle] = useState(''); const fetchModels = useCallback(() => { fetch('/api/models') .then(r => r.json()) .then(modelsData => { console.log('Models data received:', modelsData); setAvailableModels(modelsData.models || []); const persistedModel = localStorage.getItem(SELECTED_MODEL_KEY); if (modelsData.models && modelsData.models.length > 0) { if (persistedModel === 'random') { // Keep random selection setSelectedModel('random'); } else if (persistedModel && modelsData.models.find((m: { m_code: string; is_available: boolean }) => m.m_code === persistedModel && m.is_available)) { setSelectedModel(persistedModel); } else { const firstAvailableModel = modelsData.models.find((m: { is_available: boolean }) => m.is_available) || modelsData.models[0]; setSelectedModel(firstAvailableModel.m_code); localStorage.setItem(SELECTED_MODEL_KEY, firstAvailableModel.m_code); } } }) .catch(() => { // Handle error silently }); // Fetch current fallback model fetch('/api/admin/fallback-model', { headers: { 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` } }) .then(r => r.json()) .then(fallbackData => { console.log('Fallback model data received:', fallbackData); if (fallbackData.fallback_model) { setSelectedFallbackModel(fallbackData.fallback_model.m_code); } else { setSelectedFallbackModel(''); } }) .catch(() => { // Handle error silently }); }, []); const fetchPrompts = useCallback(() => { console.log('=== fetchPrompts called ==='); fetch('/api/prompts') .then(r => r.json()) .then(promptsData => { console.log('Prompts data received:', promptsData); setAvailablePrompts(promptsData || []); console.log('State update triggered with:', promptsData || []); }) .catch((error) => { console.error('Error fetching prompts:', error); // Handle error silently }); }, []); const fetchImageTypes = useCallback(() => { fetch('/api/image-types') .then(r => r.json()) .then(imageTypesData => { console.log('Image types data received:', imageTypesData); setImageTypes(imageTypesData || []); }) .catch(() => { // Handle error silently }); }, []); const fetchSchemas = useCallback(() => { console.log('=== fetchSchemas called ==='); fetch('/api/schemas', { headers: { 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` } }) .then(r => r.json()) .then(schemasData => { console.log('Schemas data received:', schemasData); setAvailableSchemas(schemasData || []); }) .catch((error) => { console.error('Error fetching schemas:', error); // Handle error silently }); }, []); useEffect(() => { if (isAuthenticated) { fetchModels(); fetchPrompts(); fetchImageTypes(); fetchSchemas(); } }, [isAuthenticated, fetchModels, fetchPrompts, fetchImageTypes, fetchSchemas]); const handleEditPrompt = (prompt: PromptData) => { setEditingPrompt(prompt); setNewPromptData({ p_code: prompt.p_code, label: prompt.label || '', metadata_instructions: prompt.metadata_instructions || '', image_type: prompt.image_type || 'crisis_map', is_active: prompt.is_active || false }); setShowEditPromptForm(true); }; const handleSavePrompt = async () => { try { if (!editingPrompt) { alert('No prompt selected for editing'); return; } const response = await fetch(`/api/prompts/${editingPrompt.p_code}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ label: newPromptData.label, metadata_instructions: newPromptData.metadata_instructions, image_type: newPromptData.image_type, is_active: newPromptData.is_active }), }); if (response.ok) { // Refresh prompts and close form fetchPrompts(); setShowEditPromptForm(false); setEditingPrompt(null); setNewPromptData({ p_code: '', label: '', metadata_instructions: '', image_type: 'crisis_map', is_active: false }); } else { const errorData = await response.json(); alert(`Failed to update prompt: ${errorData.error || 'Unknown error'}`); } } catch { alert('Error updating prompt'); } }; const handleTogglePromptActive = async (promptCode: string, imageType: string) => { try { const response = await fetch(`/api/prompts/${promptCode}/toggle-active?image_type=${imageType}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); if (response.ok) { // Refresh prompts to show updated status fetchPrompts(); } else { const errorData = await response.json(); alert(`Failed to toggle prompt active status: ${errorData.detail || 'Unknown error'}`); } } catch { alert('Error toggling prompt active status'); } }; const handleAddPrompt = (imageType: 'crisis_map' | 'drone_image') => { setAddingPromptType(imageType); setNewPromptData({ p_code: '', label: '', metadata_instructions: '', image_type: imageType, is_active: false }); setShowAddPromptForm(true); }; const handleSaveNewPrompt = async () => { try { const response = await fetch('/api/prompts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(newPromptData), }); if (response.ok) { // Refresh prompts and close form fetchPrompts(); setShowAddPromptForm(false); setAddingPromptType(null); setNewPromptData({ p_code: '', label: '', metadata_instructions: '', image_type: 'crisis_map', is_active: false }); } else { const errorData = await response.json(); alert(`Failed to create prompt: ${errorData.detail || 'Unknown error'}`); } } catch { alert('Error creating prompt'); } }; // Schema management handlers const handleEditSchema = (schema: SchemaData) => { setEditingSchema(schema); setNewSchemaData({ schema_id: schema.schema_id, title: schema.title, version: schema.version, schema: schema.schema }); setShowEditSchemaForm(true); }; const handleSaveSchema = async () => { try { if (!editingSchema) { alert('No schema selected for editing'); return; } const response = await fetch(`/api/schemas/${editingSchema.schema_id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` }, body: JSON.stringify(newSchemaData), }); if (response.ok) { // Refresh schemas and close form fetchSchemas(); setShowEditSchemaForm(false); setEditingSchema(null); setNewSchemaData({ schema_id: '', title: '', version: '', schema: {} }); } else { const errorData = await response.json(); alert(`Failed to save schema: ${errorData.detail || 'Unknown error'}`); } } catch (error) { console.error('Error saving schema:', error); alert('Error saving schema'); } }; const handleCancelSchema = () => { setShowEditSchemaForm(false); setEditingSchema(null); setNewSchemaData({ schema_id: '', title: '', version: '', schema: {} }); }; const toggleModelAvailability = async (modelCode: string, currentStatus: boolean) => { try { const response = await fetch(`/api/models/${modelCode}/toggle`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ is_available: !currentStatus }) }); if (response.ok) { setAvailableModels(prev => prev.map(model => model.m_code === modelCode ? { ...model, is_available: !currentStatus } : model ) ); } else { const errorData = await response.json(); alert(`Failed to toggle model availability: ${errorData.error || 'Unknown error'}`); } } catch (_error) { alert('Error toggling model availability'); } }; const handleModelChange = (modelCode: string) => { setSelectedModel(modelCode); if (modelCode === 'random') { // For random selection, we'll select a random available model when needed localStorage.setItem(SELECTED_MODEL_KEY, 'random'); } else { localStorage.setItem(SELECTED_MODEL_KEY, modelCode); } }; const handleFallbackModelChange = async (modelCode: string) => { try { const response = await fetch(`/api/admin/models/${modelCode}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` }, body: JSON.stringify({ is_fallback: true }) }); if (response.ok) { setSelectedFallbackModel(modelCode); // Refresh models to update the is_fallback status fetchModels(); } else { const errorData = await response.json(); alert(`Failed to set fallback model: ${errorData.detail || 'Unknown error'}`); } } catch (error) { alert('Error setting fallback model'); } }; // Model management functions const handleAddModel = async () => { try { const response = await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` }, body: JSON.stringify(newModelData) }); if (response.ok) { // Prepare setup instructions const instructions = ` Model "${newModelData.label}" added successfully! ⚠️ IMPORTANT: Model will NOT work until you complete these steps: 1. 🔑 Ensure API key is set and valid. 2. 📝 Verify model_id format. 3. 📚 Check model specific documentation for details. `; setSetupInstructions(instructions); setShowSetupInstructionsModal(true); setShowAddModelForm(false); setNewModelData({ m_code: '', label: '', model_type: 'custom', provider: 'huggingface', model_id: '', is_available: false, is_fallback: false }); fetchModels(); // Refresh the models list } else { const errorData = await response.json(); alert(`Failed to add model: ${errorData.detail || 'Unknown error'}`); } } catch (error) { alert('Error adding model'); } }; const handleEditModel = (model: ModelData) => { setEditingModel(model); setNewModelData({ m_code: model.m_code, label: model.label, model_type: model.model_type || 'custom', provider: model.provider || model.config?.provider || 'huggingface', model_id: model.model_id || model.config?.model_id || model.m_code, is_available: model.is_available, is_fallback: model.is_fallback }); setShowEditModelForm(true); }; const handleUpdateModel = async () => { try { console.log('Updating model with data:', newModelData); // Create update payload without m_code (it's in the URL) const updatePayload = { label: newModelData.label, model_type: newModelData.model_type, provider: newModelData.provider, model_id: newModelData.model_id, is_available: newModelData.is_available }; console.log('Update payload:', updatePayload); if (!editingModel) { alert('No model selected for editing'); return; } const response = await fetch(`/api/admin/models/${editingModel.m_code}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` }, body: JSON.stringify(updatePayload) }); console.log('Update response status:', response.status); if (response.ok) { const result = await response.json(); console.log('Update successful:', result); setShowEditModelForm(false); setEditingModel(null); setNewModelData({ m_code: '', label: '', model_type: 'custom', provider: 'huggingface', model_id: '', is_available: false, is_fallback: false }); console.log('Refreshing models...'); fetchModels(); // Refresh the models list } else { const errorData = await response.json(); console.error('Update failed:', errorData); alert(`Failed to update model: ${errorData.detail || 'Unknown error'}`); } } catch (error) { console.error('Update error:', error); alert('Error updating model'); } }; const handleDeleteModel = async (modelCode: string) => { setModelToDelete(modelCode); setShowDeleteConfirmModal(true); }; const handleDeleteModelConfirm = async () => { try { const response = await fetch(`/api/admin/models/${modelToDelete}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('adminToken')}` } }); if (response.ok) { setShowDeleteConfirmModal(false); setModelToDelete(''); fetchModels(); // Refresh the models list } else { const errorData = await response.json(); alert(`Failed to delete model: ${errorData.detail || 'Unknown error'}`); } } catch { alert('Error deleting model'); } }; const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); if (!password.trim()) { setError('Please enter a password'); return; } setIsLoggingIn(true); setError(''); try { const success = await login(password); if (!success) { setError('Invalid password'); } } catch { setError('Login failed. Please try again.'); } finally { setIsLoggingIn(false); } }; const handleLogout = () => { logout(); setPassword(''); setError(''); }; if (isLoading) { return (

Loading...

); } if (!isAuthenticated) { return (
Admin Login
setPassword(value || '')} placeholder="Enter admin password" required className="w-full" disabled={isLoggingIn} />
{error && (

{error}

)}
{isLoggingIn ? (

Logging in...

) : ( )}
); } return (
{/* Model Selection Section */}

Select which Vision Language Model to use for caption generation.

handleModelChange(newValue || '')} options={[ { value: 'random', label: 'Random' }, ...availableModels .filter(model => model.is_available) .map(model => ({ value: model.m_code, label: model.label })) ]} keySelector={(o) => o.value} labelSelector={(o) => o.label} /> handleFallbackModelChange(newValue || '')} options={[ { value: '', label: 'No fallback (use STUB_MODEL)' }, ...availableModels .filter(model => model.is_available) .map(model => ({ value: model.m_code, label: model.label })) ]} keySelector={(o) => o.value} labelSelector={(o) => o.label} />
{/* Model Management Section */}
{/* Models Table */}
{availableModels.map(model => ( ))}
Code Label Provider Model ID Available Actions
{model.m_code} {model.label} {model.provider || model.config?.provider || 'huggingface'} {model.model_id || model.config?.model_id || model.m_code || 'N/A'}
{/* Add Model Button - now below the table */} {!showAddModelForm && (
)} {/* Add Model Form - now below the table */} {showAddModelForm && (

Add New Model

setNewModelData({...newModelData, m_code: value || ''})} placeholder="e.g., NEW_MODEL_123" />
setNewModelData({...newModelData, label: value || ''})} placeholder="e.g., New Model Name" />
setNewModelData({...newModelData, provider: value || 'huggingface'})} options={[ { value: 'huggingface', label: 'HuggingFace' }, { value: 'openai', label: 'OpenAI' }, { value: 'google', label: 'Google' } ]} keySelector={(o) => o.value} labelSelector={(o) => o.label} />
setNewModelData({...newModelData, model_id: value || ''})} placeholder="e.g., org/model-name" />
setNewModelData({...newModelData, is_available: e.target.checked})} /> Available for use
)} {/* Edit Model Form */} {showEditModelForm && (

Edit Model: {editingModel?.label}

setNewModelData({...newModelData, m_code: value || ''})} placeholder="e.g., NEW_MODEL_123" disabled />
setNewModelData({...newModelData, label: value || ''})} placeholder="e.g., New Model Name" />
setNewModelData({...newModelData, provider: value || 'huggingface'})} options={[ { value: 'huggingface', label: 'HuggingFace' }, { value: 'openai', label: 'OpenAI' }, { value: 'google', label: 'Google' } ]} keySelector={(o) => o.value} labelSelector={(o) => o.label} />
setNewModelData({...newModelData, model_id: value || ''})} placeholder="e.g., org/model-name" />
setNewModelData({...newModelData, is_available: e.target.checked})} /> Available for use
)}
{/* Prompts Management Section */}
{/* Crisis Maps Sub-section */}

Crisis Maps

{availablePrompts .filter(prompt => prompt.image_type === 'crisis_map') .sort((a, b) => a.p_code.localeCompare(b.p_code)) // Stable sort by code .map(prompt => ( ))}
Code Label Status Actions
{prompt.p_code} {prompt.label || 'No label'}
{/* Add Crisis Map Prompt Button */}
{/* Drone Images Sub-section */}

Drone Images

{availablePrompts .filter(prompt => prompt.image_type === 'drone_image') .sort((a, b) => a.p_code.localeCompare(b.p_code)) // Stable sort by code .map(prompt => ( ))}
Code Label Status Actions
{prompt.p_code} {prompt.label || 'No label'}
{/* Add Drone Image Prompt Button */}
{/* Schema Management Section */}
{availableSchemas .sort((a, b) => a.schema_id.localeCompare(b.schema_id)) .map(schema => ( ))}
Schema ID Schema Content Actions
{schema.schema_id} {JSON.stringify(schema.schema)}
{/* Utilities Section */}
{/* Delete Model Confirmation Modal */} {showDeleteConfirmModal && (
setShowDeleteConfirmModal(false)}>
e.stopPropagation()}>

Delete Model

Are you sure you want to delete model {modelToDelete}? This action cannot be undone.

)} {/* Setup Instructions Modal */} {showSetupInstructionsModal && (
setShowSetupInstructionsModal(false)}>
e.stopPropagation()}>

Model Added Successfully!

{setupInstructions}
)} {/* Test Results Modal */} {showTestResultsModal && (
setShowTestResultsModal(false)}>
e.stopPropagation()}>

{testResultsTitle}

{testResults}
)} {/* Edit Prompt Form Modal */} {showEditPromptForm && (
setShowEditPromptForm(false)}>
e.stopPropagation()}>

Edit Prompt: {editingPrompt?.p_code}

{}} // Required prop for disabled field disabled={true} className={styles.formInput} />
setNewPromptData(prev => ({ ...prev, label: value || '' }))} className={styles.formInput} />
setNewPromptData(prev => ({ ...prev, image_type: value || 'crisis_map' }))} options={imageTypes} keySelector={(o) => o.image_type} labelSelector={(o) => o.label} />
setNewPromptData(prev => ({ ...prev, is_active: e.target.checked }))} /> Active for this image type