aubm / frontend /src /components /SettingsView.tsx
cesjavi's picture
Set app version to 0.7.0 and update roadmap
ffac2f3
import React, { useEffect, useMemo, useState } from 'react';
import { Bot, CheckCircle2, KeyRound, LogOut, Server, Settings, Shield, UserCircle, Users } from 'lucide-react';
import { motion } from 'framer-motion';
import { useAuth } from '../context/useAuth';
import { getDefaultModel, getDefaultProvider, providerOptions, saveProviderDefaults } from '../services/llmConfig';
import type { SupportedProvider } from '../services/llmConfig';
import { getApiUrl, getAppVersion, getSupabaseUrl } from '../services/runtimeConfig';
import type { UiMode } from '../services/uiMode';
import { supabase } from '../services/supabase';
interface ProfileRow {
id: string;
role: 'user' | 'manager' | 'admin';
full_name: string | null;
avatar_url: string | null;
}
const SettingsView: React.FC<{ uiMode: UiMode; onUiModeChange: (mode: UiMode) => void }> = ({ uiMode, onUiModeChange }) => {
const { user, session, profile, signOut, refreshProfile } = useAuth();
const initialProvider = useMemo(() => getDefaultProvider(), []);
const [provider, setProvider] = useState<SupportedProvider>(initialProvider);
const [model, setModel] = useState(getDefaultModel(initialProvider));
const [saved, setSaved] = useState(false);
const [fullName, setFullName] = useState('');
const [avatarUrl, setAvatarUrl] = useState('');
const [profileMessage, setProfileMessage] = useState<string | null>(null);
const [savingProfile, setSavingProfile] = useState(false);
const [profiles, setProfiles] = useState<ProfileRow[]>([]);
const [profilesLoading, setProfilesLoading] = useState(false);
const [adminMessage, setAdminMessage] = useState<string | null>(null);
const config = useMemo(() => {
const apiUrl = getApiUrl() || 'Same origin';
const supabaseUrl = getSupabaseUrl() || 'Not configured';
const appVersion = getAppVersion();
return { apiUrl, supabaseUrl, appVersion };
}, []);
const providerModels = providerOptions.find((option) => option.id === provider)?.models ?? [];
const isAdmin = profile?.role === 'admin';
useEffect(() => {
setFullName(profile?.full_name ?? '');
setAvatarUrl(profile?.avatar_url ?? '');
}, [profile]);
useEffect(() => {
if (!isAdmin) return;
const loadProfiles = async () => {
setProfilesLoading(true);
const { data } = await supabase
.from('profiles')
.select('id,role,full_name,avatar_url')
.order('created_at', { ascending: false });
setProfiles(data ?? []);
setProfilesLoading(false);
};
loadProfiles();
}, [isAdmin, adminMessage]);
const updateProvider = (value: SupportedProvider) => {
setProvider(value);
setModel(getDefaultModel(value));
setSaved(false);
};
const saveDefaults = () => {
saveProviderDefaults(provider, model);
setSaved(true);
};
const saveProfile = async () => {
if (!user) return;
setSavingProfile(true);
setProfileMessage(null);
const { error } = await supabase
.from('profiles')
.upsert(
{
id: user.id,
role: profile?.role ?? 'user',
full_name: fullName.trim() || null,
avatar_url: avatarUrl.trim() || null,
updated_at: new Date().toISOString(),
},
{ onConflict: 'id' }
);
if (error) {
setProfileMessage(error.message);
setSavingProfile(false);
return;
}
await refreshProfile();
setProfileMessage('Profile updated.');
setSavingProfile(false);
};
const updateUserRole = async (profileId: string, role: 'user' | 'admin') => {
setAdminMessage(null);
const { error } = await supabase
.from('profiles')
.update({ role, updated_at: new Date().toISOString() })
.eq('id', profileId);
if (error) {
setAdminMessage(error.message);
return;
}
setProfiles((current) => current.map((entry) => (entry.id === profileId ? { ...entry, role } : entry)));
if (profileId === user?.id) {
await refreshProfile();
}
setAdminMessage('User roles updated.');
};
return (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="settings-page animate-fade-in">
<div className="panel-heading">
<Settings size={32} color="var(--accent)" />
<div>
<h2>Settings</h2>
<p style={{ color: 'var(--text-dim)', fontSize: '0.9rem' }}>Review environment, session, and security configuration.</p>
</div>
</div>
<div className="settings-grid">
<section className="glass-panel settings-section">
<div className="settings-section-title">
<UserCircle size={22} color="var(--accent)" />
<h3>Session</h3>
</div>
<SettingRow label="Email" value={user?.email ?? 'Unknown'} />
<SettingRow label="User ID" value={user?.id ?? 'Unknown'} />
<SettingRow label="Provider" value={user?.app_metadata?.provider ?? 'email'} />
<SettingRow label="Role" value={profile?.role ?? 'user'} />
<SettingRow label="Expires" value={session?.expires_at ? new Date(session.expires_at * 1000).toLocaleString() : 'Unknown'} />
</section>
<section className="glass-panel settings-section">
<div className="settings-section-title">
<UserCircle size={22} color="var(--accent)" />
<h3>Profile</h3>
</div>
<label>
<span>Full Name</span>
<input value={fullName} onChange={(event) => setFullName(event.target.value)} placeholder="Your name" />
</label>
<label>
<span>Avatar URL</span>
<input value={avatarUrl} onChange={(event) => setAvatarUrl(event.target.value)} placeholder="https://..." />
</label>
<button className="btn btn-primary" onClick={saveProfile} disabled={savingProfile}>
<CheckCircle2 size={18} />
{savingProfile ? 'Saving...' : 'Save Profile'}
</button>
{profileMessage && <p style={{ color: profileMessage.includes('updated') ? 'var(--success)' : 'var(--danger)' }}>{profileMessage}</p>}
</section>
<section className="glass-panel settings-section">
<div className="settings-section-title">
<Server size={22} color="var(--accent)" />
<h3>Runtime</h3>
</div>
<SettingRow label="API URL" value={config.apiUrl} />
<SettingRow label="Supabase URL" value={config.supabaseUrl} />
<SettingRow label="Frontend Mode" value={import.meta.env.MODE} />
<SettingRow label="App Version" value={config.appVersion} />
</section>
<section className="glass-panel settings-section">
<div className="settings-section-title">
<Bot size={22} color="var(--accent)" />
<h3>Workspace Mode</h3>
</div>
<p style={{ color: 'var(--text-dim)' }}>Guided keeps the product focused on the main workflow. Expert exposes the full tool surface.</p>
<label>
<span>Mode</span>
<select value={uiMode} onChange={(event) => onUiModeChange(event.target.value as UiMode)}>
<option value="guided">Guided</option>
<option value="expert">Expert</option>
</select>
</label>
</section>
<section className="glass-panel settings-section">
<div className="settings-section-title">
<Bot size={22} color="var(--accent)" />
<h3>LLM Defaults</h3>
</div>
<p style={{ color: 'var(--text-dim)' }}>These defaults are used when creating new agents from the UI. API keys stay in backend `.env`.</p>
<label>
<span>Default Provider</span>
<select value={provider} onChange={(event) => updateProvider(event.target.value as SupportedProvider)}>
{providerOptions.map((option) => (
<option key={option.id} value={option.id}>{option.label}</option>
))}
</select>
</label>
<label>
<span>Default Model</span>
<select value={model} onChange={(event) => { setModel(event.target.value); setSaved(false); }}>
{providerModels.map((providerModel) => (
<option key={providerModel} value={providerModel}>{providerModel}</option>
))}
</select>
</label>
<button className="btn btn-primary" onClick={saveDefaults}>
<CheckCircle2 size={18} />
Save LLM Defaults
</button>
{saved && <p style={{ color: 'var(--success)' }}>LLM defaults saved.</p>}
</section>
<section className="glass-panel settings-section">
<div className="settings-section-title">
<Shield size={22} color="var(--accent)" />
<h3>Security</h3>
</div>
<SettingRow label="Authentication" value="Supabase Auth" />
<SettingRow label="Database Access" value="RLS protected" />
<SettingRow label="Marketplace Deploy" value="Owned by user_id" />
<SettingRow label="Admin Access" value={isAdmin ? 'Enabled' : 'Disabled'} />
</section>
{isAdmin && (
<section className="glass-panel settings-section" style={{ gridColumn: '1 / -1' }}>
<div className="settings-section-title">
<Users size={22} color="var(--accent)" />
<h3>User Management</h3>
</div>
<p style={{ color: 'var(--text-dim)' }}>Manage profile roles for users in this workspace.</p>
{adminMessage && <div className="inline-status">{adminMessage}</div>}
<div className="task-list">
{profilesLoading && <p style={{ color: 'var(--text-dim)' }}>Loading users...</p>}
{!profilesLoading && profiles.length === 0 && <p style={{ color: 'var(--text-dim)' }}>No profiles found.</p>}
{profiles.map((entry) => (
<div key={entry.id} className="task-row">
<div style={{ flex: 1 }}>
<strong>{entry.full_name || 'Unnamed user'}</strong>
<p>{entry.id}</p>
</div>
<select
value={entry.role}
onChange={(event) => updateUserRole(entry.id, event.target.value as 'user' | 'admin')}
style={{ maxWidth: '160px' }}
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
))}
</div>
</section>
)}
<section className="glass-panel settings-section">
<div className="settings-section-title">
<KeyRound size={22} color="var(--accent)" />
<h3>Account</h3>
</div>
<p style={{ color: 'var(--text-dim)' }}>Signing out clears the local Supabase session and returns to the login screen.</p>
<button className="btn btn-glass" onClick={signOut}>
<LogOut size={18} />
Sign Out
</button>
</section>
</div>
</motion.div>
);
};
const SettingRow: React.FC<{ label: string; value: string }> = ({ label, value }) => (
<div className="setting-row">
<span>{label}</span>
<strong title={value}>{value}</strong>
</div>
);
export default SettingsView;