diff --git a/.gitignore b/.gitignore index 1170717..bfe0826 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,23 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Electron-specific +dist/ +*.dmg +*.app +*.exe +*.deb +*.rpm +*.AppImage +*.snap + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..beff1e5 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,347 @@ +# Deep Assistant Desktop Application - Architecture + +## Overview + +The Deep Assistant Desktop Application is a cross-platform desktop client built with Electron, providing native access to the Deep Assistant AI service on macOS, Linux, and Windows. + +## Technology Stack + +- **Runtime**: Node.js 18+ +- **Framework**: Electron 28.x +- **Build Tool**: electron-builder +- **UI**: Native HTML5/CSS3/JavaScript (no framework dependencies) +- **Architecture**: Multi-process (Main + Renderer with Context Isolation) + +## Architecture Pattern + +### Electron Multi-Process Architecture + +The application follows Electron's recommended security architecture with three distinct processes: + +``` +┌─────────────────────────────────────────────────────────┐ +│ Main Process │ +│ (src/main.js) │ +│ │ +│ - Window Management │ +│ - Menu Creation │ +│ - IPC Handlers │ +│ - System Integration │ +└────────────────────┬────────────────────────────────────┘ + │ + │ IPC Communication + │ +┌────────────────────┴────────────────────────────────────┐ +│ Preload Script │ +│ (src/preload.js) │ +│ │ +│ - Secure IPC Bridge │ +│ - Context Bridge API │ +│ - sandboxed environment │ +└────────────────────┬────────────────────────────────────┘ + │ + │ electronAPI + │ +┌────────────────────┴────────────────────────────────────┐ +│ Renderer Process │ +│ (src/index.html + src/renderer.js) │ +│ │ +│ - User Interface │ +│ - Chat Management │ +│ - Settings UI │ +│ - Event Handling │ +└──────────────────────────────────────────────────────────┘ +``` + +### Security Model + +The application implements Electron's security best practices: + +1. **Context Isolation**: Enabled - Renderer process cannot directly access Node.js or Electron APIs +2. **Node Integration**: Disabled in renderer - Prevents potential XSS attacks +3. **Preload Script**: Acts as a secure bridge, exposing only necessary APIs +4. **Content Security Policy**: Enforced to prevent loading external scripts +5. **Sandboxing**: Renderer process runs in a restricted environment + +### IPC Communication Flow + +``` +User Action (Renderer) + ↓ +electronAPI.sendMessage() (Preload) + ↓ +ipcRenderer.invoke() (Secure Channel) + ↓ +ipcMain.handle() (Main Process) + ↓ +API Request (Future: HTTP to API Gateway) + ↓ +Response Processing (Main Process) + ↓ +Return to Renderer + ↓ +UI Update +``` + +## Components + +### Main Process (`src/main.js`) + +**Responsibilities:** +- Create and manage browser windows +- Handle application lifecycle events +- Create native menus +- Process IPC requests from renderer +- Future: HTTP API communication + +**Key Functions:** +- `createWindow()`: Initializes main application window with security settings +- `createMenu()`: Builds native application menu with keyboard shortcuts +- IPC Handlers: + - `get-api-config`: Retrieves stored API configuration + - `save-api-config`: Persists API settings + - `send-message`: Processes chat messages (placeholder for API integration) + +### Preload Script (`src/preload.js`) + +**Responsibilities:** +- Bridge between renderer and main processes +- Expose minimal, secure API to renderer +- Implement context bridge + +**Exposed API (`window.electronAPI`):** +- `getApiConfig()`: Fetch API configuration +- `saveApiConfig(config)`: Save API settings +- `sendMessage(data)`: Send chat message to API +- `onNewChat(callback)`: Listen for new chat menu events +- `onOpenSettings(callback)`: Listen for settings menu events + +### Renderer Process + +#### HTML (`src/index.html`) + +**Structure:** +- Sidebar: Chat history and settings access +- Main area: Message display and input +- Settings panel: Configuration overlay + +**Security:** +- Content Security Policy header +- No inline scripts or styles (except 'unsafe-inline' for styles, which could be improved) + +#### JavaScript (`src/renderer.js`) + +**State Management:** +- `apiConfig`: Stores API URL and key +- `currentChatId`: Tracks active conversation + +**Key Functions:** +- `init()`: Initializes application state and event listeners +- `handleSendMessage()`: Processes outgoing messages +- `addMessage()`: Renders messages to UI +- `handleNewChat()`: Resets conversation +- Settings management functions + +#### CSS (`src/styles.css`) + +**Design System:** +- CSS Variables for theming +- Dark mode by default +- Responsive layout with flexbox +- Custom scrollbar styling + +**Color Scheme:** +```css +--bg-primary: #1a1a1a (Main background) +--bg-secondary: #2d2d2d (Sidebar, panels) +--bg-tertiary: #3d3d3d (Hover states) +--accent-color: #0066cc (Interactive elements) +``` + +## Build System + +### electron-builder Configuration + +Located in `package.json` under `build` section: + +**Targets:** +- **macOS**: DMG and ZIP formats, requires ICNS icon +- **Linux**: AppImage, DEB, RPM packages +- **Windows**: NSIS installer and portable executable + +**Output:** +- Build artifacts saved to `dist/` directory +- Different formats for each platform + +### Scripts + +```json +"start": "electron ." // Run application +"dev": "electron . --dev" // Run with DevTools +"pack": "electron-builder --dir" // Build without packaging +"dist": "electron-builder" // Build for current platform +"dist:mac": "..." // Build for macOS +"dist:linux": "..." // Build for Linux +"dist:win": "..." // Build for Windows +"dist:all": "..." // Build for all platforms +``` + +## Data Flow + +### Message Sending Flow + +1. User types message in ` + + + + + + + + + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..d184a37 --- /dev/null +++ b/src/main.js @@ -0,0 +1,159 @@ +const { app, BrowserWindow, ipcMain, Menu } = require('electron'); +const path = require('path'); + +let mainWindow; + +// Default API gateway URL +const DEFAULT_API_URL = 'https://api.deep-assistant.com'; // Placeholder - will be configurable + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js') + }, + icon: path.join(__dirname, '../assets/icon.png') + }); + + // Load the index.html of the app + mainWindow.loadFile(path.join(__dirname, 'index.html')); + + // Open DevTools in development mode + if (process.argv.includes('--dev')) { + mainWindow.webContents.openDevTools(); + } + + // Create application menu + createMenu(); + + mainWindow.on('closed', function () { + mainWindow = null; + }); +} + +function createMenu() { + const template = [ + { + label: 'File', + submenu: [ + { + label: 'New Chat', + accelerator: 'CmdOrCtrl+N', + click: () => { + if (mainWindow) { + mainWindow.webContents.send('new-chat'); + } + } + }, + { type: 'separator' }, + { + label: 'Settings', + accelerator: 'CmdOrCtrl+,', + click: () => { + if (mainWindow) { + mainWindow.webContents.send('open-settings'); + } + } + }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' } + ] + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + { + label: 'Help', + submenu: [ + { + label: 'Documentation', + click: async () => { + const { shell } = require('electron'); + await shell.openExternal('https://github.com/deep-assistant/master-plan'); + } + }, + { + label: 'Report Issue', + click: async () => { + const { shell } = require('electron'); + await shell.openExternal('https://github.com/deep-assistant/master-plan/issues'); + } + } + ] + } + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +// This method will be called when Electron has finished initialization +app.whenReady().then(() => { + createWindow(); + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +// Quit when all windows are closed, except on macOS +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit(); +}); + +// IPC handlers for API requests +ipcMain.handle('get-api-config', async () => { + // In a full implementation, this would load from a config file + return { + apiUrl: DEFAULT_API_URL, + apiKey: '' // User will need to configure this + }; +}); + +ipcMain.handle('save-api-config', async (event, config) => { + // In a full implementation, this would save to a config file + return { success: true }; +}); + +ipcMain.handle('send-message', async (event, { message, apiUrl, apiKey }) => { + // This is a placeholder for API integration + // In a full implementation, this would: + // 1. Make HTTP request to the API gateway + // 2. Handle streaming responses + // 3. Return the response to the renderer + + return { + success: true, + message: 'API integration pending - this will connect to the API gateway' + }; +}); diff --git a/src/preload.js b/src/preload.js new file mode 100644 index 0000000..3de533d --- /dev/null +++ b/src/preload.js @@ -0,0 +1,11 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +// Expose protected methods that allow the renderer process to use +// the ipcRenderer without exposing the entire object +contextBridge.exposeInMainWorld('electronAPI', { + getApiConfig: () => ipcRenderer.invoke('get-api-config'), + saveApiConfig: (config) => ipcRenderer.invoke('save-api-config', config), + sendMessage: (data) => ipcRenderer.invoke('send-message', data), + onNewChat: (callback) => ipcRenderer.on('new-chat', callback), + onOpenSettings: (callback) => ipcRenderer.on('open-settings', callback) +}); diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 0000000..76bfb62 --- /dev/null +++ b/src/renderer.js @@ -0,0 +1,224 @@ +// DOM Elements +const messageInput = document.getElementById('message-input'); +const sendBtn = document.getElementById('send-btn'); +const messagesContainer = document.getElementById('messages'); +const newChatBtn = document.getElementById('new-chat-btn'); +const settingsBtn = document.getElementById('settings-btn'); +const settingsPanel = document.getElementById('settings-panel'); +const closeSettingsBtn = document.getElementById('close-settings-btn'); +const saveSettingsBtn = document.getElementById('save-settings-btn'); +const apiUrlInput = document.getElementById('api-url'); +const apiKeyInput = document.getElementById('api-key'); +const settingsStatus = document.getElementById('settings-status'); + +// State +let currentChatId = 1; +let apiConfig = { + apiUrl: '', + apiKey: '' +}; + +// Initialize +async function init() { + // Load API configuration + apiConfig = await window.electronAPI.getApiConfig(); + apiUrlInput.value = apiConfig.apiUrl || ''; + apiKeyInput.value = apiConfig.apiKey || ''; + + // Setup event listeners + setupEventListeners(); + + // Listen for menu events + window.electronAPI.onNewChat(() => handleNewChat()); + window.electronAPI.onOpenSettings(() => showSettings()); + + // Auto-resize textarea + messageInput.addEventListener('input', autoResizeTextarea); +} + +function setupEventListeners() { + sendBtn.addEventListener('click', handleSendMessage); + messageInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }); + + messageInput.addEventListener('input', () => { + sendBtn.disabled = messageInput.value.trim() === ''; + }); + + newChatBtn.addEventListener('click', handleNewChat); + settingsBtn.addEventListener('click', showSettings); + closeSettingsBtn.addEventListener('click', hideSettings); + saveSettingsBtn.addEventListener('click', handleSaveSettings); +} + +function autoResizeTextarea() { + messageInput.style.height = 'auto'; + messageInput.style.height = messageInput.scrollHeight + 'px'; +} + +async function handleSendMessage() { + const message = messageInput.value.trim(); + if (!message) return; + + // Check if API is configured + if (!apiConfig.apiUrl || !apiConfig.apiKey) { + showSettingsError('Please configure API settings first'); + showSettings(); + return; + } + + // Clear input + messageInput.value = ''; + messageInput.style.height = 'auto'; + sendBtn.disabled = true; + + // Remove welcome message if present + const welcomeMsg = messagesContainer.querySelector('.welcome-message'); + if (welcomeMsg) { + welcomeMsg.remove(); + } + + // Add user message to chat + addMessage(message, 'user'); + + // Add assistant loading message + const loadingId = 'loading-' + Date.now(); + addMessage('Thinking...', 'assistant', loadingId); + + try { + // Send message to API via IPC + const response = await window.electronAPI.sendMessage({ + message: message, + apiUrl: apiConfig.apiUrl, + apiKey: apiConfig.apiKey + }); + + // Remove loading message + const loadingElement = document.getElementById(loadingId); + if (loadingElement) { + loadingElement.remove(); + } + + // Add assistant response + if (response.success) { + addMessage(response.message, 'assistant'); + } else { + addMessage('Error: ' + (response.error || 'Failed to get response'), 'assistant'); + } + } catch (error) { + // Remove loading message + const loadingElement = document.getElementById(loadingId); + if (loadingElement) { + loadingElement.remove(); + } + + addMessage('Error: ' + error.message, 'assistant'); + } +} + +function addMessage(content, role, id) { + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${role}`; + if (id) { + messageDiv.id = id; + } + + const avatar = document.createElement('div'); + avatar.className = 'message-avatar'; + avatar.textContent = role === 'user' ? 'U' : 'A'; + + const messageContent = document.createElement('div'); + messageContent.className = 'message-content'; + messageContent.textContent = content; + + messageDiv.appendChild(avatar); + messageDiv.appendChild(messageContent); + + messagesContainer.appendChild(messageDiv); + + // Scroll to bottom + messagesContainer.scrollTop = messagesContainer.scrollHeight; +} + +function handleNewChat() { + // Clear messages + messagesContainer.innerHTML = ` +
+

