import { useState, useEffect, useCallback } from 'react' import { User, Brain, Wrench, Monitor, AlertTriangle, Bot, Sparkles, Zap, GitBranch, Container, Circle, Hexagon, Code, Rocket, Download } from 'lucide-react' import { useI18n } from '../i18n' const PANELS = [ { id: 'profile', icon: User }, { id: 'providers', icon: Brain }, { 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 [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]) useEffect(() => { loadData() }, [loadData]) const showToast = (msg) => { setToast(msg) setTimeout(() => setToast(null), 2500) } 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.api_key || '', model: p.model || '', base_url: p.base_url || '', }, })) setEditProvider(p.name) } return (
{toast &&
{toast}
}
{PANELS.map(p => { const Icon = p.icon return (
setActivePanel(p.id)} > {t(`config.panels.${p.id}`)}
) })}
{activePanel === 'profile' && ( )} {activePanel === 'providers' && ( )} {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.api_key, model: p.model, base_url: p.base_url || '' }) 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.api_key && !keyStatus[p.name]) { validateKey(p) } else if (!p.api_key) { 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 === 'mimo') 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')}}
{ if (!isEditing) openProviderEdit(p) setProviderForm(prev => ({ ...prev, [p.name]: { ...(prev[p.name] || {}), api_key: e.target.value }, })) }} />
{isEditing && ( )}
{t('config.model')} {p.model || '—'}
) })}
) } function PanelSkills({ skillList, api, loadData, t }) { const [deploying, setDeploying] = useState(null) const handleDeploy = async (name) => { setDeploying(name + '-deploy') try { await api.deploySkill(name) loadData() } catch (err) { console.error('deploy skill:', err) } setDeploying(null) } const handleUndeploy = async (name) => { setDeploying(name + '-undeploy') try { await api.undeploySkill(name) loadData() } catch (err) { console.error('undeploy skill:', err) } setDeploying(null) } if (skillList.length === 0) { return
{t('config.noSkills')}
} return (
{skillList.map((s, i) => (
{s.name} {s.deployed ? ( {t('config.installed')} ) : ( {t('config.notInstalled')} )}
{s.description}
))}
) } function PanelSystem({ api, t }) { const [showResetModal, setShowResetModal] = useState(false) const [toast, setToast] = useState(null) const [isSudo, setIsSudo] = useState(false) useEffect(() => { api.getInfo().then(d => setIsSudo(!!d.sudo)).catch(() => {}) }, [api]) 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 handleSystemUpdate = () => { window.dispatchEvent(new CustomEvent('navigate-to-shell')) if (isSudo) { window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Mets à jour le système et tous les outils utilisés par l'application Muyue. Exécute les commandes suivantes dans l'ordre :\n1. Met à jour les paquets système : sudo apt update && sudo apt upgrade -y\n2. Installe les dépendances utiles si manquantes : sudo apt install -y sshpass git curl wget\n3. Mets à jour les outils installés : crush, claude, gh, go, node/npm, python3/pip3/uv, docker, starship\n4. Pour chaque outil, vérifie la version actuelle, mets à jour si possible, puis vérifie la nouvelle version\n5. Donne un récapitulatif final de tout ce qui a été mis à jour ou installé` } })) } else { window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: `Je n'ai pas les droits sudo sur ce système. Donne-moi les commandes nécessaires pour mettre à jour le système et les outils suivants. Pour chaque outil, indique la commande exacte à exécuter :\n1. Paquets système (apt update && apt upgrade)\n2. Outils à mettre à jour : crush, claude, gh, go, node/npm, python3/pip3/uv, docker, starship\n3. Dépendances utiles à installer : sshpass, git, curl, wget\n4. Présente les commandes dans un tableau markdown avec le nom de l'outil, la commande, et si sudo est requis` } })) } } const configureTool = (tool) => { window.dispatchEvent(new CustomEvent('navigate-to-shell')) window.dispatchEvent(new CustomEvent('ask-ai-terminal', { detail: { message: tool.prompt } })) } const AI_TOOLS = [ { id: 'crush', name: 'Crush', icon: 'Zap', description: t('config.toolCrushDesc'), prompt: `Configure l'outil Crush sur ce système. Vérifie d'abord s'il est installé avec "crush --version". S'il n'est pas installé, installe-le avec la méthode appropriée (npm install -g @anthropic/crush ou via le script officiel). S'il est déjà installé, vérifie sa configuration dans ~/.config/crush/ et affiche son état. Demande-moi les informations nécessaires si besoin (clés API, préférences, etc.).`, }, { id: 'claude', name: 'Claude Code', icon: 'Bot', description: t('config.toolClaudeDesc'), prompt: `Configure l'outil Claude Code (claude) sur ce système. Vérifie d'abord s'il est installé avec "claude --version". S'il n'est pas installé, installe-le avec npm install -g @anthropic-ai/claude-code. S'il est installé, vérifie sa configuration et son authentification. Demande-moi les informations nécessaires si besoin (clé API Anthropic, etc.).`, }, { id: 'gh', name: 'GitHub CLI', icon: 'GitBranch', description: t('config.toolGhDesc'), prompt: `Configure l'outil GitHub CLI (gh) sur ce système. Vérifie d'abord s'il est installé avec "gh --version". S'il n'est pas installé, installe-le avec la méthode appropriée pour ce système. S'il est installé, vérifie son authentification avec "gh auth status". Si non authentifié, guide-moi pour le configurer avec "gh auth login". Demande-moi le token si nécessaire.`, }, { id: 'docker', name: 'Docker', icon: 'Container', description: t('config.toolDockerDesc'), prompt: `Configure Docker sur ce système. Vérifie d'abord s'il est installé avec "docker --version". Vérifie aussi si le daemon tourne avec "docker info". S'il n'est pas installé, installe-le avec la méthode appropriée. Vérifie que l'utilisateur est dans le groupe docker. Si des problèmes de permissions existent, explique comment les résoudre.`, }, { id: 'go', name: 'Go', icon: 'Circle', description: t('config.toolGoDesc'), prompt: `Configure l'environnement Go sur ce système. Vérifie s'il est installé avec "go version". Vérifie le GOPATH, GOROOT et les variables d'environnement. S'il n'est pas installé, installe-le avec la méthode appropriée. Vérifie que les binaires Go sont dans le PATH.`, }, { id: 'node', name: 'Node.js', icon: 'Hexagon', description: t('config.toolNodeDesc'), prompt: `Configure l'environnement Node.js sur ce système. Vérifie s'il est installé avec "node --version" et "npm --version". Vérifie aussi pnpm et npx. S'il n'est pas installé, installe-le avec la méthode recommandée (nvm, fnm ou le gestionnaire de paquets). Vérifie la version LTS vs Current.`, }, { id: 'python', name: 'Python', icon: 'Code', description: t('config.toolPythonDesc'), prompt: `Configure l'environnement Python sur ce système. Vérifie python3 --version, pip3 --version, et uv --version. S'ils ne sont pas installés, installe-les avec la méthode appropriée. Vérifie les paquets essentiels (venv, pip). Configure uv si nécessaire.`, }, { id: 'starship', name: 'Starship', icon: 'Rocket', description: t('config.toolStarshipDesc'), prompt: `Configure Starship (prompt shell) sur ce système. Vérifie s'il est installé avec "starship --version". S'il n'est pas installé, installe-le. Ensuite, configure le thème "charm" dans ~/.config/starship.toml. Assure-toi que starship est initialisé dans le shell de l'utilisateur (.bashrc, .zshrc ou config fish).`, }, ] const ICON_MAP = { Zap, Bot, GitBranch, Container, Circle, Hexagon, Code, Rocket } return ( <> {toast &&
{toast}
}
{t('config.systemConfig')}
{t('config.aiToolsConfig')}
{AI_TOOLS.map(tool => { const Icon = ICON_MAP[tool.icon] || Bot return (
{tool.name}
{tool.description}
) })}
{t('config.systemUpdate')}
{isSudo ? t('config.systemUpdateDescSudo') : t('config.systemUpdateDescNoSudo')}
Zone Rouge
{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)} />
) }