284 lines
9.4 KiB
TypeScript
284 lines
9.4 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { SessionCreationForm } from './components/SessionCreationForm';
|
||
import { SessionProgressMonitor } from './components/SessionProgressMonitor';
|
||
import type { HostedSessionConfig } from '../../../packages/domain/entities/HostedSessionConfig';
|
||
|
||
interface SessionProgress {
|
||
sessionId: string;
|
||
currentStep: number;
|
||
state: string;
|
||
completedSteps: number[];
|
||
hasError: boolean;
|
||
errorMessage: string | null;
|
||
}
|
||
|
||
interface PermissionStatus {
|
||
accessibility: boolean;
|
||
screenRecording: boolean;
|
||
platform: string;
|
||
}
|
||
|
||
export function App() {
|
||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||
const [progress, setProgress] = useState<SessionProgress | null>(null);
|
||
const [isRunning, setIsRunning] = useState(false);
|
||
const [permissionStatus, setPermissionStatus] = useState<PermissionStatus | null>(null);
|
||
const [permissionChecking, setPermissionChecking] = useState(true);
|
||
const [missingPermissions, setMissingPermissions] = useState<string[]>([]);
|
||
|
||
const checkPermissions = useCallback(async () => {
|
||
if (!window.electronAPI) return;
|
||
|
||
setPermissionChecking(true);
|
||
try {
|
||
const result = await window.electronAPI.checkPermissions();
|
||
setPermissionStatus(result.status);
|
||
setMissingPermissions(result.missingPermissions);
|
||
} catch (error) {
|
||
console.error('Failed to check permissions:', error);
|
||
} finally {
|
||
setPermissionChecking(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
// Check permissions on app start
|
||
checkPermissions();
|
||
|
||
if (window.electronAPI) {
|
||
window.electronAPI.onSessionProgress((newProgress: SessionProgress) => {
|
||
setProgress(newProgress);
|
||
if (newProgress.state === 'COMPLETED' ||
|
||
newProgress.state === 'FAILED' ||
|
||
newProgress.state === 'STOPPED_AT_STEP_18') {
|
||
setIsRunning(false);
|
||
}
|
||
});
|
||
}
|
||
}, [checkPermissions]);
|
||
|
||
const handleOpenPermissionSettings = async (pane?: 'accessibility' | 'screenRecording') => {
|
||
if (!window.electronAPI) return;
|
||
await window.electronAPI.openPermissionSettings(pane);
|
||
};
|
||
|
||
const handleRequestAccessibility = async () => {
|
||
if (!window.electronAPI) return;
|
||
await window.electronAPI.requestAccessibility();
|
||
// Recheck permissions after request
|
||
setTimeout(checkPermissions, 500);
|
||
};
|
||
|
||
const handleStartAutomation = async (config: HostedSessionConfig) => {
|
||
// Recheck permissions before starting
|
||
await checkPermissions();
|
||
|
||
if (missingPermissions.length > 0) {
|
||
alert(`Cannot start automation: Missing permissions: ${missingPermissions.join(', ')}`);
|
||
return;
|
||
}
|
||
|
||
setIsRunning(true);
|
||
const result = await window.electronAPI.startAutomation(config);
|
||
|
||
if (result.success && result.sessionId) {
|
||
setSessionId(result.sessionId);
|
||
} else {
|
||
setIsRunning(false);
|
||
if (result.permissionError) {
|
||
// Update permission status
|
||
await checkPermissions();
|
||
alert(`Permission Error: ${result.error}`);
|
||
} else {
|
||
alert(`Failed to start automation: ${result.error}`);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleStopAutomation = async () => {
|
||
if (sessionId) {
|
||
const result = await window.electronAPI.stopAutomation(sessionId);
|
||
if (result.success) {
|
||
setIsRunning(false);
|
||
setProgress(prev => prev ? { ...prev, state: 'STOPPED', hasError: false, errorMessage: 'User stopped automation' } : null);
|
||
} else {
|
||
alert(`Failed to stop automation: ${result.error}`);
|
||
}
|
||
}
|
||
};
|
||
|
||
const isMacOS = permissionStatus?.platform === 'darwin';
|
||
const hasAllPermissions = missingPermissions.length === 0;
|
||
|
||
return (
|
||
<div style={{
|
||
display: 'flex',
|
||
minHeight: '100vh',
|
||
backgroundColor: '#1a1a1a'
|
||
}}>
|
||
<div style={{
|
||
flex: 1,
|
||
padding: '2rem',
|
||
borderRight: '1px solid #333'
|
||
}}>
|
||
<h1 style={{ marginBottom: '2rem', color: '#fff' }}>
|
||
GridPilot Companion
|
||
</h1>
|
||
<p style={{ marginBottom: '2rem', color: '#aaa' }}>
|
||
Hosted Session Automation POC
|
||
</p>
|
||
|
||
{/* Permission Banner */}
|
||
{isMacOS && !permissionChecking && !hasAllPermissions && (
|
||
<div style={{
|
||
marginBottom: '1.5rem',
|
||
padding: '1rem',
|
||
backgroundColor: '#3d2020',
|
||
border: '1px solid #dc3545',
|
||
borderRadius: '8px',
|
||
}}>
|
||
<h3 style={{ color: '#ff6b6b', margin: '0 0 0.5rem 0', fontSize: '1rem' }}>
|
||
⚠️ Missing Permissions
|
||
</h3>
|
||
<p style={{ color: '#ffaaaa', margin: '0 0 1rem 0', fontSize: '0.9rem' }}>
|
||
GridPilot requires macOS permissions to control your computer for automation.
|
||
Please grant the following permissions:
|
||
</p>
|
||
<ul style={{ color: '#ffaaaa', margin: '0 0 1rem 0', paddingLeft: '1.5rem', fontSize: '0.9rem' }}>
|
||
{!permissionStatus?.accessibility && (
|
||
<li style={{ marginBottom: '0.5rem' }}>
|
||
<strong>Accessibility:</strong> Required for keyboard and mouse control
|
||
</li>
|
||
)}
|
||
{!permissionStatus?.screenRecording && (
|
||
<li style={{ marginBottom: '0.5rem' }}>
|
||
<strong>Screen Recording:</strong> Required for screen capture and window detection
|
||
</li>
|
||
)}
|
||
</ul>
|
||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||
{!permissionStatus?.accessibility && (
|
||
<button
|
||
onClick={handleRequestAccessibility}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
backgroundColor: '#007bff',
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '0.9rem',
|
||
}}
|
||
>
|
||
Request Accessibility
|
||
</button>
|
||
)}
|
||
{!permissionStatus?.accessibility && (
|
||
<button
|
||
onClick={() => handleOpenPermissionSettings('accessibility')}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
backgroundColor: '#6c757d',
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '0.9rem',
|
||
}}
|
||
>
|
||
Open Accessibility Settings
|
||
</button>
|
||
)}
|
||
{!permissionStatus?.screenRecording && (
|
||
<button
|
||
onClick={() => handleOpenPermissionSettings('screenRecording')}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
backgroundColor: '#6c757d',
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '0.9rem',
|
||
}}
|
||
>
|
||
Open Screen Recording Settings
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={checkPermissions}
|
||
style={{
|
||
padding: '0.5rem 1rem',
|
||
backgroundColor: '#28a745',
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '0.9rem',
|
||
}}
|
||
>
|
||
Recheck Permissions
|
||
</button>
|
||
</div>
|
||
<p style={{ color: '#888', margin: '1rem 0 0 0', fontSize: '0.8rem' }}>
|
||
After granting permissions in System Preferences, click "Recheck Permissions" or restart the app.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Permission Status Indicator */}
|
||
{isMacOS && !permissionChecking && hasAllPermissions && (
|
||
<div style={{
|
||
marginBottom: '1.5rem',
|
||
padding: '0.75rem 1rem',
|
||
backgroundColor: '#1e3d1e',
|
||
border: '1px solid #28a745',
|
||
borderRadius: '8px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.5rem',
|
||
}}>
|
||
<span style={{ color: '#28a745', fontSize: '1.2rem' }}>✓</span>
|
||
<span style={{ color: '#8eff8e', fontSize: '0.9rem' }}>
|
||
All permissions granted - Ready for automation
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
<SessionCreationForm
|
||
onSubmit={handleStartAutomation}
|
||
disabled={isRunning || (isMacOS && !hasAllPermissions)}
|
||
/>
|
||
{isRunning && (
|
||
<button
|
||
onClick={handleStopAutomation}
|
||
style={{
|
||
marginTop: '1rem',
|
||
padding: '0.75rem 1.5rem',
|
||
backgroundColor: '#dc3545',
|
||
color: '#fff',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '1rem',
|
||
fontWeight: 'bold'
|
||
}}
|
||
>
|
||
Stop Automation
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div style={{
|
||
flex: 1,
|
||
padding: '2rem',
|
||
backgroundColor: '#0d0d0d'
|
||
}}>
|
||
<SessionProgressMonitor
|
||
sessionId={sessionId}
|
||
progress={progress}
|
||
isRunning={isRunning}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |