import { useState, useEffect, useCallback } from 'react'
import { User, Brain, RefreshCw, Wrench, Monitor, AlertTriangle } from 'lucide-react'
import { useI18n } from '../i18n'
const PANELS = [
{ id: 'profile', icon: User },
{ id: 'providers', icon: Brain },
{ id: 'updates', icon: RefreshCw },
{ id: 'skills', icon: Wrench },
{ id: 'system', icon: Monitor },
]
export default function Config({ api }) {
const { t, language, keyboard, setLanguage, setKeyboard } = useI18n()
const [activePanel, setActivePanel] = useState('profile')
const [config, setConfig] = useState(null)
const [providers, setProviders] = useState([])
const [skillList, setSkillList] = useState([])
const [updates, setUpdates] = useState([])
const [tools, setTools] = useState([])
const [checking, setChecking] = useState(false)
const [updating, setUpdating] = useState(null)
const [editProfile, setEditProfile] = useState(false)
const [editProvider, setEditProvider] = useState(null)
const [profileForm, setProfileForm] = useState({})
const [providerForm, setProviderForm] = useState({}) // keyed by provider name
const [toast, setToast] = useState(null)
const loadData = useCallback(() => {
api.getConfig().then(d => {
setConfig(d)
setProfileForm(d.profile ? JSON.parse(JSON.stringify(d.profile)) : {})
}).catch(() => {})
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
}, [api])
useEffect(() => { loadData() }, [loadData])
const showToast = (msg) => {
setToast(msg)
setTimeout(() => setToast(null), 2500)
}
const handleCheckUpdates = async () => {
setChecking(true)
try {
await api.runScan()
const d = await api.getUpdates()
setUpdates(d.updates || [])
const td = await api.getTools()
setTools(td.tools || [])
showToast(t('config.upToDate'))
} catch (err) {
showToast(`${t('config.error')}: ${err.message}`)
}
setChecking(false)
}
const handleUpdateTool = (tool) => {
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Met à jour l'outil ${tool} sur mon système. Exécute les commandes nécessaires.` } }))
}
const handleUpdateAll = () => {
const toUpdate = updates.filter(u => u.needsUpdate).map(u => u.tool)
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Met à jour tous les outils suivants sur mon système : ${toUpdate.join(', ')}. Exécute les commandes nécessaires une par une.` } }))
}
const handleSaveProfile = async () => {
try {
await api.saveProfile(profileForm)
setEditProfile(false)
loadData()
showToast(t('config.saved'))
} catch (err) {
showToast(`${t('config.error')}: ${err.message}`)
}
}
const handleSaveProvider = async (name) => {
const form = providerForm[name]
if (!form) return
try {
await api.saveProvider({ name, ...form })
setEditProvider(null)
loadData()
showToast(t('config.saved'))
} catch (err) {
showToast(`${t('config.error')}: ${err.message}`)
}
}
const openProviderEdit = (p) => {
setProviderForm(prev => ({
...prev,
[p.name]: {
name: p.name,
api_key: p.apiKey || '',
model: p.model || '',
base_url: p.baseURL || '',
},
}))
setEditProvider(p.name)
}
const needsUpdateCount = updates.filter(u => u.needsUpdate).length
const installedCount = tools.filter(tool => tool.installed).length
const missingCount = tools.filter(tool => !tool.installed).length
return (
{toast &&
{toast}
}
{PANELS.map(p => {
const Icon = p.icon
return (
setActivePanel(p.id)}
>
{t(`config.panels.${p.id}`)}
)
})}
{activePanel === 'profile' && (
)}
{activePanel === 'providers' && (
)}
{activePanel === 'updates' && (
)}
{activePanel === 'skills' && (
)}
{activePanel === 'system' && (
)}
)
}
function PanelProfile({ config, editProfile, profileForm, setProfileForm, setEditProfile, handleSaveProfile, t }) {
const updateField = (path, value) => {
setProfileForm(prev => {
const next = JSON.parse(JSON.stringify(prev))
const keys = path.split('.')
let target = next
for (let i = 0; i < keys.length - 1; i++) {
if (target[keys[i]] == null) target[keys[i]] = {}
target = target[keys[i]]
}
target[keys[keys.length - 1]] = value
return next
})
}
const profile = editProfile ? profileForm : config?.profile
if (!profile) {
return (
{t('config.loadingProfile')}
)
}
const personalKeys = Object.entries(profile).filter(([k, v]) => k !== 'preferences' && typeof v !== 'object')
const personalObj = Object.fromEntries(personalKeys)
const preferences = profile.preferences || null
return (
{t('config.profileInfo') || 'Informations personnelles'}
{t('config.profilePrefs') || 'Préférences'}
{preferences ? (
) : (
—
)}
{editProfile ? (
<>
>
) : (
)}
)
}
function RenderFields({ obj, path, editing, onChange, t }) {
if (!obj || typeof obj !== 'object') return null
return Object.entries(obj).filter(([, v]) => v === null || typeof v !== 'object').map(([key, value]) => {
const fieldPath = path ? `${path}.${key}` : key
const label = getFieldLabel(key, t)
if (editing) {
if (typeof value === 'boolean') {
return (
{label}
)
}
if (Array.isArray(value)) {
return (
onChange(fieldPath, e.target.value.split(',').map(s => s.trim()).filter(Boolean))} />
)
}
return (
onChange(fieldPath, typeof value === 'number' ? Number(e.target.value) : e.target.value)} />
)
}
if (typeof value === 'boolean') {
return (
{label}
{value ? 'On' : 'Off'}
)
}
if (Array.isArray(value)) {
return (
{label}
{value.length > 0 ? value.join(', ') : '—'}
)
}
return (
{label}
{value != null && value !== '' ? String(value) : '—'}
)
})
}
function getFieldLabel(key, t) {
const translated = t(`config.${key}`)
if (translated !== `config.${key}`) return translated
return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}
function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) {
const [validating, setValidating] = useState(null)
const [keyStatus, setKeyStatus] = useState({})
const validateKey = async (p) => {
setValidating(p.name)
try {
await api.validateProvider({ name: p.name, api_key: p.apiKey, model: p.model, base_url: p.baseURL || '' })
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: true, checked: true } }))
} catch (err) {
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: err.message || 'Clé invalide' } }))
}
setValidating(null)
}
useEffect(() => {
providers.forEach(p => {
if (p.apiKey && !keyStatus[p.name]) {
validateKey(p)
} else if (!p.apiKey) {
setKeyStatus(prev => ({ ...prev, [p.name]: { valid: false, checked: true, error: 'Aucune clé' } }))
}
})
}, [providers])
const handleValidate = async (name, apiKey, model, baseUrl) => {
setValidating(name)
try {
await api.validateProvider({ name, api_key: apiKey, model, base_url: baseUrl })
setKeyStatus(prev => ({ ...prev, [name]: { valid: true, checked: true } }))
} catch (err) {
setKeyStatus(prev => ({ ...prev, [name]: { valid: false, checked: true, error: err.message || 'Clé invalide' } }))
}
setValidating(null)
}
const displayed = providers.filter(p => p.name === 'minimax' || p.name === 'zai')
return (
{displayed.map((p, i) => {
const isEditing = editProvider === p.name
const currentModel = providerForm[p.name]?.model || p.model
const status = keyStatus[p.name]
return (
{p.name.toUpperCase()}
{p.active && active}
{status?.checked && status?.valid && ✓ {t('config.keyValid')}}
{status?.checked && !status?.valid && ✗ {status.error || t('config.keyInvalid')}}
{t('config.model')}
{p.model || '—'}
)
})}
)
}
function PanelUpdates({ updates, tools, checking, updating, needsUpdateCount, installedCount, missingCount, handleCheckUpdates, handleUpdateTool, handleUpdateAll, t }) {
const handleInstallTool = (tool) => {
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Installe l'outil ${tool} sur mon système. Vérifie d'abord s'il est déjà installé, puis installe-le si nécessaire avec les commandes appropriées.` } }))
}
const missingTools = tools.filter(tool => !tool.installed)
return (
<>
{installedCount} {t('config.installed')}
{missingCount > 0 && {missingCount} {t('config.missing')}}
{needsUpdateCount > 0 && {needsUpdateCount} {t('config.needsUpdate')}}
{needsUpdateCount > 0 && (
)}
{missingTools.length > 0 && (
<>
{t('config.missing') || 'Modules manquants'}
{missingTools.map((tool, i) => (
{tool.name}
{t('config.notInstalled') || 'Non installé'}
))}
>
)}
{updates.length === 0 ? (
) : (
{updates.map((u, i) => (
{u.tool}
{u.needsUpdate ? (
<>{u.current} → {u.latest}>
) : (
{u.current}
)}
{u.needsUpdate && (
)}
))}
)}
>
)
}
function PanelSkills({ skillList, t }) {
const [selected, setSelected] = useState(null)
if (skillList.length === 0) {
return {t('config.noSkills')}
}
return (
<>
{skillList.map((s, i) => (
setSelected(s)}>
{s.name}
{s.description}
{s.target && {s.target}}
{s.version && {s.version}}
{s.category && {s.category}}
))}
{selected && (
setSelected(null)}>
e.stopPropagation()}>
{selected.name}
Description
{selected.description}
Métadonnées
{selected.target && {selected.target}}
{selected.version && {selected.version}}
{selected.category && {selected.category}}
{selected.author && {selected.author}}
{selected.languages && selected.languages.map(l => {l})}
{selected.tags && selected.tags.length > 0 && (
Tags
{selected.tags.map(tag => {tag})}
)}
{selected.content && (
Contenu
{selected.content}
)}
{selected.dependencies && selected.dependencies.length > 0 && (
Dépendances
{selected.dependencies.map((d, i) => (
{d.type}
{d.name}
{d.required === false && optionnel}
))}
)}
)}
>
)
}
function PanelSystem({ api, t }) {
const [showResetModal, setShowResetModal] = useState(false)
const [toast, setToast] = useState(null)
const showToast = (msg) => {
setToast(msg)
setTimeout(() => setToast(null), 3000)
}
const handleReset = async () => {
try {
await api.resetConfig()
setShowResetModal(false)
showToast(t('config.resetDone'))
setTimeout(() => window.location.reload(), 1500)
} catch (err) {
showToast(`${t('config.error')}: ${err.message}`)
}
}
const handleApplyStarship = () => {
window.dispatchEvent(new CustomEvent('navigate-to-shell', {}))
window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Vérifie si starship est installé sur le système. S'il ne l'est pas, installe-le (avec curl ou le gestionnaire de paquets). Ensuite, applique la configuration du thème "charm" pour starship. Assure-toi que starship est bien initialisé dans le shell de l'utilisateur.` } }))
}
return (
<>
{toast && {toast}
}
Configuration Système
{t('config.applyStarship')}
Vérifie l'installation de starship et configure le thème charm via l'IA.
{t('config.resetConfig')}
Cette action supprimera toute votre configuration et relancera l'application.
{showResetModal && (
setShowResetModal(false)}>
e.stopPropagation()}>
{t('config.resetConfig')}
{t('config.resetConfirm')}
Cette action est irréversible. Toute votre configuration (profil, clés API, préférences) sera supprimée.
)}
>
)
}
function FormInput({ label, value, onChange, type = 'text' }) {
return (
onChange(e.target.value)}
/>
)
}