api / frontend /LocalControlPanel.tsx
gary-boon
Deploy Visualisable.ai backend with API protection
c6c8587
raw
history blame
11.5 kB
"use client";
import { useState, useEffect } from 'react';
import { Play, Pause, RefreshCw, Terminal, Activity, Wifi, WifiOff, Zap, Server, Database } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { getApiUrl, getLegacyWsUrl } from '@/lib/config';
interface ServiceStatus {
modelService: 'running' | 'stopped' | 'checking' | 'error';
websocket: 'running' | 'stopped' | 'checking' | 'error';
frontend: 'running' | 'stopped' | 'checking' | 'error';
}
interface Demo {
id: string;
name: string;
prompt: string;
description: string;
}
export function LocalControlPanel({ hideOnInspector = false }: { hideOnInspector?: boolean }) {
const [serviceStatus, setServiceStatus] = useState<ServiceStatus>({
modelService: 'checking',
websocket: 'checking',
frontend: 'running'
});
const [demos, setDemos] = useState<Demo[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [generatingDemoId, setGeneratingDemoId] = useState<string | null>(null);
const [deviceInfo, setDeviceInfo] = useState<string>('');
const [modelLoaded, setModelLoaded] = useState(false);
const [minimized, setMinimized] = useState(false);
const [shouldHide, setShouldHide] = useState(false);
// Only show in development mode
const isDevelopment = process.env.NEXT_PUBLIC_MODE === 'local' ||
process.env.NODE_ENV === 'development';
// Listen for activeView changes
useEffect(() => {
const handleViewChange = (event: CustomEvent) => {
setShouldHide(event.detail.view === 'inspector');
};
window.addEventListener('viewChanged', handleViewChange as EventListener);
return () => window.removeEventListener('viewChanged', handleViewChange as EventListener);
}, []);
useEffect(() => {
if (!isDevelopment || shouldHide) return;
const checkServices = async () => {
// Check model service
try {
const response = await fetch(`${getApiUrl()}/health`);
const data = await response.json();
setServiceStatus(prev => ({
...prev,
modelService: data.status === 'healthy' ? 'running' : 'error'
}));
setDeviceInfo(data.device || 'Unknown');
setModelLoaded(data.model_loaded || false);
} catch {
setServiceStatus(prev => ({ ...prev, modelService: 'stopped' }));
setModelLoaded(false);
}
// Check WebSocket
try {
const ws = new WebSocket(getLegacyWsUrl());
ws.onopen = () => {
setServiceStatus(prev => ({ ...prev, websocket: 'running' }));
ws.close();
};
ws.onerror = () => {
setServiceStatus(prev => ({ ...prev, websocket: 'stopped' }));
};
ws.onclose = () => {
// Connection closed event
};
} catch {
setServiceStatus(prev => ({ ...prev, websocket: 'stopped' }));
}
};
const loadDemos = async () => {
try {
const response = await fetch(`${getApiUrl()}/demos`);
const data = await response.json();
setDemos(data.demos || []);
} catch (error) {
console.error('Failed to load demos:', error);
setDemos([]);
}
};
checkServices();
loadDemos();
const interval = setInterval(checkServices, 5000); // Check every 5 seconds
return () => clearInterval(interval);
}, [isDevelopment, shouldHide]);
if (!isDevelopment || shouldHide) {
return null;
}
const runDemo = async (demoId: string) => {
if (isGenerating) return;
// Dispatch prompt variations IMMEDIATELY for PromptDiff
const demoPrompts = {
fibonacci: {
promptA: "def fibonacci(n):\n '''Calculate fibonacci number'''",
promptB: "def fibonacci(n):\n '''Calculate fibonacci number with memoization'''"
},
quicksort: {
promptA: "def quicksort(arr):\n '''Sort array using quicksort'''",
promptB: "def quicksort(arr):\n '''Sort array using optimized quicksort with pivot selection'''"
},
stack: {
promptA: "class Stack:\n '''Simple stack implementation'''",
promptB: "class Stack:\n '''Thread-safe stack implementation with size limit'''"
},
binary_search: {
promptA: "def binary_search(arr, target):\n '''Find target in sorted array'''",
promptB: "def binary_search(arr, target):\n '''Find target in sorted array using iterative approach'''"
}
};
if (demoId in demoPrompts) {
window.dispatchEvent(new CustomEvent('demo-prompts-selected', {
detail: demoPrompts[demoId as keyof typeof demoPrompts]
}));
}
// Also dispatch the primary prompt IMMEDIATELY for ConfidenceMeter
const demoPrimaryPrompts = {
fibonacci: "def fibonacci(n):\n '''Calculate fibonacci number'''",
quicksort: "def quicksort(arr):\n '''Sort array using quicksort'''",
stack: "class Stack:\n '''Simple stack implementation'''",
binary_search: "def binary_search(arr, target):\n '''Find target in sorted array'''"
};
if (demoId in demoPrimaryPrompts) {
window.dispatchEvent(new CustomEvent('demo-prompt-selected', {
detail: { prompt: demoPrimaryPrompts[demoId as keyof typeof demoPrimaryPrompts], demoId }
}));
}
// Dispatch event to indicate demo is starting (for clearing tokens)
window.dispatchEvent(new CustomEvent('demo-starting', {
detail: { demoId }
}));
setIsGenerating(true);
setGeneratingDemoId(demoId);
try {
const response = await fetch(`${getApiUrl()}/demos/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ demo_id: demoId })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Demo completed:', data);
// Dispatch custom event to notify AttentionExplorer
window.dispatchEvent(new CustomEvent('demo-completed', { detail: data }));
} catch (error) {
console.error('Failed to run demo:', error);
alert(`Failed to run demo: ${error}`);
} finally {
setIsGenerating(false);
setGeneratingDemoId(null);
}
};
if (minimized) {
return (
<div className="fixed bottom-4 right-4 bg-gray-900 border border-gray-700 rounded-lg p-2 cursor-pointer"
onClick={() => setMinimized(false)}>
<Terminal className="w-5 h-5" />
</div>
);
}
return (
<div className="fixed bottom-4 right-4 bg-gray-900 border border-gray-700 rounded-lg p-4 w-80 shadow-2xl z-50">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-bold flex items-center gap-2">
<Terminal className="w-5 h-5 text-blue-400" />
Local Development
</h3>
<button
onClick={() => setMinimized(true)}
className="text-gray-400 hover:text-white transition-colors"
>
×
</button>
</div>
{/* Service Status */}
<div className="space-y-2 mb-4">
<ServiceIndicator name="Model Service" status={serviceStatus.modelService} />
<ServiceIndicator name="WebSocket" status={serviceStatus.websocket} />
<ServiceIndicator name="Frontend" status={serviceStatus.frontend} />
</div>
{/* Device Info */}
{deviceInfo && (
<div className="mb-4 p-2 bg-gray-800 rounded text-xs">
<div className="flex items-center gap-2">
<Server className="w-3 h-3 text-gray-400" />
<span className="text-gray-400">Device:</span>
<span className="text-white">{deviceInfo}</span>
</div>
<div className="flex items-center gap-2 mt-1">
<Database className="w-3 h-3 text-gray-400" />
<span className="text-gray-400">Model:</span>
<span className={modelLoaded ? "text-green-400" : "text-yellow-400"}>
{modelLoaded ? "Loaded" : "Loading..."}
</span>
</div>
</div>
)}
{/* Quick Actions */}
<div className="space-y-2 mb-4">
<button
onClick={() => {
// checkServices and loadDemos are auto-refreshed
// Manual refresh removed to fix scope issue
}}
className="w-full px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded flex items-center justify-center gap-2 transition-colors"
>
<RefreshCw className="w-4 h-4" />
Refresh Status
</button>
</div>
{/* Demo Runners */}
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-400">Run Demos</h4>
{demos.length > 0 ? (
demos.map(demo => {
const isThisDemoGenerating = generatingDemoId === demo.id;
return (
<button
key={demo.id}
onClick={() => runDemo(demo.id)}
disabled={isGenerating || serviceStatus.modelService !== 'running'}
className={`w-full px-3 py-2 rounded text-sm flex items-center gap-2 transition-colors ${
isThisDemoGenerating
? 'bg-blue-700 cursor-wait'
: isGenerating
? 'bg-gray-700 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
} ${serviceStatus.modelService !== 'running' ? 'disabled:bg-gray-700 disabled:cursor-not-allowed' : ''}`}
>
{isThisDemoGenerating ? (
<>
<RefreshCw className="w-3 h-3 animate-spin" />
<span>Generating code...</span>
</>
) : (
<>
<Play className="w-3 h-3" />
<span>{demo.name}</span>
</>
)}
</button>
);
})
) : (
<div className="text-xs text-gray-500 text-center py-2">
No demos available
</div>
)}
</div>
{/* Generation Status */}
{isGenerating && (
<div className="mt-4 p-2 bg-blue-900/30 border border-blue-700 rounded">
<div className="flex items-center gap-2 text-sm text-blue-400">
<Zap className="w-4 h-4 animate-pulse" />
Generating code...
</div>
</div>
)}
</div>
);
}
function ServiceIndicator({ name, status }: { name: string; status: string }) {
const colors = {
running: 'text-green-400',
stopped: 'text-red-400',
checking: 'text-yellow-400',
error: 'text-red-400'
};
const icons = {
running: <Wifi className="w-3 h-3 inline mr-1" />,
stopped: <WifiOff className="w-3 h-3 inline mr-1" />,
checking: <Activity className="w-3 h-3 inline mr-1 animate-pulse" />,
error: <WifiOff className="w-3 h-3 inline mr-1" />
};
return (
<div className="flex items-center justify-between p-2 bg-gray-800 rounded">
<span className="text-sm">{name}</span>
<span className={`text-xs ${colors[status as keyof typeof colors]} flex items-center`}>
{icons[status as keyof typeof icons]}
{status}
</span>
</div>
);
}