Newer
Older
cortex-hub / frontend / src / features / settings / pages / SettingsPage.js
import React, { useState, useEffect } from 'react';
import {
    getUserConfig,
    getAdminUsers,
    getAdminGroups,
    getAdminNodes,
    updateUserRole,
    updateUserGroup,
    createAdminGroup,
    updateAdminGroup,
    deleteAdminGroup,
    getSkills,
    getUserProfile,
    getAdminConfig,
    updateAdminOIDCConfig,
    updateAdminSwarmConfig,
    updateAdminAppConfig,
    testAdminOIDCConfig,
    testAdminSwarmConfig,
    getAllProviders,
    updateUserConfig,
    getProviderModels,
    verifyProvider,
    exportUserConfig,
    importUserConfig
} from '../../../services/apiService';
import SettingsPageContent from '../components/SettingsPageContent';

const SettingsPage = () => {
    const [config, setConfig] = useState({ llm: {}, tts: {}, stt: {} });
    const [loading, setLoading] = useState(true);
    const [saving, setSaving] = useState(false);
    const [message, setMessage] = useState({ type: '', text: '' });
    const [activeAdminTab, setActiveAdminTab] = useState('groups');
    const [userSearch, setUserSearch] = useState('');
    const [providerLists, setProviderLists] = useState({ llm: [], tts: [], stt: [] });
    const [allUsers, setAllUsers] = useState([]);
    const [usersLoading, setUsersLoading] = useState(false);
    const [allGroups, setAllGroups] = useState([]);
    const [groupsLoading, setGroupsLoading] = useState(false);
    const [editingGroup, setEditingGroup] = useState(null);
    const [allNodes, setAllNodes] = useState([]);
    const [nodesLoading, setNodesLoading] = useState(false);
    const [allSkills, setAllSkills] = useState([]);
    const [skillsLoading, setSkillsLoading] = useState(false);
    const [adminConfig, setAdminConfig] = useState({ oidc: {}, swarm: {} });
    const [adminConfigLoading, setAdminConfigLoading] = useState(false);
    const [userProfile, setUserProfile] = useState(null);
    const [activeConfigTab, setActiveConfigTab] = useState('llm');
    const [expandedProvider, setExpandedProvider] = useState(null);
    const [addingSection, setAddingSection] = useState(null);
    const [addForm, setAddForm] = useState({ type: '', suffix: '', model: '' });
    const [collapsedSections, setCollapsedSections] = useState({ ai: true, identity: true, infrastructure: true });
    const [verifying, setVerifying] = useState(null);
    const [testingConnection, setTestingConnection] = useState(null); // 'oidc' or 'swarm'
    const [fetchedModels, setFetchedModels] = useState({});
    const [providerStatuses, setProviderStatuses] = useState({});
    const [confirmAction, setConfirmAction] = useState(null); // { type, id, sectionKey, label }
    const fileInputRef = React.useRef(null);

    useEffect(() => {
        if (expandedProvider) {
            const parts = expandedProvider.split('_');
            const sectionKey = parts[0];
            const providerId = parts.slice(1).join('_');
            const fetchKey = `${sectionKey}_${providerId}`;
            if (!fetchedModels[fetchKey]) {
                getProviderModels(providerId, sectionKey).then(models => {
                    setFetchedModels(prev => ({ ...prev, [fetchKey]: models }));
                }).catch(e => console.warn("Failed fetching models for", providerId));
            }
        }
    }, [expandedProvider, fetchedModels]);

    useEffect(() => {
        if (addingSection && addForm.type) {
            const fetchKey = `${addingSection}_${addForm.type}`;
            if (!fetchedModels[fetchKey]) {
                getProviderModels(addForm.type, addingSection).then(models => {
                    setFetchedModels(prev => ({ ...prev, [fetchKey]: models }));
                }).catch(() => { });
            }
        }
    }, [addingSection, addForm.type, fetchedModels]);

    useEffect(() => {
        const fetchProviders = async () => {
            try {
                const [llm, tts, stt] = await Promise.all([
                    getAllProviders('llm'),
                    getAllProviders('tts'),
                    getAllProviders('stt')
                ]);
                setProviderLists({
                    llm: llm.map(id => ({ id, label: id === 'general' ? 'General (LiteLLM / Custom)' : id.charAt(0).toUpperCase() + id.slice(1) })).sort((a, b) => a.label.localeCompare(b.label)),
                    tts: tts.map(id => ({ id, label: id === 'general' ? 'General (LiteLLM / Custom)' : id.charAt(0).toUpperCase() + id.slice(1) })).sort((a, b) => a.label.localeCompare(b.label)),
                    stt: stt.map(id => ({ id, label: id === 'general' ? 'General (LiteLLM / Custom)' : (id === 'google_gemini' ? 'Google Gemini' : id.charAt(0).toUpperCase() + id.slice(1)) })).sort((a, b) => a.label.localeCompare(b.label))
                });
            } catch (e) {
                console.error("Failed to load provider lists", e);
            }
        };
        fetchProviders();
    }, []);

    useEffect(() => {
        loadConfig();
        loadUsers();
        loadGroups();
        loadNodes();
        loadSkills();
        loadUserProfile();
    }, []);


    const loadUserProfile = async () => {
        try {
            const profile = await getUserProfile();
            setUserProfile(profile);
            if (profile.role === 'admin') {
                loadAdminConfig();
            }
        } catch (e) {
            console.error("Failed to load user profile", e);
        }
    };

    const loadAdminConfig = async () => {
        try {
            setAdminConfigLoading(true);
            const data = await getAdminConfig();
            setAdminConfig(data);
        } catch (e) {
            console.error("Failed to load admin config", e);
        } finally {
            setAdminConfigLoading(false);
        }
    };

    const loadSkills = async () => {
        try {
            setSkillsLoading(true);
            const skills = await getSkills();
            setAllSkills(skills);
        } catch (e) {
            console.error("Failed to load skills", e);
        } finally {
            setSkillsLoading(false);
        }
    };

    const loadNodes = async () => {
        try {
            setNodesLoading(true);
            const nodes = await getAdminNodes();
            setAllNodes(nodes);
        } catch (e) {
            console.error("Failed to load nodes", e);
        } finally {
            setNodesLoading(false);
        }
    };

    const loadGroups = async () => {
        try {
            setGroupsLoading(true);
            const groups = await getAdminGroups();
            setAllGroups(groups);
        } catch (e) {
            console.error("Failed to load groups", e);
        } finally {
            setGroupsLoading(false);
        }
    };

    const loadUsers = async () => {
        try {
            setUsersLoading(true);
            const users = await getAdminUsers();
            setAllUsers(users);
        } catch (e) {
            console.error("Failed to load users", e);
        } finally {
            setUsersLoading(false);
        }
    };

    const handleRoleToggle = async (user) => {
        const newRole = user.role === 'admin' ? 'user' : 'admin';
        try {
            await updateUserRole(user.id, newRole);
            setMessage({ type: 'success', text: `Role for ${user.username || user.email} updated to ${newRole}` });
            loadUsers(); // refresh list
            setTimeout(() => setMessage({ type: '', text: '' }), 3000);
        } catch (e) {
            setMessage({ type: 'error', text: e.message || 'Failed to update role' });
        }
    };

    const handleGroupChange = async (targetUserId, groupId) => {
        try {
            await updateUserGroup(targetUserId, groupId);
            setMessage({ type: 'success', text: `User group updated successfully` });
            loadUsers();
            setTimeout(() => setMessage({ type: '', text: '' }), 3000);
        } catch (e) {
            setMessage({ type: 'error', text: e.message || 'Failed to update group' });
        }
    };

    const handleSaveGroup = async (e) => {
        e.preventDefault();
        try {
            setSaving(true);
            if (editingGroup.id === 'new') {
                const { id, ...data } = editingGroup;
                await createAdminGroup(data);
            } else {
                await updateAdminGroup(editingGroup.id, editingGroup);
            }
            setMessage({ type: 'success', text: 'Group saved successfully!' });
            setEditingGroup(null);
            loadGroups();
            loadUsers();
            setTimeout(() => setMessage({ type: '', text: '' }), 3000);
        } catch (e) {
            setMessage({ type: 'error', text: e.message || 'Failed to save group' });
        } finally {
            setSaving(false);
        }
    };

    const handleDeleteGroup = (groupId) => {
        setConfirmAction({
            type: 'delete-group',
            id: groupId,
            label: "Are you sure? Users in this group will be moved to 'Ungrouped'."
        });
    };

    const confirmDeleteGroup = async (groupId) => {
        try {
            await deleteAdminGroup(groupId);
            setMessage({ type: 'success', text: 'Group deleted' });
            loadGroups();
            loadUsers();
            setTimeout(() => setMessage({ type: '', text: '' }), 3000);
        } catch (e) {
            setMessage({ type: 'error', text: e.message || 'Failed to delete group' });
        }
    };

    const handleSaveAdminConfig = async (type, data) => {
        try {
            setSaving(true);
            if (type === 'oidc') {
                if (data.enabled === false && adminConfig.app?.allow_password_login === false) {
                    throw new Error("Cannot disable OIDC: you must leave at least one login method enabled.");
                }
                
                const updatedData = { ...data };
                if (data.enabled !== undefined) {
                    updatedData.allow_oidc_login = data.enabled;
                }
                
                await updateAdminOIDCConfig(updatedData);
                setMessage({ type: 'success', text: 'OIDC configuration updated successfully' });
            } else if (type === 'swarm') {
                await updateAdminSwarmConfig(data);
                setMessage({ type: 'success', text: 'Swarm configuration updated successfully' });
            } else if (type === 'app') {
                if (data.allow_password_login === false && adminConfig.oidc?.enabled === false) {
                    throw new Error("Cannot disable password login: you must leave at least one login method enabled.");
                }
                await updateAdminAppConfig(data);
                setMessage({ type: 'success', text: 'Application configuration updated successfully' });
            }
            loadAdminConfig();
            setTimeout(() => setMessage({ type: '', text: '' }), 5000);
        } catch (e) {
            setMessage({ type: 'error', text: e.message || 'Failed to update admin config' });
        } finally {
            setSaving(false);
        }
    };

    const handleTestConnection = async (type) => {
        try {
            setTestingConnection(type);
            setMessage({ type: '', text: `Testing ${type.toUpperCase()} connection...` });
            let response;
            if (type === 'oidc') {
                response = await testAdminOIDCConfig(adminConfig.oidc);
            } else if (type === 'swarm') {
                response = await testAdminSwarmConfig(adminConfig.swarm);
            }
            
            if (response && response.success) {
                setMessage({ type: 'success', text: response.message });
            } else {
                setMessage({ type: 'error', text: (response && response.message) || 'Connection test failed.' });
            }
        } catch (e) {
            setMessage({ type: 'error', text: e.message || `Error occurred while testing ${type} connection` });
        } finally {
            setTestingConnection(null);
            setTimeout(() => setMessage({ type: '', text: '' }), 10000);
        }
    };


    const handleSaveConfig = async (e) => {
        if (e) e.preventDefault();
        try {
            setSaving(true);
            setMessage({ type: '', text: 'Saving and verifying configuration...' });

            const updatedStatuses = { ...providerStatuses };
            const sections = ['llm', 'tts', 'stt'];

            for (const section of sections) {
                const activeId = config[section]?.active_provider;
                if (activeId && !updatedStatuses[`${section}_${activeId}`]) {
                    const providerPrefs = config[section]?.providers?.[activeId];
                    if (providerPrefs && providerPrefs.api_key) {
                        try {
                            const res = await verifyProvider(section, {
                                provider_name: activeId,
                                provider_type: providerPrefs.provider_type || activeId.split('_')[0],
                                api_key: providerPrefs.api_key,
                                model: providerPrefs.model,
                                voice: providerPrefs.voice
                            });
                            updatedStatuses[`${section}_${activeId}`] = res.success ? 'success' : 'error';
                        } catch (err) {
                            updatedStatuses[`${section}_${activeId}`] = 'error';
                        }
                    }
                }
            }

            setProviderStatuses(updatedStatuses);
            const payload = { ...config, statuses: updatedStatuses };
            const data = await updateUserConfig(payload);
            
            setConfig({
                llm: data.llm || {},
                tts: data.tts || {},
                stt: data.stt || {}
            });
            setMessage({ type: 'success', text: 'Settings saved and verified successfully!' });
            setTimeout(() => setMessage({ type: '', text: '' }), 3000);
        } catch (err) {
            console.error("Error saving config:", err);
            setMessage({ type: 'error', text: 'Failed to save configuration.' });
        } finally {
            setSaving(false);
        }
    };

    const handleExport = async () => {
        try {
            const response = await exportUserConfig();
            const blob = await response.blob();
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = "config.yaml";
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(url);
            document.body.removeChild(a);
        } catch (error) {
            console.error("Export Error: ", error);
            setMessage({ type: 'error', text: 'Failed to export YAML.' });
        }
    };

    const handleImport = async (e) => {
        const file = e.target.files[0];
        if (!file) return;
        try {
            setSaving(true);
            const formData = new FormData();
            formData.append('file', file);
            await importUserConfig(formData);
            await loadConfig();
            setMessage({ type: 'success', text: 'Configuration imported successfully!' });
            setTimeout(() => setMessage({ type: '', text: '' }), 3000);
        } catch (error) {
            console.error("Import Error: ", error);
            setMessage({ type: 'error', text: 'Failed to import YAML: ' + error.message });
        } finally {
            setSaving(false);
            if (fileInputRef.current) fileInputRef.current.value = '';
        }
    };

    const handleConfigChange = (section, field, value, providerId = null) => {
        if (field === 'providers' && providerId) {
            setProviderStatuses(prev => {
                const updated = { ...prev };
                delete updated[`${section}_${providerId}`];
                return updated;
            });
        }
        setConfig((prev) => ({
            ...prev,
            [section]: {
                ...prev[section],
                [field]: value
            }
        }));
    };

    const handleVerifyProvider = async (sectionKey, providerId, providerPrefs) => {
        try {
            setVerifying(`${sectionKey}_${providerId}`);
            setMessage({ type: '', text: '' });
            const payload = {
                provider_name: providerId,
                provider_type: providerPrefs.provider_type,
                api_key: providerPrefs.api_key,
                model: providerPrefs.model,
                voice: providerPrefs.voice
            };
            const res = await verifyProvider(sectionKey, payload);
            if (res.success) {
                const newStatuses = { ...providerStatuses, [`${sectionKey}_${providerId}`]: 'success' };
                setProviderStatuses(newStatuses);
                await updateUserConfig({ ...config, statuses: newStatuses });
                setMessage({ type: 'success', text: `Verified ${providerId} successfully!` });
            } else {
                const newStatuses = { ...providerStatuses, [`${sectionKey}_${providerId}`]: 'error' };
                setProviderStatuses(newStatuses);
                await updateUserConfig({ ...config, statuses: newStatuses });
                setMessage({ type: 'error', text: `Verification failed for ${providerId}: ${res.message}` });
            }
        } catch (err) {
            setMessage({ type: 'error', text: `Error verifying ${providerId}.` });
        } finally {
            setVerifying(null);
            setTimeout(() => setMessage({ type: '', text: '' }), 5000);
        }
    };

    const handleDeleteProviderAction = (sectionKey, providerId) => {
        setConfirmAction({
            type: 'delete-provider',
            id: providerId,
            sectionKey,
            label: `Permanently delete the "${providerId}" resource instance?`
        });
    };

    const confirmDeleteProvider = (sectionKey, providerId) => {
        const newProviders = { ...((config[sectionKey] && config[sectionKey].providers) || {}) };
        delete newProviders[providerId];
        handleConfigChange(sectionKey, 'providers', newProviders, providerId);
        if (expandedProvider === `${sectionKey}_${providerId}`) setExpandedProvider(null);
    };

    const executeConfirmAction = () => {
        if (!confirmAction) return;
        if (confirmAction.type === 'delete-group') {
            confirmDeleteGroup(confirmAction.id);
        } else if (confirmAction.type === 'delete-provider') {
            confirmDeleteProvider(confirmAction.sectionKey, confirmAction.id);
        }
        setConfirmAction(null);
    };

    const handleAddInstance = (sectionKey) => {
        if (!addForm.type) return;
        const newId = addForm.suffix ? `${addForm.type}_${addForm.suffix.toLowerCase().replace(/\s+/g, '_')}` : addForm.type;

        if (config[sectionKey]?.providers?.[newId]) {
            setMessage({ type: 'error', text: `Instance "${newId}" already exists.` });
            return;
        }

        const initData = { provider_type: addForm.type };

        if (addForm.model.trim()) {
            if (sectionKey === 'tts' && addForm.type === 'gcloud_tts') {
                initData.voice = addForm.model.trim();
            } else {
                initData.model = addForm.model.trim();
            }
        }

        const newProviders = { ...(config[sectionKey]?.providers || {}) };
        newProviders[newId] = initData;
        handleConfigChange(sectionKey, 'providers', newProviders, newId);
        setAddingSection(null);
        setAddForm({ type: '', suffix: '', model: '' });
        setExpandedProvider(`${sectionKey}_${newId}`);
    };


    const loadConfig = async () => {
        try {
            setLoading(true);
            const data = await getUserConfig();
            setConfig({
                llm: data.preferences?.llm || {},
                tts: data.preferences?.tts || {},
                stt: data.preferences?.stt || {}
            });
            if (data.preferences?.statuses) {
                setProviderStatuses(data.preferences.statuses);
            }
            setMessage({ type: '', text: '' });
        } catch (err) {
            console.error("Error loading config:", err);
            setMessage({ type: 'error', text: 'Failed to load configuration.' });
        } finally {
            setLoading(false);
        }
    };

    if (loading) {
        return (
            <div className="flex h-screen items-center justify-center dark:bg-gray-900">
                <div className="text-xl text-gray-400 animate-pulse">Loading settings...</div>
            </div>
        );
    }

    const filteredUsers = allUsers.filter(u =>
        (u.username || '').toLowerCase().includes(userSearch.toLowerCase()) ||
        (u.email || '').toLowerCase().includes(userSearch.toLowerCase()) ||
        (u.full_name || '').toLowerCase().includes(userSearch.toLowerCase())
    );

    const sortedGroups = [...allGroups].sort((a, b) => {
        if (a.id === 'ungrouped') return -1;
        if (b.id === 'ungrouped') return 1;
        return a.name.localeCompare(b.name);
    });

    const context = {
        config,
        loading,
        saving,
        message,
        activeAdminTab,
        setActiveAdminTab,
        activeConfigTab,
        setActiveConfigTab,
        expandedProvider,
        setExpandedProvider,
        addingSection,
        setAddingSection,
        addForm,
        setAddForm,
        collapsedSections,
        setCollapsedSections,
        verifying,
        testingConnection,
        fetchedModels,
        providerStatuses,
        fileInputRef,
        handleExport,
        handleImport,
        handleConfigChange,
        handleVerifyProvider,
        handleTestConnection,
        handleDeleteProvider: handleDeleteProviderAction,
        handleAddInstance,
        confirmAction,
        setConfirmAction,
        executeConfirmAction,

        userSearch,
        setUserSearch,
        providerLists,
        allUsers,
        usersLoading,
        loadUsers,
        allGroups,
        groupsLoading,
        editingGroup,
        setEditingGroup,
        allNodes,
        nodesLoading,
        allSkills,
        skillsLoading,
        adminConfig,
        setAdminConfig,
        adminConfigLoading,
        userProfile,
        handleSaveAdminConfig,
        handleSaveConfig,
        setConfig,
        setProviderLists,
        handleRoleToggle,
        handleGroupChange,
        handleSaveGroup,
        handleDeleteGroup,
        filteredUsers,
        sortedGroups,
        inputClass: "w-full border border-gray-300 dark:border-gray-600 rounded-lg p-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors duration-200 shadow-sm",
        labelClass: "block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2",
        sectionClass: "animate-fade-in"
    };

    return <SettingsPageContent context={context} />;
};

export default SettingsPage;