feat(web): add i18n support with FR/EN locales and keyboard layout awareness
All checks were successful
Beta Release / beta (push) Successful in 36s
All checks were successful
Beta Release / beta (push) Successful in 36s
Add full internationalization system with React context, French/English translations, and AZERTY/QWERTY keyboard layout support. Dashboard now uses a tabbed layout (Tools, Notifications, Workflows). Config page exposes language and keyboard preferences persisted via new /api/preferences endpoint. 💕 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
This commit is contained in:
@@ -1,126 +1,101 @@
|
||||
import { useState } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Dashboard({ tools, updates, api, onRescan }) {
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [log, setLog] = useState([])
|
||||
const { t, layout } = useI18n()
|
||||
const [activeSection, setActiveSection] = useState('tools')
|
||||
const [notifications, setNotifications] = useState([])
|
||||
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
const installed = tools.filter(tool => tool.installed).length
|
||||
const total = tools.length
|
||||
const pct = total > 0 ? Math.round((installed / total) * 100) : 0
|
||||
const missing = tools.filter(t => !t.installed).map(t => t.name || t.Name)
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (missing.length === 0) return
|
||||
setInstalling(true)
|
||||
addLog(`Installing ${missing.length} tools...`, 'info')
|
||||
try {
|
||||
await api.installTools(missing)
|
||||
addLog('Install started. Rescanning...', 'ok')
|
||||
await api.runScan()
|
||||
const data = await api.getTools()
|
||||
onRescan(data.tools || [])
|
||||
addLog('Done.', 'ok')
|
||||
} catch (err) {
|
||||
addLog(err.message, 'error')
|
||||
}
|
||||
setInstalling(false)
|
||||
const addNotif = (text, type) => {
|
||||
setNotifications(prev => [{ text, type, id: Date.now(), time: new Date() }, ...prev])
|
||||
}
|
||||
|
||||
const handleScan = async () => {
|
||||
addLog('Scanning system...', 'info')
|
||||
await api.runScan()
|
||||
const data = await api.getTools()
|
||||
onRescan(data.tools || [])
|
||||
addLog('Scan complete.', 'ok')
|
||||
}
|
||||
|
||||
const handleCheckUpdates = async () => {
|
||||
const data = await api.getUpdates().catch(() => ({ updates: [] }))
|
||||
const count = (data.updates || []).filter(u => u.needsUpdate).length
|
||||
addLog(count > 0 ? `${count} updates available.` : 'All tools up to date.', count > 0 ? 'warn' : 'ok')
|
||||
}
|
||||
|
||||
const addLog = (text, type) => setLog(prev => [...prev, { text, type, id: Date.now() }])
|
||||
const sections = [
|
||||
{ id: 'tools', label: t('dashboard.systemOverview') },
|
||||
{ id: 'notifications', label: t('dashboard.activityLog') },
|
||||
{ id: 'workflows', label: t('studio.workflows') },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div style={{ overflow: 'auto' }}>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
System Overview — {installed}/{total} tools ({pct}%)
|
||||
<div className="dashboard-layout">
|
||||
<div className="dashboard-tabs">
|
||||
{sections.map(s => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`dashboard-tab ${activeSection === s.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection(s.id)}
|
||||
>
|
||||
{s.label}
|
||||
{s.id === 'tools' && total > 0 && (
|
||||
<span className="tab-count">{installed}/{total}</span>
|
||||
)}
|
||||
{s.id === 'notifications' && notifications.length > 0 && (
|
||||
<span className="tab-count warn">{notifications.length}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="progress" style={{ marginBottom: 16 }}>
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div>
|
||||
{tools.map((t, i) => {
|
||||
const name = t.name || t.Name
|
||||
const ver = extractVersion(t.Version || t.version)
|
||||
return (
|
||||
<div key={i} className="tool-row">
|
||||
<span className={`badge ${t.installed ? 'ok' : 'error'}`}>
|
||||
{t.installed ? 'Installed' : 'Missing'}
|
||||
</span>
|
||||
<span className="tool-name">{name}</span>
|
||||
{ver && <span className="tool-version">{ver}</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div className="card">
|
||||
<div className="card-header">Quick Actions</div>
|
||||
<div className="actions-stack">
|
||||
<button onClick={handleInstall} disabled={installing || missing.length === 0}>
|
||||
{installing && <span className="spinner" />}
|
||||
Install missing ({missing.length})
|
||||
</button>
|
||||
<button onClick={handleCheckUpdates}>Check for updates</button>
|
||||
<button onClick={handleScan}>Rescan system</button>
|
||||
<button onClick={() => api.configureMCP().then(() => addLog('MCP configured.', 'ok'))}>
|
||||
Configure MCP
|
||||
</button>
|
||||
<div className="dashboard-content">
|
||||
{activeSection === 'tools' && (
|
||||
<div className="dashboard-tools">
|
||||
{tools.length === 0 ? (
|
||||
<div className="empty-state">{t('dashboard.noUpdateData')}</div>
|
||||
) : (
|
||||
<div className="tools-compact">
|
||||
{tools.map((tool, i) => {
|
||||
const name = tool.name || tool.Name
|
||||
const ver = extractVersion(tool.Version || tool.version)
|
||||
return (
|
||||
<div key={i} className="tool-compact-row">
|
||||
<span className={`badge sm ${tool.installed ? 'ok' : 'error'}`}>
|
||||
{tool.installed ? '\u2713' : '\u2717'}
|
||||
</span>
|
||||
<span className="tool-compact-name">{name}</span>
|
||||
{ver && <span className="tool-compact-ver">{ver}</span>}
|
||||
{tool.installed && <span className="tool-compact-installed">{t('dashboard.installed')}</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card" style={{ flex: 1 }}>
|
||||
<div className="card-header">Updates</div>
|
||||
{updates.length === 0 ? (
|
||||
<div className="empty-state">No update data yet.</div>
|
||||
) : (
|
||||
updates.map((u, i) => (
|
||||
<div key={i} className="tool-row">
|
||||
<span className={`badge ${u.needsUpdate ? 'warn' : 'ok'}`}>
|
||||
{u.needsUpdate ? 'Update' : 'Latest'}
|
||||
</span>
|
||||
<span className="tool-name">{u.tool}</span>
|
||||
{u.needsUpdate && (
|
||||
<span style={{ color: 'var(--warning)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
|
||||
{u.current} → {u.latest}
|
||||
{activeSection === 'notifications' && (
|
||||
<div className="dashboard-notifications">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="empty-state">{t('dashboard.noUpdateData')}</div>
|
||||
) : (
|
||||
notifications.map(n => (
|
||||
<div key={n.id} className={`notif-row notif-${n.type}`}>
|
||||
<span className="notif-time">
|
||||
{n.time.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<span className="notif-text">{n.text}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.length > 0 && (
|
||||
<div className="card">
|
||||
<div className="card-header">Activity Log</div>
|
||||
{log.map(entry => (
|
||||
<div key={entry.id} style={{
|
||||
fontSize: 12,
|
||||
padding: '4px 0',
|
||||
color: entry.type === 'error' ? 'var(--error)' :
|
||||
entry.type === 'warn' ? 'var(--warning)' :
|
||||
entry.type === 'ok' ? 'var(--success)' : 'var(--text-tertiary)',
|
||||
}}>
|
||||
{entry.text}
|
||||
{activeSection === 'workflows' && (
|
||||
<div className="dashboard-workflows">
|
||||
<div className="workflow-section">
|
||||
<div className="section-label">{t('studio.workflows')}</div>
|
||||
<div className="empty-state" style={{ padding: 20 }}>
|
||||
{t('studio.noWorkflow')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="workflow-section">
|
||||
<div className="section-label">{t('studio.activeAgents')}</div>
|
||||
<div className="empty-state" style={{ padding: 20 }}>
|
||||
{t('studio.noWorkflow')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user