refactor: redesign Config as settings window with sidebar panels, remove system overview from Dashboard
All checks were successful
Beta Release / beta (push) Successful in 38s
All checks were successful
Beta Release / beta (push) Successful in 38s
- Config: sidebar navigation with 5 panels (Profile, AI Providers, Updates, Locale, Skills) - Dashboard: remove duplicated system overview section, keep workflows and activity log - New CSS for config window layout, cards, provider cards, update rows - Add i18n panel keys (FR/EN) 💾 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { User, Brain, RefreshCw, Globe, Wrench } from 'lucide-react'
|
||||
import { useI18n, LANGUAGES } from '../i18n'
|
||||
import { getLayoutList } from '../i18n/keyboards'
|
||||
|
||||
const PANELS = [
|
||||
{ id: 'profile', icon: User },
|
||||
{ id: 'providers', icon: Brain },
|
||||
{ id: 'updates', icon: RefreshCw },
|
||||
{ id: 'locale', icon: Globe },
|
||||
{ id: 'skills', icon: Wrench },
|
||||
]
|
||||
|
||||
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([])
|
||||
@@ -119,132 +129,238 @@ export default function Config({ api }) {
|
||||
const missingCount = tools.filter(t => !t.installed).length
|
||||
|
||||
return (
|
||||
<div className="config-layout">
|
||||
<div className="config-window">
|
||||
{toast && <div className="config-toast">{toast}</div>}
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.systemUpdates')}</div>
|
||||
<div className="config-actions-row">
|
||||
<button className="sm" onClick={handleCheckUpdates} disabled={checking}>
|
||||
{checking ? t('config.checking') : t('config.checkUpdates')}
|
||||
</button>
|
||||
{needsUpdateCount > 0 && (
|
||||
<button className="sm primary" onClick={handleUpdateAll} disabled={updating === '__all__'}>
|
||||
{updating === '__all__' ? t('config.updating') : `${t('config.updateAll')} (${needsUpdateCount})`}
|
||||
</button>
|
||||
<div className="config-sidebar">
|
||||
{PANELS.map(p => {
|
||||
const Icon = p.icon
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`config-sidebar-item ${activePanel === p.id ? 'active' : ''}`}
|
||||
onClick={() => setActivePanel(p.id)}
|
||||
>
|
||||
<Icon size={16} />
|
||||
<span>{t(`config.panels.${p.id}`)}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="config-panel-area">
|
||||
<div className="config-panel-header">
|
||||
<h2 className="config-panel-title">{t(`config.panels.${activePanel}`)}</h2>
|
||||
</div>
|
||||
|
||||
<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 === 'updates' && (
|
||||
<PanelUpdates
|
||||
updates={updates} tools={tools}
|
||||
checking={checking} updating={updating}
|
||||
needsUpdateCount={needsUpdateCount}
|
||||
installedCount={installedCount} missingCount={missingCount}
|
||||
handleCheckUpdates={handleCheckUpdates}
|
||||
handleUpdateTool={handleUpdateTool}
|
||||
handleUpdateAll={handleUpdateAll}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{activePanel === 'locale' && (
|
||||
<PanelLocale
|
||||
language={keyboard} layouts={layouts}
|
||||
setLanguage={setLanguage} setKeyboard={setKeyboard}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
{activePanel === 'skills' && (
|
||||
<PanelSkills skillList={skillList} t={t} />
|
||||
)}
|
||||
</div>
|
||||
<div className="config-stats">
|
||||
<span className="badge ok">{installedCount} {t('config.installed')}</span>
|
||||
{missingCount > 0 && <span className="badge error">{missingCount} {t('config.missing')}</span>}
|
||||
{needsUpdateCount > 0 && <span className="badge warn">{needsUpdateCount} {t('config.needsUpdate')}</span>}
|
||||
</div>
|
||||
|
||||
{updates.length === 0 ? (
|
||||
<div className="empty-state">{t('config.noUpdates')}</div>
|
||||
) : (
|
||||
<div className="config-update-list">
|
||||
{updates.map((u, i) => (
|
||||
<div key={i} className="config-update-row">
|
||||
<div className="config-update-info">
|
||||
<span className="config-update-name">{u.tool}</span>
|
||||
<span className="config-update-versions">
|
||||
{u.needsUpdate ? (
|
||||
<>{u.current} → <span style={{ color: 'var(--warning)' }}>{u.latest}</span></>
|
||||
) : (
|
||||
<span style={{ color: 'var(--success)' }}>{u.current}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{u.needsUpdate && (
|
||||
<button
|
||||
className="sm"
|
||||
onClick={() => handleUpdateTool(u.tool)}
|
||||
disabled={updating === u.tool}
|
||||
>
|
||||
{updating === u.tool ? t('config.updating') : t('config.updateTool')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">
|
||||
{t('config.profile')}
|
||||
<button className="ghost sm" onClick={() => setEditProfile(!editProfile)}>
|
||||
{editProfile ? t('config.cancel') : t('config.editProfile')}
|
||||
</button>
|
||||
</div>
|
||||
{config?.profile && !editProfile ? (
|
||||
<div>
|
||||
<FieldRow label={t('config.name')} value={config.profile.name} />
|
||||
<FieldRow label={t('config.pseudo')} value={config.profile.pseudo} />
|
||||
<FieldRow label={t('config.email')} value={config.profile.email} />
|
||||
<FieldRow label={t('config.editor')} value={config.profile.preferences?.editor} />
|
||||
<FieldRow label={t('config.shell')} value={config.profile.preferences?.shell} />
|
||||
<FieldRow label={t('config.languages')} value={config.profile.languages?.join(', ')} />
|
||||
function PanelProfile({ config, editProfile, profileForm, setProfileForm, setEditProfile, handleSaveProfile, t }) {
|
||||
return (
|
||||
<div className="config-card">
|
||||
{config?.profile && !editProfile ? (
|
||||
<>
|
||||
<div className="config-card-row">
|
||||
<span className="config-card-label">{t('config.name')}</span>
|
||||
<span className="config-card-value">{config.profile.name || '—'}</span>
|
||||
</div>
|
||||
) : editProfile ? (
|
||||
<div className="config-form">
|
||||
<FormInput label={t('config.name')} value={profileForm.name} onChange={v => setProfileForm(p => ({ ...p, name: v }))} />
|
||||
<FormInput label={t('config.pseudo')} value={profileForm.pseudo} onChange={v => setProfileForm(p => ({ ...p, pseudo: v }))} />
|
||||
<FormInput label={t('config.email')} value={profileForm.email} onChange={v => setProfileForm(p => ({ ...p, email: v }))} />
|
||||
<FormInput label={t('config.editor')} value={profileForm.editor} onChange={v => setProfileForm(p => ({ ...p, editor: v }))} />
|
||||
<FormInput label={t('config.shell')} value={profileForm.shell} onChange={v => setProfileForm(p => ({ ...p, shell: v }))} />
|
||||
<div className="config-form-actions">
|
||||
<button className="primary sm" onClick={handleSaveProfile}>{t('config.save')}</button>
|
||||
<button className="ghost sm" onClick={() => setEditProfile(false)}>{t('config.cancel')}</button>
|
||||
<div className="config-card-row">
|
||||
<span className="config-card-label">{t('config.pseudo')}</span>
|
||||
<span className="config-card-value">{config.profile.pseudo || '—'}</span>
|
||||
</div>
|
||||
<div className="config-card-row">
|
||||
<span className="config-card-label">{t('config.email')}</span>
|
||||
<span className="config-card-value">{config.profile.email || '—'}</span>
|
||||
</div>
|
||||
<div className="config-card-row">
|
||||
<span className="config-card-label">{t('config.editor')}</span>
|
||||
<span className="config-card-value mono">{config.profile.preferences?.editor || '—'}</span>
|
||||
</div>
|
||||
<div className="config-card-row">
|
||||
<span className="config-card-label">{t('config.shell')}</span>
|
||||
<span className="config-card-value mono">{config.profile.preferences?.shell || '—'}</span>
|
||||
</div>
|
||||
<div className="config-card-row">
|
||||
<span className="config-card-label">{t('config.languages')}</span>
|
||||
<span className="config-card-value">{config.profile.languages?.join(', ') || '—'}</span>
|
||||
</div>
|
||||
<div className="config-card-actions">
|
||||
<button className="primary sm" onClick={() => setEditProfile(true)}>{t('config.editProfile')}</button>
|
||||
</div>
|
||||
</>
|
||||
) : editProfile ? (
|
||||
<>
|
||||
<FormInput label={t('config.name')} value={profileForm.name} onChange={v => setProfileForm(p => ({ ...p, name: v }))} />
|
||||
<FormInput label={t('config.pseudo')} value={profileForm.pseudo} onChange={v => setProfileForm(p => ({ ...p, pseudo: v }))} />
|
||||
<FormInput label={t('config.email')} value={profileForm.email} onChange={v => setProfileForm(p => ({ ...p, email: v }))} type="email" />
|
||||
<FormInput label={t('config.editor')} value={profileForm.editor} onChange={v => setProfileForm(p => ({ ...p, editor: v }))} />
|
||||
<FormInput label={t('config.shell')} value={profileForm.shell} onChange={v => setProfileForm(p => ({ ...p, shell: v }))} />
|
||||
<div className="config-card-actions">
|
||||
<button className="primary sm" onClick={handleSaveProfile}>{t('config.save')}</button>
|
||||
<button className="ghost sm" onClick={() => setEditProfile(false)}>{t('config.cancel')}</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="empty-state">{t('config.loadingProfile')}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) {
|
||||
return (
|
||||
<div className="config-providers-list">
|
||||
{providers.map((p, i) => (
|
||||
<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}</span>
|
||||
{p.active && <span className="badge accent">{t('config.active')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">{t('config.loadingProfile')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.aiProviders')}</div>
|
||||
{providers.map((p, i) => (
|
||||
<div key={i} className="provider-card">
|
||||
<div className="provider-info">
|
||||
<div className="provider-name">
|
||||
{p.name}
|
||||
{p.active && <span className="badge accent" style={{ marginLeft: 8 }}>{t('config.active')}</span>}
|
||||
</div>
|
||||
{editProvider !== p.name ? (
|
||||
<div className="provider-meta">
|
||||
<span>{p.model}</span>
|
||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||
{p.apiKey ? t('config.keyConfigured') : t('config.noKey')}
|
||||
</span>
|
||||
<button className="ghost sm" onClick={() => openProviderEdit(p)}>{t('config.editProvider')}</button>
|
||||
{!p.active && (
|
||||
<button className="sm" onClick={async () => {
|
||||
await api.saveProvider({ name: p.name, active: true })
|
||||
loadData()
|
||||
}}>{t('config.activate')}</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="config-form" style={{ marginTop: 8 }}>
|
||||
<FormInput label={t('config.apiKey')} value={providerForm.api_key} onChange={v => setProviderForm(f => ({ ...f, api_key: v }))} type="password" />
|
||||
<FormInput label={t('config.model')} value={providerForm.model} onChange={v => setProviderForm(f => ({ ...f, model: v }))} />
|
||||
<FormInput label={t('config.baseUrl')} value={providerForm.base_url} onChange={v => setProviderForm(f => ({ ...f, base_url: v }))} />
|
||||
<div className="config-form-actions">
|
||||
<button className="primary sm" onClick={handleSaveProvider}>{t('config.save')}</button>
|
||||
<button className="ghost sm" onClick={() => setEditProvider(null)}>{t('config.cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="provider-card-actions">
|
||||
{editProvider !== p.name && (
|
||||
<button className="ghost sm" onClick={() => openProviderEdit(p)}>{t('config.editProvider')}</button>
|
||||
)}
|
||||
{!p.active && editProvider !== p.name && (
|
||||
<button className="sm" onClick={async () => {
|
||||
await api.saveProvider({ name: p.name, active: true })
|
||||
loadData()
|
||||
}}>{t('config.activate')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{editProvider !== p.name ? (
|
||||
<div className="provider-card-meta">
|
||||
<span className="mono">{p.model || '—'}</span>
|
||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||
{p.apiKey ? t('config.keyConfigured') : t('config.noKey')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="provider-card-form">
|
||||
<FormInput label={t('config.apiKey')} value={providerForm.api_key} onChange={v => setProviderForm(f => ({ ...f, api_key: v }))} type="password" />
|
||||
<FormInput label={t('config.model')} value={providerForm.model} onChange={v => setProviderForm(f => ({ ...f, model: v }))} />
|
||||
<FormInput label={t('config.baseUrl')} value={providerForm.base_url} onChange={v => setProviderForm(f => ({ ...f, base_url: v }))} />
|
||||
<div className="config-card-actions">
|
||||
<button className="primary sm" onClick={handleSaveProvider}>{t('config.save')}</button>
|
||||
<button className="ghost sm" onClick={() => setEditProvider(null)}>{t('config.cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PanelUpdates({ updates, checking, updating, needsUpdateCount, installedCount, missingCount, handleCheckUpdates, handleUpdateTool, handleUpdateAll, t }) {
|
||||
return (
|
||||
<>
|
||||
<div className="config-card">
|
||||
<div className="config-update-controls">
|
||||
<div className="config-update-stats">
|
||||
<span className="badge ok">{installedCount} {t('config.installed')}</span>
|
||||
{missingCount > 0 && <span className="badge error">{missingCount} {t('config.missing')}</span>}
|
||||
{needsUpdateCount > 0 && <span className="badge warn">{needsUpdateCount} {t('config.needsUpdate')}</span>}
|
||||
</div>
|
||||
<div className="config-update-buttons">
|
||||
<button className="sm" onClick={handleCheckUpdates} disabled={checking}>
|
||||
{checking ? <><RefreshCw size={12} className="spin-icon" /> {t('config.checking')}</> : t('config.checkUpdates')}
|
||||
</button>
|
||||
{needsUpdateCount > 0 && (
|
||||
<button className="sm primary" onClick={handleUpdateAll} disabled={updating === '__all__'}>
|
||||
{updating === '__all__' ? t('config.updating') : `${t('config.updateAll')} (${needsUpdateCount})`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.language')}</div>
|
||||
{updates.length === 0 ? (
|
||||
<div className="config-card">
|
||||
<div className="empty-state">{t('config.noUpdates')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="config-update-list">
|
||||
{updates.map((u, i) => (
|
||||
<div key={i} className="config-update-row">
|
||||
<div className="config-update-info">
|
||||
<span className="config-update-name">{u.tool}</span>
|
||||
<span className="config-update-versions">
|
||||
{u.needsUpdate ? (
|
||||
<>{u.current} → <span style={{ color: 'var(--warning)' }}>{u.latest}</span></>
|
||||
) : (
|
||||
<span style={{ color: 'var(--success)' }}>{u.current}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{u.needsUpdate && (
|
||||
<button
|
||||
className="sm"
|
||||
onClick={() => handleUpdateTool(u.tool)}
|
||||
disabled={updating === u.tool}
|
||||
>
|
||||
{updating === u.tool ? t('config.updating') : t('config.updateTool')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PanelLocale({ language, keyboard, layouts, setLanguage, setKeyboard, t }) {
|
||||
return (
|
||||
<div className="config-card">
|
||||
<div className="config-card-group">
|
||||
<span className="config-card-group-label">{t('config.language')}</span>
|
||||
<div className="chip-row">
|
||||
{LANGUAGES.map(lang => (
|
||||
<div
|
||||
@@ -257,9 +373,8 @@ export default function Config({ api }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.keyboardLayout')}</div>
|
||||
<div className="config-card-group">
|
||||
<span className="config-card-group-label">{t('config.keyboardLayout')}</span>
|
||||
<div className="chip-row">
|
||||
{layouts.map(l => (
|
||||
<div
|
||||
@@ -272,43 +387,37 @@ export default function Config({ api }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.skills')} ({skillList.length})</div>
|
||||
{skillList.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
{t('config.noSkills')}
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{t('config.runSkillsInit')}</span>
|
||||
</div>
|
||||
) : (
|
||||
skillList.map((s, i) => (
|
||||
<div key={i} className="config-skill-row">
|
||||
<span className="config-skill-name">{s.name}</span>
|
||||
<span className="badge neutral">{s.target || 'both'}</span>
|
||||
<span className="config-skill-desc">{s.description}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldRow({ label, value }) {
|
||||
function PanelSkills({ skillList, t }) {
|
||||
return (
|
||||
<div className="field-row">
|
||||
<span className="field-label">{label}</span>
|
||||
<span className={`field-value ${!value ? 'empty' : ''}`}>{value || '—'}</span>
|
||||
<div className="config-card">
|
||||
{skillList.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
{t('config.noSkills')}
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{t('config.runSkillsInit')}</span>
|
||||
</div>
|
||||
) : (
|
||||
skillList.map((s, i) => (
|
||||
<div key={i} className="config-skill-row">
|
||||
<span className="config-skill-name">{s.name}</span>
|
||||
<span className="badge neutral">{s.target || 'both'}</span>
|
||||
<span className="config-skill-desc">{s.description}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FormInput({ label, value, onChange, type = 'text' }) {
|
||||
return (
|
||||
<div className="field-row">
|
||||
<span className="field-label">{label}</span>
|
||||
<div className="config-form-field">
|
||||
<label className="config-form-label">{label}</label>
|
||||
<input
|
||||
className="config-input"
|
||||
className="config-form-input"
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user