From c7e88b07115a2ad0124e6eadd66eefb39904c824 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang Date: Thu, 25 Dec 2025 13:49:48 +0900 Subject: [PATCH] Remove Electron settings panel (breaking change) - Remove electron/settings/ folder - Remove config-manager.ts and settings-window.ts - Remove Configuration menu from app menu - Remove config-presets IPC handlers - Update README.md - Clean up electron.d.ts Note: This is a breaking change - users will lose existing presets --- README.md | 5 +- electron.d.ts | 47 +--- electron/main/app-menu.ts | 120 +------- electron/main/config-manager.ts | 460 ------------------------------- electron/main/index.ts | 9 - electron/main/ipc-handlers.ts | 125 --------- electron/main/settings-window.ts | 78 ------ electron/preload/settings.ts | 35 --- electron/settings/index.html | 116 -------- electron/settings/settings.css | 344 ----------------------- electron/settings/settings.js | 311 --------------------- package.json | 2 +- scripts/electron-dev.mjs | 156 +---------- 13 files changed, 15 insertions(+), 1793 deletions(-) delete mode 100644 electron/main/config-manager.ts delete mode 100644 electron/main/settings-window.ts delete mode 100644 electron/preload/settings.ts delete mode 100644 electron/settings/index.html delete mode 100644 electron/settings/settings.css delete mode 100644 electron/settings/settings.js diff --git a/README.md b/README.md index ec17213..0c385a3 100644 --- a/README.md +++ b/README.md @@ -147,14 +147,13 @@ Download the native desktop app for your platform from the [Releases page](https | Linux | `.AppImage` or `.deb` (x64 & ARM64) | **Features:** -- **Secure API key storage**: Credentials encrypted using OS keychain -- **Configuration presets**: Save and switch between AI providers via menu - **Native file dialogs**: Open/save `.drawio` files directly - **Offline capable**: Works without internet after first launch +- **Built-in settings**: Configure AI providers directly in the app **Quick Setup:** 1. Download and install for your platform -2. Open the app → **Menu → Configuration → Manage Presets** +2. Click the settings icon in the chat panel 3. Add your AI provider credentials 4. Start creating diagrams! diff --git a/electron.d.ts b/electron.d.ts index a629e9b..9fa0d81 100644 --- a/electron.d.ts +++ b/electron.d.ts @@ -2,29 +2,6 @@ * Type declarations for Electron API exposed via preload script */ -/** Configuration preset interface */ -interface ConfigPreset { - id: string - name: string - createdAt: number - updatedAt: number - config: { - AI_PROVIDER?: string - AI_MODEL?: string - AI_API_KEY?: string - AI_BASE_URL?: string - TEMPERATURE?: string - [key: string]: string | undefined - } -} - -/** Result of applying a preset */ -interface ApplyPresetResult { - success: boolean - error?: string - env?: Record -} - declare global { interface Window { /** Main window Electron API */ @@ -46,29 +23,7 @@ declare global { /** Save data to file via save dialog */ saveFile: (data: string) => Promise } - - /** Settings window Electron API */ - settingsAPI?: { - /** Get all configuration presets */ - getPresets: () => Promise - /** Get current preset ID */ - getCurrentPresetId: () => Promise - /** Get current preset */ - getCurrentPreset: () => Promise - /** Save (create or update) a preset */ - savePreset: (preset: { - id?: string - name: string - config: Record - }) => Promise - /** Delete a preset */ - deletePreset: (id: string) => Promise - /** Apply a preset (sets environment variables and restarts server) */ - applyPreset: (id: string) => Promise - /** Close settings window */ - close: () => void - } } } -export { ConfigPreset, ApplyPresetResult } +export {} diff --git a/electron/main/app-menu.ts b/electron/main/app-menu.ts index 4a99a85..85a8979 100644 --- a/electron/main/app-menu.ts +++ b/electron/main/app-menu.ts @@ -1,19 +1,4 @@ -import { - app, - BrowserWindow, - dialog, - Menu, - type MenuItemConstructorOptions, - shell, -} from "electron" -import { - applyPresetToEnv, - getAllPresets, - getCurrentPresetId, - setCurrentPreset, -} from "./config-manager" -import { restartNextServer } from "./next-server" -import { showSettingsWindow } from "./settings-window" +import { app, Menu, type MenuItemConstructorOptions, shell } from "electron" /** * Build and set the application menu @@ -24,13 +9,6 @@ export function buildAppMenu(): void { Menu.setApplicationMenu(menu) } -/** - * Rebuild the menu (call this when presets change) - */ -export function rebuildAppMenu(): void { - buildAppMenu() -} - /** * Get the menu template */ @@ -46,15 +24,6 @@ function getMenuTemplate(): MenuItemConstructorOptions[] { submenu: [ { role: "about" }, { type: "separator" }, - { - label: "Settings...", - accelerator: "CmdOrCtrl+,", - click: () => { - const win = BrowserWindow.getFocusedWindow() - showSettingsWindow(win || undefined) - }, - }, - { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, @@ -69,22 +38,7 @@ function getMenuTemplate(): MenuItemConstructorOptions[] { // File menu template.push({ label: "File", - submenu: [ - ...(isMac - ? [] - : [ - { - label: "Settings", - accelerator: "CmdOrCtrl+,", - click: () => { - const win = BrowserWindow.getFocusedWindow() - showSettingsWindow(win || undefined) - }, - }, - { type: "separator" } as MenuItemConstructorOptions, - ]), - isMac ? { role: "close" } : { role: "quit" }, - ], + submenu: [isMac ? { role: "close" } : { role: "quit" }], }) // Edit menu @@ -129,9 +83,6 @@ function getMenuTemplate(): MenuItemConstructorOptions[] { ], }) - // Configuration menu with presets - template.push(buildConfigMenu()) - // Window menu template.push({ label: "Window", @@ -172,70 +123,3 @@ function getMenuTemplate(): MenuItemConstructorOptions[] { return template } - -/** - * Build the Configuration menu with presets - */ -function buildConfigMenu(): MenuItemConstructorOptions { - const presets = getAllPresets() - const currentPresetId = getCurrentPresetId() - - const presetItems: MenuItemConstructorOptions[] = presets.map((preset) => ({ - label: preset.name, - type: "radio", - checked: preset.id === currentPresetId, - click: async () => { - const previousPresetId = getCurrentPresetId() - const env = applyPresetToEnv(preset.id) - - if (env) { - try { - await restartNextServer() - rebuildAppMenu() // Rebuild menu to update checkmarks - } catch (error) { - console.error("Failed to restart server:", error) - - // Revert to previous preset on failure - if (previousPresetId) { - applyPresetToEnv(previousPresetId) - } else { - setCurrentPreset(null) - } - - // Rebuild menu to restore previous checkmark state - rebuildAppMenu() - - // Show error dialog to notify user - dialog.showErrorBox( - "Configuration Error", - `Failed to apply preset "${preset.name}". The server could not be restarted.\n\nThe previous configuration has been restored.\n\nError: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - }, - })) - - return { - label: "Configuration", - submenu: [ - ...(presetItems.length > 0 - ? [ - { label: "Switch Preset", enabled: false }, - { type: "separator" } as MenuItemConstructorOptions, - ...presetItems, - { type: "separator" } as MenuItemConstructorOptions, - ] - : []), - { - label: - presetItems.length > 0 - ? "Manage Presets..." - : "Add Configuration Preset...", - click: () => { - const win = BrowserWindow.getFocusedWindow() - showSettingsWindow(win || undefined) - }, - }, - ], - } -} diff --git a/electron/main/config-manager.ts b/electron/main/config-manager.ts deleted file mode 100644 index 2a66d35..0000000 --- a/electron/main/config-manager.ts +++ /dev/null @@ -1,460 +0,0 @@ -import { randomUUID } from "node:crypto" -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" -import path from "node:path" -import { app, safeStorage } from "electron" - -/** - * Fields that contain sensitive data and should be encrypted - */ -const SENSITIVE_FIELDS = ["AI_API_KEY"] as const - -/** - * Prefix to identify encrypted values - */ -const ENCRYPTED_PREFIX = "encrypted:" - -/** - * Check if safeStorage encryption is available - */ -function isEncryptionAvailable(): boolean { - return safeStorage.isEncryptionAvailable() -} - -/** - * Track if we've already warned about plaintext storage - */ -let hasWarnedAboutPlaintext = false - -/** - * Encrypt a sensitive value using safeStorage - * Warns if encryption is not available (API key stored in plaintext) - */ -function encryptValue(value: string): string { - if (!value) { - return value - } - - if (!isEncryptionAvailable()) { - if (!hasWarnedAboutPlaintext) { - console.warn( - "⚠️ SECURITY WARNING: safeStorage not available. " + - "API keys will be stored in PLAINTEXT. " + - "On Linux, install gnome-keyring or similar for secure storage.", - ) - hasWarnedAboutPlaintext = true - } - return value - } - - try { - const encrypted = safeStorage.encryptString(value) - return ENCRYPTED_PREFIX + encrypted.toString("base64") - } catch (error) { - console.error("Encryption failed:", error) - // Fail secure: don't store if encryption fails - throw new Error( - "Failed to encrypt API key. Cannot securely store credentials.", - ) - } -} - -/** - * Decrypt a sensitive value using safeStorage - * Returns the original value if it's not encrypted or decryption fails - */ -function decryptValue(value: string): string { - if (!value || !value.startsWith(ENCRYPTED_PREFIX)) { - return value - } - if (!isEncryptionAvailable()) { - console.warn( - "Cannot decrypt value: safeStorage encryption is not available", - ) - return value - } - try { - const base64Data = value.slice(ENCRYPTED_PREFIX.length) - const buffer = Buffer.from(base64Data, "base64") - return safeStorage.decryptString(buffer) - } catch (error) { - console.error("Failed to decrypt value:", error) - return value - } -} - -/** - * Encrypt sensitive fields in a config object - */ -function encryptConfig( - config: Record, -): Record { - const encrypted = { ...config } - for (const field of SENSITIVE_FIELDS) { - if (encrypted[field]) { - encrypted[field] = encryptValue(encrypted[field] as string) - } - } - return encrypted -} - -/** - * Decrypt sensitive fields in a config object - */ -function decryptConfig( - config: Record, -): Record { - const decrypted = { ...config } - for (const field of SENSITIVE_FIELDS) { - if (decrypted[field]) { - decrypted[field] = decryptValue(decrypted[field] as string) - } - } - return decrypted -} - -/** - * Configuration preset interface - */ -export interface ConfigPreset { - id: string - name: string - createdAt: number - updatedAt: number - config: { - AI_PROVIDER?: string - AI_MODEL?: string - AI_API_KEY?: string - AI_BASE_URL?: string - TEMPERATURE?: string - [key: string]: string | undefined - } -} - -/** - * Configuration file structure - */ -interface ConfigPresetsFile { - version: 1 - currentPresetId: string | null - presets: ConfigPreset[] -} - -const CONFIG_FILE_NAME = "config-presets.json" - -/** - * Get the path to the config file - */ -function getConfigFilePath(): string { - const userDataPath = app.getPath("userData") - return path.join(userDataPath, CONFIG_FILE_NAME) -} - -/** - * Load presets from the config file - * Decrypts sensitive fields automatically - */ -export function loadPresets(): ConfigPresetsFile { - const configPath = getConfigFilePath() - - if (!existsSync(configPath)) { - return { - version: 1, - currentPresetId: null, - presets: [], - } - } - - try { - const content = readFileSync(configPath, "utf-8") - const data = JSON.parse(content) as ConfigPresetsFile - - // Decrypt sensitive fields in each preset - data.presets = data.presets.map((preset) => ({ - ...preset, - config: decryptConfig(preset.config) as ConfigPreset["config"], - })) - - return data - } catch (error) { - console.error("Failed to load config presets:", error) - return { - version: 1, - currentPresetId: null, - presets: [], - } - } -} - -/** - * Save presets to the config file - * Encrypts sensitive fields automatically - */ -export function savePresets(data: ConfigPresetsFile): void { - const configPath = getConfigFilePath() - const userDataPath = app.getPath("userData") - - // Ensure the directory exists - if (!existsSync(userDataPath)) { - mkdirSync(userDataPath, { recursive: true }) - } - - // Encrypt sensitive fields before saving - const dataToSave: ConfigPresetsFile = { - ...data, - presets: data.presets.map((preset) => ({ - ...preset, - config: encryptConfig(preset.config) as ConfigPreset["config"], - })), - } - - try { - writeFileSync(configPath, JSON.stringify(dataToSave, null, 2), "utf-8") - } catch (error) { - console.error("Failed to save config presets:", error) - throw error - } -} - -/** - * Get all presets - */ -export function getAllPresets(): ConfigPreset[] { - const data = loadPresets() - return data.presets -} - -/** - * Get current preset ID - */ -export function getCurrentPresetId(): string | null { - const data = loadPresets() - return data.currentPresetId -} - -/** - * Get current preset - */ -export function getCurrentPreset(): ConfigPreset | null { - const data = loadPresets() - if (!data.currentPresetId) { - return null - } - return data.presets.find((p) => p.id === data.currentPresetId) || null -} - -/** - * Create a new preset - */ -export function createPreset( - preset: Omit, -): ConfigPreset { - const data = loadPresets() - const now = Date.now() - - const newPreset: ConfigPreset = { - id: randomUUID(), - name: preset.name, - config: preset.config, - createdAt: now, - updatedAt: now, - } - - data.presets.push(newPreset) - savePresets(data) - - return newPreset -} - -/** - * Update an existing preset - */ -export function updatePreset( - id: string, - updates: Partial>, -): ConfigPreset | null { - const data = loadPresets() - const index = data.presets.findIndex((p) => p.id === id) - - if (index === -1) { - return null - } - - const updatedPreset: ConfigPreset = { - ...data.presets[index], - ...updates, - updatedAt: Date.now(), - } - - data.presets[index] = updatedPreset - savePresets(data) - - return updatedPreset -} - -/** - * Delete a preset - */ -export function deletePreset(id: string): boolean { - const data = loadPresets() - const index = data.presets.findIndex((p) => p.id === id) - - if (index === -1) { - return false - } - - data.presets.splice(index, 1) - - // Clear current preset if it was deleted - if (data.currentPresetId === id) { - data.currentPresetId = null - } - - savePresets(data) - return true -} - -/** - * Set the current preset - */ -export function setCurrentPreset(id: string | null): boolean { - const data = loadPresets() - - if (id !== null) { - const preset = data.presets.find((p) => p.id === id) - if (!preset) { - return false - } - } - - data.currentPresetId = id - savePresets(data) - return true -} - -/** - * Map generic AI_API_KEY and AI_BASE_URL to provider-specific environment variables - */ -const PROVIDER_ENV_MAP: Record = { - openai: { apiKey: "OPENAI_API_KEY", baseUrl: "OPENAI_BASE_URL" }, - anthropic: { apiKey: "ANTHROPIC_API_KEY", baseUrl: "ANTHROPIC_BASE_URL" }, - google: { - apiKey: "GOOGLE_GENERATIVE_AI_API_KEY", - baseUrl: "GOOGLE_BASE_URL", - }, - azure: { apiKey: "AZURE_API_KEY", baseUrl: "AZURE_BASE_URL" }, - openrouter: { - apiKey: "OPENROUTER_API_KEY", - baseUrl: "OPENROUTER_BASE_URL", - }, - deepseek: { apiKey: "DEEPSEEK_API_KEY", baseUrl: "DEEPSEEK_BASE_URL" }, - siliconflow: { - apiKey: "SILICONFLOW_API_KEY", - baseUrl: "SILICONFLOW_BASE_URL", - }, - gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_BASE_URL" }, - // bedrock and ollama don't use API keys in the same way - bedrock: { apiKey: "", baseUrl: "" }, - ollama: { apiKey: "", baseUrl: "OLLAMA_BASE_URL" }, -} - -/** - * Apply preset environment variables to the current process - * Returns the environment variables that were applied - */ -export function applyPresetToEnv(id: string): Record | null { - const data = loadPresets() - const preset = data.presets.find((p) => p.id === id) - - if (!preset) { - return null - } - - const appliedEnv: Record = {} - const provider = preset.config.AI_PROVIDER?.toLowerCase() - - for (const [key, value] of Object.entries(preset.config)) { - if (value !== undefined && value !== "") { - // Map generic AI_API_KEY to provider-specific key - if ( - key === "AI_API_KEY" && - provider && - PROVIDER_ENV_MAP[provider] - ) { - const providerApiKey = PROVIDER_ENV_MAP[provider].apiKey - if (providerApiKey) { - process.env[providerApiKey] = value - appliedEnv[providerApiKey] = value - } - } - // Map generic AI_BASE_URL to provider-specific key - else if ( - key === "AI_BASE_URL" && - provider && - PROVIDER_ENV_MAP[provider] - ) { - const providerBaseUrl = PROVIDER_ENV_MAP[provider].baseUrl - if (providerBaseUrl) { - process.env[providerBaseUrl] = value - appliedEnv[providerBaseUrl] = value - } - } - // Apply other env vars directly - else { - process.env[key] = value - appliedEnv[key] = value - } - } - } - - // Set as current preset - data.currentPresetId = id - savePresets(data) - - return appliedEnv -} - -/** - * Get environment variables from current preset - * Maps generic AI_API_KEY/AI_BASE_URL to provider-specific keys - */ -export function getCurrentPresetEnv(): Record { - const preset = getCurrentPreset() - if (!preset) { - return {} - } - - const env: Record = {} - const provider = preset.config.AI_PROVIDER?.toLowerCase() - - for (const [key, value] of Object.entries(preset.config)) { - if (value !== undefined && value !== "") { - // Map generic AI_API_KEY to provider-specific key - if ( - key === "AI_API_KEY" && - provider && - PROVIDER_ENV_MAP[provider] - ) { - const providerApiKey = PROVIDER_ENV_MAP[provider].apiKey - if (providerApiKey) { - env[providerApiKey] = value - } - } - // Map generic AI_BASE_URL to provider-specific key - else if ( - key === "AI_BASE_URL" && - provider && - PROVIDER_ENV_MAP[provider] - ) { - const providerBaseUrl = PROVIDER_ENV_MAP[provider].baseUrl - if (providerBaseUrl) { - env[providerBaseUrl] = value - } - } - // Apply other env vars directly - else { - env[key] = value - } - } - } - return env -} diff --git a/electron/main/index.ts b/electron/main/index.ts index 60254de..4654651 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,10 +1,8 @@ import { app, BrowserWindow, dialog, shell } from "electron" import { buildAppMenu } from "./app-menu" -import { getCurrentPresetEnv } from "./config-manager" import { loadEnvFile } from "./env-loader" import { registerIpcHandlers } from "./ipc-handlers" import { startNextServer, stopNextServer } from "./next-server" -import { registerSettingsWindowHandlers } from "./settings-window" import { createWindow, getMainWindow } from "./window-manager" // Single instance lock @@ -24,19 +22,12 @@ if (!gotTheLock) { // Load environment variables from .env files loadEnvFile() - // Apply saved preset environment variables (overrides .env) - const presetEnv = getCurrentPresetEnv() - for (const [key, value] of Object.entries(presetEnv)) { - process.env[key] = value - } - const isDev = process.env.NODE_ENV === "development" let serverUrl: string | null = null app.whenReady().then(async () => { // Register IPC handlers registerIpcHandlers() - registerSettingsWindowHandlers() // Build application menu buildAppMenu() diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 7e801d1..2aa41eb 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -1,43 +1,4 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron" -import { - applyPresetToEnv, - type ConfigPreset, - createPreset, - deletePreset, - getAllPresets, - getCurrentPreset, - getCurrentPresetId, - setCurrentPreset, - updatePreset, -} from "./config-manager" -import { restartNextServer } from "./next-server" - -/** - * Allowed configuration keys for presets - * This whitelist prevents arbitrary environment variable injection - */ -const ALLOWED_CONFIG_KEYS = new Set([ - "AI_PROVIDER", - "AI_MODEL", - "AI_API_KEY", - "AI_BASE_URL", - "TEMPERATURE", -]) - -/** - * Sanitize preset config to only include allowed keys - */ -function sanitizePresetConfig( - config: Record, -): Record { - const sanitized: Record = {} - for (const key of ALLOWED_CONFIG_KEYS) { - if (key in config && typeof config[key] === "string") { - sanitized[key] = config[key] - } - } - return sanitized -} /** * Register all IPC handlers @@ -123,90 +84,4 @@ export function registerIpcHandlers(): void { return false } }) - - // ==================== Config Presets ==================== - - ipcMain.handle("config-presets:get-all", () => { - return getAllPresets() - }) - - ipcMain.handle("config-presets:get-current", () => { - return getCurrentPreset() - }) - - ipcMain.handle("config-presets:get-current-id", () => { - return getCurrentPresetId() - }) - - ipcMain.handle( - "config-presets:save", - ( - _event, - preset: Omit & { - id?: string - }, - ) => { - // Validate preset name - if (typeof preset.name !== "string" || !preset.name.trim()) { - throw new Error("Invalid preset name") - } - - // Sanitize config to only allow whitelisted keys - const sanitizedConfig = sanitizePresetConfig(preset.config ?? {}) - - if (preset.id) { - // Update existing preset - return updatePreset(preset.id, { - name: preset.name.trim(), - config: sanitizedConfig, - }) - } - // Create new preset - return createPreset({ - name: preset.name.trim(), - config: sanitizedConfig, - }) - }, - ) - - ipcMain.handle("config-presets:delete", (_event, id: string) => { - return deletePreset(id) - }) - - ipcMain.handle("config-presets:apply", async (_event, id: string) => { - const env = applyPresetToEnv(id) - if (!env) { - return { success: false, error: "Preset not found" } - } - - const isDev = process.env.NODE_ENV === "development" - - if (isDev) { - // In development mode, the config file change will trigger - // the file watcher in electron-dev.mjs to restart Next.js - // We just need to save the preset (already done in applyPresetToEnv) - return { success: true, env, devMode: true } - } - - // Production mode: restart the Next.js server to apply new environment variables - try { - await restartNextServer() - return { success: true, env } - } catch (error) { - return { - success: false, - error: - error instanceof Error - ? error.message - : "Failed to restart server", - } - } - }) - - ipcMain.handle( - "config-presets:set-current", - (_event, id: string | null) => { - return setCurrentPreset(id) - }, - ) } diff --git a/electron/main/settings-window.ts b/electron/main/settings-window.ts deleted file mode 100644 index b5ee490..0000000 --- a/electron/main/settings-window.ts +++ /dev/null @@ -1,78 +0,0 @@ -import path from "node:path" -import { app, BrowserWindow, ipcMain } from "electron" - -let settingsWindow: BrowserWindow | null = null - -/** - * Create and show the settings window - */ -export function showSettingsWindow(parentWindow?: BrowserWindow): void { - // If settings window already exists, focus it - if (settingsWindow && !settingsWindow.isDestroyed()) { - settingsWindow.focus() - return - } - - // Determine path to settings preload script - // In compiled output: dist-electron/preload/settings.js - const preloadPath = path.join(__dirname, "..", "preload", "settings.js") - - // Determine path to settings HTML - // In packaged app: app.asar/dist-electron/settings/index.html - // In development: electron/settings/index.html - const settingsHtmlPath = app.isPackaged - ? path.join(__dirname, "..", "settings", "index.html") - : path.join(__dirname, "..", "..", "electron", "settings", "index.html") - - settingsWindow = new BrowserWindow({ - width: 600, - height: 700, - minWidth: 500, - minHeight: 500, - parent: parentWindow, - modal: false, - show: false, - title: "Settings - Next AI Draw.io", - webPreferences: { - preload: preloadPath, - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - }, - }) - settingsWindow.loadFile(settingsHtmlPath) - - settingsWindow.once("ready-to-show", () => { - settingsWindow?.show() - }) - - settingsWindow.on("closed", () => { - settingsWindow = null - }) -} - -/** - * Close the settings window if it exists - */ -export function closeSettingsWindow(): void { - if (settingsWindow && !settingsWindow.isDestroyed()) { - settingsWindow.close() - settingsWindow = null - } -} - -/** - * Check if settings window is open - */ -export function isSettingsWindowOpen(): boolean { - return settingsWindow !== null && !settingsWindow.isDestroyed() -} - -/** - * Register settings window IPC handlers - */ -export function registerSettingsWindowHandlers(): void { - ipcMain.on("settings:close", () => { - closeSettingsWindow() - }) -} diff --git a/electron/preload/settings.ts b/electron/preload/settings.ts deleted file mode 100644 index d3fe011..0000000 --- a/electron/preload/settings.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Preload script for settings window - * Exposes APIs for managing configuration presets - */ -import { contextBridge, ipcRenderer } from "electron" - -// Expose settings API to the renderer process -contextBridge.exposeInMainWorld("settingsAPI", { - // Get all presets - getPresets: () => ipcRenderer.invoke("config-presets:get-all"), - - // Get current preset ID - getCurrentPresetId: () => - ipcRenderer.invoke("config-presets:get-current-id"), - - // Get current preset - getCurrentPreset: () => ipcRenderer.invoke("config-presets:get-current"), - - // Save (create or update) a preset - savePreset: (preset: { - id?: string - name: string - config: Record - }) => ipcRenderer.invoke("config-presets:save", preset), - - // Delete a preset - deletePreset: (id: string) => - ipcRenderer.invoke("config-presets:delete", id), - - // Apply a preset (sets environment variables and restarts server) - applyPreset: (id: string) => ipcRenderer.invoke("config-presets:apply", id), - - // Close settings window - close: () => ipcRenderer.send("settings:close"), -}) diff --git a/electron/settings/index.html b/electron/settings/index.html deleted file mode 100644 index 81ad8ff..0000000 --- a/electron/settings/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - Settings - Next AI Draw.io - - - -
-
- ⚠️ Deprecation Notice -

This settings panel will be removed in a future update.

-

Please use the AI Model Configuration button (left of the Send button in the chat panel) to configure your AI providers. Your settings there will persist across updates.

-
- -

Configuration Presets

- -
-

Presets

-
- -
- -
-
- - - - - - - - -
- - - - diff --git a/electron/settings/settings.css b/electron/settings/settings.css deleted file mode 100644 index 32fbd18..0000000 --- a/electron/settings/settings.css +++ /dev/null @@ -1,344 +0,0 @@ -:root { - --bg-primary: #ffffff; - --bg-secondary: #f5f5f5; - --bg-hover: #e8e8e8; - --text-primary: #1a1a1a; - --text-secondary: #666666; - --border-color: #e0e0e0; - --accent-color: #0066cc; - --accent-hover: #0052a3; - --danger-color: #dc3545; - --success-color: #28a745; -} - -@media (prefers-color-scheme: dark) { - :root { - --bg-primary: #1a1a1a; - --bg-secondary: #2d2d2d; - --bg-hover: #3d3d3d; - --text-primary: #ffffff; - --text-secondary: #a0a0a0; - --border-color: #404040; - --accent-color: #4da6ff; - --accent-hover: #66b3ff; - } -} - -.deprecation-notice { - background-color: #fff3cd; - border: 1px solid #ffc107; - border-radius: 8px; - padding: 16px; - margin-bottom: 20px; -} - -.deprecation-notice strong { - color: #856404; - display: block; - margin-bottom: 8px; - font-size: 14px; -} - -.deprecation-notice p { - color: #856404; - font-size: 13px; - margin: 4px 0; -} - -@media (prefers-color-scheme: dark) { - .deprecation-notice { - background-color: #332701; - border-color: #665200; - } - - .deprecation-notice strong, - .deprecation-notice p { - color: #ffc107; - } -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - sans-serif; - background-color: var(--bg-primary); - color: var(--text-primary); - line-height: 1.5; -} - -.container { - max-width: 560px; - margin: 0 auto; - padding: 24px; -} - -h1 { - font-size: 24px; - font-weight: 600; - margin-bottom: 24px; - padding-bottom: 16px; - border-bottom: 1px solid var(--border-color); -} - -h2 { - font-size: 16px; - font-weight: 600; - margin-bottom: 16px; - color: var(--text-secondary); -} - -.section { - margin-bottom: 32px; -} - -.preset-list { - display: flex; - flex-direction: column; - gap: 12px; - margin-bottom: 16px; -} - -.preset-card { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 16px; - cursor: pointer; - transition: all 0.2s ease; -} - -.preset-card:hover { - background: var(--bg-hover); -} - -.preset-card.active { - border-color: var(--accent-color); - box-shadow: 0 0 0 1px var(--accent-color); -} - -.preset-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.preset-name { - font-weight: 600; - font-size: 15px; -} - -.preset-badge { - background: var(--accent-color); - color: white; - font-size: 11px; - padding: 2px 8px; - border-radius: 10px; -} - -.preset-info { - font-size: 13px; - color: var(--text-secondary); -} - -.preset-actions { - display: flex; - gap: 8px; - margin-top: 12px; -} - -.btn { - padding: 8px 16px; - border: none; - border-radius: 6px; - font-size: 14px; - cursor: pointer; - transition: all 0.2s ease; - font-weight: 500; -} - -.btn-primary { - background: var(--accent-color); - color: white; -} - -.btn-primary:hover { - background: var(--accent-hover); -} - -.btn-secondary { - background: var(--bg-secondary); - color: var(--text-primary); - border: 1px solid var(--border-color); -} - -.btn-secondary:hover { - background: var(--bg-hover); -} - -.btn-danger { - background: var(--danger-color); - color: white; -} - -.btn-danger:hover { - opacity: 0.9; -} - -.btn-sm { - padding: 6px 12px; - font-size: 13px; -} - -.empty-state { - text-align: center; - padding: 40px 20px; - color: var(--text-secondary); -} - -.empty-state p { - margin-bottom: 16px; -} - -/* Modal */ -.modal-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 100; - align-items: center; - justify-content: center; -} - -.modal-overlay.show { - display: flex; -} - -.modal { - background: var(--bg-primary); - border-radius: 12px; - width: 90%; - max-width: 480px; - max-height: 90vh; - overflow-y: auto; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); -} - -.modal-header { - padding: 20px 24px; - border-bottom: 1px solid var(--border-color); -} - -.modal-header h3 { - font-size: 18px; - font-weight: 600; -} - -.modal-body { - padding: 24px; -} - -.modal-footer { - padding: 16px 24px; - border-top: 1px solid var(--border-color); - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - font-size: 14px; - font-weight: 500; - margin-bottom: 6px; -} - -.form-group input, -.form-group select { - width: 100%; - padding: 10px 12px; - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 14px; - background: var(--bg-primary); - color: var(--text-primary); -} - -.form-group input:focus, -.form-group select:focus { - outline: none; - border-color: var(--accent-color); - box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2); -} - -.form-group .hint { - font-size: 12px; - color: var(--text-secondary); - margin-top: 4px; -} - -.loading { - display: inline-block; - width: 16px; - height: 16px; - border: 2px solid var(--border-color); - border-top-color: var(--accent-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.toast { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: var(--text-primary); - color: var(--bg-primary); - padding: 12px 24px; - border-radius: 8px; - font-size: 14px; - z-index: 200; - opacity: 0; - transition: opacity 0.3s ease; -} - -.toast.show { - opacity: 1; -} - -.toast.success { - background: var(--success-color); - color: white; -} - -.toast.error { - background: var(--danger-color); - color: white; -} - -/* Inline style replacements */ -.delete-warning { - color: var(--text-secondary); - margin-top: 8px; - font-size: 14px; -} diff --git a/electron/settings/settings.js b/electron/settings/settings.js deleted file mode 100644 index 3e550c9..0000000 --- a/electron/settings/settings.js +++ /dev/null @@ -1,311 +0,0 @@ -// Settings page JavaScript -// This file handles the UI interactions for the settings window - -let presets = [] -let currentPresetId = null -let editingPresetId = null -let deletingPresetId = null - -// DOM Elements -const presetList = document.getElementById("preset-list") -const addPresetBtn = document.getElementById("add-preset-btn") -const presetModal = document.getElementById("preset-modal") -const deleteModal = document.getElementById("delete-modal") -const presetForm = document.getElementById("preset-form") -const modalTitle = document.getElementById("modal-title") -const toast = document.getElementById("toast") - -// Form fields -const presetIdField = document.getElementById("preset-id") -const presetNameField = document.getElementById("preset-name") -const aiProviderField = document.getElementById("ai-provider") -const aiModelField = document.getElementById("ai-model") -const aiApiKeyField = document.getElementById("ai-api-key") -const aiBaseUrlField = document.getElementById("ai-base-url") -const temperatureField = document.getElementById("temperature") - -// Buttons -const cancelBtn = document.getElementById("cancel-btn") -const saveBtn = document.getElementById("save-btn") -const deleteCancelBtn = document.getElementById("delete-cancel-btn") -const deleteConfirmBtn = document.getElementById("delete-confirm-btn") - -// Initialize -document.addEventListener("DOMContentLoaded", async () => { - await loadPresets() - setupEventListeners() -}) - -// Load presets from main process -async function loadPresets() { - try { - presets = await window.settingsAPI.getPresets() - currentPresetId = await window.settingsAPI.getCurrentPresetId() - renderPresets() - } catch (error) { - console.error("Failed to load presets:", error) - showToast("Failed to load presets", "error") - } -} - -// Render presets list -function renderPresets() { - if (presets.length === 0) { - presetList.innerHTML = ` -
-

No presets configured yet.

-

Add a preset to quickly switch between different AI configurations.

-
- ` - return - } - - presetList.innerHTML = presets - .map((preset) => { - const isActive = preset.id === currentPresetId - const providerLabel = getProviderLabel(preset.config.AI_PROVIDER) - - return ` -
-
- ${escapeHtml(preset.name)} - ${isActive ? 'Active' : ""} -
-
- ${providerLabel ? `Provider: ${providerLabel}` : "No provider configured"} - ${preset.config.AI_MODEL ? ` • Model: ${escapeHtml(preset.config.AI_MODEL)}` : ""} -
-
- ${!isActive ? `` : ""} - - -
-
- ` - }) - .join("") - - // Add event listeners to buttons - presetList.querySelectorAll(".apply-btn").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation() - applyPreset(btn.dataset.id) - }) - }) - - presetList.querySelectorAll(".edit-btn").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation() - openEditModal(btn.dataset.id) - }) - }) - - presetList.querySelectorAll(".delete-btn").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation() - openDeleteModal(btn.dataset.id) - }) - }) -} - -// Setup event listeners -function setupEventListeners() { - addPresetBtn.addEventListener("click", () => openAddModal()) - cancelBtn.addEventListener("click", () => closeModal()) - saveBtn.addEventListener("click", () => savePreset()) - deleteCancelBtn.addEventListener("click", () => closeDeleteModal()) - deleteConfirmBtn.addEventListener("click", () => confirmDelete()) - - // Close modal on overlay click - presetModal.addEventListener("click", (e) => { - if (e.target === presetModal) closeModal() - }) - deleteModal.addEventListener("click", (e) => { - if (e.target === deleteModal) closeDeleteModal() - }) - - // Handle Enter key in form - presetForm.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault() - savePreset() - } - }) -} - -// Open add modal -function openAddModal() { - editingPresetId = null - modalTitle.textContent = "Add Preset" - presetForm.reset() - presetIdField.value = "" - presetModal.classList.add("show") - presetNameField.focus() -} - -// Open edit modal -function openEditModal(id) { - const preset = presets.find((p) => p.id === id) - if (!preset) return - - editingPresetId = id - modalTitle.textContent = "Edit Preset" - - presetIdField.value = preset.id - presetNameField.value = preset.name - aiProviderField.value = preset.config.AI_PROVIDER || "" - aiModelField.value = preset.config.AI_MODEL || "" - aiApiKeyField.value = preset.config.AI_API_KEY || "" - aiBaseUrlField.value = preset.config.AI_BASE_URL || "" - temperatureField.value = preset.config.TEMPERATURE || "" - - presetModal.classList.add("show") - presetNameField.focus() -} - -// Close modal -function closeModal() { - presetModal.classList.remove("show") - editingPresetId = null -} - -// Open delete modal -function openDeleteModal(id) { - const preset = presets.find((p) => p.id === id) - if (!preset) return - - deletingPresetId = id - document.getElementById("delete-preset-name").textContent = preset.name - deleteModal.classList.add("show") -} - -// Close delete modal -function closeDeleteModal() { - deleteModal.classList.remove("show") - deletingPresetId = null -} - -// Save preset -async function savePreset() { - const name = presetNameField.value.trim() - if (!name) { - showToast("Please enter a preset name", "error") - presetNameField.focus() - return - } - - const preset = { - id: editingPresetId || undefined, - name: name, - config: { - AI_PROVIDER: aiProviderField.value || undefined, - AI_MODEL: aiModelField.value.trim() || undefined, - AI_API_KEY: aiApiKeyField.value.trim() || undefined, - AI_BASE_URL: aiBaseUrlField.value.trim() || undefined, - TEMPERATURE: temperatureField.value.trim() || undefined, - }, - } - - // Remove undefined values - Object.keys(preset.config).forEach((key) => { - if (preset.config[key] === undefined) { - delete preset.config[key] - } - }) - - try { - saveBtn.disabled = true - saveBtn.innerHTML = '' - - await window.settingsAPI.savePreset(preset) - await loadPresets() - closeModal() - showToast( - editingPresetId ? "Preset updated" : "Preset created", - "success", - ) - } catch (error) { - console.error("Failed to save preset:", error) - showToast("Failed to save preset", "error") - } finally { - saveBtn.disabled = false - saveBtn.textContent = "Save" - } -} - -// Confirm delete -async function confirmDelete() { - if (!deletingPresetId) return - - try { - deleteConfirmBtn.disabled = true - deleteConfirmBtn.innerHTML = '' - - await window.settingsAPI.deletePreset(deletingPresetId) - await loadPresets() - closeDeleteModal() - showToast("Preset deleted", "success") - } catch (error) { - console.error("Failed to delete preset:", error) - showToast("Failed to delete preset", "error") - } finally { - deleteConfirmBtn.disabled = false - deleteConfirmBtn.textContent = "Delete" - } -} - -// Apply preset -async function applyPreset(id) { - try { - const btn = presetList.querySelector(`.apply-btn[data-id="${id}"]`) - if (btn) { - btn.disabled = true - btn.innerHTML = '' - } - - const result = await window.settingsAPI.applyPreset(id) - if (result.success) { - currentPresetId = id - renderPresets() - showToast("Preset applied, server restarting...", "success") - } else { - showToast(result.error || "Failed to apply preset", "error") - } - } catch (error) { - console.error("Failed to apply preset:", error) - showToast("Failed to apply preset", "error") - } -} - -// Get provider display label -function getProviderLabel(provider) { - const labels = { - openai: "OpenAI", - anthropic: "Anthropic", - google: "Google AI", - azure: "Azure OpenAI", - bedrock: "AWS Bedrock", - openrouter: "OpenRouter", - deepseek: "DeepSeek", - siliconflow: "SiliconFlow", - ollama: "Ollama", - } - return labels[provider] || provider -} - -// Show toast notification -function showToast(message, type = "") { - toast.textContent = message - toast.className = "toast show" + (type ? ` ${type}` : "") - - setTimeout(() => { - toast.classList.remove("show") - }, 3000) -} - -// Escape HTML to prevent XSS -function escapeHtml(text) { - const div = document.createElement("div") - div.textContent = text - return div.innerHTML -} diff --git a/package.json b/package.json index 921582e..713933a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "prepare": "husky", "electron:dev": "node scripts/electron-dev.mjs", "electron:build": "npm run build && npm run electron:compile", - "electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts electron/preload/settings.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external && npx shx cp -r electron/settings dist-electron/", + "electron:compile": "npx esbuild electron/main/index.ts electron/preload/index.ts --bundle --platform=node --outdir=dist-electron --external:electron --sourcemap --packages=external", "electron:start": "npx cross-env NODE_ENV=development npx electron .", "electron:prepare": "node scripts/prepare-electron-build.mjs", "dist": "npm run electron:build && npm run electron:prepare && npx electron-builder", diff --git a/scripts/electron-dev.mjs b/scripts/electron-dev.mjs index c8eb946..b023c92 100644 --- a/scripts/electron-dev.mjs +++ b/scripts/electron-dev.mjs @@ -2,17 +2,13 @@ /** * Development script for running Electron with Next.js - * 1. Reads preset configuration (if exists) - * 2. Starts Next.js dev server with preset env vars - * 3. Waits for it to be ready - * 4. Compiles Electron TypeScript - * 5. Launches Electron - * 6. Watches for preset changes and restarts Next.js + * 1. Starts Next.js dev server + * 2. Waits for it to be ready + * 3. Compiles Electron TypeScript + * 4. Launches Electron */ import { spawn } from "node:child_process" -import { existsSync, readFileSync, watch } from "node:fs" -import os from "node:os" import path from "node:path" import { fileURLToPath } from "node:url" @@ -22,64 +18,6 @@ const rootDir = path.join(__dirname, "..") const NEXT_PORT = 6002 const NEXT_URL = `http://localhost:${NEXT_PORT}` -/** - * Get the user data path (same as Electron's app.getPath("userData")) - */ -function getUserDataPath() { - const appName = "next-ai-draw-io" - switch (process.platform) { - case "darwin": - return path.join( - os.homedir(), - "Library", - "Application Support", - appName, - ) - case "win32": - return path.join( - process.env.APPDATA || - path.join(os.homedir(), "AppData", "Roaming"), - appName, - ) - default: - return path.join(os.homedir(), ".config", appName) - } -} - -/** - * Load preset configuration from config file - */ -function loadPresetConfig() { - const configPath = path.join(getUserDataPath(), "config-presets.json") - - if (!existsSync(configPath)) { - console.log("📋 No preset configuration found, using .env.local") - return null - } - - try { - const content = readFileSync(configPath, "utf-8") - const data = JSON.parse(content) - - if (!data.currentPresetId) { - console.log("📋 No active preset, using .env.local") - return null - } - - const preset = data.presets.find((p) => p.id === data.currentPresetId) - if (!preset) { - console.log("📋 Active preset not found, using .env.local") - return null - } - - console.log(`📋 Using preset: "${preset.name}"`) - return preset.config - } catch (error) { - console.error("Failed to load preset config:", error.message) - return null - } -} - /** * Wait for the Next.js server to be ready */ @@ -129,25 +67,14 @@ function runCommand(command, args, options = {}) { } /** - * Start Next.js dev server with preset environment + * Start Next.js dev server */ -function startNextServer(presetEnv) { - const env = { ...process.env } - - // Apply preset environment variables - if (presetEnv) { - for (const [key, value] of Object.entries(presetEnv)) { - if (value !== undefined && value !== "") { - env[key] = value - } - } - } - +function startNextServer() { const nextProcess = spawn("npm", ["run", "dev"], { cwd: rootDir, stdio: "inherit", shell: true, - env, + env: process.env, }) nextProcess.on("error", (err) => { @@ -163,12 +90,9 @@ function startNextServer(presetEnv) { async function main() { console.log("🚀 Starting Electron development environment...\n") - // Load preset configuration - const presetEnv = loadPresetConfig() - - // Start Next.js dev server with preset env + // Start Next.js dev server console.log("1. Starting Next.js development server...") - let nextProcess = startNextServer(presetEnv) + const nextProcess = startNextServer() // Wait for Next.js to be ready try { @@ -203,75 +127,14 @@ async function main() { }, }) - // Watch for preset config changes - const configPath = path.join(getUserDataPath(), "config-presets.json") - let configWatcher = null - let restartPending = false - - function setupConfigWatcher() { - if (!existsSync(path.dirname(configPath))) { - // Directory doesn't exist yet, check again later - setTimeout(setupConfigWatcher, 5000) - return - } - - try { - configWatcher = watch( - configPath, - { persistent: false }, - async (eventType) => { - if (eventType === "change" && !restartPending) { - restartPending = true - console.log( - "\n🔄 Preset configuration changed, restarting Next.js server...", - ) - - // Kill current Next.js process - nextProcess.kill() - - // Wait a bit for process to die - await new Promise((r) => setTimeout(r, 1000)) - - // Reload preset and restart - const newPresetEnv = loadPresetConfig() - nextProcess = startNextServer(newPresetEnv) - - try { - await waitForServer(NEXT_URL) - console.log( - "✅ Next.js server restarted with new configuration\n", - ) - } catch (err) { - console.error( - "❌ Failed to restart Next.js:", - err.message, - ) - } - - restartPending = false - } - }, - ) - console.log("👀 Watching for preset configuration changes...") - } catch (err) { - // File might not exist yet, that's ok - setTimeout(setupConfigWatcher, 5000) - } - } - - // Start watching after a delay (config file might not exist yet) - setTimeout(setupConfigWatcher, 2000) - electronProcess.on("close", (code) => { console.log(`\nElectron exited with code ${code}`) - if (configWatcher) configWatcher.close() nextProcess.kill() process.exit(code || 0) }) electronProcess.on("error", (err) => { console.error("Electron error:", err) - if (configWatcher) configWatcher.close() nextProcess.kill() process.exit(1) }) @@ -279,7 +142,6 @@ async function main() { // Handle termination signals const cleanup = () => { console.log("\n🛑 Shutting down...") - if (configWatcher) configWatcher.close() electronProcess.kill() nextProcess.kill() process.exit(0)