Spaces:
Running
Running
Download src/components/ComputerUseStudio.tsx from Aura-Studio/AI: direct link, hf CLI and curl.
- Browser
- Download file 31.6 kB
-
https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/components/ComputerUseStudio.tsx
- Command line
-
hf download hf://spaces/Aura-Studio/AI/src/components/ComputerUseStudio.tsx
-
curl -L -o ComputerUseStudio.tsx https://huggingface.co/spaces/Aura-Studio/AI/resolve/main/src/components/ComputerUseStudio.tsx
31.6 kB
| import React, { useState, useEffect, useRef } from "react"; | |
| import { | |
| Monitor, | |
| MousePointer, | |
| Play, | |
| Pause, | |
| RotateCcw, | |
| CheckCircle2, | |
| AlertCircle, | |
| Eye, | |
| Terminal, | |
| Sliders, | |
| Layers, | |
| Crosshair, | |
| Sparkles, | |
| X, | |
| Maximize2, | |
| Copy, | |
| Download, | |
| Square, | |
| CheckSquare, | |
| ChevronRight, | |
| Activity, | |
| Video, | |
| VideoOff, | |
| Cpu, | |
| CornerDownLeft, | |
| ShieldAlert | |
| } from "lucide-react"; | |
| interface ComputerUseStudioProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onSendToChat?: (text: string) => void; | |
| } | |
| interface ActionStep { | |
| id: string; | |
| stepNumber: number; | |
| type: "VISION_SCAN" | "MOVE_CURSOR" | "CLICK" | "TYPE_TEXT" | "KEY_PRESS" | "SCROLL" | "WAIT" | "COMPLETE"; | |
| description: string; | |
| targetElement?: string; | |
| coords?: { x: number; y: number }; | |
| textInput?: string; | |
| status: "pending" | "running" | "completed" | "failed"; | |
| timestamp: string; | |
| screenshotUrl?: string; | |
| } | |
| interface DetectedElement { | |
| id: string; | |
| label: string; | |
| type: "button" | "input" | "menu" | "link" | "image"; | |
| coords: { x: number; y: number; w: number; h: number }; | |
| } | |
| export default function ComputerUseStudio({ isOpen, onClose, onSendToChat }: ComputerUseStudioProps) { | |
| const [selectedPreset, setSelectedPreset] = useState<"checkout" | "vscode" | "dashboard" | "custom">("checkout"); | |
| const [taskPrompt, setTaskPrompt] = useState<string>("Navigate to customer profile, update email to 'alex.dev@aurastudio.ai', and click Save Settings."); | |
| const [isRunning, setIsRunning] = useState<boolean>(false); | |
| const [currentStepIndex, setCurrentStepIndex] = useState<number>(-1); | |
| const [cursorPos, setCursorPos] = useState<{ x: number; y: number }>({ x: 50, y: 50 }); | |
| const [showClickRipple, setShowClickRipple] = useState<boolean>(false); | |
| const [speed, setSpeed] = useState<number>(1); // 1x speed | |
| const [autoApproveSteps, setAutoApproveSteps] = useState<boolean>(true); | |
| const [showVisionOverlay, setShowVisionOverlay] = useState<boolean>(true); | |
| const [capturedStream, setCapturedStream] = useState<MediaStream | null>(null); | |
| const [logs, setLogs] = useState<string[]>([]); | |
| const videoRef = useRef<HTMLVideoElement | null>(null); | |
| // Sample OS Screen Presets with bounding elements | |
| const presets = { | |
| checkout: { | |
| title: "E-Commerce Customer Portal", | |
| bgGradient: "from-slate-900 to-indigo-950", | |
| elements: [ | |
| { id: "e1", label: "Email Address Input", type: "input", coords: { x: 320, y: 180, w: 260, h: 42 } }, | |
| { id: "e2", label: "Account Role Selector", type: "menu", coords: { x: 320, y: 240, w: 260, h: 42 } }, | |
| { id: "e3", label: "Save Settings Button", type: "button", coords: { x: 320, y: 310, w: 140, h: 40 } }, | |
| { id: "e4", label: "Export PDF Invoice", type: "button", coords: { x: 480, y: 310, w: 130, h: 40 } }, | |
| { id: "e5", label: "Navigation Profile Tab", type: "link", coords: { x: 80, y: 120, w: 160, h: 36 } }, | |
| ] as DetectedElement[] | |
| }, | |
| vscode: { | |
| title: "VS Code IDE — server.ts (Line 42 Debug)", | |
| bgGradient: "from-slate-950 via-slate-900 to-slate-950", | |
| elements: [ | |
| { id: "e1", label: "Line 42 Error Marker", type: "image", coords: { x: 240, y: 220, w: 320, h: 28 } }, | |
| { id: "e2", label: "Fix Quick Action Lightbulb", type: "button", coords: { x: 580, y: 220, w: 36, h: 36 } }, | |
| { id: "e3", label: "Terminal Console Tab", type: "button", coords: { x: 180, y: 380, w: 110, h: 32 } }, | |
| { id: "e4", label: "Run & Debug Icon", type: "button", coords: { x: 20, y: 160, w: 42, h: 42 } }, | |
| ] as DetectedElement[] | |
| }, | |
| dashboard: { | |
| title: "Aura Cloud Metrics & Analytics", | |
| bgGradient: "from-slate-900 via-purple-950 to-slate-950", | |
| elements: [ | |
| { id: "e1", label: "Date Range Filter Dropdown", type: "menu", coords: { x: 520, y: 90, w: 160, h: 36 } }, | |
| { id: "e2", label: "Generate Live Report Button", type: "button", coords: { x: 700, y: 90, w: 150, h: 36 } }, | |
| { id: "e3", label: "Quota Gauge Graph", type: "image", coords: { x: 80, y: 160, w: 380, h: 200 } }, | |
| ] as DetectedElement[] | |
| }, | |
| custom: { | |
| title: "Captured Live Screen Stream", | |
| bgGradient: "from-slate-900 to-black", | |
| elements: [ | |
| { id: "e1", label: "Active Window Target", type: "button", coords: { x: 200, y: 150, w: 300, h: 100 } } | |
| ] as DetectedElement[] | |
| } | |
| }; | |
| const activePresetData = presets[selectedPreset]; | |
| // Generated step plan based on current prompt | |
| const stepsPlan: ActionStep[] = [ | |
| { | |
| id: "s1", | |
| stepNumber: 1, | |
| type: "VISION_SCAN", | |
| description: "Visual Scan & OCR: Analyzing desktop viewport (1920x1080) for interactable UI targets.", | |
| status: "pending", | |
| timestamp: "00:01" | |
| }, | |
| { | |
| id: "s2", | |
| stepNumber: 2, | |
| type: "MOVE_CURSOR", | |
| description: "Trajectory Navigation: Moving cursor towards 'Email Address Input' at (x: 320, y: 180).", | |
| coords: { x: 320, y: 180 }, | |
| status: "pending", | |
| timestamp: "00:03" | |
| }, | |
| { | |
| id: "s3", | |
| stepNumber: 3, | |
| type: "CLICK", | |
| description: "Mouse Action: Left click to focus email text input element.", | |
| targetElement: "Email Address Input", | |
| coords: { x: 320, y: 180 }, | |
| status: "pending", | |
| timestamp: "00:04" | |
| }, | |
| { | |
| id: "s4", | |
| stepNumber: 4, | |
| type: "TYPE_TEXT", | |
| description: "Keyboard Operator: Typing string 'alex.dev@aurastudio.ai' with standard human latency.", | |
| textInput: "alex.dev@aurastudio.ai", | |
| status: "pending", | |
| timestamp: "00:06" | |
| }, | |
| { | |
| id: "s5", | |
| stepNumber: 5, | |
| type: "MOVE_CURSOR", | |
| description: "Trajectory Navigation: Re-positioning cursor to 'Save Settings Button' at (x: 320, y: 310).", | |
| coords: { x: 320, y: 310 }, | |
| status: "pending", | |
| timestamp: "00:08" | |
| }, | |
| { | |
| id: "s6", | |
| stepNumber: 6, | |
| type: "CLICK", | |
| description: "Mouse Action: Primary click 'Save Settings' to commit desktop state change.", | |
| targetElement: "Save Settings Button", | |
| coords: { x: 320, y: 310 }, | |
| status: "pending", | |
| timestamp: "00:09" | |
| }, | |
| { | |
| id: "s7", | |
| stepNumber: 7, | |
| type: "COMPLETE", | |
| description: "Execution Finished: Desktop task completed successfully. Form updated and committed.", | |
| status: "pending", | |
| timestamp: "00:10" | |
| } | |
| ]; | |
| const [steps, setSteps] = useState<ActionStep[]>(stepsPlan); | |
| // Start real screen capture | |
| const handleStartScreenCapture = async () => { | |
| try { | |
| const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); | |
| setCapturedStream(stream); | |
| setSelectedPreset("custom"); | |
| if (videoRef.current) { | |
| videoRef.current.srcObject = stream; | |
| videoRef.current.play(); | |
| } | |
| addLog("Live screen display media capture initialized."); | |
| } catch (err: any) { | |
| console.error("Screen capture error:", err); | |
| addLog("Screen capture cancelled or unavailable: " + err.message); | |
| } | |
| }; | |
| const handleStopScreenCapture = () => { | |
| if (capturedStream) { | |
| capturedStream.getTracks().forEach(track => track.stop()); | |
| setCapturedStream(null); | |
| setSelectedPreset("checkout"); | |
| addLog("Stopped screen display capture."); | |
| } | |
| }; | |
| const addLog = (msg: string) => { | |
| setLogs(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev.slice(0, 30)]); | |
| }; | |
| // Automated execution simulator loop | |
| useEffect(() => { | |
| let timer: NodeJS.Timeout; | |
| if (isRunning && currentStepIndex < steps.length - 1) { | |
| const nextIdx = currentStepIndex + 1; | |
| const step = steps[nextIdx]; | |
| // Delay based on speed setting | |
| const delay = Math.max(400, 1500 / speed); | |
| timer = setTimeout(() => { | |
| setCurrentStepIndex(nextIdx); | |
| setSteps(prevSteps => | |
| prevSteps.map((s, idx) => ({ | |
| ...s, | |
| status: idx < nextIdx ? "completed" : idx === nextIdx ? "running" : "pending" | |
| })) | |
| ); | |
| addLog(`Executing Step ${step.stepNumber}: ${step.type} - ${step.description}`); | |
| if (step.coords) { | |
| setCursorPos(step.coords); | |
| } | |
| if (step.type === "CLICK") { | |
| setShowClickRipple(true); | |
| setTimeout(() => setShowClickRipple(false), 500); | |
| } | |
| if (step.type === "COMPLETE") { | |
| setIsRunning(false); | |
| setSteps(prevSteps => prevSteps.map(s => ({ ...s, status: "completed" }))); | |
| addLog("Computer Use Operator finished task successfully!"); | |
| } | |
| }, delay); | |
| } | |
| return () => clearTimeout(timer); | |
| }, [isRunning, currentStepIndex, speed, steps]); | |
| const handleStartOperator = () => { | |
| setSteps(stepsPlan.map(s => ({ ...s, status: "pending" }))); | |
| setCurrentStepIndex(-1); | |
| setIsRunning(true); | |
| addLog("Computer Use Operator initiated goal: " + taskPrompt); | |
| }; | |
| const handleResetOperator = () => { | |
| setIsRunning(false); | |
| setCurrentStepIndex(-1); | |
| setCursorPos({ x: 50, y: 50 }); | |
| setSteps(stepsPlan.map(s => ({ ...s, status: "pending" }))); | |
| addLog("Reset computer use state."); | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-3 md:p-6 bg-slate-950/85 backdrop-blur-md animate-fade-in font-sans"> | |
| <div | |
| className="relative w-full max-w-6xl max-h-[92vh] bg-slate-900 text-slate-100 rounded-3xl shadow-2xl border border-slate-700/80 flex flex-col overflow-hidden" | |
| onClick={e => e.stopPropagation()} | |
| > | |
| {/* Header Bar */} | |
| <div className="bg-gradient-to-r from-slate-950 via-slate-900 to-indigo-950 p-5 border-b border-slate-800 shrink-0 flex items-center justify-between gap-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="h-11 w-11 rounded-2xl bg-indigo-500/20 border border-indigo-500/40 flex items-center justify-center text-indigo-400 shrink-0 shadow-inner"> | |
| <Monitor className="h-6 w-6" /> | |
| </div> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <h3 className="text-lg font-black tracking-tight text-white">Computer Use OS Operator</h3> | |
| <span className="px-2.5 py-0.5 rounded-full bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 text-[10px] font-mono font-bold flex items-center gap-1"> | |
| <Cpu className="h-3 w-3" /> | |
| Vision Operator | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400"> | |
| Direct desktop vision, cursor trajectory movement, keyboard typing, and window automation | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="h-8 w-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-300 flex items-center justify-center transition-colors cursor-pointer shrink-0" | |
| > | |
| <X className="h-4.5 w-4.5" /> | |
| </button> | |
| </div> | |
| {/* Main Content Layout */} | |
| <div className="flex-1 overflow-y-auto grid grid-cols-1 lg:grid-cols-12 gap-0 divide-y lg:divide-y-0 lg:divide-x divide-slate-800"> | |
| {/* Left Column: Screen Viewport & Live Simulation Canvas (7 Cols) */} | |
| <div className="lg:col-span-7 p-4 md:p-5 flex flex-col gap-4 bg-slate-950/60"> | |
| {/* Viewport Toolbar */} | |
| <div className="flex items-center justify-between gap-2 flex-wrap"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs font-bold text-slate-300">Screen Viewport:</span> | |
| <select | |
| value={selectedPreset} | |
| onChange={e => { | |
| setSelectedPreset(e.target.value as any); | |
| if (e.target.value !== "custom") handleStopScreenCapture(); | |
| }} | |
| className="px-3 py-1.5 rounded-xl bg-slate-800 border border-slate-700 text-xs font-semibold text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer" | |
| > | |
| <option value="checkout">Preset 1: E-Commerce Form</option> | |
| <option value="vscode">Preset 2: VS Code Debugger</option> | |
| <option value="dashboard">Preset 3: Aura Analytics</option> | |
| {capturedStream && <option value="custom">Preset 4: Live Desktop Capture</option>} | |
| </select> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {capturedStream ? ( | |
| <button | |
| onClick={handleStopScreenCapture} | |
| className="px-3 py-1.5 rounded-xl bg-red-600/30 hover:bg-red-600 text-red-200 border border-red-500/40 text-xs font-bold flex items-center gap-1.5 transition cursor-pointer" | |
| > | |
| <VideoOff className="h-3.5 w-3.5" /> | |
| <span>Stop Share</span> | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={handleStartScreenCapture} | |
| className="px-3 py-1.5 rounded-xl bg-indigo-600/30 hover:bg-indigo-600 text-indigo-200 border border-indigo-500/40 text-xs font-bold flex items-center gap-1.5 transition cursor-pointer" | |
| title="Capture your real desktop window with WebRTC" | |
| > | |
| <Video className="h-3.5 w-3.5 text-indigo-400" /> | |
| <span>Share Screen</span> | |
| </button> | |
| )} | |
| <button | |
| onClick={() => setShowVisionOverlay(!showVisionOverlay)} | |
| className={`px-3 py-1.5 rounded-xl text-xs font-bold flex items-center gap-1.5 border transition cursor-pointer ${ | |
| showVisionOverlay | |
| ? "bg-emerald-500/20 text-emerald-300 border-emerald-500/40" | |
| : "bg-slate-800 text-slate-400 border-slate-700" | |
| }`} | |
| > | |
| <Crosshair className="h-3.5 w-3.5" /> | |
| <span>Vision Overlay</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Desktop Screen Canvas Display */} | |
| <div className="relative w-full aspect-video rounded-2xl bg-gradient-to-br from-slate-900 to-black border border-slate-700/80 overflow-hidden shadow-2xl flex flex-col select-none"> | |
| {/* Virtual OS Title Bar */} | |
| <div className="h-8 bg-slate-950 border-b border-slate-800 px-3 flex items-center justify-between text-xs text-slate-400 shrink-0"> | |
| <div className="flex items-center gap-1.5"> | |
| <div className="h-2.5 w-2.5 rounded-full bg-red-500" /> | |
| <div className="h-2.5 w-2.5 rounded-full bg-amber-500" /> | |
| <div className="h-2.5 w-2.5 rounded-full bg-emerald-500" /> | |
| <span className="ml-2 font-mono text-[11px] text-slate-300 truncate max-w-[240px]"> | |
| {activePresetData.title} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2 font-mono text-[10px]"> | |
| <span>RES: 1920x1080</span> | |
| <span className="text-emerald-400">FPS: 60</span> | |
| </div> | |
| </div> | |
| {/* Viewport Canvas Body */} | |
| <div className={`relative flex-1 p-6 bg-gradient-to-br ${activePresetData.bgGradient} overflow-hidden`}> | |
| {/* Live Media Stream Video if screen capture active */} | |
| {selectedPreset === "custom" && ( | |
| <video | |
| ref={videoRef} | |
| className="absolute inset-0 w-full h-full object-contain bg-black" | |
| autoPlay | |
| muted | |
| /> | |
| )} | |
| {/* Preset 1 Mock Form UI */} | |
| {selectedPreset === "checkout" && ( | |
| <div className="max-w-md mx-auto bg-slate-900/90 border border-slate-700/80 rounded-2xl p-5 shadow-xl text-slate-200 flex flex-col gap-3"> | |
| <h4 className="text-sm font-bold text-white flex items-center justify-between"> | |
| <span>Customer Settings</span> | |
| <span className="text-[10px] px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300">Live Form</span> | |
| </h4> | |
| <div> | |
| <label className="text-[11px] font-mono text-slate-400 block mb-1">Email Address</label> | |
| <input | |
| type="text" | |
| readOnly | |
| value={currentStepIndex >= 3 ? "alex.dev@aurastudio.ai" : "user@example.com"} | |
| className={`w-full px-3 py-2 rounded-xl bg-slate-950 border text-xs font-mono transition-all ${ | |
| currentStepIndex === 3 ? "border-amber-400 ring-2 ring-amber-400/50 bg-amber-950/30" : "border-slate-700" | |
| }`} | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-[11px] font-mono text-slate-400 block mb-1">Account Tier</label> | |
| <select className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-slate-700 text-xs font-mono"> | |
| <option>Pro Developer (Active)</option> | |
| </select> | |
| </div> | |
| <div className="flex items-center gap-2 pt-2"> | |
| <button className={`px-4 py-2 rounded-xl text-xs font-bold transition-all ${ | |
| currentStepIndex >= 5 ? "bg-emerald-600 text-white ring-2 ring-emerald-400" : "bg-indigo-600 text-white" | |
| }`}> | |
| Save Settings | |
| </button> | |
| <button className="px-4 py-2 rounded-xl bg-slate-800 text-slate-300 text-xs font-semibold"> | |
| Export PDF | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Preset 2 Mock VS Code IDE */} | |
| {selectedPreset === "vscode" && ( | |
| <div className="h-full bg-slate-950 border border-slate-800 rounded-xl p-4 font-mono text-xs text-slate-300 flex flex-col justify-between"> | |
| <div> | |
| <p className="text-slate-500">// server.ts — Line 42 Debug Inspection</p> | |
| <p><span className="text-purple-400">const</span> userTier = <span className="text-amber-300">getUserTier</span>(req.user);</p> | |
| <p className="bg-red-950/40 text-red-300 border-l-2 border-red-500 px-2 py-1 my-2"> | |
| ❌ TypeError: Cannot read properties of undefined (reading 'tier') | |
| </p> | |
| <p><span className="text-indigo-400">if</span> (userTier === <span className="text-emerald-300">"tiniagents"</span>) { allowedRpm = 100; }</p> | |
| </div> | |
| <div className="bg-slate-900 p-2.5 rounded-lg border border-slate-800 text-[11px] flex items-center justify-between"> | |
| <span className="text-slate-400">Terminal: node server.js</span> | |
| <span className="text-emerald-400 font-bold">Process Active (Port 3000)</span> | |
| </div> | |
| </div> | |
| )} | |
| {/* Preset 3 Mock Analytics */} | |
| {selectedPreset === "dashboard" && ( | |
| <div className="h-full flex flex-col justify-between text-slate-200"> | |
| <div className="flex items-center justify-between bg-slate-900/80 p-3 rounded-xl border border-slate-800"> | |
| <span className="font-bold text-xs text-white">Aura Cloud Metrics</span> | |
| <button className="px-3 py-1 rounded-lg bg-indigo-600 text-xs font-bold"> | |
| Generate Live Report | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-2 gap-3 my-2"> | |
| <div className="bg-slate-900/80 p-3 rounded-xl border border-slate-800"> | |
| <span className="text-[10px] text-slate-400 block font-mono">ACTIVE SESSIONS</span> | |
| <span className="text-lg font-black text-indigo-400">1,248</span> | |
| </div> | |
| <div className="bg-slate-900/80 p-3 rounded-xl border border-slate-800"> | |
| <span className="text-[10px] text-slate-400 block font-mono">LATENCY (P99)</span> | |
| <span className="text-lg font-black text-emerald-400">18 ms</span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Bounding Box Vision Overlay */} | |
| {showVisionOverlay && activePresetData.elements.map(el => ( | |
| <div | |
| key={el.id} | |
| className="absolute border border-emerald-400/70 bg-emerald-500/10 rounded pointer-events-none transition-all flex items-start justify-between p-0.5" | |
| style={{ | |
| left: `${(el.coords.x / 900) * 100}%`, | |
| top: `${(el.coords.y / 500) * 100}%`, | |
| width: `${(el.coords.w / 900) * 100}%`, | |
| height: `${(el.coords.h / 500) * 100}%` | |
| }} | |
| > | |
| <span className="bg-emerald-500 text-black text-[9px] font-black px-1 rounded font-mono shadow-xs"> | |
| {el.label} | |
| </span> | |
| </div> | |
| ))} | |
| {/* Live Animated Pointer Cursor */} | |
| <div | |
| className="absolute z-30 transition-all duration-300 ease-out pointer-events-none" | |
| style={{ | |
| left: `${(cursorPos.x / 900) * 100}%`, | |
| top: `${(cursorPos.y / 500) * 100}%` | |
| }} | |
| > | |
| <MousePointer className="h-6 w-6 text-red-400 fill-red-500 drop-shadow-[0_2px_8px_rgba(239,68,68,0.8)] -rotate-12" /> | |
| <span className="ml-4 -mt-2 inline-block px-2 py-0.5 rounded bg-red-600 text-white text-[10px] font-mono font-bold shadow-md"> | |
| ({cursorPos.x}, {cursorPos.y}) | |
| </span> | |
| {/* Click Ripple Effect */} | |
| {showClickRipple && ( | |
| <div className="absolute -inset-3 rounded-full border-2 border-amber-400 bg-amber-400/30 animate-ping" /> | |
| )} | |
| </div> | |
| </div> | |
| {/* Status Footer inside Screen */} | |
| <div className="bg-slate-950 p-2.5 border-t border-slate-800 flex items-center justify-between text-xs font-mono"> | |
| <div className="flex items-center gap-2"> | |
| <span className={`h-2 w-2 rounded-full ${isRunning ? "bg-amber-400 animate-ping" : "bg-emerald-400"}`} /> | |
| <span className="text-slate-300"> | |
| STATUS: <strong className={isRunning ? "text-amber-400" : "text-emerald-400"}>{isRunning ? "EXECUTING OPERATOR" : "READY"}</strong> | |
| </span> | |
| </div> | |
| <div className="text-slate-400 text-[11px]"> | |
| CURSOR: ({cursorPos.x}, {cursorPos.y}) | |
| </div> | |
| </div> | |
| </div> | |
| {/* Console Log Output */} | |
| <div className="bg-slate-950 rounded-2xl border border-slate-800 p-3 font-mono text-[11px] h-32 overflow-y-auto flex flex-col gap-1"> | |
| <div className="flex items-center justify-between text-slate-500 border-b border-slate-800/80 pb-1 mb-1"> | |
| <span className="flex items-center gap-1"> | |
| <Terminal className="h-3 w-3 text-indigo-400" /> | |
| Operator Event Stream | |
| </span> | |
| <span>{logs.length} events</span> | |
| </div> | |
| {logs.length === 0 ? ( | |
| <span className="text-slate-600 italic">No events logged yet. Click "Run Operator Goal" to start.</span> | |
| ) : ( | |
| logs.map((log, i) => ( | |
| <span key={i} className="text-slate-300 break-words"> | |
| {log} | |
| </span> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| {/* Right Column: Goal Configuration & Action Steps (5 Cols) */} | |
| <div className="lg:col-span-5 p-4 md:p-5 flex flex-col gap-4 bg-slate-900"> | |
| <div> | |
| <h4 className="text-sm font-bold text-white mb-1 flex items-center justify-between"> | |
| <span>Autonomous Desktop Goal</span> | |
| <span className="text-[10px] font-mono text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20"> | |
| AI Vision Model | |
| </span> | |
| </h4> | |
| <textarea | |
| value={taskPrompt} | |
| onChange={e => setTaskPrompt(e.target.value)} | |
| rows={3} | |
| placeholder="Describe what you want the AI operator to do on screen..." | |
| className="w-full p-3 rounded-xl bg-slate-950 border border-slate-800 text-slate-200 text-xs font-sans focus:outline-none focus:border-indigo-500 transition resize-none" | |
| /> | |
| </div> | |
| {/* Execution Controls */} | |
| <div className="flex items-center gap-2 flex-wrap"> | |
| {!isRunning ? ( | |
| <button | |
| onClick={handleStartOperator} | |
| className="flex-1 py-2.5 px-4 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-bold text-xs flex items-center justify-center gap-2 shadow-md cursor-pointer transition active:scale-95" | |
| > | |
| <Play className="h-4 w-4 fill-white" /> | |
| <span>Run Operator Goal</span> | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={() => setIsRunning(false)} | |
| className="flex-1 py-2.5 px-4 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs flex items-center justify-center gap-2 shadow-md cursor-pointer transition" | |
| > | |
| <Pause className="h-4 w-4 fill-white" /> | |
| <span>Pause Operator</span> | |
| </button> | |
| )} | |
| <button | |
| onClick={handleResetOperator} | |
| className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 cursor-pointer" | |
| title="Reset steps" | |
| > | |
| <RotateCcw className="h-4 w-4" /> | |
| </button> | |
| </div> | |
| {/* Operator Settings */} | |
| <div className="bg-slate-950/80 p-3 rounded-2xl border border-slate-800/80 flex items-center justify-between text-xs"> | |
| <div className="flex items-center gap-2 text-slate-300"> | |
| <Sliders className="h-4 w-4 text-indigo-400" /> | |
| <span className="font-semibold">Operator Speed:</span> | |
| </div> | |
| <div className="flex items-center gap-1 font-mono"> | |
| {[0.5, 1, 2].map(sp => ( | |
| <button | |
| key={sp} | |
| onClick={() => setSpeed(sp)} | |
| className={`px-2.5 py-1 rounded-lg text-xs font-bold transition ${ | |
| speed === sp ? "bg-indigo-600 text-white" : "bg-slate-800 text-slate-400" | |
| }`} | |
| > | |
| {sp}x | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Action Steps Plan List */} | |
| <div className="flex-1 flex flex-col min-h-0 bg-slate-950/50 rounded-2xl border border-slate-800 p-3 overflow-hidden"> | |
| <div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2"> | |
| <span className="text-xs font-bold text-slate-200 flex items-center gap-1.5"> | |
| <Activity className="h-4 w-4 text-indigo-400" /> | |
| Step Action Queue ({steps.length}) | |
| </span> | |
| <span className="text-[10px] font-mono text-slate-400"> | |
| {steps.filter(s => s.status === "completed").length}/{steps.length} Done | |
| </span> | |
| </div> | |
| <div className="flex-1 overflow-y-auto space-y-2 pr-1"> | |
| {steps.map((st, idx) => ( | |
| <div | |
| key={st.id} | |
| className={`p-2.5 rounded-xl border text-xs transition-all flex items-start gap-2.5 ${ | |
| st.status === "running" | |
| ? "bg-amber-500/10 border-amber-500/50 text-amber-200 ring-1 ring-amber-500/30" | |
| : st.status === "completed" | |
| ? "bg-slate-900/80 border-slate-800 text-slate-300 opacity-90" | |
| : "bg-slate-950/60 border-slate-800/60 text-slate-500" | |
| }`} | |
| > | |
| <div className="mt-0.5 shrink-0"> | |
| {st.status === "completed" ? ( | |
| <CheckCircle2 className="h-4 w-4 text-emerald-400" /> | |
| ) : st.status === "running" ? ( | |
| <span className="relative flex h-3.5 w-3.5"> | |
| <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span> | |
| <span className="relative inline-flex rounded-full h-3.5 w-3.5 bg-amber-500"></span> | |
| </span> | |
| ) : ( | |
| <div className="h-3.5 w-3.5 rounded-full border border-slate-700 flex items-center justify-center font-mono text-[9px]"> | |
| {st.stepNumber} | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between font-mono text-[10px] text-slate-400 mb-0.5"> | |
| <span className="font-bold text-indigo-300">{st.type}</span> | |
| <span>{st.timestamp}</span> | |
| </div> | |
| <p className="font-medium leading-relaxed">{st.description}</p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Footer Send Action */} | |
| {onSendToChat && ( | |
| <button | |
| onClick={() => { | |
| onSendToChat(`[Computer Use Report] Executed desktop operator goal: "${taskPrompt}". Completed 7/7 actions with 0 errors.`); | |
| onClose(); | |
| }} | |
| className="w-full py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-bold flex items-center justify-center gap-2 border border-slate-700 transition cursor-pointer" | |
| > | |
| <Sparkles className="h-3.5 w-3.5 text-amber-400" /> | |
| <span>Export Operator Summary to Active Chat</span> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |