- Dashboard Overview with real-time status display - Live Log Viewer (scrollable, filterable) - Config Editor with JSON syntax highlighting - Model Switcher for provider management - Provider Manager for API key configuration - Quick Actions for common tasks - API Routes: status, logs, config, actions, models, providers Tech Stack: - Next.js 14 (App Router) - TypeScript - Tailwind CSS - shadcn/ui components - CodeMirror for JSON editing - Docker support with docker-compose
225 lines
6.5 KiB
TypeScript
225 lines
6.5 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||
import { Save, Download, Upload, RefreshCw, Check, X } from 'lucide-react';
|
||
import CodeMirror from '@uiw/react-codemirror';
|
||
import { json } from '@codemirror/lang-json';
|
||
import { oneDark } from '@codemirror/theme-one-dark';
|
||
|
||
export function ConfigEditor() {
|
||
const [config, setConfig] = useState<any>(null);
|
||
const [editing, setEditing] = useState(false);
|
||
const [editValue, setEditValue] = useState('');
|
||
const [saving, setSaving] = useState(false);
|
||
const [loading, setLoading] = useState(true);
|
||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||
|
||
const fetchConfig = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await fetch('/api/config');
|
||
const data = await response.json();
|
||
setConfig(data);
|
||
setEditValue(JSON.stringify(data, null, 2));
|
||
} catch (error) {
|
||
setMessage({ type: 'error', text: 'Failed to load config' });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchConfig();
|
||
}, []);
|
||
|
||
const handleSave = async () => {
|
||
setSaving(true);
|
||
setMessage(null);
|
||
|
||
try {
|
||
// Validate JSON
|
||
const parsed = JSON.parse(editValue);
|
||
|
||
const response = await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(parsed),
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
setMessage({ type: 'success', text: result.message || 'Config saved successfully!' });
|
||
setConfig(parsed);
|
||
setEditing(false);
|
||
} else {
|
||
setMessage({ type: 'error', text: result.message || 'Failed to save config' });
|
||
}
|
||
} catch (error) {
|
||
setMessage({ type: 'error', text: 'Invalid JSON format' });
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
setEditValue(JSON.stringify(config, null, 2));
|
||
setEditing(false);
|
||
setMessage(null);
|
||
};
|
||
|
||
const handleDownload = () => {
|
||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'openclaw-config.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (event) => {
|
||
try {
|
||
const content = event.target?.result as string;
|
||
JSON.parse(content); // Validate
|
||
setEditValue(content);
|
||
setEditing(true);
|
||
setMessage(null);
|
||
} catch {
|
||
setMessage({ type: 'error', text: 'Invalid JSON file' });
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center h-64">
|
||
<RefreshCw className="w-8 h-8 animate-spin text-orange-400" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* Actions */}
|
||
<div className="flex flex-wrap gap-2 items-center">
|
||
<Button
|
||
onClick={() => setEditing(true)}
|
||
disabled={editing}
|
||
className="bg-orange-500 hover:bg-orange-600"
|
||
>
|
||
Edit Config
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
onClick={fetchConfig}
|
||
disabled={loading}
|
||
className="border-zinc-700 hover:bg-zinc-800"
|
||
>
|
||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||
Reload
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleDownload}
|
||
className="border-zinc-700 hover:bg-zinc-800"
|
||
>
|
||
<Download className="w-4 h-4 mr-2" />
|
||
Export
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => document.getElementById('config-upload')?.click()}
|
||
className="border-zinc-700 hover:bg-zinc-800"
|
||
>
|
||
<Upload className="w-4 h-4 mr-2" />
|
||
Import
|
||
</Button>
|
||
<input
|
||
id="config-upload"
|
||
type="file"
|
||
accept=".json"
|
||
onChange={handleUpload}
|
||
className="hidden"
|
||
/>
|
||
</div>
|
||
|
||
{/* Message */}
|
||
{message && (
|
||
<Alert variant={message.type === 'error' ? 'destructive' : 'default'} className={
|
||
message.type === 'success' ? 'bg-green-500/10 border-green-500/20 text-green-400' : ''
|
||
}>
|
||
{message.type === 'success' ? (
|
||
<Check className="h-4 w-4" />
|
||
) : (
|
||
<X className="h-4 w-4" />
|
||
)}
|
||
<AlertDescription>{message.text}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* Editor */}
|
||
<Card className="bg-zinc-900/50 border-zinc-800">
|
||
<CardHeader>
|
||
<CardTitle>openclaw.json</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="rounded-lg overflow-hidden border border-zinc-700">
|
||
<CodeMirror
|
||
value={editValue}
|
||
height="600px"
|
||
theme={oneDark}
|
||
extensions={[json()]}
|
||
editable={editing}
|
||
onChange={setEditValue}
|
||
className="text-sm"
|
||
/>
|
||
</div>
|
||
{editing && (
|
||
<div className="flex justify-end gap-2 mt-4">
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleCancel}
|
||
disabled={saving}
|
||
className="border-zinc-700 hover:bg-zinc-800"
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
className="bg-green-600 hover:bg-green-700"
|
||
>
|
||
{saving ? (
|
||
<>
|
||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||
Saving...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Save className="w-4 h-4 mr-2" />
|
||
Save & Apply
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<div className="text-sm text-zinc-500">
|
||
<p>⚠️ Warning: Editing the config directly restarts the Gateway to apply changes.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|