diff --git a/.vscode/launch.json b/.vscode/launch.json index df23b16..67bfac4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,8 @@ "name": "Tauri: Debug Rust (Backend)", "cargo": { "args": [ - "build" + "build", + "--manifest-path=./src-tauri/Cargo.toml" ], "filter": { "kind": "bin" @@ -17,8 +18,7 @@ "cwd": "${workspaceFolder}/src-tauri", "env": { "RUST_LOG": "debug" - }, - "preLaunchTask": "Start Tauri Dev" + } }, { "type": "chrome", diff --git a/package-lock.json b/package-lock.json index 1adfe74..902e480 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,11 @@ "name": "ExoLauncher", "version": "0.0.0", "dependencies": { + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-dom": "^19.2.5", + "react-i18next": "^17.0.6" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -206,6 +209,14 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1820,6 +1831,49 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.8.tgz", + "integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2407,6 +2461,32 @@ "react": "^19.2.5" } }, + "node_modules/react-i18next": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.6.tgz", + "integrity": "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", @@ -2541,7 +2621,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2618,6 +2698,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "8.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", @@ -2695,6 +2783,14 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9f3140b..6fe5ace 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,15 @@ "tauri": "tauri" }, "dependencies": { + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-dom": "^19.2.5", + "react-i18next": "^17.0.6" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tauri-apps/cli": "^1.5", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -26,7 +30,6 @@ "globals": "^17.5.0", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", - "vite": "^8.0.10", - "@tauri-apps/cli": "^1.5" + "vite": "^8.0.10" } } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0beb4f3..e34a7db 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,6 +6,9 @@ edition = "2021" build = "build.rs" +[features] +custom-protocol = ["tauri/custom-protocol"] + [build-dependencies] tauri-build = { version = "1.5", features = [] } @@ -20,9 +23,9 @@ anyhow = "1.0" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.0", features = ["v4", "serde"] } -async_trait = "0.1" -axum = { version = "0.7", features = ["sse"] } +async-trait = "0.1" +axum = "0.7" tower-http = { version = "0.5", features = ["cors"] } reqwest = { version = "0.12", features = ["json"] } futures-util = "0.3" -tauri = { version = "1.5", features = ["api-all"] } +tauri = { version = "1.5", features = ["api-all", "custom-protocol"] } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index fce75f2..4126684 100644 --- a/src-tauri/icons/128x128.png +++ b/src-tauri/icons/128x128.png Binary files differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index fce75f2..a0ef144 100644 --- a/src-tauri/icons/128x128@2x.png +++ b/src-tauri/icons/128x128@2x.png Binary files differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index fce75f2..ea98f38 100644 --- a/src-tauri/icons/32x32.png +++ b/src-tauri/icons/32x32.png Binary files differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..6f2ceb1 --- /dev/null +++ b/src-tauri/icons/Square107x107Logo.png Binary files differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..35537ef --- /dev/null +++ b/src-tauri/icons/Square142x142Logo.png Binary files differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..3c080db --- /dev/null +++ b/src-tauri/icons/Square150x150Logo.png Binary files differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..d1200f9 --- /dev/null +++ b/src-tauri/icons/Square284x284Logo.png Binary files differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..16f3310 --- /dev/null +++ b/src-tauri/icons/Square30x30Logo.png Binary files differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..4bd28d4 --- /dev/null +++ b/src-tauri/icons/Square310x310Logo.png Binary files differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..1213df6 --- /dev/null +++ b/src-tauri/icons/Square44x44Logo.png Binary files differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..1149c9d --- /dev/null +++ b/src-tauri/icons/Square71x71Logo.png Binary files differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..a621f37 --- /dev/null +++ b/src-tauri/icons/Square89x89Logo.png Binary files differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..b786530 --- /dev/null +++ b/src-tauri/icons/StoreLogo.png Binary files differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index fce75f2..3efe799 100644 --- a/src-tauri/icons/icon.icns +++ b/src-tauri/icons/icon.icns Binary files differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index fce75f2..b9851da 100644 --- a/src-tauri/icons/icon.ico +++ b/src-tauri/icons/icon.ico Binary files differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index fce75f2..4e47469 100644 --- a/src-tauri/icons/icon.png +++ b/src-tauri/icons/icon.png Binary files differ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 90a814f..9100ea9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -286,7 +286,7 @@ async fn sse_handler( State(state): State, ) -> Sse>> { - let mut rx = state.tx.subscribe(); + let rx = state.tx.subscribe(); let stream = stream::unfold(rx, |mut rx| async move { match rx.recv().await { diff --git a/src/App.tsx b/src/App.tsx index c2fa2ce..56ce01f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,7 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import './index.css'; +import logo from './assets/logo.png'; interface QueuedOutput { id: string; @@ -12,10 +14,79 @@ } const App: React.FC = () => { + const { t, i18n } = useTranslation(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); - const [sidebarOpen, setSidebarOpen] = useState(false); const [currentView, setCurrentView] = useState<'pending' | 'settings'>('pending'); + const [sidebarWidth, setSidebarWidth] = useState(280); + const [masterWidth, setMasterWidth] = useState(320); + const [resizingType, setResizingType] = useState<'sidebar' | 'master' | null>(null); + const [selectedItemId, setSelectedItemId] = useState(null); + + // Auto-select first item + useEffect(() => { + if (items.length > 0 && !selectedItemId) { + setSelectedItemId(items[0].id); + } + }, [items]); + + useEffect(() => { + document.documentElement.style.setProperty('--dynamic-sidebar-width', `${sidebarWidth}px`); + }, [sidebarWidth]); + + useEffect(() => { + document.documentElement.style.setProperty('--dynamic-master-width', `${masterWidth}px`); + }, [masterWidth]); + + const handleLanguageChange = (lang: string) => { + i18n.changeLanguage(lang); + }; + + const startResizing = (type: 'sidebar' | 'master') => (e: React.MouseEvent) => { + setResizingType(type); + e.preventDefault(); + }; + + const stopResizing = () => { + setResizingType(null); + }; + + const resize = (e: MouseEvent) => { + if (resizingType === 'sidebar') { + const newWidth = e.clientX - 64; // 64px is the activity bar width + if (newWidth > 150 && newWidth < 600) { + setSidebarWidth(newWidth); + } + } else if (resizingType === 'master') { + const sidebarTotal = 64 + sidebarWidth; + const newWidth = e.clientX - sidebarTotal; + if (newWidth > 200 && newWidth < 800) { + setMasterWidth(newWidth); + } + } + }; + + useEffect(() => { + if (resizingType) { + window.addEventListener('mousemove', resize); + window.addEventListener('mouseup', stopResizing); + } else { + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResizing); + } + return () => { + window.removeEventListener('mousemove', resize); + window.removeEventListener('mouseup', stopResizing); + }; + }, [resizingType, sidebarWidth]); // need sidebarWidth for master resizing calc + + useEffect(() => { + document.documentElement.style.setProperty('--dynamic-sidebar-width', `${sidebarWidth}px`); + }, [sidebarWidth]); + + useEffect(() => { + document.documentElement.style.setProperty('--dynamic-master-width', `${masterWidth}px`); + }, [masterWidth]); const fetchItems = async (showLoading = true) => { try { @@ -87,104 +158,189 @@ const mcpConfigSnippet = { "mcpServers": { "exolauncher": { - "command": "ExoLauncher.exe", - "args": [], - "env": {} + "type": "sse", + "url": "http://localhost:3001/mcp/sse" } } }; + useEffect(() => { + document.documentElement.style.setProperty('--dynamic-sidebar-width', `${sidebarWidth}px`); + }, [sidebarWidth]); + return (
- {/* Sidebar Overlay */} -
setSidebarOpen(false)}>
- - {/* Sidebar */} - -
- -

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

-
Live
-
+
+
+

{currentView === 'pending' ? t('views.pending_approvals') : t('views.mcp_settings')}

+
{t('common.status')}
+
-
- {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} +
+ {currentView === 'pending' ? ( + loading ? ( +
{t('common.loading')}
+ ) : ( +
+
+ {items.length === 0 ? ( +
+

{t('views.empty')}

+
+ ) : ( + items.map((item) => ( +
setSelectedItemId(item.id)} + > +
+ {item.target} + {new Date(item.created_at).toLocaleTimeString(i18n.language === 'ja' ? 'ja-JP' : 'en-US', { hour: '2-digit', minute: '2-digit' })} +
+
{item.content.substring(0, 40)}...
- )} -
-
- - -
+ )) + )}
- ))} -
- ) - ) : ( -
-
-

MCP Configuration

-

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

-
-
{JSON.stringify(mcpConfigSnippet, null, 2)}
- + + {/* Master-Detail Resizer */} +
+ +
+ {items.length > 0 && items.find(i => i.id === selectedItemId) ? ( + (() => { + const selectedItem = items.find(i => i.id === selectedItemId)!; + return ( +
+
+
+

{selectedItem.target}

+ {new Date(selectedItem.created_at).toLocaleString()} +
+
+ + +
+
+
+
{selectedItem.content}
+ {selectedItem.metadata && ( +
+

Metadata

+ {selectedItem.metadata} +
+ )} +
+
+ ); + })() + ) : ( +
+
+ + + + +
+

{items.length === 0 ? '' : 'Select an item to preview'}

+
+ )} +
-
- Note: Ensure the ExoLauncher.exe is in your PATH or specify the full path. + ) + ) : ( +
+
+

{t('sidebar.mcp_servers')}

+

{t('views.mcp_config_help')}

+
+
{JSON.stringify(mcpConfigSnippet, null, 2)}
+ +
+
+ {t('views.note')}: {t('views.sse_note')} +
+
+ +
+

{t('settings.language')}

+
+ +
-
- )} -
+ )} + +
); }; diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..fce75f2 --- /dev/null +++ b/src/assets/logo.png Binary files differ diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..5e2d812 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import en from './locales/en.json'; +import ja from './locales/ja.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + ja: { translation: ja } + }, + fallbackLng: 'en', + interpolation: { + escapeValue: false + }, + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'] + } + }); + +export default i18n; diff --git a/src/index.css b/src/index.css index 84d317e..d191b8b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,162 +1,206 @@ :root { --primary: #6366f1; --primary-hover: #4f46e5; - --bg-dark: #0f172a; - --card-bg: rgba(30, 41, 59, 0.7); + --bg-dark: #050505; + --bg-sidebar: #0a0a0a; --text-main: #f8fafc; --text-muted: #94a3b8; - --border: rgba(255, 255, 255, 0.1); + --border: rgba(255, 255, 255, 0.08); --success: #10b981; --danger: #ef4444; --sidebar-width: 280px; + --dynamic-sidebar-width: 280px; } * { - box-sizing: border-box; margin: 0; padding: 0; + box-sizing: border-box; } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: var(--bg-dark); color: var(--text-main); - overflow-x: hidden; + overflow: hidden; } .app-container { display: flex; - min-height: 100vh; + height: 100vh; + width: 100vw; } -/* Sidebar Styles */ .sidebar { - width: var(--sidebar-width); - background: #1e293b; + position: sticky; + left: 0; + top: 0; + height: 100vh; + display: flex; + z-index: 1000; border-right: 1px solid var(--border); + width: calc(64px + var(--dynamic-sidebar-width, 280px)); +} + +.resizer { + width: 4px; + cursor: col-resize; + background: transparent; + transition: background 0.2s; + z-index: 1001; +} + +.resizer:hover, .resizer:active { + background: var(--primary); +} + +.activity-bar { + width: 64px; + background: #000000; display: flex; flex-direction: column; - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - z-index: 1000; + align-items: center; + padding: 1rem 0; + gap: 1.5rem; + border-right: 1px solid var(--border); } -.sidebar-header { - padding: 2rem 1.5rem; +.logo-mini img { + width: 32px; + height: 32px; + filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.2)); +} + +.activity-item { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 8px; + transition: all 0.2s; +} + +.activity-item .icon { + width: 24px; + height: 24px; + opacity: 0.6; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: var(--text-muted); +} + +.activity-item:hover .icon { + opacity: 1; + color: var(--text-main); + transform: scale(1.1); +} + +.activity-item.active .icon { + opacity: 1; + color: var(--primary); + filter: drop-shadow(0 0 8px rgba(99, 102, 241, 0.5)); +} + +.activity-item:hover { + background: rgba(255, 255, 255, 0.05); +} + +.activity-item.active { + background: rgba(99, 102, 241, 0.1); +} + +/* Side Panel (Details) */ +.side-panel { + width: var(--dynamic-sidebar-width, 280px); + background: rgba(10, 10, 10, 0.98); + -webkit-backdrop-filter: blur(20px); + backdrop-filter: blur(20px); + display: flex; + flex-direction: column; + border-right: 1px solid var(--border); +} + +.panel-header { + padding: 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; +.panel-header h3 { + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.1em; color: var(--text-muted); - cursor: pointer; - transition: all 0.2s; - text-align: left; - font-size: 1rem; - margin-bottom: 0.5rem; + font-weight: 700; } -.nav-item:hover { - background: rgba(255, 255, 255, 0.05); +.panel-nav { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.panel-label { + padding: 0.5rem 0.75rem; + font-size: 0.7rem; + font-weight: 700; + color: #475569; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-top: 1rem; +} + +.panel-item { + padding: 0.75rem 1rem; + border: none; + background: transparent; + color: var(--text-muted); + text-align: left; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s; +} + +.panel-item:hover { + background: rgba(255, 255, 255, 0.03); color: var(--text-main); } -.nav-item.active { - background: rgba(99, 102, 241, 0.1); - color: var(--primary); +.panel-item.active { + background: rgba(255, 255, 255, 0.05); + color: var(--text-main); font-weight: 600; } .sidebar-footer { + margin-top: auto; padding: 1.5rem; - color: var(--text-muted); - font-size: 0.875rem; + font-size: 0.75rem; + color: #475569; border-top: 1px solid var(--border); } -/* Mobile Sidebar & Hamburger */ -.hamburger { - display: none; +.main-content { + flex-grow: 1; + display: flex; flex-direction: column; - gap: 5px; - background: transparent; - border: none; - cursor: pointer; - padding: 10px; - z-index: 1100; + background: var(--bg-dark); + min-width: 0; } -.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; + padding: 0 2rem; 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; + background: rgba(0, 0, 0, 0.3); } .main-header h1 { @@ -189,136 +233,228 @@ .content { flex-grow: 1; - padding: 2rem; - max-width: 1200px; - margin: 0 auto; + display: flex; + flex-direction: column; + height: calc(100vh - 70px); width: 100%; } -/* Card Styles */ -.card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); - gap: 1.5rem; +/* Split View (Master-Detail) */ +.split-view { + display: flex; + height: 100%; + overflow: hidden; } -.item-card { - background: var(--card-bg); - backdrop-filter: blur(8px); - border: 1px solid var(--border); - border-radius: 1rem; +.master-list { + width: var(--dynamic-master-width, 320px); + background: rgba(0, 0, 0, 0.2); + overflow-y: auto; +} + +.resizer-v { + width: 1px; + cursor: col-resize; + background: var(--border); + position: relative; + transition: all 0.2s; + z-index: 10; +} + +.resizer-v::after { + content: ''; + position: absolute; + left: -4px; + right: -4px; + top: 0; + bottom: 0; +} + +.resizer-v:hover, .resizer-v:active { + background: var(--primary); + width: 2px; + box-shadow: 0 0 10px rgba(99, 102, 241, 0.5); +} + +.detail-pane { + flex: 1; /* Take remaining space */ + overflow-y: auto; + background: var(--bg-dark); + min-width: 0; +} + +.empty-list-message { + padding: 3rem 1.5rem; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; + opacity: 0.5; +} + +.list-item { + padding: 1.25rem; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: all 0.2s; + border-left: 3px solid transparent; +} + +.list-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.list-item.active { + background: rgba(99, 102, 241, 0.1); + border-left-color: var(--primary); +} + +.item-meta { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.item-target { + font-weight: 600; + color: var(--text-main); + font-size: 0.9rem; +} + +.item-time { + font-size: 0.75rem; + color: var(--text-muted); +} + +.item-snippet { + font-size: 0.8rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Detail Pane Contents */ +.preview-container { display: flex; flex-direction: column; - overflow: hidden; - transition: transform 0.2s, box-shadow 0.2s; + height: 100%; } -.item-card:hover { - transform: translateY(-4px); - box-shadow: 0 12px 24px -10px rgba(0, 0, 0, 0.5); -} - -.card-header { - padding: 1.25rem; +.preview-header { + padding: 1.5rem 2.5rem; 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); + position: sticky; + top: 0; + z-index: 10; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} + +.header-info h2 { + font-size: 1.5rem; + margin-bottom: 0.25rem; + color: var(--text-main); +} + +.full-timestamp { + font-size: 0.85rem; + color: var(--text-muted); +} + +.header-actions { 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; +.preview-body { + padding: 2.5rem; + overflow-y: auto; } -.btn-approve { - background: var(--success); - color: white; +.full-content { + background: #000000; + padding: 2.5rem; + border-radius: 1rem; + font-family: 'Fira Code', 'Courier New', monospace; + font-size: 1rem; + line-height: 1.7; + color: #e2e8f0; + border: 1px solid var(--border); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + white-space: pre-wrap; + word-break: break-word; } -.btn-reject { - background: rgba(239, 68, 68, 0.1); - color: var(--danger); - border: 1px solid rgba(239, 68, 68, 0.2) !important; +.detail-metadata { + margin-top: 2.5rem; + padding-top: 2rem; + border-top: 1px solid var(--border); } -button:hover { - filter: brightness(1.1); +.detail-metadata h4 { + margin-bottom: 1rem; + color: var(--text-muted); + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.detail-metadata code { + display: block; + padding: 1.25rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 0.75rem; + font-size: 0.85rem; + border: 1px solid var(--border); + color: var(--text-muted); +} + +.no-selection { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + font-style: italic; } /* Settings View */ +.settings-view { + padding: 2.5rem; + max-width: 900px; +} + .settings-card { - background: var(--card-bg); + background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border); - padding: 2rem; border-radius: 1rem; + padding: 2rem; + margin-bottom: 2rem; +} + +.settings-card h3 { + margin-bottom: 1rem; + font-size: 1.1rem; } .code-block { - background: #000; + background: #000000; padding: 1.5rem; - border-radius: 0.5rem; - margin: 1rem 0; + border-radius: 0.75rem; position: relative; + margin: 1.5rem 0; + border: 1px solid var(--border); } .code-block pre { - color: #a5b4fc; - font-family: monospace; + font-family: 'Fira Code', monospace; + font-size: 0.9rem; + color: #94a3b8; + overflow-x: auto; } .copy-btn { @@ -328,35 +464,123 @@ background: var(--primary); color: white; border: none; - padding: 0.5rem 1rem; - border-radius: 0.25rem; - font-size: 0.8rem; + padding: 0.4rem 0.8rem; + border-radius: 4px; + font-size: 0.75rem; cursor: pointer; + transition: background 0.2s; +} + +.copy-btn:hover { + background: var(--primary-hover); } .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; + border-radius: 4px; + font-size: 0.9rem; + color: var(--text-muted); } -/* Utils */ -.loading { - text-align: center; - padding: 4rem; +.select-wrapper { + position: relative; + max-width: 240px; + margin-top: 1rem; +} + +.lang-select { + width: 100%; + padding: 0.75rem 1.25rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: 0.5rem; + color: var(--text-main); + font-size: 0.95rem; + cursor: pointer; + appearance: none; + transition: all 0.2s; + outline: none; +} + +.lang-select:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); +} + +.lang-select:focus { + border-color: var(--primary); + box-shadow: 0 0 15px rgba(99, 102, 241, 0.2); +} + +.select-wrapper::after { + content: '▼'; + font-size: 0.7rem; color: var(--text-muted); + position: absolute; + right: 1.25rem; + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} + +.lang-select option { + background: #1a1a1a; + color: var(--text-main); + padding: 1rem; } .empty-state { - text-align: center; - padding: 6rem 2rem; + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; color: var(--text-muted); + text-align: center; + padding: 4rem; } .empty-icon { - font-size: 4rem; margin-bottom: 1.5rem; - opacity: 0.5; + opacity: 0.3; + color: var(--text-muted); +} + +.empty-icon svg { + display: block; + margin: 0 auto; +} + +/* Common Buttons */ +.btn-approve { + background: var(--success); + color: white; + border: none; + padding: 0.6rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-approve:hover { + filter: brightness(1.1); + box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); +} + +.btn-reject { + background: transparent; + color: var(--danger); + border: 1px solid var(--danger); + padding: 0.6rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-reject:hover { + background: rgba(239, 68, 68, 0.1); } diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..e98b420 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,35 @@ +{ + "common": { + "dashboard": "Dashboard", + "configuration": "Configuration", + "version": "v{{version}}", + "loading": "Loading items...", + "status": "Live" + }, + "sidebar": { + "queues": "QUEUES", + "pending": "Pending Items", + "history": "Approved History", + "settings": "SETTINGS", + "mcp_servers": "MCP Servers", + "network": "Network & Security" + }, + "views": { + "pending_approvals": "Pending Approvals", + "mcp_settings": "MCP Connection Settings", + "empty": "Queue is empty. Everything is processed.", + "approve": "Approve & Send", + "reject": "Reject", + "rejection_comment": "Enter rejection feedback:", + "copy_json": "Copy JSON", + "copied": "Copied!", + "note": "Note", + "sse_note": "ExoLauncher is running as an SSE-based MCP gateway. This allows multiple clients to connect concurrently without managing local executable paths.", + "mcp_config_help": "Copy this snippet into your agent's mcp_config.json to connect to ExoLauncher." + }, + "settings": { + "language": "Language", + "en": "English", + "ja": "Japanese" + } +} diff --git a/src/locales/ja.json b/src/locales/ja.json new file mode 100644 index 0000000..7f6b735 --- /dev/null +++ b/src/locales/ja.json @@ -0,0 +1,35 @@ +{ + "common": { + "dashboard": "ダッシュボード", + "configuration": "システム設定", + "version": "v{{version}}", + "loading": "読み込み中...", + "status": "接続中" + }, + "sidebar": { + "queues": "キュー管理", + "pending": "承認待ち", + "history": "処理済み履歴", + "settings": "設定", + "mcp_servers": "MCP サーバー", + "network": "ネットワークとセキュリティ" + }, + "views": { + "pending_approvals": "承認待ちアイテム", + "mcp_settings": "MCP 接続設定", + "empty": "キューは空です。すべて処理されました。", + "approve": "承認して送信", + "reject": "却下", + "rejection_comment": "却下の理由を入力してください:", + "copy_json": "JSONをコピー", + "copied": "コピー完了!", + "note": "注記", + "sse_note": "ExoLauncherはSSE方式のMCPゲートウェイとして動作しています。これにより、ローカルパスの管理なしで複数のクライアントから同時接続が可能です。", + "mcp_config_help": "以下のコードをエージェントの設定にコピーして、ExoLauncherに接続してください。" + }, + "settings": { + "language": "表示言語", + "en": "英語", + "ja": "日本語" + } +} diff --git a/src/main.tsx b/src/main.tsx index 3d7150d..5296545 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' +import './i18n' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render(