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;