Welcome to Deep Assistant

+

Start a conversation by typing a message below.

+
+ Note: Please configure your API settings before starting. +
+
+ `; + + currentChatId++; + + // In a full implementation, this would create a new chat history item +} + +function showSettings() { + settingsPanel.classList.remove('hidden'); + settingsPanel.classList.add('visible'); +} + +function hideSettings() { + settingsPanel.classList.remove('visible'); + setTimeout(() => { + settingsPanel.classList.add('hidden'); + }, 300); +} + +async function handleSaveSettings() { + const apiUrl = apiUrlInput.value.trim(); + const apiKey = apiKeyInput.value.trim(); + + if (!apiUrl || !apiKey) { + showSettingsError('Please fill in all fields'); + return; + } + + // Validate URL format + try { + new URL(apiUrl); + } catch (e) { + showSettingsError('Invalid API URL format'); + return; + } + + // Save configuration + apiConfig = { apiUrl, apiKey }; + const result = await window.electronAPI.saveApiConfig(apiConfig); + + if (result.success) { + showSettingsSuccess('Settings saved successfully'); + setTimeout(() => { + hideSettings(); + }, 1500); + } else { + showSettingsError('Failed to save settings'); + } +} + +function showSettingsSuccess(message) { + settingsStatus.textContent = message; + settingsStatus.className = 'status-message success'; + setTimeout(() => { + settingsStatus.className = 'status-message hidden'; + }, 3000); +} + +function showSettingsError(message) { + settingsStatus.textContent = message; + settingsStatus.className = 'status-message error'; + setTimeout(() => { + settingsStatus.className = 'status-message hidden'; + }, 3000); +} + +// Initialize when DOM is ready +init(); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..79f1f70 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,402 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-tertiary: #3d3d3d; + --text-primary: #ffffff; + --text-secondary: #b0b0b0; + --accent-color: #0066cc; + --accent-hover: #0052a3; + --border-color: #404040; + --success-color: #28a745; + --error-color: #dc3545; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + overflow: hidden; +} + +.app-container { + display: flex; + height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 260px; + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.sidebar-header h1 { + font-size: 18px; + font-weight: 600; +} + +.btn-icon { + background: none; + border: none; + color: var(--text-primary); + cursor: pointer; + padding: 8px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; +} + +.btn-icon:hover { + background-color: var(--bg-tertiary); +} + +.chat-history { + flex: 1; + overflow-y: auto; + padding: 10px; +} + +.chat-item { + padding: 12px 16px; + margin-bottom: 4px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; + font-size: 14px; + color: var(--text-secondary); +} + +.chat-item:hover { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +.chat-item.active { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-footer { + padding: 16px; + border-top: 1px solid var(--border-color); +} + +.btn-settings { + width: 100%; + padding: 12px; + background: none; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + transition: background-color 0.2s; +} + +.btn-settings:hover { + background-color: var(--bg-tertiary); +} + +/* Main Content */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; + position: relative; +} + +.chat-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.messages { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.welcome-message { + text-align: center; + padding: 60px 20px; + color: var(--text-secondary); +} + +.welcome-message h2 { + font-size: 28px; + margin-bottom: 12px; + color: var(--text-primary); +} + +.welcome-message p { + font-size: 16px; + margin-bottom: 24px; +} + +.info-notice { + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + max-width: 500px; + margin: 0 auto; + text-align: left; +} + +.message { + display: flex; + gap: 12px; + max-width: 800px; +} + +.message.user { + align-self: flex-end; + flex-direction: row-reverse; +} + +.message.assistant { + align-self: flex-start; +} + +.message-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.message-content { + background-color: var(--bg-secondary); + padding: 12px 16px; + border-radius: 12px; + line-height: 1.5; +} + +.message.user .message-content { + background-color: var(--accent-color); +} + +/* Input Area */ +.input-area { + border-top: 1px solid var(--border-color); + padding: 20px; +} + +.input-container { + display: flex; + gap: 12px; + align-items: flex-end; + max-width: 800px; + margin: 0 auto; +} + +#message-input { + flex: 1; + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 12px 16px; + color: var(--text-primary); + font-family: inherit; + font-size: 14px; + resize: none; + max-height: 200px; +} + +#message-input:focus { + outline: none; + border-color: var(--accent-color); +} + +.btn-send { + background-color: var(--accent-color); + border: none; + border-radius: 12px; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--text-primary); + transition: background-color 0.2s; +} + +.btn-send:hover:not(:disabled) { + background-color: var(--accent-hover); +} + +.btn-send:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Settings Panel */ +.settings-panel { + position: absolute; + top: 0; + right: 0; + width: 400px; + height: 100%; + background-color: var(--bg-secondary); + border-left: 1px solid var(--border-color); + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 0.3s ease; +} + +.settings-panel.visible { + transform: translateX(0); +} + +.settings-panel.hidden { + display: flex; +} + +.settings-header { + padding: 20px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.settings-header h2 { + font-size: 20px; +} + +.btn-close { + background: none; + border: none; + color: var(--text-primary); + cursor: pointer; + padding: 8px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; +} + +.btn-close:hover { + background-color: var(--bg-tertiary); +} + +.settings-content { + padding: 20px; + overflow-y: auto; +} + +.setting-group { + margin-bottom: 24px; +} + +.setting-group label { + display: block; + margin-bottom: 8px; + font-size: 14px; + font-weight: 500; +} + +.input-field { + width: 100%; + background-color: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 10px 12px; + color: var(--text-primary); + font-family: inherit; + font-size: 14px; +} + +.input-field:focus { + outline: none; + border-color: var(--accent-color); +} + +.btn-primary { + width: 100%; + background-color: var(--accent-color); + border: none; + border-radius: 8px; + padding: 12px; + color: var(--text-primary); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.btn-primary:hover { + background-color: var(--accent-hover); +} + +.status-message { + margin-top: 16px; + padding: 12px; + border-radius: 8px; + font-size: 14px; +} + +.status-message.success { + background-color: rgba(40, 167, 69, 0.2); + color: var(--success-color); + border: 1px solid var(--success-color); +} + +.status-message.error { + background-color: rgba(220, 53, 69, 0.2); + color: var(--error-color); + border: 1px solid var(--error-color); +} + +.status-message.hidden { + display: none; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +::-webkit-scrollbar-thumb { + background: var(--bg-tertiary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +}