From eca85ab5c84bbb652a5303c0ca56445e51489d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Tue, 29 Apr 2025 22:26:15 +0800 Subject: [PATCH 01/27] Function: Add General Settings Page --- app/main.ts | 4 +- src/components/pages/SettingsPage.tsx | 96 +++++++--- src/components/settings/GeneralSettings.tsx | 186 ++++++++++++++++++++ src/components/settings/index.ts | 3 +- src/locales/en/translation.json | 16 +- src/locales/es/translation.json | 16 +- src/locales/ja/translation.json | 16 +- src/locales/ko/translation.json | 16 +- src/locales/zh-CN/translation.json | 16 +- src/locales/zh-TW/translation.json | 16 +- src/styles/tensorblock-light.css | 33 ++++ 11 files changed, 385 insertions(+), 33 deletions(-) create mode 100644 src/components/settings/GeneralSettings.tsx diff --git a/app/main.ts b/app/main.ts index 833cd81..8abe564 100644 --- a/app/main.ts +++ b/app/main.ts @@ -29,8 +29,8 @@ function createWindow(): BrowserWindow { frame: false, fullscreenable: false, autoHideMenuBar: true, - minWidth: 600, - minHeight: 600, + minWidth: 700, + minHeight: 1000, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, diff --git a/src/components/pages/SettingsPage.tsx b/src/components/pages/SettingsPage.tsx index d25bac9..d7c4c0a 100644 --- a/src/components/pages/SettingsPage.tsx +++ b/src/components/pages/SettingsPage.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Server, MessageSquare, Languages } from 'lucide-react'; +import { Server, MessageSquare, Languages, Sliders } from 'lucide-react'; import { SettingsService } from '../../services/settings-service'; import { ProviderSettings } from '../../types/settings'; -import { ApiManagement, ChatSettings, LanguageSettings } from '../settings'; +import { ApiManagement, ChatSettings, LanguageSettings, GeneralSettings } from '../settings'; import { DatabaseIntegrationService } from '../../services/database-integration'; import { AIService } from '../../services/ai-service'; import { v4 as uuidv4 } from 'uuid'; @@ -13,7 +13,7 @@ interface SettingsPageProps { isOpen: boolean; } -type SettingsTab = 'api' | 'models' | 'chat' | 'language'; +type SettingsTab = 'api' | 'models' | 'chat' | 'language' | 'general'; export const SettingsPage: React.FC = ({ isOpen, @@ -26,6 +26,13 @@ export const SettingsPage: React.FC = ({ const [hasApiKeyChanged, setHasApiKeyChanged] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + // General settings state + const [startWithSystem, setStartWithSystem] = useState(false); + const [startupToTray, setStartupToTray] = useState(false); + const [closeToTray, setCloseToTray] = useState(true); + const [proxyMode, setProxyMode] = useState<'system' | 'custom' | 'none'>('system'); + const [sendErrorReports, setSendErrorReports] = useState(true); + const settingsService = SettingsService.getInstance(); const aiService = AIService.getInstance(); const { t } = useTranslation(); @@ -57,6 +64,14 @@ export const SettingsPage: React.FC = ({ setUseWebSearch(settings.enableWebSearch_Preview); setHasApiKeyChanged(false); lastOpenedSettings.current = true; + + // In a real implementation, we would load these from settings service + // This is just for the UI prototype + // setStartWithSystem(settings.startWithSystem || false); + // setStartupToTray(settings.startupToTray || false); + // setCloseToTray(settings.closeToTray || true); + // setProxyMode(settings.proxyMode || 'system'); + // setSendErrorReports(settings.sendErrorReports || true); } if(!isOpen && lastOpenedSettings.current){ @@ -95,6 +110,31 @@ export const SettingsPage: React.FC = ({ console.log('Provider settings: ', providerSettings); }; + // Handle general settings changes + const handleGeneralSettingChange = (key: string, value: unknown) => { + console.log(`Setting ${key} changed to: `, value); + + switch(key) { + case 'startWithSystem': + setStartWithSystem(value as boolean); + break; + case 'startupToTray': + setStartupToTray(value as boolean); + break; + case 'closeToTray': + setCloseToTray(value as boolean); + break; + case 'proxyMode': + setProxyMode(value as 'system' | 'custom' | 'none'); + break; + case 'sendErrorReports': + setSendErrorReports(value as boolean); + break; + default: + break; + } + }; + const handleSave = async () => { console.log('Saving settings'); @@ -111,6 +151,12 @@ export const SettingsPage: React.FC = ({ await settingsService.updateSettings({ providers: providerSettings, enableWebSearch_Preview: useWebSearch + // In a real implementation, we would save general settings here + // startWithSystem, + // startupToTray, + // closeToTray, + // proxyMode, + // sendErrorReports }); // Refresh models if API key has changed @@ -216,6 +262,16 @@ export const SettingsPage: React.FC = ({
+ + - - {/**/}
- - {/*
- -
*/} {/* Main content */}
{/* Content area */}
+ {/* General Settings Tab */} + {activeTab === 'general' && ( + + )} + {/* API Management Tab */} {activeTab === 'api' && ( void; + onSaveSettings: () => void; +} + +export const GeneralSettings: React.FC = ({ + startWithSystem, + startupToTray, + closeToTray, + proxyMode, + sendErrorReports, + onSettingChange, + onSaveSettings +}) => { + const { t } = useTranslation(); + const [customProxyUrl, setCustomProxyUrl] = useState(''); + + const handleProxyModeChange = (mode: 'system' | 'custom' | 'none') => { + onSettingChange('proxyMode', mode); + onSaveSettings(); + }; + + const handleToggleChange = (key: string) => (e: React.ChangeEvent) => { + onSettingChange(key, e.target.checked); + onSaveSettings(); + }; + + return ( +
+
+

{t('settings.general')}

+ + {/* Startup Settings */} +
+

{t('settings.startup')}

+ +
+
+ + +
+ +
+ + +
+
+
+ + {/* Tray Settings */} +
+

{t('settings.trayOptions')}

+ +
+
+ + +
+
+
+ + {/* Network Settings */} +
+

{t('settings.networkProxy')}

+ +
+
+ handleProxyModeChange('system')} + className="w-4 h-4 text-blue-600 form-radio" + /> + +
+ +
+ handleProxyModeChange('custom')} + className="w-4 h-4 text-blue-600 form-radio" + /> + +
+ + {proxyMode === 'custom' && ( +
+ setCustomProxyUrl(e.target.value)} + onBlur={() => { + onSettingChange('customProxyUrl', customProxyUrl); + onSaveSettings(); + }} + placeholder="http://proxy.example.com:8080" + className="w-full p-2 input-box" + /> +
+ )} + +
+ handleProxyModeChange('none')} + className="w-4 h-4 text-blue-600 form-radio" + /> + +
+
+
+ + {/* Privacy Settings */} +
+

{t('settings.privacy')}

+ +
+
+ + +
+

+ {t('settings.sendErrorReports_description')} +

+
+
+
+
+ ); +}; + +export default GeneralSettings; \ No newline at end of file diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 1d27d9e..786ac54 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -1,3 +1,4 @@ export * from './ApiManagement'; export * from './ChatSettings'; -export * from './LanguageSettings'; \ No newline at end of file +export * from './LanguageSettings'; +export * from './GeneralSettings'; \ No newline at end of file diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index dd8e203..1ce5d24 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -148,6 +148,20 @@ "webSearch_title": "Web Search (Preview)", "webSearch_toggle_label": "Enable Web Search Function", - "webSearch_description": "When enabled, the AI can search the web to provide more up-to-date information. Please note that web search is currently only supported with OpenAI and Gemini models. Also, when web search is enabled, streaming responses (where text appears incrementally) will not be available." + "webSearch_description": "When enabled, the AI can search the web to provide more up-to-date information. Please note that web search is currently only supported with OpenAI and Gemini models. Also, when web search is enabled, streaming responses (where text appears incrementally) will not be available.", + + "general": "General", + "startup": "Startup", + "startWithSystem": "Start with system", + "startupToTray": "Start minimized to tray", + "trayOptions": "Tray Options", + "closeToTray": "Close to tray instead of quitting", + "networkProxy": "Network Proxy", + "systemProxy": "Use system proxy", + "customProxy": "Custom proxy", + "noProxy": "No proxy", + "privacy": "Privacy", + "sendErrorReports": "Send anonymous error reports and usage statistics", + "sendErrorReports_description": "Help improve the application by sending anonymous crash reports and usage data." } } \ No newline at end of file diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 0bf7a0c..e1bd9ba 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -145,6 +145,20 @@ "models_modelCapabilities_chat": "Chat", "webSearch_title": "Búsqueda Web (Vista Previa)", "webSearch_toggle_label": "Habilitar Función de Búsqueda Web", - "webSearch_description": "Cuando está habilitado, la IA puede buscar en la web para proporcionar información más actualizada. Ten en cuenta que la búsqueda web actualmente solo es compatible con los modelos de OpenAI y Gemini. Además, cuando la búsqueda web está habilitada, las respuestas en streaming (donde el texto aparece de forma incremental) no estarán disponibles." + "webSearch_description": "Cuando está habilitado, la IA puede buscar en la web para proporcionar información más actualizada. Ten en cuenta que la búsqueda web actualmente solo es compatible con los modelos de OpenAI y Gemini. Además, cuando la búsqueda web está habilitada, las respuestas en streaming (donde el texto aparece de forma incremental) no estarán disponibles.", + + "general": "Configuración General", + "startup": "Inicio", + "startWithSystem": "Iniciar con el sistema", + "startupToTray": "Iniciar minimizado en la bandeja", + "trayOptions": "Opciones de Bandeja", + "closeToTray": "Minimizar a la bandeja en lugar de cerrar", + "networkProxy": "Proxy de Red", + "systemProxy": "Usar proxy del sistema", + "customProxy": "Proxy personalizado", + "noProxy": "Sin proxy", + "privacy": "Privacidad", + "sendErrorReports": "Enviar informes de errores anónimos y estadísticas de uso", + "sendErrorReports_description": "Ayuda a mejorar la aplicación enviando informes de fallos anónimos y datos de uso." } } \ No newline at end of file diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index 49c01c6..30d33be 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -145,6 +145,20 @@ "models_modelCapabilities_chat": "チャット", "webSearch_title": "ウェブ検索(プレビュー)", "webSearch_toggle_label": "ウェブ検索機能を有効にする", - "webSearch_description": "有効にすると、AIはウェブを検索してより最新の情報を提供できます。注意: ウェブ検索は現在、OpenAIとGeminiモデルでのみサポートされています。また、ウェブ検索が有効な場合、ストリーミング応答(テキストが段階的に表示される)は利用できません。" + "webSearch_description": "有効にすると、AIはウェブを検索してより最新の情報を提供できます。注意: ウェブ検索は現在、OpenAIとGeminiモデルでのみサポートされています。また、ウェブ検索が有効な場合、ストリーミング応答(テキストが段階的に表示される)は利用できません。", + + "general": "一般設定", + "startup": "起動設定", + "startWithSystem": "システム起動時に起動する", + "startupToTray": "起動時にトレイに最小化する", + "trayOptions": "トレイオプション", + "closeToTray": "閉じる時に終了せずトレイに最小化する", + "networkProxy": "ネットワークプロキシ", + "systemProxy": "システムプロキシを使用", + "customProxy": "カスタムプロキシ", + "noProxy": "プロキシを使用しない", + "privacy": "プライバシー設定", + "sendErrorReports": "匿名のエラーレポートと使用統計を送信する", + "sendErrorReports_description": "匿名のクラッシュレポートと使用データを送信してアプリケーションの改善に協力する。" } } \ No newline at end of file diff --git a/src/locales/ko/translation.json b/src/locales/ko/translation.json index 952eb2e..d4bef76 100644 --- a/src/locales/ko/translation.json +++ b/src/locales/ko/translation.json @@ -145,6 +145,20 @@ "models_modelCapabilities_chat": "채팅", "webSearch_title": "웹 검색 (미리보기)", "webSearch_toggle_label": "웹 검색 기능 활성화", - "webSearch_description": "활성화되면 AI가 웹을 검색하여 더 최신 정보를 제공합니다. 웹 검색은 현재 OpenAI 및 Gemini 모델에서만 지원됩니다. 또한 웹 검색이 활성화되면 스트리밍 응답(텍스트가 점진적으로 나타나는)이 제공되지 않습니다." + "webSearch_description": "활성화되면 AI가 웹을 검색하여 더 최신 정보를 제공합니다. 웹 검색은 현재 OpenAI 및 Gemini 모델에서만 지원됩니다. 또한 웹 검색이 활성화되면 스트리밍 응답(텍스트가 점진적으로 나타나는)이 제공되지 않습니다.", + + "general": "일반 설정", + "startup": "시작 설정", + "startWithSystem": "시스템과 함께 시작", + "startupToTray": "트레이로 최소화하여 시작", + "trayOptions": "트레이 옵션", + "closeToTray": "종료 대신 트레이로 최소화", + "networkProxy": "네트워크 프록시", + "systemProxy": "시스템 프록시 사용", + "customProxy": "사용자 정의 프록시", + "noProxy": "프록시 사용 안 함", + "privacy": "개인정보 설정", + "sendErrorReports": "익명의 오류 보고서 및 사용 통계 보내기", + "sendErrorReports_description": "익명의 충돌 보고서와 사용 데이터를 보내 애플리케이션 개선에 도움을 줍니다." } } \ No newline at end of file diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 5069f0c..f609a13 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -145,6 +145,20 @@ "models_modelCapabilities_chat": "聊天", "webSearch_title": "网页搜索(预览)", "webSearch_toggle_label": "启用网页搜索功能", - "webSearch_description": "启用后,AI 可以搜索网页以提供更及时的信息。请注意,网页搜索目前仅支持 OpenAI 和 Gemini 模型。此外,启用网页搜索时,流式响应(文本逐步出现)将不可用。" + "webSearch_description": "启用后,AI 可以搜索网页以提供更及时的信息。请注意,网页搜索目前仅支持 OpenAI 和 Gemini 模型。此外,启用网页搜索时,流式响应(文本逐步出现)将不可用。", + + "general": "常规设置", + "startup": "启动设置", + "startWithSystem": "开机自启动", + "startupToTray": "启动时最小化到托盘", + "trayOptions": "托盘选项", + "closeToTray": "关闭时最小化到托盘而不是退出", + "networkProxy": "网络代理", + "systemProxy": "使用系统代理", + "customProxy": "自定义代理", + "noProxy": "不使用代理", + "privacy": "隐私设置", + "sendErrorReports": "发送匿名错误报告和数据统计", + "sendErrorReports_description": "通过发送匿名崩溃报告和使用数据来帮助改进应用程序。" } } \ No newline at end of file diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index 5a6389f..3f789e8 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -145,6 +145,20 @@ "models_modelCapabilities_chat": "聊天", "webSearch_title": "網頁搜尋(預覽)", "webSearch_toggle_label": "啟用網頁搜尋功能", - "webSearch_description": "啟用後,AI 可以搜尋網頁以提供更即時的信息。請注意,網頁搜尋目前僅支援 OpenAI 和 Gemini 模型。此外,啟用網頁搜尋時,將無法使用流式回應(文字逐步顯示)。" + "webSearch_description": "啟用後,AI 可以搜尋網頁以提供更即時的信息。請注意,網頁搜尋目前僅支援 OpenAI 和 Gemini 模型。此外,啟用網頁搜尋時,將無法使用流式回應(文字逐步顯示)。", + + "general": "一般設定", + "startup": "啟動設定", + "startWithSystem": "隨系統啟動", + "startupToTray": "啟動時最小化到系統匣", + "trayOptions": "系統匣選項", + "closeToTray": "關閉時最小化到系統匣而非退出", + "networkProxy": "網路代理", + "systemProxy": "使用系統代理", + "customProxy": "自定義代理", + "noProxy": "不使用代理", + "privacy": "隱私設定", + "sendErrorReports": "發送匿名錯誤報告和使用統計", + "sendErrorReports_description": "通過發送匿名當機報告和使用數據來幫助改進應用程式。" } } \ No newline at end of file diff --git a/src/styles/tensorblock-light.css b/src/styles/tensorblock-light.css index 0dcf580..da01e7f 100644 --- a/src/styles/tensorblock-light.css +++ b/src/styles/tensorblock-light.css @@ -418,6 +418,39 @@ color: var(--primary-700); } + /* General Settings */ + .settings-section { + background-color: var(--surface-0); + border: 1px solid var(--primary-100); + border-radius: 0.5rem; + } + + .settings-section-title { + color: var(--surface-700); + } + + .settings-toggle-label { + color: var(--primary-700); + } + + .settings-toggle-description { + color: var(--surface-500); + } + + .settings-radio-group { + border-radius: 0.5rem; + background-color: var(--primary-50); + } + + .settings-radio-item { + color: var(--surface-500); + } + + .settings-radio-item-active { + background-color: var(--primary-200); + color: var(--primary-700); + } + /* Message */ .message-model-tag { background-color: transparent; From c418683fb8a70e0cd767c5f570bc5a33cc261f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Fri, 2 May 2025 21:48:55 +0800 Subject: [PATCH 02/27] Function: Implement auto startup --- app/main.ts | 224 +++++++++++++++++++- app/preload.ts | 7 + src/components/pages/SettingsPage.tsx | 58 +++-- src/components/settings/GeneralSettings.tsx | 15 +- src/services/settings-service.ts | 7 + src/types/settings.ts | 7 + src/types/window.d.ts | 6 + 7 files changed, 303 insertions(+), 21 deletions(-) diff --git a/app/main.ts b/app/main.ts index 8abe564..6ae98ce 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import {app, BrowserWindow, ipcMain, shell, dialog} from 'electron'; +import {app, BrowserWindow, ipcMain, shell, dialog, Tray, Menu, nativeImage} from 'electron'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; @@ -8,11 +8,147 @@ import { execSync } from 'child_process'; // Main window reference let win: BrowserWindow | null = null; +// Tray reference +let tray: Tray | null = null; + +// Settings +let closeToTray = true; // Check if app is running in development mode const args = process.argv.slice(1); const serve = args.some(val => val === '--serve'); +/** + * Create the system tray + */ +function createTray() { + if (tray) { + return; + } + + // Get appropriate icon based on platform + const iconPath = path.join(__dirname, '..', 'dist', 'logos', 'favicon.256x256.png'); + const icon = nativeImage.createFromPath(iconPath); + + tray = new Tray(icon); + tray.setToolTip('TensorBlock Desktop'); + + const contextMenu = Menu.buildFromTemplate([ + { label: 'Open TensorBlock', click: () => { + win?.show(); + win?.setSkipTaskbar(false); // Show in taskbar + }}, + { type: 'separator' }, + { label: 'Quit', click: () => { app.quit(); } } + ]); + + tray.setContextMenu(contextMenu); + + tray.on('click', () => { + if (win) { + if (win.isVisible()) { + win.hide(); + win.setSkipTaskbar(true); // Hide from taskbar + } else { + win.show(); + win.setSkipTaskbar(false); // Show in taskbar + } + } + }); +} + +/** + * Set or remove auto launch on system startup + */ +function setAutoLaunch(enable: boolean): boolean { + try { + if (process.platform === 'win32') { + const appPath = app.getPath('exe'); + const regKey = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; + const appName = app.getName(); + + if (enable) { + // Add to registry to enable auto launch + execSync(`reg add ${regKey} /v ${appName} /t REG_SZ /d "${appPath}" /f`); + } else { + // Remove from registry to disable auto launch + execSync(`reg delete ${regKey} /v ${appName} /f`); + } + return true; + } else if (process.platform === 'darwin') { + const appPath = app.getPath('exe'); + const loginItemSettings = app.getLoginItemSettings(); + + // Set login item settings for macOS + app.setLoginItemSettings({ + openAtLogin: enable, + path: appPath + }); + + return app.getLoginItemSettings().openAtLogin === enable; + } else if (process.platform === 'linux') { + // For Linux, create or remove a .desktop file in autostart directory + const desktopFilePath = path.join(os.homedir(), '.config', 'autostart', `${app.getName()}.desktop`); + + if (enable) { + // Create directory if it doesn't exist + const autoStartDir = path.dirname(desktopFilePath); + if (!fs.existsSync(autoStartDir)) { + fs.mkdirSync(autoStartDir, { recursive: true }); + } + + // Create .desktop file + const desktopFileContent = ` + [Desktop Entry] + Type=Application + Exec=${app.getPath('exe')} + Hidden=false + NoDisplay=false + X-GNOME-Autostart-enabled=true + Name=${app.getName()} + Comment=${app.getName()} startup script + `; + fs.writeFileSync(desktopFilePath, desktopFileContent); + } else if (fs.existsSync(desktopFilePath)) { + // Remove .desktop file + fs.unlinkSync(desktopFilePath); + } + + return true; + } + + return false; + } catch (error) { + console.error('Error setting auto launch:', error); + return false; + } +} + +/** + * Check if app is set to auto launch on system startup + */ +function getAutoLaunchEnabled(): boolean { + try { + if (process.platform === 'win32') { + const regKey = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; + const appName = app.getName(); + + const output = execSync(`reg query ${regKey} /v ${appName} 2>nul`).toString(); + return output.includes(appName); + } else if (process.platform === 'darwin') { + return app.getLoginItemSettings().openAtLogin; + } else if (process.platform === 'linux') { + const desktopFilePath = path.join(os.homedir(), '.config', 'autostart', `${app.getName()}.desktop`); + return fs.existsSync(desktopFilePath); + } + + return false; + } catch (error) { + // If command fails (e.g., key doesn't exist), auto launch is not enabled + return false; + } +} + /** * Creates the main application window */ @@ -29,8 +165,8 @@ function createWindow(): BrowserWindow { frame: false, fullscreenable: false, autoHideMenuBar: true, - minWidth: 700, - minHeight: 1000, + minWidth: 800, + minHeight: 700, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, @@ -180,7 +316,12 @@ function createWindow(): BrowserWindow { // Close application ipcMain.on('close-app', () => { - app.quit(); + if (closeToTray) { + win?.hide(); + win?.setSkipTaskbar(true); + } else { + app.quit(); + } }); // Open URL in default browser @@ -188,6 +329,53 @@ function createWindow(): BrowserWindow { shell.openExternal(url); }); + // Auto-startup handlers + ipcMain.handle('get-auto-launch', () => { + return getAutoLaunchEnabled(); + }); + + ipcMain.handle('set-auto-launch', (event, enable) => { + return setAutoLaunch(enable); + }); + + // Tray handlers + ipcMain.handle('set-close-to-tray', (event, enable) => { + closeToTray = enable; + return true; + }); + + ipcMain.handle('get-close-to-tray', () => { + return closeToTray; + }); + + ipcMain.handle('set-startup-to-tray', (event, enable) => { + // Store this preference for the next app start + try { + const configPath = path.join(app.getPath('userData'), 'config.json'); + let config = {}; + + if (fs.existsSync(configPath)) { + config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + } + + config = { ...config, startupToTray: enable }; + fs.writeFileSync(configPath, JSON.stringify(config)); + return true; + } catch (error) { + console.error('Error saving startup to tray setting:', error); + return false; + } + }); + + // Listen for window close event + win.on('close', (e) => { + if (closeToTray) { + e.preventDefault(); + win?.hide(); + win?.setSkipTaskbar(true); + } + }); + // Disable page refresh in production if (process.env.NODE_ENV === 'production') { win.webContents.on('before-input-event', (event, input) => { @@ -293,7 +481,28 @@ try { // Initialize app when Electron is ready // Added delay to fix black background issue with transparent windows // See: https://github.com/electron/electron/issues/15947 - app.on('ready', () => setTimeout(createWindow, 400)); + app.on('ready', () => { + setTimeout(() => { + // Create window + win = createWindow(); + + // Create tray + createTray(); + + // Check if we should start minimized to tray + try { + const configPath = path.join(app.getPath('userData'), 'config.json'); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + if (config.startupToTray) { + win.hide(); + } + } + } catch (error) { + console.error('Error reading config file:', error); + } + }, 400); + }); // Quit when all windows are closed app.on('window-all-closed', () => { @@ -303,7 +512,10 @@ try { // Re-create window if activated and no windows exist app.on('activate', () => { if (win === null) { - createWindow(); + win = createWindow(); + } else { + win.show(); + win.setSkipTaskbar(false); // Show in taskbar } }); } catch (_e) { diff --git a/app/preload.ts b/app/preload.ts index 2f19fba..2f59608 100644 --- a/app/preload.ts +++ b/app/preload.ts @@ -29,4 +29,11 @@ contextBridge.exposeInMainWorld('electron', { saveFile: (fileBuffer: ArrayBuffer | string, fileName: string, fileType: string) => ipcRenderer.invoke('save-file', { fileBuffer, fileName, fileType }), openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath), + + // Auto-startup and tray functions + getAutoLaunch: () => ipcRenderer.invoke('get-auto-launch'), + setAutoLaunch: (enable: boolean) => ipcRenderer.invoke('set-auto-launch', enable), + setCloseToTray: (enable: boolean) => ipcRenderer.invoke('set-close-to-tray', enable), + getCloseToTray: () => ipcRenderer.invoke('get-close-to-tray'), + setStartupToTray: (enable: boolean) => ipcRenderer.invoke('set-startup-to-tray', enable), }); \ No newline at end of file diff --git a/src/components/pages/SettingsPage.tsx b/src/components/pages/SettingsPage.tsx index d7c4c0a..81ac32f 100644 --- a/src/components/pages/SettingsPage.tsx +++ b/src/components/pages/SettingsPage.tsx @@ -65,13 +65,31 @@ export const SettingsPage: React.FC = ({ setHasApiKeyChanged(false); lastOpenedSettings.current = true; - // In a real implementation, we would load these from settings service - // This is just for the UI prototype - // setStartWithSystem(settings.startWithSystem || false); - // setStartupToTray(settings.startupToTray || false); - // setCloseToTray(settings.closeToTray || true); - // setProxyMode(settings.proxyMode || 'system'); - // setSendErrorReports(settings.sendErrorReports || true); + // Load general settings from settings service + setStartWithSystem(settings.startWithSystem || false); + setStartupToTray(settings.startupToTray || false); + setCloseToTray(settings.closeToTray || true); + setProxyMode(settings.proxyMode || 'system'); + setSendErrorReports(settings.sendErrorReports || true); + + // Load current auto-start setting from system + const checkAutoLaunch = async () => { + if (window.electron && window.electron.getAutoLaunch) { + const autoLaunchEnabled = await window.electron.getAutoLaunch(); + setStartWithSystem(autoLaunchEnabled); + } + }; + + // Load current close-to-tray setting + const checkCloseToTray = async () => { + if (window.electron && window.electron.getCloseToTray) { + const closeToTrayEnabled = await window.electron.getCloseToTray(); + setCloseToTray(closeToTrayEnabled); + } + }; + + checkAutoLaunch(); + checkCloseToTray(); } if(!isOpen && lastOpenedSettings.current){ @@ -117,12 +135,24 @@ export const SettingsPage: React.FC = ({ switch(key) { case 'startWithSystem': setStartWithSystem(value as boolean); + // Apply auto-launch setting to system + if (window.electron && window.electron.setAutoLaunch) { + window.electron.setAutoLaunch(value as boolean); + } break; case 'startupToTray': setStartupToTray(value as boolean); + // Store startup-to-tray setting + if (window.electron && window.electron.setStartupToTray) { + window.electron.setStartupToTray(value as boolean); + } break; case 'closeToTray': setCloseToTray(value as boolean); + // Apply close-to-tray setting + if (window.electron && window.electron.setCloseToTray) { + window.electron.setCloseToTray(value as boolean); + } break; case 'proxyMode': setProxyMode(value as 'system' | 'custom' | 'none'); @@ -150,13 +180,13 @@ export const SettingsPage: React.FC = ({ // Update all settings in one go await settingsService.updateSettings({ providers: providerSettings, - enableWebSearch_Preview: useWebSearch - // In a real implementation, we would save general settings here - // startWithSystem, - // startupToTray, - // closeToTray, - // proxyMode, - // sendErrorReports + enableWebSearch_Preview: useWebSearch, + // Save general settings + startWithSystem, + startupToTray, + closeToTray, + proxyMode, + sendErrorReports }); // Refresh models if API key has changed diff --git a/src/components/settings/GeneralSettings.tsx b/src/components/settings/GeneralSettings.tsx index 8849b6f..3a593a1 100644 --- a/src/components/settings/GeneralSettings.tsx +++ b/src/components/settings/GeneralSettings.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; interface GeneralSettingsProps { @@ -22,6 +22,19 @@ export const GeneralSettings: React.FC = ({ }) => { const { t } = useTranslation(); const [customProxyUrl, setCustomProxyUrl] = useState(''); + const [isWindows, setIsWindows] = useState(false); + + // Check if running on Windows platform + useEffect(() => { + const checkPlatform = async () => { + if (window.electron && window.electron.getPlatform) { + const platform = await window.electron.getPlatform(); + setIsWindows(platform === 'win32'); + } + }; + + checkPlatform(); + }, []); const handleProxyModeChange = (mode: 'system' | 'custom' | 'none') => { onSettingChange('proxyMode', mode); diff --git a/src/services/settings-service.ts b/src/services/settings-service.ts index d8c8a43..72dfbfb 100644 --- a/src/services/settings-service.ts +++ b/src/services/settings-service.ts @@ -239,6 +239,13 @@ const DEFAULT_SETTINGS: UserSettings = { useStreaming: true, webSearchEnabled: false, enableWebSearch_Preview: false, + // General settings + startWithSystem: false, + startupToTray: false, + closeToTray: true, + proxyMode: 'system', + customProxyUrl: '', + sendErrorReports: true, }; /** diff --git a/src/types/settings.ts b/src/types/settings.ts index 74f1d18..9527370 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -10,6 +10,13 @@ export interface UserSettings { useStreaming: boolean; webSearchEnabled: boolean; enableWebSearch_Preview: boolean; + // General settings + startWithSystem?: boolean; + startupToTray?: boolean; + closeToTray?: boolean; + proxyMode?: 'system' | 'custom' | 'none'; + customProxyUrl?: string; + sendErrorReports?: boolean; } /** diff --git a/src/types/window.d.ts b/src/types/window.d.ts index 09b725b..934971d 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -24,5 +24,11 @@ interface Window { success: boolean; error?: string; }>; + // Auto-startup and tray functions + getAutoLaunch: () => Promise; + setAutoLaunch: (enable: boolean) => Promise; + setCloseToTray: (enable: boolean) => Promise; + getCloseToTray: () => Promise; + setStartupToTray: (enable: boolean) => Promise; }; } \ No newline at end of file From 57e490a9ccb486ca147a95a3164e4b9aeaa0956c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Fri, 2 May 2025 21:54:23 +0800 Subject: [PATCH 03/27] Fix Bug: Cannot quit by the tray button --- app/main.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/main.ts b/app/main.ts index 6ae98ce..9378fc9 100644 --- a/app/main.ts +++ b/app/main.ts @@ -13,6 +13,7 @@ let tray: Tray | null = null; // Settings let closeToTray = true; +let forceQuit = false; // Flag to indicate we're trying to actually quit // Check if app is running in development mode const args = process.argv.slice(1); @@ -39,7 +40,10 @@ function createTray() { win?.setSkipTaskbar(false); // Show in taskbar }}, { type: 'separator' }, - { label: 'Quit', click: () => { app.quit(); } } + { label: 'Quit', click: () => { + forceQuit = true; // Set flag to bypass close-to-tray + app.quit(); + }} ]); tray.setContextMenu(contextMenu); @@ -316,10 +320,11 @@ function createWindow(): BrowserWindow { // Close application ipcMain.on('close-app', () => { - if (closeToTray) { + if (closeToTray && !forceQuit) { win?.hide(); - win?.setSkipTaskbar(true); + win?.setSkipTaskbar(true); // Hide from taskbar } else { + forceQuit = true; // Ensure we're really quitting app.quit(); } }); @@ -369,10 +374,10 @@ function createWindow(): BrowserWindow { // Listen for window close event win.on('close', (e) => { - if (closeToTray) { + if (closeToTray && !forceQuit) { e.preventDefault(); win?.hide(); - win?.setSkipTaskbar(true); + win?.setSkipTaskbar(true); // Hide from taskbar } }); @@ -478,6 +483,11 @@ function getCPUName() { try { app.commandLine.appendSwitch('class', 'tensorblock-desktop'); + // Set force quit flag when app is about to quit + app.on('before-quit', () => { + forceQuit = true; + }); + // Initialize app when Electron is ready // Added delay to fix black background issue with transparent windows // See: https://github.com/electron/electron/issues/15947 From bd784016bc9522f2b35b1be0481cb0bd51ea75d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Fri, 2 May 2025 22:07:55 +0800 Subject: [PATCH 04/27] Fix Linter Error --- src/components/settings/GeneralSettings.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/settings/GeneralSettings.tsx b/src/components/settings/GeneralSettings.tsx index 3a593a1..86582a0 100644 --- a/src/components/settings/GeneralSettings.tsx +++ b/src/components/settings/GeneralSettings.tsx @@ -22,6 +22,7 @@ export const GeneralSettings: React.FC = ({ }) => { const { t } = useTranslation(); const [customProxyUrl, setCustomProxyUrl] = useState(''); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isWindows, setIsWindows] = useState(false); // Check if running on Windows platform From 92a30255683d431b3582c34d1efa07976e87fb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sat, 3 May 2025 12:04:17 +0800 Subject: [PATCH 05/27] Fix: Move Language Settings to General Settings --- src/components/pages/SettingsPage.tsx | 24 +--- src/components/settings/GeneralSettings.tsx | 129 ++++++------------- src/components/settings/LanguageSettings.tsx | 46 ------- src/components/settings/index.ts | 1 - 4 files changed, 45 insertions(+), 155 deletions(-) delete mode 100644 src/components/settings/LanguageSettings.tsx diff --git a/src/components/pages/SettingsPage.tsx b/src/components/pages/SettingsPage.tsx index 81ac32f..15017ec 100644 --- a/src/components/pages/SettingsPage.tsx +++ b/src/components/pages/SettingsPage.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Server, MessageSquare, Languages, Sliders } from 'lucide-react'; +import { Server, MessageSquare, Sliders } from 'lucide-react'; import { SettingsService } from '../../services/settings-service'; import { ProviderSettings } from '../../types/settings'; -import { ApiManagement, ChatSettings, LanguageSettings, GeneralSettings } from '../settings'; +import { ApiManagement, ChatSettings, GeneralSettings } from '../settings'; import { DatabaseIntegrationService } from '../../services/database-integration'; import { AIService } from '../../services/ai-service'; import { v4 as uuidv4 } from 'uuid'; @@ -13,7 +13,7 @@ interface SettingsPageProps { isOpen: boolean; } -type SettingsTab = 'api' | 'models' | 'chat' | 'language' | 'general'; +type SettingsTab = 'api' | 'models' | 'chat' | 'general'; export const SettingsPage: React.FC = ({ isOpen, @@ -321,16 +321,6 @@ export const SettingsPage: React.FC = ({ {t('chat.sendMessage')} - -
@@ -371,14 +361,6 @@ export const SettingsPage: React.FC = ({ onSaveSettings={handleSave} /> )} - - {/* Language Settings Tab */} - {activeTab === 'language' && ( -
-

{t('settings.language')}

- -
- )} diff --git a/src/components/settings/GeneralSettings.tsx b/src/components/settings/GeneralSettings.tsx index 86582a0..9271215 100644 --- a/src/components/settings/GeneralSettings.tsx +++ b/src/components/settings/GeneralSettings.tsx @@ -15,15 +15,13 @@ export const GeneralSettings: React.FC = ({ startWithSystem, startupToTray, closeToTray, - proxyMode, - sendErrorReports, onSettingChange, onSaveSettings }) => { - const { t } = useTranslation(); - const [customProxyUrl, setCustomProxyUrl] = useState(''); + const { t, i18n } = useTranslation(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [isWindows, setIsWindows] = useState(false); + const [currentLanguage, setCurrentLanguage] = useState(i18n.language); // Check if running on Windows platform useEffect(() => { @@ -37,16 +35,35 @@ export const GeneralSettings: React.FC = ({ checkPlatform(); }, []); - const handleProxyModeChange = (mode: 'system' | 'custom' | 'none') => { - onSettingChange('proxyMode', mode); - onSaveSettings(); - }; + // Update currentLanguage when i18n.language changes + useEffect(() => { + setCurrentLanguage(i18n.language); + }, [i18n.language]); + + // const handleProxyModeChange = (mode: 'system' | 'custom' | 'none') => { + // onSettingChange('proxyMode', mode); + // onSaveSettings(); + // }; const handleToggleChange = (key: string) => (e: React.ChangeEvent) => { onSettingChange(key, e.target.checked); onSaveSettings(); }; + const handleLanguageChange = (langCode: string) => { + i18n.changeLanguage(langCode); + setCurrentLanguage(langCode); + }; + + const languages = [ + { code: 'en', name: 'English' }, + { code: 'zh_CN', name: '简体中文' }, + { code: 'zh_TW', name: '繁體中文' }, + { code: 'ja', name: '日本語' }, + { code: 'ko', name: '한국어' }, + { code: 'es', name: 'Español' } + ]; + return (
@@ -105,93 +122,31 @@ export const GeneralSettings: React.FC = ({
- {/* Network Settings */} + {/* Language Settings */}
-

{t('settings.networkProxy')}

+

{t('settings.language')}

-
-
- handleProxyModeChange('system')} - className="w-4 h-4 text-blue-600 form-radio" - /> - -
- -
- handleProxyModeChange('custom')} - className="w-4 h-4 text-blue-600 form-radio" - /> - -
- - {proxyMode === 'custom' && ( -
+
+ {languages.map((lang) => ( +
setCustomProxyUrl(e.target.value)} - onBlur={() => { - onSettingChange('customProxyUrl', customProxyUrl); - onSaveSettings(); - }} - placeholder="http://proxy.example.com:8080" - className="w-full p-2 input-box" + type="radio" + id={`lang-${lang.code}`} + name="language" + value={lang.code} + checked={currentLanguage === lang.code} + onChange={() => handleLanguageChange(lang.code)} + className="w-4 h-4 text-blue-600 form-radio" /> +
- )} - -
- handleProxyModeChange('none')} - className="w-4 h-4 text-blue-600 form-radio" - /> - -
+ ))}
- {/* Privacy Settings */} -
-

{t('settings.privacy')}

- -
-
- - -
-

- {t('settings.sendErrorReports_description')} -

-
-
+ {/* Network Proxy and Privacy sections are hidden as requested */}
); diff --git a/src/components/settings/LanguageSettings.tsx b/src/components/settings/LanguageSettings.tsx deleted file mode 100644 index 5bdfa28..0000000 --- a/src/components/settings/LanguageSettings.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { useState, useEffect } from 'react'; - -export const LanguageSettings = () => { - const { t, i18n } = useTranslation(); - const [currentLanguage, setCurrentLanguage] = useState(i18n.language); - - const languages = [ - { code: 'en', name: 'English' }, - { code: 'zh_CN', name: '简体中文' }, - { code: 'zh_TW', name: '繁體中文' }, - { code: 'ja', name: '日本語' }, - { code: 'ko', name: '한국어' }, - { code: 'es', name: 'Español' } - ]; - - const handleLanguageChange = (langCode: string) => { - i18n.changeLanguage(langCode); - setCurrentLanguage(langCode); - }; - - useEffect(() => { - setCurrentLanguage(i18n.language); - }, [i18n.language]); - - return ( -
-

{t('settings.language')}

-
- {languages.map((lang) => ( - - ))} -
-
- ); -}; \ No newline at end of file diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 786ac54..bd6824a 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -1,4 +1,3 @@ export * from './ApiManagement'; export * from './ChatSettings'; -export * from './LanguageSettings'; export * from './GeneralSettings'; \ No newline at end of file From f20fcd8c2dd919cbb91293a6360c7239a0acfa4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sat, 3 May 2025 16:46:17 +0800 Subject: [PATCH 06/27] Function: Adding MCP Server --- src/App.tsx | 2 + src/components/chat/ChatMessageArea.tsx | 95 +++++++- src/components/layout/Sidebar.tsx | 17 +- src/components/pages/ChatPage.tsx | 76 +++++-- src/components/pages/MCPServerPage.tsx | 278 +++++++++++++++++++++++ src/locales/en/translation.json | 22 +- src/locales/es/translation.json | 22 +- src/locales/ja/translation.json | 22 +- src/locales/ko/translation.json | 22 +- src/locales/zh-CN/translation.json | 22 +- src/locales/zh-TW/translation.json | 22 +- src/services/ai-service.ts | 27 +++ src/services/chat-service.ts | 145 ++++++++++++ src/services/core/ai-service-provider.ts | 2 + src/services/mcp-service.ts | 105 +++++++++ src/services/mcp-tool-adapter.ts | 196 ++++++++++++++++ src/services/settings-service.ts | 71 +++++- src/types/capabilities.ts | 1 + src/types/settings.ts | 14 ++ 19 files changed, 1136 insertions(+), 25 deletions(-) create mode 100644 src/components/pages/MCPServerPage.tsx create mode 100644 src/services/mcp-service.ts create mode 100644 src/services/mcp-tool-adapter.ts diff --git a/src/App.tsx b/src/App.tsx index 9f552d6..9fb46a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { ChatPage } from './components/pages/ChatPage'; import { ImageGenerationPage } from './components/pages/ImageGenerationPage'; import { TranslationPage } from './components/pages/TranslationPage'; import { FileManagementPage } from './components/pages/FileManagementPage'; +import { MCPServerPage } from './components/pages/MCPServerPage'; import MainLayout from './components/layout/MainLayout'; import DatabaseInitializer from './components/core/DatabaseInitializer'; @@ -38,6 +39,7 @@ function App() { {activePage === 'image' && } {activePage === 'translation' && } {activePage === 'files' && } + {activePage === 'mcpserver' && } ); diff --git a/src/components/chat/ChatMessageArea.tsx b/src/components/chat/ChatMessageArea.tsx index 6bb96c1..9d3e5b5 100644 --- a/src/components/chat/ChatMessageArea.tsx +++ b/src/components/chat/ChatMessageArea.tsx @@ -1,6 +1,7 @@ import React, { useState, FormEvent, useRef, useEffect } from 'react'; import { Conversation, Message } from '../../types/chat'; -import { Send, Square, Copy, Pencil, Loader2, Globe, RefreshCw, Check, X } from 'lucide-react'; +import { MCPServerSettings } from '../../types/settings'; +import { Send, Square, Copy, Pencil, Loader2, Globe, RefreshCw, Check, X, ServerCog } from 'lucide-react'; import MarkdownContent from './MarkdownContent'; import MessageToolboxMenu, { ToolboxAction } from '../ui/MessageToolboxMenu'; import { MessageHelper } from '../../services/message-helper'; @@ -26,6 +27,9 @@ interface ChatMessageAreaProps { isCurrentlyStreaming?: boolean; selectedProvider: string; selectedModel: string; + mcpServers?: Record; + selectedMcpServers?: string[]; + onToggleMcpServer?: (serverId: string) => void; } export const ChatMessageArea: React.FC = ({ @@ -40,6 +44,9 @@ export const ChatMessageArea: React.FC = ({ isCurrentlyStreaming = false, selectedProvider, selectedModel, + mcpServers, + selectedMcpServers, + onToggleMcpServer, }) => { const { t } = useTranslation(); const [inputValue, setInput] = useState(''); @@ -54,6 +61,9 @@ export const ChatMessageArea: React.FC = ({ const [webSearchActive, setWebSearchActive] = useState(false); const [isWebSearchPreviewEnabled, setIsWebSearchPreviewEnabled] = useState(false); const [selectedFiles, setSelectedFiles] = useState([]); + const [mcpPopupOpen, setMcpPopupOpen] = useState(false); + const mcpButtonRef = useRef(null); + const mcpPopupRef = useRef(null); // Scroll to bottom when messages change useEffect(() => { @@ -278,6 +288,25 @@ export const ChatMessageArea: React.FC = ({ textarea.style.height = `${newHeight}px`; } + // Add useEffect to handle click outside for MCP popup + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + mcpPopupRef.current && + !mcpPopupRef.current.contains(event.target as Node) && + mcpButtonRef.current && + !mcpButtonRef.current.contains(event.target as Node) + ) { + setMcpPopupOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + // If no active conversation is selected if (!activeConversation) { return ( @@ -552,6 +581,70 @@ export const ChatMessageArea: React.FC = ({ webSearchElement } + {/* MCP Servers dropdown */} + {mcpServers && Object.keys(mcpServers).length > 0 && ( +
+ + + {mcpPopupOpen && ( +
+
+
+ {t('chat.availableMcpServers')} +
+
+ {Object.values(mcpServers).map((server) => ( +
onToggleMcpServer?.(server.id)} + > + +
+ {server.name} + {server.isDefault && ( + {t('mcpServer.default')} + )} + {server.isImageGeneration && ( + {t('mcpServer.imageGeneration')} + )} +
+
+ ))} + + {Object.keys(mcpServers).length === 0 && ( +
+ {t('chat.noMcpServersAvailable')} +
+ )} +
+
+
+ )} +
+ )} + diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index ae492e7..2f97b9e 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { MessageSquare, Settings, Image, Languages, FolderClosed } from 'lucide-react'; +import { MessageSquare, Settings, Image, Languages, FolderClosed, ServerCog } from 'lucide-react'; interface SidebarProps { activePage: string; @@ -31,6 +31,9 @@ export const Sidebar: React.FC = ({ else if(activePage === 'files'){ return 'files'; } + else if(activePage === 'mcpserver'){ + return 'mcpserver'; + } return ''; } @@ -87,6 +90,18 @@ export const Sidebar: React.FC = ({ > + + {/* Settings button at bottom */} diff --git a/src/components/pages/ChatPage.tsx b/src/components/pages/ChatPage.tsx index 15a4151..3a22fc9 100644 --- a/src/components/pages/ChatPage.tsx +++ b/src/components/pages/ChatPage.tsx @@ -5,6 +5,7 @@ import { Conversation, ConversationFolder } from '../../types/chat'; import { SETTINGS_CHANGE_EVENT, SettingsService } from '../../services/settings-service'; import { ChatService } from '../../services/chat-service'; import { AIService } from '../../services/ai-service'; +import { MCPServerSettings } from '../../types/settings'; export const ChatPage = () => { const [conversations, setConversations] = useState([]); @@ -17,6 +18,8 @@ export const ChatPage = () => { const [selectedModel, setSelectedModel] = useState(''); const [selectedProvider, setSelectedProvider] = useState(''); const [isApiKeyMissing, setIsApiKeyMissing] = useState(true); + const [mcpServers, setMcpServers] = useState>({}); + const [selectedMcpServers, setSelectedMcpServers] = useState([]); // Initialize the services useEffect(() => { @@ -43,6 +46,10 @@ export const ChatPage = () => { const foldersList = chatService.getFolders(); setFolders(foldersList); + // Load MCP servers + const mcpServersList = chatService.getAvailableMCPServers(); + setMcpServers(mcpServersList); + // Set active conversation from chat service const activeId = chatService.getActiveConversationId(); if (activeId) { @@ -65,6 +72,22 @@ export const ChatPage = () => { }, [isServiceInitialized]); + // Load MCP servers when settings change + useEffect(() => { + const handleSettingsChange = () => { + if (chatServiceRef.current) { + const mcpServersList = chatServiceRef.current.getAvailableMCPServers(); + setMcpServers(mcpServersList); + } + }; + + window.addEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange); + + return () => { + window.removeEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange); + }; + }, []); + // Load active conversation details when selected useEffect(() => { if (activeConversationId && isServiceInitialized && chatServiceRef.current) { @@ -206,25 +229,32 @@ export const ChatPage = () => { try { const chatService = chatServiceRef.current; - - // Send user message with streaming - await chatService.sendMessage( - content, - activeConversationId, - true, - (updatedConversation) => { - setConversations(updatedConversation); - } - ); + // Check if there are selected MCP servers to use + if (selectedMcpServers.length > 0) { + // Send message with MCP tools + await chatService.sendMessageWithMCPTools( + content, + activeConversationId, + selectedMcpServers, + true, + (updatedConversation) => { + setConversations(updatedConversation); + } + ); + } else { + // Send regular message + await chatService.sendMessage( + content, + activeConversationId, + true, + (updatedConversation) => { + setConversations(updatedConversation); + } + ); + } } catch (err) { console.error('Error sending streaming message:', err); - - // // If streaming fails, we'll try to fall back to regular mode - // const error = err as Error; - // if (error.message && error.message.includes('does not support streaming')) { - // await handleSendMessage(content); - // } } }; @@ -368,6 +398,17 @@ export const ChatPage = () => { } }; + // Toggle selection of an MCP server + const handleToggleMcpServer = (serverId: string) => { + setSelectedMcpServers(prev => { + if (prev.includes(serverId)) { + return prev.filter(id => id !== serverId); + } else { + return [...prev, serverId]; + } + }); + }; + return (
@@ -406,6 +447,9 @@ export const ChatPage = () => { isCurrentlyStreaming={chatServiceRef.current?.isCurrentlyStreaming(activeConversationId) || false} selectedProvider={selectedProvider} selectedModel={selectedModel} + mcpServers={mcpServers} + selectedMcpServers={selectedMcpServers} + onToggleMcpServer={handleToggleMcpServer} />
diff --git a/src/components/pages/MCPServerPage.tsx b/src/components/pages/MCPServerPage.tsx new file mode 100644 index 0000000..60f663d --- /dev/null +++ b/src/components/pages/MCPServerPage.tsx @@ -0,0 +1,278 @@ +import React, { useState, useEffect } from 'react'; +import { Plus, Edit, Trash2, Server } from 'lucide-react'; +import { MCPServerSettings } from '../../types/settings'; +import { MCPService } from '../../services/mcp-service'; +import { SETTINGS_CHANGE_EVENT } from '../../services/settings-service'; +import { useTranslation } from '../../hooks/useTranslation'; + +interface MCPServerFormProps { + server?: MCPServerSettings; + onSave: (name: string, type: 'sse' | 'stdio' | 'streamableHttp', url: string, headers?: Record) => void; + onCancel: () => void; +} + +const MCPServerForm: React.FC = ({ server, onSave, onCancel }) => { + const { t } = useTranslation(); + const [name, setName] = useState(server?.name || ''); + const [type, setType] = useState<'sse' | 'stdio' | 'streamableHttp'>(server?.type || 'sse'); + const [url, setUrl] = useState(server?.url || ''); + const [headers, setHeaders] = useState(server?.headers ? JSON.stringify(server.headers, null, 2) : '{}'); + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + setIsValid(name.trim() !== '' && url.trim() !== ''); + }, [name, url]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + let parsedHeaders: Record | undefined; + try { + parsedHeaders = JSON.parse(headers); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + alert('Invalid JSON in headers field'); + return; + } + + onSave(name, type, url, parsedHeaders); + }; + + return ( +
+
+ + setName(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-300" + placeholder={t('mcpServer.serverNamePlaceholder')} + required + /> +
+ +
+ + +
+ +
+ + setUrl(e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-300" + placeholder={t('mcpServer.serverURLPlaceholder')} + required + /> +
+ +
+ +
*/} - {/*
+
{[ {capability: AIServiceCapability.TextCompletion, label: 'Text Completion'}, - {capability: AIServiceCapability.Reasoning, label: 'Reasoning'}, - {capability: AIServiceCapability.VisionAnalysis, label: 'Vision'}, - {capability: AIServiceCapability.ToolUsage, label: 'Tool Usage'}, - {capability: AIServiceCapability.Embedding, label: 'Embedding'} + {capability: AIServiceCapability.ImageGeneration, label: 'Image Generation'}, ].map(({capability, label}) => (
= ({
))}
-
*/} +
diff --git a/src/services/providers/custom-service.ts b/src/services/providers/custom-service.ts index a7569b7..471c653 100644 --- a/src/services/providers/custom-service.ts +++ b/src/services/providers/custom-service.ts @@ -143,13 +143,29 @@ export class CustomService implements AiServiceProvider { */ public async getImageGeneration( prompt: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars options: { size?: `${number}x${number}`; + aspectRatio?: `${number}:${number}`; style?: string; quality?: string; - } = {} - ): Promise { - throw new Error('Not implemented'); + } + ): Promise[]> { + + const imageModel = this.openAIProvider.imageModel('dall-e-3'); + + const result = await imageModel.doGenerate({ + prompt: prompt, + n: 1, + size: options.size || '1024x1024', + aspectRatio: options.aspectRatio || '1:1', + seed: 42, + providerOptions: { + "openai": { + "style": options.style || 'vivid' + } + } + }); + + return result.images; } } \ No newline at end of file diff --git a/src/services/settings-service.ts b/src/services/settings-service.ts index 2d20e53..e3495b6 100644 --- a/src/services/settings-service.ts +++ b/src/services/settings-service.ts @@ -9,31 +9,31 @@ import { v4 as uuidv4 } from 'uuid'; */ const DEFAULT_SETTINGS: UserSettings = { providers: { - ['TensorBlock']: { - providerId: 'TensorBlock', - providerName: 'TensorBlock', - apiKey: '', - baseUrl: 'http://54.177.123.202:8000/v1', - customProvider: false, - models:[ - { - modelName: 'GPT-4o', - modelId: 'gpt-4o', - modelCategory: 'GPT 4', - modelDescription: 'GPT-4o is the latest and most powerful model from OpenAI.', - modelCapabilities: [AIServiceCapability.TextCompletion, AIServiceCapability.WebSearch], - modelRefUUID: uuidv4(), - }, - { - modelName: 'DALL-E 3', - modelId: 'dall-e-3', - modelCategory: 'Image Generation', - modelDescription: 'DALL-E 3 is OpenAI\'s advanced image generation model.', - modelCapabilities: [AIServiceCapability.ImageGeneration], - modelRefUUID: uuidv4(), - }, - ] - }, + // ['TensorBlock']: { + // providerId: 'TensorBlock', + // providerName: 'TensorBlock', + // apiKey: '', + // baseUrl: 'http://54.177.123.202:8000/v1', + // customProvider: false, + // models:[ + // { + // modelName: 'GPT-4o', + // modelId: 'gpt-4o', + // modelCategory: 'GPT 4', + // modelDescription: 'GPT-4o is the latest and most powerful model from OpenAI.', + // modelCapabilities: [AIServiceCapability.TextCompletion, AIServiceCapability.WebSearch], + // modelRefUUID: uuidv4(), + // }, + // { + // modelName: 'DALL-E 3', + // modelId: 'dall-e-3', + // modelCategory: 'Image Generation', + // modelDescription: 'DALL-E 3 is OpenAI\'s advanced image generation model.', + // modelCapabilities: [AIServiceCapability.ImageGeneration], + // modelRefUUID: uuidv4(), + // }, + // ] + // }, ['OpenAI']: { providerId: 'OpenAI', providerName: 'OpenAI', @@ -348,6 +348,10 @@ export class SettingsService { }); } } + else if(provider === 'TensorBlock'){ + // Remove Default TensorBlock provider + delete this.settings.providers[provider]; + } } } From 9c74b4ae623d198023cc67bd46481bf301094678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sat, 17 May 2025 00:21:51 +0800 Subject: [PATCH 23/27] Fix: Add custom image generation capabilities edit --- src/components/pages/ImageGenerationPage.tsx | 2 + src/services/ai-service.ts | 122 ++++++++++++------ src/services/core/ai-service-provider.ts | 2 +- src/services/providers/anthropic-service.ts | 14 +- .../providers/common-provider-service.ts | 14 +- src/services/providers/custom-service.ts | 24 ++-- src/services/providers/fireworks-service.ts | 19 ++- src/services/providers/forge-service.ts | 19 ++- src/services/providers/gemini-service.ts | 17 ++- src/services/providers/openai-service.ts | 19 ++- src/services/providers/openrouter-service.ts | 17 ++- src/services/providers/together-service.ts | 19 ++- 12 files changed, 198 insertions(+), 90 deletions(-) diff --git a/src/components/pages/ImageGenerationPage.tsx b/src/components/pages/ImageGenerationPage.tsx index 687196f..0d54207 100644 --- a/src/components/pages/ImageGenerationPage.tsx +++ b/src/components/pages/ImageGenerationPage.tsx @@ -12,6 +12,8 @@ import { ImageGenerationManager, ImageGenerationStatus, ImageGenerationHandler } import { DatabaseIntegrationService } from "../../services/database-integration"; import { ImageGenerationResult } from "../../types/image"; import ImageGenerateHistoryItem from "../image/ImageGenerateHistoryItem"; +import { AiServiceProvider } from "../../types/ai-service"; +import { AiServiceCapability } from "../../types/ai-service"; export const ImageGenerationPage = () => { const { t } = useTranslation(); diff --git a/src/services/ai-service.ts b/src/services/ai-service.ts index df2e2d5..246f44d 100644 --- a/src/services/ai-service.ts +++ b/src/services/ai-service.ts @@ -4,6 +4,7 @@ import { Message } from '../types/chat'; import { StreamControlHandler } from './streaming-control'; import { SETTINGS_CHANGE_EVENT, SettingsService } from './settings-service'; import { MCPService } from './mcp-service'; +import { AIServiceCapability } from '../types/capabilities'; export interface ModelOption { id: string; @@ -35,7 +36,7 @@ export class AIService { private state: AIState = { status: 'idle', error: null, - isCachingModels: false + isCachingModels: false, }; private listeners: Set<() => void> = new Set(); private modelCache: Map = new Map(); @@ -68,11 +69,17 @@ export class AIService { for (const providerID of Object.keys(settings.providers)) { const providerSettings = settings.providers[providerID]; - if(this.providers.has(providerID)) { + if (this.providers.has(providerID)) { this.providers.delete(providerID); - this.providers.set(providerID, ProviderFactory.getNewProvider(providerID)); - } - else if (providerSettings && providerSettings.apiKey && providerSettings.apiKey.length > 0) { + this.providers.set( + providerID, + ProviderFactory.getNewProvider(providerID) + ); + } else if ( + providerSettings && + providerSettings.apiKey && + providerSettings.apiKey.length > 0 + ) { const providerInstance = ProviderFactory.getNewProvider(providerID); if (providerInstance) { this.providers.set(providerID, providerInstance); @@ -90,7 +97,7 @@ export class AIService { // Refresh models when settings change this.refreshModels(); }; - + window.addEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange); } @@ -108,7 +115,7 @@ export class AIService { * Notify all listeners of state changes */ private notifyListeners(): void { - this.listeners.forEach(listener => listener()); + this.listeners.forEach((listener) => listener()); } /** @@ -125,7 +132,7 @@ export class AIService { private handleSuccess(): void { this.setState({ status: 'success', - error: null + error: null, }); } @@ -136,7 +143,7 @@ export class AIService { console.error('AI request error:', error); this.setState({ status: 'error', - error + error, }); } @@ -154,14 +161,14 @@ export class AIService { if (this.providers.has(name)) { return this.providers.get(name); } - + // If provider not in cache, try to create it const provider = ProviderFactory.getNewProvider(name); if (provider) { this.providers.set(name, provider); return provider; } - + return undefined; } @@ -172,11 +179,26 @@ export class AIService { return Array.from(this.providers.values()); } + /** + * Get all providers that support image generation + */ + public getImageGenerationProviders(): AiServiceProvider[] { + const providers = this.getAllProviders(); + return providers.filter((provider) => { + // Check if the provider has any models with image generation capability + const models = provider.availableModels || []; + return models.some((model) => { + const capabilities = provider.getModelCapabilities(model.modelId); + return capabilities.includes(AIServiceCapability.ImageGeneration); + }); + }); + } + /** * Get a streaming chat completion from the AI */ public async getChatCompletion( - messages: Message[], + messages: Message[], options: CompletionOptions, streamController: StreamControlHandler ): Promise { @@ -185,18 +207,25 @@ export class AIService { const providerName = options.provider; const modelName = options.model; const useStreaming = options.stream; - + // Get provider instance const provider = this.getProvider(providerName); - console.log('Provider: ', providerName, ' Model: ', modelName, ' Use streaming: ', useStreaming); - + console.log( + 'Provider: ', + providerName, + ' Model: ', + modelName, + ' Use streaming: ', + useStreaming + ); + if (!provider) { throw new Error(`Provider ${providerName} not available`); } - + const result = await provider.getChatCompletion( - messages, + messages, { model: modelName, provider: providerName, @@ -212,7 +241,6 @@ export class AIService { }, streamController ); - return result; } catch (e) { @@ -221,8 +249,11 @@ export class AIService { this.handleSuccess(); return null; } - - const error = e instanceof Error ? e : new Error('Unknown error during streaming chat completion'); + + const error = + e instanceof Error + ? e + : new Error('Unknown error during streaming chat completion'); this.handleError(error); return null; } @@ -244,20 +275,20 @@ export class AIService { throw new Error('Not implemented'); // this.startRequest(); - + // try { // const provider = this.getImageGenerationProvider(); - + // if (!provider) { // throw new Error('No image generation provider available'); // } - + // if (!provider.generateImage) { // throw new Error(`Provider ${provider.name} does not support image generation`); // } - + // const result = await provider.generateImage(prompt, options); - + // this.handleSuccess(); // return result; // } catch (e) { @@ -317,62 +348,67 @@ export class AIService { const cacheKey = 'all_providers'; const cachedTime = this.lastFetchTime.get(cacheKey) || 0; const now = Date.now(); - + // Return cached models if they're still valid if (this.modelCache.has(cacheKey) && now - cachedTime < this.CACHE_TTL) { return this.modelCache.get(cacheKey) || []; } - + // Otherwise, collect models from all providers const allModels: ModelOption[] = []; const providerPromises = []; - + for (const provider of this.getAllProviders()) { providerPromises.push(this.getModelsForProvider(provider.id)); } - + const results = await Promise.all(providerPromises); - + // Flatten results and filter out duplicates - results.forEach(models => { + results.forEach((models) => { allModels.push(...models); }); - + // Cache and return results this.modelCache.set(cacheKey, allModels); this.lastFetchTime.set(cacheKey, now); - + return allModels; } /** * Get models for a specific provider */ - public async getModelsForProvider(providerName: string): Promise { + public async getModelsForProvider( + providerName: string + ): Promise { // Check if we already have a cached result const cachedTime = this.lastFetchTime.get(providerName) || 0; const now = Date.now(); - + // Return cached models if they're still valid - if (this.modelCache.has(providerName) && now - cachedTime < this.CACHE_TTL) { + if ( + this.modelCache.has(providerName) && + now - cachedTime < this.CACHE_TTL + ) { return this.modelCache.get(providerName) || []; } - + // Get provider instance const provider = this.getProvider(providerName); if (!provider) { console.warn(`Provider ${providerName} not available`); return []; } - + this.setState({ isCachingModels: true }); - + try { // Fetch models from provider const models = await provider.fetchAvailableModels(); - + // Convert to ModelOption format - const modelOptions: ModelOption[] = models.map(model => ({ + const modelOptions: ModelOption[] = models.map((model) => ({ id: model.modelId, name: model.modelName, provider: providerName, @@ -381,7 +417,7 @@ export class AIService { // Cache results this.modelCache.set(providerName, modelOptions); this.lastFetchTime.set(providerName, now); - + this.setState({ isCachingModels: false }); return modelOptions; } catch (error) { @@ -398,7 +434,7 @@ export class AIService { // Clear cache this.modelCache.clear(); this.lastFetchTime.clear(); - + this.refreshProviders(); // Re-fetch all models diff --git a/src/services/core/ai-service-provider.ts b/src/services/core/ai-service-provider.ts index 10b6b63..d8685ed 100644 --- a/src/services/core/ai-service-provider.ts +++ b/src/services/core/ai-service-provider.ts @@ -51,7 +51,7 @@ export interface AiServiceProvider { /** * Get the capabilities of a model with this provider */ - getModelCapabilities(model: string): AIServiceCapability[]; + getModelCapabilities(modelId: string): AIServiceCapability[]; /** * Fetch available models from the provider API diff --git a/src/services/providers/anthropic-service.ts b/src/services/providers/anthropic-service.ts index 911dabb..6650631 100644 --- a/src/services/providers/anthropic-service.ts +++ b/src/services/providers/anthropic-service.ts @@ -76,10 +76,18 @@ export class AnthropicService implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/common-provider-service.ts b/src/services/providers/common-provider-service.ts index 0d238d7..4802439 100644 --- a/src/services/providers/common-provider-service.ts +++ b/src/services/providers/common-provider-service.ts @@ -104,10 +104,18 @@ export class CommonProviderHelper implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.providerID); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/custom-service.ts b/src/services/providers/custom-service.ts index 471c653..8eb743d 100644 --- a/src/services/providers/custom-service.ts +++ b/src/services/providers/custom-service.ts @@ -13,6 +13,7 @@ import { CommonProviderHelper } from './common-provider-service'; */ export class CustomService implements AiServiceProvider { + private settingsService: SettingsService; private openAIProvider: OpenAIProvider; private apiModels: ModelSettings[] = []; @@ -29,8 +30,8 @@ export class CustomService implements AiServiceProvider { constructor(providerID: string) { this.providerID = providerID; - const settingsService = SettingsService.getInstance(); - const providerSettings = settingsService.getProviderSettings(this.providerID); + this.settingsService = SettingsService.getInstance(); + const providerSettings = this.settingsService.getProviderSettings(this.providerID); const apiKey = providerSettings.apiKey; const baseURL = providerSettings.baseUrl; @@ -49,8 +50,7 @@ export class CustomService implements AiServiceProvider { * Get the name of the service provider */ get name(): string { - const settingsService = SettingsService.getInstance(); - const providerSettings = settingsService.getProviderSettings(this.providerID); + const providerSettings = this.settingsService.getProviderSettings(this.providerID); const error = new Error('Custom provider settings: ' + JSON.stringify(providerSettings)); console.log(error); console.log('Provider Name: ', providerSettings.providerName); @@ -75,8 +75,7 @@ export class CustomService implements AiServiceProvider { * Fetch the list of available models from Forge */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(this.providerID); + const models = this.settingsService.getModels(this.providerID); this.apiModels = models; @@ -87,9 +86,18 @@ export class CustomService implements AiServiceProvider { * Get the capabilities of a model with this provider */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.providerID); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/fireworks-service.ts b/src/services/providers/fireworks-service.ts index 46740f6..3206809 100644 --- a/src/services/providers/fireworks-service.ts +++ b/src/services/providers/fireworks-service.ts @@ -15,6 +15,7 @@ export const FIREWORKS_PROVIDER_NAME = 'Fireworks.ai'; */ export class FireworksService implements AiServiceProvider { + private settingsService: SettingsService; private commonProviderHelper: CommonProviderHelper; private apiModels: ModelSettings[] = []; @@ -22,6 +23,7 @@ export class FireworksService implements AiServiceProvider { * Create a new OpenAI service provider */ constructor() { + this.settingsService = SettingsService.getInstance(); this.commonProviderHelper = new CommonProviderHelper(FIREWORKS_PROVIDER_NAME, this.createClient); } @@ -55,8 +57,7 @@ export class FireworksService implements AiServiceProvider { * Fetch the list of available models from OpenAI */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(FIREWORKS_PROVIDER_NAME); + const models = this.settingsService.getModels(FIREWORKS_PROVIDER_NAME); this.apiModels = models; @@ -66,10 +67,18 @@ export class FireworksService implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/forge-service.ts b/src/services/providers/forge-service.ts index c83496a..bdb5de7 100644 --- a/src/services/providers/forge-service.ts +++ b/src/services/providers/forge-service.ts @@ -17,11 +17,13 @@ export class ForgeService implements AiServiceProvider { private commonProviderHelper: CommonProviderHelper; private apiModels: ModelSettings[] = []; + private settingsService: SettingsService; /** * Create a new Forge service provider */ constructor() { + this.settingsService = SettingsService.getInstance(); this.commonProviderHelper = new CommonProviderHelper(FORGE_PROVIDER_NAME, this.createClient); } @@ -59,8 +61,7 @@ export class ForgeService implements AiServiceProvider { * Fetch the list of available models from Forge */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(FORGE_PROVIDER_NAME); + const models = this.settingsService.getModels(FORGE_PROVIDER_NAME); this.apiModels = models; @@ -70,10 +71,18 @@ export class ForgeService implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/gemini-service.ts b/src/services/providers/gemini-service.ts index 7fe2050..5579a79 100644 --- a/src/services/providers/gemini-service.ts +++ b/src/services/providers/gemini-service.ts @@ -60,8 +60,7 @@ export class GeminiService implements AiServiceProvider { * Fetch the list of available models from OpenAI */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(GEMINI_PROVIDER_NAME); + const models = this.settingsService.getModels(GEMINI_PROVIDER_NAME); this.apiModels = models; @@ -71,10 +70,18 @@ export class GeminiService implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/openai-service.ts b/src/services/providers/openai-service.ts index 2045ceb..027860d 100644 --- a/src/services/providers/openai-service.ts +++ b/src/services/providers/openai-service.ts @@ -17,11 +17,13 @@ export class OpenAIService implements AiServiceProvider { private commonProviderHelper: CommonProviderHelper; private apiModels: ModelSettings[] = []; + private settingsService: SettingsService; /** * Create a new OpenAI service provider */ constructor() { + this.settingsService = SettingsService.getInstance(); this.commonProviderHelper = new CommonProviderHelper(OPENAI_PROVIDER_NAME, this.createClient); } @@ -58,8 +60,7 @@ export class OpenAIService implements AiServiceProvider { * Fetch the list of available models from OpenAI */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(OPENAI_PROVIDER_NAME); + const models = this.settingsService.getModels(OPENAI_PROVIDER_NAME); this.apiModels = models; @@ -70,15 +71,19 @@ export class OpenAIService implements AiServiceProvider { * Get the capabilities of a model with this provider */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { - // Add image generation capability for DALL-E 3 - if (model === 'dall-e-3') { - return [AIServiceCapability.ImageGeneration]; + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; } // Default capabilities for chat models return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/openrouter-service.ts b/src/services/providers/openrouter-service.ts index eba39ca..22df980 100644 --- a/src/services/providers/openrouter-service.ts +++ b/src/services/providers/openrouter-service.ts @@ -58,8 +58,7 @@ export class OpenRouterService implements AiServiceProvider { * Fetch the list of available models from OpenAI */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(OPENROUTER_PROVIDER_NAME); + const models = this.settingsService.getModels(OPENROUTER_PROVIDER_NAME); this.apiModels = models; @@ -69,10 +68,18 @@ export class OpenRouterService implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, diff --git a/src/services/providers/together-service.ts b/src/services/providers/together-service.ts index 84a00b8..8fcb463 100644 --- a/src/services/providers/together-service.ts +++ b/src/services/providers/together-service.ts @@ -17,11 +17,13 @@ export class TogetherService implements AiServiceProvider { private commonProviderHelper: CommonProviderHelper; private apiModels: ModelSettings[] = []; + private settingsService: SettingsService; /** * Create a new OpenAI service provider */ constructor() { + this.settingsService = SettingsService.getInstance(); this.commonProviderHelper = new CommonProviderHelper(TOGETHER_PROVIDER_NAME, this.createClient); } @@ -56,8 +58,7 @@ export class TogetherService implements AiServiceProvider { * Fetch the list of available models from OpenAI */ public async fetchAvailableModels(): Promise { - const settingsService = SettingsService.getInstance(); - const models = settingsService.getModels(TOGETHER_PROVIDER_NAME); + const models = this.settingsService.getModels(TOGETHER_PROVIDER_NAME); this.apiModels = models; @@ -67,10 +68,18 @@ export class TogetherService implements AiServiceProvider { /** * Get the capabilities of a model with this provider */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getModelCapabilities(model: string): AIServiceCapability[] { + getModelCapabilities(modelId: string): AIServiceCapability[] { + // Get model data by modelId + const models = this.settingsService.getModels(this.name); + const modelData = models.find(x => x.modelId === modelId); + let hasImageGeneration = false; + + if(modelData?.modelCapabilities.findIndex(x => x === AIServiceCapability.ImageGeneration) !== -1){ + hasImageGeneration = true; + } + return mapModelCapabilities( - false, + hasImageGeneration, false, false, false, From 01bb14a7e7149a2d1eaa9a3c31349deb901f1cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E7=A9=BA?= <2793500992@qq.com> Date: Sat, 17 May 2025 00:49:53 +0800 Subject: [PATCH 24/27] Fix: Custom Provider Image Generation --- src/components/pages/ImageGenerationPage.tsx | 105 ++++++++++++------ src/services/providers/anthropic-service.ts | 2 + .../providers/common-provider-service.ts | 4 +- src/services/providers/custom-service.ts | 8 +- src/services/providers/fireworks-service.ts | 1 + src/services/providers/forge-service.ts | 1 + src/services/providers/gemini-service.ts | 1 + src/services/providers/openai-service.ts | 1 + src/services/providers/openrouter-service.ts | 1 + src/services/providers/together-service.ts | 1 + src/types/capabilities.ts | 2 +- 11 files changed, 87 insertions(+), 40 deletions(-) diff --git a/src/components/pages/ImageGenerationPage.tsx b/src/components/pages/ImageGenerationPage.tsx index 0d54207..8c41add 100644 --- a/src/components/pages/ImageGenerationPage.tsx +++ b/src/components/pages/ImageGenerationPage.tsx @@ -7,13 +7,11 @@ import { ChevronDown, RefreshCw, Settings, Zap } from "lucide-react"; import { useTranslation } from "react-i18next"; import { AIService } from "../../services/ai-service"; import { OPENAI_PROVIDER_NAME } from "../../services/providers/openai-service"; -import { FORGE_PROVIDER_NAME as TENSORBLOCK_PROVIDER_NAME } from "../../services/providers/forge-service"; import { ImageGenerationManager, ImageGenerationStatus, ImageGenerationHandler } from "../../services/image-generation-handler"; import { DatabaseIntegrationService } from "../../services/database-integration"; import { ImageGenerationResult } from "../../types/image"; import ImageGenerateHistoryItem from "../image/ImageGenerateHistoryItem"; -import { AiServiceProvider } from "../../types/ai-service"; -import { AiServiceCapability } from "../../types/ai-service"; + export const ImageGenerationPage = () => { const { t } = useTranslation(); @@ -33,6 +31,8 @@ export const ImageGenerationPage = () => { const [isLoadingHistory, setIsLoadingHistory] = useState(true); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [selectedProvider, setSelectedProvider] = useState(OPENAI_PROVIDER_NAME); + const [selectedModel, setSelectedModel] = useState("dall-e-3"); + const [availableProviders, setAvailableProviders] = useState<{id: string, name: string}[]>([]); const settingsButtonRef = useRef(null); const settingsPopupRef = useRef(null); @@ -62,6 +62,31 @@ export const ImageGenerationPage = () => { } }, []); + // Load available image generation providers + const loadImageGenerationProviders = useCallback(async () => { + const aiService = AIService.getInstance(); + const providers = aiService.getImageGenerationProviders(); + + const providerOptions = providers.map(provider => ({ + id: provider.id, + name: provider.name || provider.id + })); + + setAvailableProviders(providerOptions); + + // Set default provider if none is selected or current one isn't available + if (!selectedProvider || !providerOptions.some(p => p.id === selectedProvider)) { + if (providerOptions.length > 0) { + setSelectedProvider(providerOptions[0].id); + } + } + }, [selectedProvider]); + + const handleGetProviderNameById = (id: string) => { + const provider = availableProviders.find(p => p.id === id); + return provider ? provider.name : id; + } + // Initialize image generation manager and load settings useEffect(() => { const initialize = async () => { @@ -88,6 +113,12 @@ export const ImageGenerationPage = () => { if (settings.imageGenerationProvider) { setSelectedProvider(settings.imageGenerationProvider); } + if (settings.imageGenerationModel) { + setSelectedModel(settings.imageGenerationModel); + } + + // Load available providers + await loadImageGenerationProviders(); // Load image generation history from database await refreshImageHistory(); @@ -100,7 +131,19 @@ export const ImageGenerationPage = () => { }; initialize(); - }, [refreshImageHistory]); + }, [refreshImageHistory, loadImageGenerationProviders]); + + // Listen for settings changes + useEffect(() => { + const handleSettingsChange = () => { + loadImageGenerationProviders(); + }; + + window.addEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange); + return () => { + window.removeEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange); + }; + }, [loadImageGenerationProviders]); // Check if API key is available useEffect(() => { @@ -183,14 +226,9 @@ export const ImageGenerationPage = () => { setError(null); try { - let providerService; - // Get the appropriate service based on selected provider - if (selectedProvider === TENSORBLOCK_PROVIDER_NAME) { - providerService = AIService.getInstance().getProvider(TENSORBLOCK_PROVIDER_NAME); - } else { - providerService = AIService.getInstance().getProvider(OPENAI_PROVIDER_NAME); - } + const aiService = AIService.getInstance(); + const providerService = aiService.getProvider(selectedProvider); if (!providerService) { throw new Error(`${selectedProvider} service not available`); @@ -201,10 +239,10 @@ export const ImageGenerationPage = () => { const handler = imageManager.createHandler({ prompt: prompt, seed: randomSeed, - number: imageCount, + number: 1, aspectRatio: aspectRatio, provider: selectedProvider, - model: "dall-e-3", + model: selectedModel, }); // Set status to generating @@ -480,7 +518,7 @@ export const ImageGenerationPage = () => {
-