121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import type { AppConfig } from '../api/client';
|
||
import { api } from '../api/client';
|
||
|
||
export default function SettingsPage() {
|
||
const [config, setConfig] = useState<AppConfig | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [saving, setSaving] = useState(false);
|
||
const [message, setMessage] = useState('');
|
||
|
||
useEffect(() => {
|
||
api.getConfig().then(c => { setConfig(c); setLoading(false); }).catch(() => setLoading(false));
|
||
}, []);
|
||
|
||
async function handleSave(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
if (!config) return;
|
||
setSaving(true);
|
||
setMessage('');
|
||
try {
|
||
await api.updateConfig({
|
||
OPENCLAW_WORKSPACE: config.OPENCLAW_WORKSPACE,
|
||
GITEA_SERVER: config.GITEA_SERVER,
|
||
DEPLOYMENT_SERVER: config.DEPLOYMENT_SERVER,
|
||
DEPLOYMENT_USER: config.DEPLOYMENT_USER,
|
||
LOG_LEVEL: config.LOG_LEVEL,
|
||
});
|
||
setMessage('✅ Configuration sauvegardée');
|
||
} catch (err) {
|
||
setMessage('❌ ' + (err instanceof Error ? err.message : 'Erreur'));
|
||
}
|
||
setSaving(false);
|
||
}
|
||
|
||
if (loading || !config) {
|
||
return <div className="flex items-center justify-center h-64"><div className="text-fox-500 animate-spin-slow text-4xl">🦊</div></div>;
|
||
}
|
||
|
||
const fields: { key: keyof AppConfig; label: string; icon: string; editable: boolean; secret?: boolean }[] = [
|
||
{ key: 'OPENCLAW_WORKSPACE', label: 'Workspace OpenClaw', icon: '📁', editable: true },
|
||
{ key: 'GITEA_SERVER', label: 'Serveur Gitea', icon: '🌐', editable: true },
|
||
{ key: 'GITEA_OPENCLAW_TOKEN', label: 'Token Gitea', icon: '🔑', editable: false, secret: true },
|
||
{ key: 'DEPLOYMENT_SERVER', label: 'Serveur de déploiement', icon: '🖥️', editable: true },
|
||
{ key: 'DEPLOYMENT_USER', label: 'Utilisateur déploiement', icon: '👤', editable: true },
|
||
{ key: 'DEPLOYMENT_PWD', label: 'Mot de passe déploiement', icon: '🔒', editable: false, secret: true },
|
||
{ key: 'TELEGRAM_BOT_TOKEN', label: 'Token Telegram', icon: '🤖', editable: false, secret: true },
|
||
{ key: 'TELEGRAM_CHAT_ID', label: 'Chat ID Telegram', icon: '💬', editable: true },
|
||
{ key: 'LOG_LEVEL', label: 'Niveau de log', icon: '📊', editable: true },
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-6 animate-fade-in">
|
||
<h1 className="text-xl font-bold text-white">⚙️ Configuration</h1>
|
||
|
||
<form onSubmit={handleSave} className="glass-card p-6 space-y-4">
|
||
{message && (
|
||
<div className={`text-sm p-3 rounded-lg ${message.startsWith('✅') ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}`}>
|
||
{message}
|
||
</div>
|
||
)}
|
||
|
||
{fields.map(f => (
|
||
<div key={f.key}>
|
||
<label className="text-xs text-gray-400 uppercase tracking-wider mb-1 flex items-center gap-2">
|
||
<span>{f.icon}</span> {f.label}
|
||
{f.secret && <span className="text-fox-500 text-[10px]">(protégé)</span>}
|
||
</label>
|
||
<input
|
||
className="input"
|
||
value={config[f.key]}
|
||
onChange={e => setConfig({ ...config, [f.key]: e.target.value })}
|
||
disabled={!f.editable}
|
||
type={f.secret ? 'password' : 'text'}
|
||
/>
|
||
</div>
|
||
))}
|
||
|
||
<div className="pt-2">
|
||
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||
{saving ? '⏳ Sauvegarde...' : '💾 Sauvegarder'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
{/* Workflows info */}
|
||
<WorkflowsInfo />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function WorkflowsInfo() {
|
||
const [workflows, setWorkflows] = useState<{ type: string; label: string; steps: { agent: string; model: string }[] }[]>([]);
|
||
|
||
useEffect(() => {
|
||
api.listWorkflows().then(setWorkflows).catch(() => {});
|
||
}, []);
|
||
|
||
if (workflows.length === 0) return null;
|
||
|
||
return (
|
||
<div className="glass-card p-6">
|
||
<h2 className="text-lg font-bold text-white mb-4">🔄 Workflows disponibles</h2>
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{workflows.map(wf => (
|
||
<div key={wf.type} className="p-4 rounded-xl bg-surface-800/50">
|
||
<h3 className="text-white font-semibold mb-3">{wf.label}</h3>
|
||
<div className="flex items-center gap-1 flex-wrap">
|
||
{wf.steps.map((s, i) => (
|
||
<div key={i} className="flex items-center gap-1">
|
||
<span className="badge bg-fox-600/15 text-fox-400 text-[10px]">{s.agent}</span>
|
||
{i < wf.steps.length - 1 && <span className="text-gray-600">→</span>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|