Some checks failed
Beta Release / beta (push) Failing after 33s
- Agent slot limiter for concurrent tool execution - Conversation summarization with soft-delete (MarkSummarized) - ANSI stripping in terminal tool output - Configurable crush-run timeout (default 600s, max 900s) - Starship theme refactor, AI tools config grid, system update UI - Streaming segments refactor, summarized messages block in feed - CSS: headings, scrollbars, tool cards, summary block styles - i18n additions (en+fr) for tools, updates, config 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
629 lines
26 KiB
JavaScript
629 lines
26 KiB
JavaScript
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 (
|
|
<div className="config-window">
|
|
{toast && <div className="config-toast">{toast}</div>}
|
|
|
|
<div className="config-tabs-bar">
|
|
{PANELS.map(p => {
|
|
const Icon = p.icon
|
|
return (
|
|
<div
|
|
key={p.id}
|
|
className={`nav-tab ${activePanel === p.id ? 'active' : ''}`}
|
|
onClick={() => setActivePanel(p.id)}
|
|
>
|
|
<span className="tab-icon"><Icon size={15} /></span>
|
|
{t(`config.panels.${p.id}`)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div className="config-panel-area">
|
|
<div className="config-panel-body">
|
|
{activePanel === 'profile' && (
|
|
<PanelProfile
|
|
config={config} editProfile={editProfile}
|
|
profileForm={profileForm} setProfileForm={setProfileForm}
|
|
setEditProfile={setEditProfile} handleSaveProfile={handleSaveProfile}
|
|
t={t}
|
|
/>
|
|
)}
|
|
{activePanel === 'providers' && (
|
|
<PanelProviders
|
|
providers={providers} editProvider={editProvider}
|
|
providerForm={providerForm} setProviderForm={setProviderForm}
|
|
setEditProvider={setEditProvider} openProviderEdit={openProviderEdit}
|
|
handleSaveProvider={handleSaveProvider} api={api} loadData={loadData}
|
|
t={t}
|
|
/>
|
|
)}
|
|
{activePanel === 'skills' && (
|
|
<PanelSkills skillList={skillList} api={api} loadData={loadData} t={t} />
|
|
)}
|
|
{activePanel === 'system' && (
|
|
<PanelSystem api={api} t={t} />
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className="config-profile-center">
|
|
<div className="config-card">
|
|
<div className="empty-state">{t('config.loadingProfile')}</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const personalKeys = Object.entries(profile).filter(([k, v]) => k !== 'preferences' && typeof v !== 'object')
|
|
const personalObj = Object.fromEntries(personalKeys)
|
|
const preferences = profile.preferences || null
|
|
|
|
return (
|
|
<div className="config-profile-center">
|
|
<div className="config-card">
|
|
<div className="section-title">{t('config.profileInfo') || 'Informations personnelles'}</div>
|
|
<RenderFields obj={personalObj} path="" editing={editProfile} onChange={updateField} t={t} />
|
|
</div>
|
|
<div className="config-card">
|
|
<div className="section-title">{t('config.profilePrefs') || 'Préférences'}</div>
|
|
{preferences ? (
|
|
<RenderFields obj={preferences} path="preferences" editing={editProfile} onChange={updateField} t={t} />
|
|
) : (
|
|
<div className="config-card-row"><span className="config-card-value" style={{ color: 'var(--text-disabled)' }}>—</span></div>
|
|
)}
|
|
</div>
|
|
<div className="config-card">
|
|
<div className="config-card-actions" style={{ justifyContent: 'center' }}>
|
|
{editProfile ? (
|
|
<>
|
|
<button className="primary sm" onClick={handleSaveProfile}>{t('config.save')}</button>
|
|
<button className="ghost sm" onClick={() => setEditProfile(false)}>{t('config.cancel')}</button>
|
|
</>
|
|
) : (
|
|
<button className="primary sm" onClick={() => {
|
|
setProfileForm(config.profile ? JSON.parse(JSON.stringify(config.profile)) : {})
|
|
setEditProfile(true)
|
|
}}>{t('config.editProfile')}</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div key={key} className="config-card-row">
|
|
<span className="config-card-label">{label}</span>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
|
|
<input type="checkbox" checked={value} onChange={e => onChange(fieldPath, e.target.checked)} />
|
|
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{value ? 'On' : 'Off'}</span>
|
|
</label>
|
|
</div>
|
|
)
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return (
|
|
<div key={key} className="config-form-field">
|
|
<label className="config-form-label">{label}</label>
|
|
<input className="config-form-input" value={value.join(', ')} onChange={e => onChange(fieldPath, e.target.value.split(',').map(s => s.trim()).filter(Boolean))} />
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<div key={key} className="config-form-field">
|
|
<label className="config-form-label">{label}</label>
|
|
<input className="config-form-input" type={typeof value === 'number' ? 'number' : 'text'} value={value ?? ''} onChange={e => onChange(fieldPath, typeof value === 'number' ? Number(e.target.value) : e.target.value)} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return (
|
|
<div key={key} className="config-card-row">
|
|
<span className="config-card-label">{label}</span>
|
|
<span className="config-card-value">{value ? 'On' : 'Off'}</span>
|
|
</div>
|
|
)
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return (
|
|
<div key={key} className="config-card-row">
|
|
<span className="config-card-label">{label}</span>
|
|
<span className="config-card-value">{value.length > 0 ? value.join(', ') : '—'}</span>
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<div key={key} className="config-card-row">
|
|
<span className="config-card-label">{label}</span>
|
|
<span className="config-card-value">{value != null && value !== '' ? String(value) : '—'}</span>
|
|
</div>
|
|
)
|
|
})
|
|
}
|
|
|
|
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 (
|
|
<div className="config-providers-list">
|
|
{displayed.map((p, i) => {
|
|
const isEditing = editProvider === p.name
|
|
const currentModel = providerForm[p.name]?.model || p.model
|
|
const status = keyStatus[p.name]
|
|
|
|
return (
|
|
<div key={i} className="config-card provider-card-v2">
|
|
<div className="provider-card-top">
|
|
<div className="provider-card-identity">
|
|
<span className="provider-card-name">{p.name.toUpperCase()}</span>
|
|
{p.active && <span className="badge ok" style={{ marginLeft: 6 }}>active</span>}
|
|
{status?.checked && status?.valid && <span className="badge ok">✓ {t('config.keyValid')}</span>}
|
|
{status?.checked && !status?.valid && <span className="badge error">✗ {status.error || t('config.keyInvalid')}</span>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="provider-card-form">
|
|
<div className="provider-setup-token-row">
|
|
<div className="provider-setup-token-input">
|
|
<label className="config-form-label">{t('config.apiKey')}</label>
|
|
<input
|
|
className="config-form-input"
|
|
type="password"
|
|
placeholder={p.api_key ? '••••••••' : t('config.tokenPlaceholder')}
|
|
value={isEditing ? (providerForm[p.name]?.api_key || '') : ''}
|
|
onChange={e => {
|
|
if (!isEditing) openProviderEdit(p)
|
|
setProviderForm(prev => ({
|
|
...prev,
|
|
[p.name]: { ...(prev[p.name] || {}), api_key: e.target.value },
|
|
}))
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="provider-setup-token-actions">
|
|
<button
|
|
className="sm primary"
|
|
disabled={validating === p.name || !providerForm[p.name]?.api_key}
|
|
onClick={() => handleValidate(p.name, providerForm[p.name]?.api_key, currentModel, providerForm[p.name]?.base_url)}
|
|
>
|
|
{validating === p.name ? t('config.validating') : t('config.validateKey')}
|
|
</button>
|
|
{isEditing && (
|
|
<button className="sm" onClick={() => handleSaveProvider(p.name)}>{t('config.save')}</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="provider-card-model">
|
|
<span className="provider-card-model-label">{t('config.model')}</span>
|
|
<span className="provider-card-model-value">{p.model || '—'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 <div className="empty-state" style={{ color: 'var(--text-disabled)', padding: 40 }}>{t('config.noSkills')}</div>
|
|
}
|
|
|
|
return (
|
|
<div className="skills-list">
|
|
{skillList.map((s, i) => (
|
|
<div key={i} className="config-update-row" style={{ alignItems: 'center' }}>
|
|
<div className="skill-list-info">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<span className="config-update-name">{s.name}</span>
|
|
{s.deployed ? (
|
|
<span className="badge ok">{t('config.installed')}</span>
|
|
) : (
|
|
<span className="badge neutral">{t('config.notInstalled')}</span>
|
|
)}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}>{s.description}</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
|
<button
|
|
className="sm primary"
|
|
disabled={s.deployed || deploying === s.name + '-deploy'}
|
|
onClick={() => handleDeploy(s.name)}
|
|
>
|
|
{deploying === s.name + '-deploy' ? '...' : t('config.apply')}
|
|
</button>
|
|
<button
|
|
className="sm ghost"
|
|
disabled={!s.deployed || deploying === s.name + '-undeploy'}
|
|
onClick={() => handleUndeploy(s.name)}
|
|
>
|
|
{deploying === s.name + '-undeploy' ? '...' : t('config.remove')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 && <div className="config-toast">{toast}</div>}
|
|
|
|
<div className="section-title" style={{ marginBottom: 8 }}>{t('config.systemConfig')}</div>
|
|
|
|
<div className="section-title" style={{ marginTop: 4, marginBottom: 8, fontSize: 12, color: 'var(--text-tertiary)', textTransform: 'none', letterSpacing: 0 }}>
|
|
<Bot size={13} style={{ verticalAlign: 'middle', marginRight: 6 }} />
|
|
{t('config.aiToolsConfig')}
|
|
</div>
|
|
<div className="config-ai-tools-grid">
|
|
{AI_TOOLS.map(tool => {
|
|
const Icon = ICON_MAP[tool.icon] || Bot
|
|
return (
|
|
<div key={tool.id} className="config-ai-tool-card">
|
|
<div className="config-ai-tool-header">
|
|
<span className="config-ai-tool-icon"><Icon size={16} /></span>
|
|
<span className="config-ai-tool-name">{tool.name}</span>
|
|
</div>
|
|
<div className="config-ai-tool-desc">{tool.description}</div>
|
|
<button className="sm primary" onClick={() => configureTool(tool)} style={{ marginTop: 'auto' }}>
|
|
<Sparkles size={12} style={{ verticalAlign: 'middle', marginRight: 4 }} />
|
|
{t('config.configureViaAI')}
|
|
</button>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div className="config-card" style={{ marginTop: 12, marginBottom: 4 }}>
|
|
<div className="config-card-row" style={{ alignItems: 'center' }}>
|
|
<div>
|
|
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.systemUpdate')}</span>
|
|
<div style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 2 }}>
|
|
{isSudo ? t('config.systemUpdateDescSudo') : t('config.systemUpdateDescNoSudo')}
|
|
</div>
|
|
</div>
|
|
<button className="sm primary" onClick={handleSystemUpdate}>
|
|
<Download size={12} style={{ verticalAlign: 'middle', marginRight: 4 }} />
|
|
{t('config.updateBtn')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="section-title" style={{ marginTop: 20, marginBottom: 8, color: 'var(--danger)' }}>
|
|
<AlertTriangle size={14} style={{ verticalAlign: 'middle', marginRight: 6 }} />
|
|
Zone Rouge
|
|
</div>
|
|
<div className="config-card" style={{ borderColor: 'var(--danger)', borderWidth: 1, borderStyle: 'solid' }}>
|
|
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
|
<span className="config-card-label" style={{ fontWeight: 600, color: 'var(--danger)' }}>{t('config.resetConfig')}</span>
|
|
</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginBottom: 12 }}>
|
|
Cette action supprimera toute votre configuration et relancera l'application.
|
|
</div>
|
|
<button className="sm ghost danger" onClick={() => setShowResetModal(true)}>
|
|
{t('config.resetConfig')}
|
|
</button>
|
|
</div>
|
|
|
|
{showResetModal && (
|
|
<div className="shell-modal-overlay" onClick={() => setShowResetModal(false)}>
|
|
<div className="shell-modal" onClick={e => e.stopPropagation()}>
|
|
<div className="shell-modal-header" style={{ color: 'var(--danger)' }}>
|
|
<AlertTriangle size={16} style={{ verticalAlign: 'middle', marginRight: 8 }} />
|
|
{t('config.resetConfig')}
|
|
</div>
|
|
<div className="shell-modal-body">
|
|
<p style={{ color: 'var(--warning)', fontSize: 13, marginBottom: 12 }}>
|
|
{t('config.resetConfirm')}
|
|
</p>
|
|
<p style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>
|
|
Cette action est irréversible. Toute votre configuration (profil, clés API, préférences) sera supprimée.
|
|
</p>
|
|
</div>
|
|
<div className="shell-modal-footer">
|
|
<button className="ghost" onClick={() => setShowResetModal(false)}>{t('config.cancel')}</button>
|
|
<button className="danger" onClick={handleReset}>{t('config.resetConfig')}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function FormInput({ label, value, onChange, type = 'text' }) {
|
|
return (
|
|
<div className="config-form-field">
|
|
<label className="config-form-label">{label}</label>
|
|
<input
|
|
className="config-form-input"
|
|
type={type}
|
|
value={value}
|
|
onChange={e => onChange(e.target.value)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|