Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| "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> | |
| ); | |
| } |