chowdary1's picture
Fix: Remove ALL localhost:8000 hardcoded URLs - use relative paths for production
91be275
Raw History Blame Contribute Delete
20.8 kB
/**
* OpenAI API Logs Panel Component
*
* Simple, clean panel to monitor OpenAI API calls:
* - Real-time log viewer (last 100 calls from memory)
* - Token usage statistics
* - Response time tracking
* - Operation breakdown
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
Zap, RefreshCw, Clock, Brain, Trash2,
Check, XCircle, AlertTriangle, BarChart3,
Activity, Hash, Cpu, TrendingUp
} from 'lucide-react';
import { getToken } from '../../services/api';
const API_BASE = import.meta.env.VITE_API_URL || '';
// Status badge
const StatusBadge = ({ status }) => {
const isSuccess = status === 'success';
return (
<span style={{
backgroundColor: isSuccess ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)',
color: isSuccess ? '#10b981' : '#ef4444',
padding: '4px 10px',
borderRadius: '12px',
fontSize: '0.75rem',
fontWeight: '600',
display: 'inline-flex',
alignItems: 'center',
gap: '4px'
}}>
{isSuccess ? <Check size={12} /> : <XCircle size={12} />}
{status}
</span>
);
};
// Response time indicator
const ResponseTimeIndicator = ({ ms }) => {
if (!ms) return <span style={{ color: '#6b7280' }}>-</span>;
let color;
if (ms < 1000) color = '#10b981';
else if (ms < 3000) color = '#f59e0b';
else color = '#ef4444';
return (
<span style={{ color, fontWeight: '600', fontFamily: 'monospace' }}>
{ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`}
</span>
);
};
function OpenAILogsPanel() {
const [logs, setLogs] = useState([]);
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [autoRefresh, setAutoRefresh] = useState(true);
// Fetch logs
const fetchLogs = useCallback(async () => {
setLoading(true);
setError(null);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/openai-logs?limit=50`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setLogs(data.logs || []);
setStats(data.stats || null);
}
} else {
throw new Error('Failed to fetch OpenAI logs');
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
// Clear logs
const clearLogs = async () => {
if (!window.confirm('Clear all OpenAI API logs from memory?')) return;
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/openai-logs`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setLogs([]);
setStats(null);
}
} catch (err) {
setError(err.message);
}
};
useEffect(() => {
fetchLogs();
}, [fetchLogs]);
// Auto-refresh every 5 seconds
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(fetchLogs, 5000);
return () => clearInterval(interval);
}, [autoRefresh, fetchLogs]);
// Format timestamp
const formatTime = (isoString) => {
if (!isoString) return '-';
const date = new Date(isoString);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
return (
<div className="openai-logs-panel">
{/* Header */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '1.5rem',
padding: '1rem 1.5rem',
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(59, 130, 246, 0.1))',
borderRadius: '12px',
border: '1px solid rgba(16, 185, 129, 0.2)'
}}>
<div>
<h2 style={{
margin: 0,
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
color: '#f3f4f6'
}}>
<Brain size={24} style={{ color: '#10b981' }} />
OpenAI API Logs
</h2>
<p style={{ margin: '0.5rem 0 0', color: '#9ca3af', fontSize: '0.9rem' }}>
Real-time monitoring • Last 100 calls (in-memory)
</p>
</div>
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
cursor: 'pointer',
fontSize: '0.85rem',
color: '#9ca3af'
}}>
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
style={{ accentColor: '#10b981' }}
/>
Auto-refresh
</label>
<button onClick={fetchLogs} style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 1rem',
background: 'rgba(59, 130, 246, 0.2)',
border: '1px solid rgba(59, 130, 246, 0.3)',
borderRadius: '8px',
color: '#60a5fa',
cursor: 'pointer',
fontSize: '0.85rem',
fontWeight: '500'
}}>
<RefreshCw size={14} className={loading ? 'spinning' : ''} />
Refresh
</button>
<button onClick={clearLogs} style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 1rem',
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '8px',
color: '#ef4444',
cursor: 'pointer',
fontSize: '0.85rem',
fontWeight: '500'
}}>
<Trash2 size={14} />
Clear
</button>
</div>
</div>
{/* Stats Cards */}
{stats && (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
gap: '1rem',
marginBottom: '1.5rem'
}}>
<div style={{
padding: '1rem',
background: 'rgba(59, 130, 246, 0.1)',
borderRadius: '12px',
border: '1px solid rgba(59, 130, 246, 0.2)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
<Activity size={16} style={{ color: '#60a5fa' }} />
<span style={{ color: '#9ca3af', fontSize: '0.8rem' }}>Total Calls</span>
</div>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: '#f3f4f6' }}>
{stats.total_calls || 0}
</span>
</div>
<div style={{
padding: '1rem',
background: 'rgba(16, 185, 129, 0.1)',
borderRadius: '12px',
border: '1px solid rgba(16, 185, 129, 0.2)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
<TrendingUp size={16} style={{ color: '#10b981' }} />
<span style={{ color: '#9ca3af', fontSize: '0.8rem' }}>Success Rate</span>
</div>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: '#10b981' }}>
{stats.success_rate || 100}%
</span>
</div>
<div style={{
padding: '1rem',
background: 'rgba(139, 92, 246, 0.1)',
borderRadius: '12px',
border: '1px solid rgba(139, 92, 246, 0.2)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
<Hash size={16} style={{ color: '#a78bfa' }} />
<span style={{ color: '#9ca3af', fontSize: '0.8rem' }}>Total Tokens</span>
</div>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: '#f3f4f6' }}>
{(stats.total_tokens || 0).toLocaleString()}
</span>
</div>
<div style={{
padding: '1rem',
background: 'rgba(245, 158, 11, 0.1)',
borderRadius: '12px',
border: '1px solid rgba(245, 158, 11, 0.2)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
<Clock size={16} style={{ color: '#f59e0b' }} />
<span style={{ color: '#9ca3af', fontSize: '0.8rem' }}>Avg Response</span>
</div>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: '#f3f4f6' }}>
{stats.avg_response_time_ms || 0}ms
</span>
</div>
</div>
)}
{/* Operations Breakdown */}
{stats?.operations && Object.keys(stats.operations).length > 0 && (
<div style={{
padding: '1rem',
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.1)',
marginBottom: '1.5rem'
}}>
<h4 style={{ margin: '0 0 0.75rem', color: '#9ca3af', fontSize: '0.85rem' }}>
<Cpu size={14} style={{ marginRight: '0.5rem', verticalAlign: 'middle' }} />
Operations Breakdown
</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
{Object.entries(stats.operations).map(([op, count]) => (
<span key={op} style={{
padding: '0.25rem 0.75rem',
background: 'rgba(59, 130, 246, 0.2)',
borderRadius: '16px',
fontSize: '0.8rem',
color: '#93c5fd'
}}>
{op}: <strong>{count}</strong>
</span>
))}
</div>
</div>
)}
{/* Error display */}
{error && (
<div style={{
padding: '1rem',
backgroundColor: 'rgba(239, 68, 68, 0.1)',
borderRadius: '8px',
border: '1px solid rgba(239, 68, 68, 0.3)',
color: '#ef4444',
marginBottom: '1rem',
display: 'flex',
alignItems: 'center',
gap: '0.5rem'
}}>
<AlertTriangle size={16} />
{error}
</div>
)}
{/* Logs Table */}
<div style={{
background: 'rgba(0, 0, 0, 0.3)',
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.1)',
overflow: 'hidden'
}}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
fontSize: '0.85rem'
}}>
<thead>
<tr style={{
background: 'rgba(0, 0, 0, 0.4)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)'
}}>
<th style={{ padding: '0.75rem 1rem', textAlign: 'left', color: '#9ca3af', fontWeight: '600' }}>Time</th>
<th style={{ padding: '0.75rem 1rem', textAlign: 'left', color: '#9ca3af', fontWeight: '600' }}>Operation</th>
<th style={{ padding: '0.75rem 1rem', textAlign: 'left', color: '#9ca3af', fontWeight: '600' }}>Model</th>
<th style={{ padding: '0.75rem 1rem', textAlign: 'center', color: '#9ca3af', fontWeight: '600' }}>Tokens</th>
<th style={{ padding: '0.75rem 1rem', textAlign: 'center', color: '#9ca3af', fontWeight: '600' }}>Time</th>
<th style={{ padding: '0.75rem 1rem', textAlign: 'center', color: '#9ca3af', fontWeight: '600' }}>Status</th>
</tr>
</thead>
<tbody>
{logs.length === 0 ? (
<tr>
<td colSpan="6" style={{
padding: '3rem',
textAlign: 'center',
color: '#6b7280'
}}>
<Brain size={32} style={{ opacity: 0.5, marginBottom: '0.5rem' }} />
<p style={{ margin: 0 }}>No API calls logged yet</p>
<p style={{ margin: '0.25rem 0 0', fontSize: '0.8rem' }}>
Logs appear here when the app makes OpenAI API calls
</p>
</td>
</tr>
) : (
logs.map((log, idx) => (
<tr key={idx} style={{
borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
background: log.status === 'error' ? 'rgba(239, 68, 68, 0.05)' : 'transparent'
}}>
<td style={{ padding: '0.75rem 1rem', color: '#9ca3af', fontFamily: 'monospace' }}>
{formatTime(log.timestamp)}
</td>
<td style={{ padding: '0.75rem 1rem', color: '#f3f4f6', fontWeight: '500' }}>
{log.operation}
</td>
<td style={{ padding: '0.75rem 1rem' }}>
<span style={{
padding: '2px 8px',
background: 'rgba(16, 185, 129, 0.15)',
borderRadius: '6px',
color: '#10b981',
fontSize: '0.8rem',
fontFamily: 'monospace'
}}>
{log.model}
</span>
</td>
<td style={{ padding: '0.75rem 1rem', textAlign: 'center', color: '#a78bfa', fontFamily: 'monospace' }}>
{log.total_tokens ? (
<span title={`Prompt: ${log.prompt_tokens || '?'} | Completion: ${log.completion_tokens || '?'}`}>
{log.total_tokens.toLocaleString()}
</span>
) : '-'}
</td>
<td style={{ padding: '0.75rem 1rem', textAlign: 'center' }}>
<ResponseTimeIndicator ms={log.response_time_ms} />
</td>
<td style={{ padding: '0.75rem 1rem', textAlign: 'center' }}>
<StatusBadge status={log.status} />
{log.error_message && (
<div style={{
fontSize: '0.75rem',
color: '#ef4444',
marginTop: '0.25rem',
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}} title={log.error_message}>
{log.error_message}
</div>
)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Footer Info */}
<div style={{
marginTop: '1rem',
padding: '0.75rem 1rem',
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: '8px',
fontSize: '0.8rem',
color: '#6b7280',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span>
📊 Showing {logs.length} of last 100 calls (stored in-memory)
</span>
<span>
{autoRefresh && '🔄 Auto-refreshing every 5s'}
</span>
</div>
{/* Spinner styles */}
<style>{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinning {
animation: spin 1s linear infinite;
}
`}</style>
</div>
);
}
export default OpenAILogsPanel;