AI Assistant c7f037c58a feat: Complete OpenClaw Dashboard with all features
- 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
2026-02-27 05:55:23 +00:00

167 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useEffect, useState, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { RefreshCw, Download, Search, Pause, Play } from 'lucide-react';
interface LogViewerProps {}
export function LogViewer() {
const [logs, setLogs] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [autoRefresh, setAutoRefresh] = useState(true);
const [filter, setFilter] = useState('');
const filteredLogsRef = useRef<string[]>([]);
const fetchLogs = async () => {
try {
const response = await fetch('/api/logs?lines=200');
const data = await response.json();
setLogs(data.logs || []);
} catch (error) {
console.error('Failed to fetch logs:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLogs();
let interval: NodeJS.Timeout;
if (autoRefresh) {
interval = setInterval(fetchLogs, 3000); // Refresh every 3s
}
return () => clearInterval(interval);
}, [autoRefresh]);
const toggleAutoRefresh = () => {
setAutoRefresh(!autoRefresh);
};
const handleDownload = () => {
const content = logs.join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `openclaw-logs-${new Date().toISOString()}.log`;
a.click();
URL.revokeObjectURL(url);
};
// Filter logs
const filteredLogs = filter
? logs.filter((log) =>
log.toLowerCase().includes(filter.toLowerCase())
)
: logs;
filteredLogsRef.current = filteredLogs;
// Parse log level for styling
const getLogStyle = (log: string) => {
if (log.toLowerCase().includes('error') || log.toLowerCase().includes('failed'))
return 'text-red-400';
if (log.toLowerCase().includes('warn'))
return 'text-yellow-400';
if (log.toLowerCase().includes('info'))
return 'text-blue-400';
return 'text-zinc-300';
};
return (
<div className="space-y-4">
{/* Controls */}
<div className="flex flex-wrap gap-2 items-center">
<Button
variant="outline"
onClick={fetchLogs}
disabled={loading}
className="border-zinc-700 hover:bg-zinc-800"
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<Button
variant={autoRefresh ? 'default' : 'outline'}
onClick={toggleAutoRefresh}
className={autoRefresh ? 'bg-orange-500 hover:bg-orange-600' : 'border-zinc-700 hover:bg-zinc-800'}
>
{autoRefresh ? (
<>
<Pause className="w-4 h-4 mr-2" />
Auto-refresh On
</>
) : (
<>
<Play className="w-4 h-4 mr-2" />
Auto-refresh Off
</>
)}
</Button>
<Button
variant="outline"
onClick={handleDownload}
className="border-zinc-700 hover:bg-zinc-800"
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<div className="flex-1 min-w-[200px]">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
<Input
placeholder="Filter logs..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="pl-10 bg-zinc-900/50 border-zinc-700 focus:border-orange-500"
/>
</div>
</div>
</div>
{/* Log Display */}
<Card className="bg-zinc-900/50 border-zinc-800">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Gateway Logs</span>
<span className="text-sm font-normal text-zinc-400">
{filteredLogs.length} entries
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[600px] overflow-auto rounded-lg bg-black/50 p-4 font-mono text-xs">
{loading && logs.length === 0 ? (
<div className="flex items-center justify-center h-full">
<RefreshCw className="w-8 h-8 animate-spin text-orange-400" />
</div>
) : filteredLogs.length === 0 ? (
<div className="flex items-center justify-center h-full text-zinc-500">
{filter ? 'No logs match your filter' : 'No logs available'}
</div>
) : (
<div className="space-y-1">
{filteredLogs.map((log, index) => (
<div
key={index}
className={`whitespace-pre-wrap break-words ${getLogStyle(log)}`}
>
{log}
</div>
))}
</div>
)}
</div>
</CardContent>
</Card>
<div className="text-sm text-zinc-500">
<p> Logs refresh automatically every 3 seconds when auto-refresh is enabled.</p>
</div>
</div>
);
}