import React, { useState, useEffect } from 'react';
import './index.css';
interface QueuedOutput {
id: string;
target: string;
content: string;
status: 'pending' | 'approved' | 'rejected';
metadata: string | null;
comment: string | null;
created_at: string;
}
const App: React.FC = () => {
const [items, setItems] = useState<QueuedOutput[]>([]);
const [loading, setLoading] = useState(true);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [currentView, setCurrentView] = useState<'pending' | 'settings'>('pending');
const fetchItems = async (showLoading = true) => {
try {
if (showLoading) setLoading(true);
const response = await fetch('http://localhost:3001/api/pending');
const data = await response.json();
setItems(data);
} catch (err) {
console.error('Failed to fetch items:', err);
} finally {
if (showLoading) setLoading(false);
}
};
useEffect(() => {
let isMounted = true;
const initialFetch = async () => {
await fetchItems(true);
};
initialFetch();
// SSE Setup
const eventSource = new EventSource('http://localhost:3001/api/sse');
eventSource.onmessage = (event) => {
console.log('SSE Event received:', event.data);
if (isMounted) {
fetchItems(false);
}
};
eventSource.onerror = (err) => {
console.error('SSE Error:', err);
eventSource.close();
};
return () => {
isMounted = false;
eventSource.close();
};
}, []);
const handleApprove = async (id: string) => {
try {
await fetch(`http://localhost:3001/api/approve/${id}`, { method: 'POST' });
fetchItems(false);
} catch (err) {
alert('Failed to approve');
}
};
const handleReject = async (id: string) => {
const comment = prompt('Enter rejection feedback:');
if (comment === null) return;
try {
await fetch(`http://localhost:3001/api/reject/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ comment }),
});
fetchItems(false);
} catch (err) {
alert('Failed to reject');
}
};
const mcpConfigSnippet = {
"mcpServers": {
"exolauncher": {
"command": "ExoLauncher.exe",
"args": [],
"env": {}
}
}
};
return (
<div className="app-container">
{/* Sidebar Overlay */}
<div className={`sidebar-overlay ${sidebarOpen ? 'active' : ''}`} onClick={() => setSidebarOpen(false)}></div>
{/* Sidebar */}
<aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
<div className="sidebar-header">
<h2>ExoLauncher</h2>
</div>
<nav className="sidebar-nav">
<button
className={`nav-item ${currentView === 'pending' ? 'active' : ''}`}
onClick={() => { setCurrentView('pending'); setSidebarOpen(false); }}
>
<span className="icon">📥</span> Pending Queue
</button>
<button
className={`nav-item ${currentView === 'settings' ? 'active' : ''}`}
onClick={() => { setCurrentView('settings'); setSidebarOpen(false); }}
>
<span className="icon">⚙️</span> MCP Settings
</button>
</nav>
<div className="sidebar-footer">
v0.1.0-alpha
</div>
</aside>
<header className="main-header">
<button className="hamburger" onClick={() => setSidebarOpen(true)}>
<span></span>
<span></span>
<span></span>
</button>
<h1>{currentView === 'pending' ? 'Pending Approvals' : 'MCP Connection Settings'}</h1>
<div className="status-badge">Live</div>
</header>
<main className="content">
{currentView === 'pending' ? (
loading ? (
<div className="loading">Loading items...</div>
) : items.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">✨</div>
<p>Queue is empty. Everything is processed.</p>
</div>
) : (
<div className="card-grid">
{items.map((item) => (
<div key={item.id} className="item-card">
<div className="card-header">
<span className="target-tag">{item.target}</span>
<span className="timestamp">{new Date(item.created_at).toLocaleString()}</span>
</div>
<div className="card-body">
<pre className="content-preview">{item.content}</pre>
{item.metadata && (
<div className="metadata-box">
<small>Metadata:</small>
<code>{item.metadata}</code>
</div>
)}
</div>
<div className="card-footer">
<button className="btn-reject" onClick={() => handleReject(item.id)}>Reject</button>
<button className="btn-approve" onClick={() => handleApprove(item.id)}>Approve & Send</button>
</div>
</div>
))}
</div>
)
) : (
<div className="settings-view">
<div className="settings-card">
<h3>MCP Configuration</h3>
<p>Copy this snippet into your agent's <code>mcp_config.json</code> to connect to ExoLauncher.</p>
<div className="code-block">
<pre>{JSON.stringify(mcpConfigSnippet, null, 2)}</pre>
<button className="copy-btn" onClick={() => navigator.clipboard.writeText(JSON.stringify(mcpConfigSnippet, null, 2))}>
Copy JSON
</button>
</div>
<div className="info-box">
<strong>Note:</strong> Ensure the <code>ExoLauncher.exe</code> is in your PATH or specify the full path.
</div>
</div>
</div>
)}
</main>
</div>
);
};
export default App;