diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..0555d61 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,192 @@ +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([]); + 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 ( +
+ {/* Sidebar Overlay */} +
setSidebarOpen(false)}>
+ + {/* Sidebar */} + + +
+ +

{currentView === 'pending' ? 'Pending Approvals' : 'MCP Connection Settings'}

+
Live
+
+ +
+ {currentView === 'pending' ? ( + loading ? ( +
Loading items...
+ ) : items.length === 0 ? ( +
+
+

Queue is empty. Everything is processed.

+
+ ) : ( +
+ {items.map((item) => ( +
+
+ {item.target} + {new Date(item.created_at).toLocaleString()} +
+
+
{item.content}
+ {item.metadata && ( +
+ Metadata: + {item.metadata} +
+ )} +
+
+ + +
+
+ ))} +
+ ) + ) : ( +
+
+

MCP Configuration

+

Copy this snippet into your agent's mcp_config.json to connect to ExoLauncher.

+
+
{JSON.stringify(mcpConfigSnippet, null, 2)}
+ +
+
+ Note: Ensure the ExoLauncher.exe is in your PATH or specify the full path. +
+
+
+ )} +
+
+ ); +}; + +export default App; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..84d317e --- /dev/null +++ b/src/index.css @@ -0,0 +1,362 @@ +:root { + --primary: #6366f1; + --primary-hover: #4f46e5; + --bg-dark: #0f172a; + --card-bg: rgba(30, 41, 59, 0.7); + --text-main: #f8fafc; + --text-muted: #94a3b8; + --border: rgba(255, 255, 255, 0.1); + --success: #10b981; + --danger: #ef4444; + --sidebar-width: 280px; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Inter', system-ui, -apple-system, sans-serif; + background-color: var(--bg-dark); + color: var(--text-main); + overflow-x: hidden; +} + +.app-container { + display: flex; + min-height: 100vh; +} + +/* Sidebar Styles */ +.sidebar { + width: var(--sidebar-width); + background: #1e293b; + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1000; +} + +.sidebar-header { + padding: 2rem 1.5rem; + border-bottom: 1px solid var(--border); +} + +.sidebar-header h2 { + font-size: 1.5rem; + background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.sidebar-nav { + padding: 1rem 0.75rem; + flex-grow: 1; +} + +.nav-item { + width: 100%; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + background: transparent; + border: none; + border-radius: 0.5rem; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s; + text-align: left; + font-size: 1rem; + margin-bottom: 0.5rem; +} + +.nav-item:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--text-main); +} + +.nav-item.active { + background: rgba(99, 102, 241, 0.1); + color: var(--primary); + font-weight: 600; +} + +.sidebar-footer { + padding: 1.5rem; + color: var(--text-muted); + font-size: 0.875rem; + border-top: 1px solid var(--border); +} + +/* Mobile Sidebar & Hamburger */ +.hamburger { + display: none; + flex-direction: column; + gap: 5px; + background: transparent; + border: none; + cursor: pointer; + padding: 10px; + z-index: 1100; +} + +.hamburger span { + display: block; + width: 25px; + height: 2px; + background: var(--text-main); + transition: 0.3s; +} + +@media (max-width: 768px) { + .hamburger { + display: flex; + } + .sidebar { + position: fixed; + height: 100vh; + left: 0; + top: 0; + transform: translateX(-100%); + } + .sidebar.open { + transform: translateX(0); + } + .sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + z-index: 999; + } + .sidebar-overlay.active { + display: block; + } +} + +/* Main Layout */ +.main-header { + height: 70px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; + background: rgba(15, 23, 42, 0.8); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + width: 100%; + z-index: 900; +} + +.main-header h1 { + font-size: 1.25rem; + font-weight: 500; +} + +.status-badge { + padding: 0.25rem 0.75rem; + background: rgba(16, 185, 129, 0.1); + color: var(--success); + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.status-badge::before { + content: ''; + width: 8px; + height: 8px; + background: var(--success); + border-radius: 50%; + box-shadow: 0 0 8px var(--success); +} + +.content { + flex-grow: 1; + padding: 2rem; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +/* Card Styles */ +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 1.5rem; +} + +.item-card { + background: var(--card-bg); + backdrop-filter: blur(8px); + border: 1px solid var(--border); + border-radius: 1rem; + display: flex; + flex-direction: column; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; +} + +.item-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.5); +} + +.card-header { + padding: 1.25rem; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.target-tag { + background: var(--primary); + padding: 0.25rem 0.6rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; +} + +.timestamp { + color: var(--text-muted); + font-size: 0.8rem; +} + +.card-body { + padding: 1.25rem; + flex-grow: 1; +} + +.content-preview { + background: rgba(0, 0, 0, 0.3); + padding: 1rem; + border-radius: 0.5rem; + white-space: pre-wrap; + font-family: 'Fira Code', monospace; + font-size: 0.9rem; + line-height: 1.5; + color: #e2e8f0; +} + +.metadata-box { + margin-top: 1rem; + padding: 0.75rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 0.375rem; +} + +.metadata-box small { + display: block; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.card-footer { + padding: 1.25rem; + background: rgba(255, 255, 255, 0.02); + display: flex; + gap: 1rem; +} + +.card-footer button { + flex: 1; + padding: 0.75rem; + border: none; + border-radius: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: filter 0.2s; +} + +.btn-approve { + background: var(--success); + color: white; +} + +.btn-reject { + background: rgba(239, 68, 68, 0.1); + color: var(--danger); + border: 1px solid rgba(239, 68, 68, 0.2) !important; +} + +button:hover { + filter: brightness(1.1); +} + +/* Settings View */ +.settings-card { + background: var(--card-bg); + border: 1px solid var(--border); + padding: 2rem; + border-radius: 1rem; +} + +.code-block { + background: #000; + padding: 1.5rem; + border-radius: 0.5rem; + margin: 1rem 0; + position: relative; +} + +.code-block pre { + color: #a5b4fc; + font-family: monospace; +} + +.copy-btn { + position: absolute; + top: 1rem; + right: 1rem; + background: var(--primary); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + font-size: 0.8rem; + cursor: pointer; +} + +.info-box { + margin-top: 1.5rem; + padding: 1rem; + background: rgba(99, 102, 241, 0.05); + border-left: 4px solid var(--primary); + border-radius: 0.25rem; +} + +/* Utils */ +.loading { + text-align: center; + padding: 4rem; + color: var(--text-muted); +} + +.empty-state { + text-align: center; + padding: 6rem 2rem; + color: var(--text-muted); +} + +.empty-icon { + font-size: 4rem; + margin-bottom: 1.5rem; + opacity: 0.5; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..3d7150d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +///