From ba5d84ffb3e3c412530dc143a46afd323fa3ae60 Mon Sep 17 00:00:00 2001 From: ftboy Date: Mon, 8 Sep 2025 15:13:25 +0800 Subject: [PATCH] =?UTF-8?q?chore(=E5=AF=BC=E5=85=A5):=20=E4=BB=8E=20deepch?= =?UTF-8?q?at@v0.3.5=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=EF=BC=8C=E9=81=B5=E5=BE=AA=20Apache-2.0=20=E8=AE=B8=E5=8F=AF?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/agents/electron-architecture-agent.md | 61 + .claude/agents/i18n-code-reviewer.md | 58 + .claude/agents/llm-provider-agent.md | 53 + .claude/agents/mcp-integration-agent.md | 81 + .cursor/rules/development-setup.mdc | 26 + .cursor/rules/electron-best-practices.mdc | 18 + .cursor/rules/error-logging.mdc | 93 + .cursor/rules/i18n.mdc | 74 + .cursor/rules/llm-agent-loop.mdc | 91 + .cursor/rules/performance.mdc | 5 + .cursor/rules/pinia-best-practices.mdc | 10 + .cursor/rules/project-structure.mdc | 32 + .cursor/rules/provider-guidelines.mdc | 40 + .cursor/rules/vue-best-practices.mdc | 10 + .cursor/rules/vue-router-best-practices.mdc | 10 + .cursor/rules/vue-shadcn.mdc | 79 + .cursorignore | 2 + .editorconfig | 9 + .env.example | 9 + .github/ISSUE_TEMPLATE/bug_report.md | 63 + .github/ISSUE_TEMPLATE/feature_request.md | 33 + .github/pull_request_template.md | 45 + .github/workflows/build.yml | 187 + .github/workflows/deploycdn.yml | 142 + .github/workflows/prcheck.yml | 49 + .github/workflows/release.yml | 116 + .gitignore | 21 + .oxlintrc.json | 17 + .prettierignore | 25 + .prettierrc.yaml | 4 + CLAUDE.md | 343 + CONTRIBUTING.md | 130 + CONTRIBUTING.zh.md | 130 + Dockerfile.build.linux | 26 + LICENSE | 202 + README.en.md | 36 - README.jp.md | 335 + README.md | 355 +- README.zh.md | 335 + brand-config.example-banana.json | 58 + brand-config.template.json | 80 + build/VERSION-README.md | 65 + build/entitlements.mac.plist | 18 + build/generate-version-files.mjs | 85 + build/icnsmaker.sh | 40 + build/icon.icns | Bin 0 -> 208560 bytes build/icon.ico | Bin 0 -> 26472 bytes build/icon.png | Bin 0 -> 51176 bytes build/nsis-installer.nsh | 158 + components.json | 18 + docs/agent/implementation-tasks.md | 50 + docs/agent/message-architecture.md | 857 + docs/agent/presenter-split-plan.md | 95 + docs/app-lifecycle.md | 96 + docs/config-presenter-architecture.md | 135 + docs/deepchat-architecture-overview.md | 299 + docs/deeplinks.md | 184 + docs/developer-guide.md | 138 + docs/i18n-review-progress.md | 93 + docs/ipc/eventbus-usage.md | 443 + docs/ipc/ipc-architecture-complete.md | 474 + docs/ipc/optimization-recommendations.md | 127 + docs/knowledge-presenter-complete.md | 229 + docs/linux-build-guide.md | 67 + docs/markdown-support.md | 86 + docs/mcp-architecture.md | 476 + docs/provider-optimization-summary.md | 99 + docs/rebrand-guide.md | 515 + docs/tool-calling-system.md | 608 + docs/user-guide.md | 183 + electron-builder-macx64.yml | 71 + electron-builder.yml | 91 + electron.vite.config.ts | 100 + package.json | 197 + pnpm-workspace.yaml | 5 + resources/blankSearch.html | 13 + resources/cdn/Recharts.js | 3 + resources/cdn/babel.min.js | 2 + resources/cdn/lucide.js | 16713 ++++++++++++++++ resources/cdn/prop-types.min.js | 1 + resources/cdn/react-dom.production.min.js | 267 + resources/cdn/react.production.min.js | 31 + resources/cdn/tailwind.min.css | 1 + resources/icon.ico | Bin 0 -> 26472 bytes resources/icon.png | Bin 0 -> 51176 bytes resources/linux_tray.png | Bin 0 -> 2738 bytes resources/macTrayTemplate.png | Bin 0 -> 1066 bytes resources/win_tray.ico | Bin 0 -> 22108 bytes scripts/afterPack.js | 19 + scripts/brand-assets/.gitkeep | 14 + scripts/generate-i18n-types.js | 62 + scripts/install-sharp-for-platform.js | 95 + scripts/installVss.js | 52 + scripts/notarize.js | 33 + scripts/rebrand.js | 510 + scripts/verify-commit.js | 28 + src/main/contextMenuHelper.ts | 397 + src/main/env.d.ts | 12 + src/main/eventbus.ts | 151 + src/main/events.ts | 214 + src/main/index.ts | 60 + src/main/lib/scrollCapture.ts | 361 + src/main/lib/svgSanitizer.ts | 310 + src/main/lib/system.ts | 19 + .../lib/textsplitters/document/document.ts | 45 + src/main/lib/textsplitters/document/index.ts | 1 + src/main/lib/textsplitters/index.ts | 16 + src/main/lib/textsplitters/text_splitter.ts | 699 + src/main/lib/watermark.ts | 162 + src/main/presenter/anthropicOAuth.ts | 224 + src/main/presenter/configPresenter/aes.ts | 92 + src/main/presenter/configPresenter/index.ts | 1434 ++ .../configPresenter/knowledgeConfHelper.ts | 51 + .../configPresenter/mcpConfHelper.ts | 939 + .../presenter/configPresenter/modelConfig.ts | 338 + .../configPresenter/modelDefaultSettings.ts | 1966 ++ .../configPresenter/providerModelSettings.ts | 3269 +++ .../presenter/configPresenter/providers.ts | 627 + .../configPresenter/shortcutKeySettings.ts | 36 + src/main/presenter/deeplinkPresenter/index.ts | 522 + src/main/presenter/devicePresenter/index.ts | 508 + src/main/presenter/dialogPresenter/index.ts | 90 + .../filePresenter/AudioFileAdapter.ts | 25 + .../filePresenter/BaseFileAdapter.ts | 74 + .../filePresenter/CodeFileAdapter.ts | 356 + .../presenter/filePresenter/CsvFileAdapter.ts | 83 + .../filePresenter/DirectoryAdapter.ts | 47 + .../presenter/filePresenter/DocFileAdapter.ts | 115 + .../filePresenter/ExcelFileAdapter.ts | 101 + .../filePresenter/FileAdapterConstructor.ts | 25 + .../presenter/filePresenter/FilePresenter.ts | 317 + .../filePresenter/FileValidationService.ts | 227 + .../filePresenter/ImageFileAdapter.ts | 105 + .../presenter/filePresenter/PdfFileAdapter.ts | 258 + .../presenter/filePresenter/PptFileAdapter.ts | 164 + .../filePresenter/TextFileAdapter.ts | 42 + .../filePresenter/UnsupportFileAdapter.ts | 29 + src/main/presenter/filePresenter/mime.ts | 207 + .../FloatingButtonWindow.ts | 223 + .../floatingButtonPresenter/index.ts | 393 + .../floatingButtonPresenter/types.ts | 56 + src/main/presenter/githubCopilotDeviceFlow.ts | 490 + src/main/presenter/githubCopilotOAuth.ts | 280 + src/main/presenter/index.ts | 295 + .../database/duckdbPresenter.ts | 954 + .../presenter/knowledgePresenter/index.ts | 461 + .../knowledgeStorePresenter.ts | 445 + .../knowledgeTaskPresenter.ts | 183 + .../lifecyclePresenter/DatabaseInitializer.ts | 90 + .../lifecyclePresenter/SplashWindowManager.ts | 133 + .../presenter/lifecyclePresenter/coreHooks.ts | 13 + .../hooks/after-start/traySetupHook.ts | 28 + .../hooks/after-start/windowCreationHook.ts | 56 + .../beforeQuit/builtinKnowledgeDestroyHook.ts | 32 + .../hooks/beforeQuit/floatingDestroyHook.ts | 32 + .../hooks/beforeQuit/presenterDestroyHook.ts | 26 + .../hooks/beforeQuit/trayDestroyHook.ts | 32 + .../hooks/beforeQuit/windowQuittingHook.ts | 29 + .../beforeStart/protocolRegistrationHook.ts | 133 + .../lifecyclePresenter/hooks/index.ts | 17 + .../hooks/init/configInitHook.ts | 39 + .../hooks/init/databaseInitHook.ts | 37 + .../hooks/ready/eventListenerSetupHook.ts | 111 + .../hooks/ready/presenterInitHook.ts | 22 + .../presenter/lifecyclePresenter/index.ts | 543 + .../presenter/lifecyclePresenter/types.ts | 57 + src/main/presenter/llamaCppPresenter/index.ts | 107 + src/main/presenter/llamaCppPresenter/llama.ts | 147 + .../llmProviderPresenter/baseProvider.ts | 684 + .../presenter/llmProviderPresenter/index.ts | 2033 ++ .../llmProviderPresenter/oauthHelper.ts | 139 + .../providers/_302AIProvider.ts | 246 + .../providers/aihubmixProvider.ts | 61 + .../providers/anthropicProvider.ts | 1425 ++ .../providers/awsBedrockProvider.ts | 907 + .../providers/dashscopeProvider.ts | 204 + .../providers/deepseekProvider.ts | 153 + .../providers/doubaoProvider.ts | 284 + .../providers/geminiProvider.ts | 1175 ++ .../providers/githubCopilotProvider.ts | 696 + .../providers/githubProvider.ts | 61 + .../providers/grokProvider.ts | 221 + .../providers/groqProvider.ts | 187 + .../providers/lmstudioProvider.ts | 7 + .../providers/minimaxProvider.ts | 89 + .../providers/modelscopeProvider.ts | 332 + .../providers/ollamaProvider.ts | 1183 ++ .../providers/openAICompatibleProvider.ts | 1499 ++ .../providers/openAIProvider.ts | 54 + .../providers/openAIResponsesProvider.ts | 1318 ++ .../providers/openRouterProvider.ts | 291 + .../providers/ppioProvider.ts | 236 + .../providers/siliconcloudProvider.ts | 241 + .../providers/togetherProvider.ts | 81 + .../providers/tokenfluxProvider.ts | 219 + .../providers/vercelAIGatewayProvider.ts | 55 + .../providers/zhipuProvider.ts | 232 + .../inMemoryServers/appleServer.ts | 1898 ++ .../inMemoryServers/artifactsServer.ts | 625 + .../inMemoryServers/autoPromptingServer.ts | 220 + .../inMemoryServers/bochaSearchServer.ts | 357 + .../inMemoryServers/braveSearchServer.ts | 396 + .../mcpPresenter/inMemoryServers/builder.ts | 97 + .../inMemoryServers/builtinKnowledgeServer.ts | 148 + .../conversationSearchServer.ts | 662 + .../inMemoryServers/deepResearchServer.ts | 669 + .../inMemoryServers/difyKnowledgeServer.ts | 296 + .../inMemoryServers/fastGptKnowledgeServer.ts | 269 + .../inMemoryServers/filesystem.ts | 1353 ++ .../inMemoryServers/imageServer.ts | 428 + .../inMemoryServers/meetingServer.ts | 404 + .../inMemoryServers/powerpackServer.ts | 512 + .../inMemoryServers/ragflowKnowledgeServer.ts | 300 + src/main/presenter/mcpPresenter/index.ts | 1217 ++ src/main/presenter/mcpPresenter/mcpClient.ts | 1083 + .../mcpPresenter/mcprouterManager.ts | 124 + .../presenter/mcpPresenter/serverManager.ts | 343 + .../presenter/mcpPresenter/toolManager.ts | 590 + src/main/presenter/notifactionPresenter.ts | 83 + src/main/presenter/oauthPresenter.ts | 521 + src/main/presenter/proxyConfig.ts | 209 + src/main/presenter/shortcutPresenter.ts | 278 + .../presenter/sqlitePresenter/importData.ts | 452 + src/main/presenter/sqlitePresenter/index.ts | 356 + .../sqlitePresenter/tables/attachments.ts | 39 + .../sqlitePresenter/tables/baseTable.ts | 36 + .../sqlitePresenter/tables/conversations.ts | 428 + .../tables/messageAttachments.ts | 61 + .../sqlitePresenter/tables/messages.ts | 361 + src/main/presenter/syncPresenter/index.ts | 543 + src/main/presenter/tabPresenter.ts | 989 + src/main/presenter/threadPresenter/const.ts | 120 + .../threadPresenter/contentEnricher.ts | 383 + .../presenter/threadPresenter/fileContext.ts | 22 + src/main/presenter/threadPresenter/index.ts | 4359 ++++ .../threadPresenter/messageManager.ts | 309 + .../threadPresenter/searchManager.ts | 1404 ++ src/main/presenter/trayPresenter.ts | 80 + src/main/presenter/upgradePresenter/index.ts | 453 + .../windowPresenter/FloatingChatWindow.ts | 348 + src/main/presenter/windowPresenter/index.ts | 1219 ++ src/main/utils/index.ts | 33 + src/main/utils/strings.ts | 29 + src/main/utils/vector.ts | 102 + src/preload/floating-preload.ts | 83 + src/preload/index.d.ts | 15 + src/preload/index.ts | 61 + src/renderer/floating/FloatingButton.vue | 348 + src/renderer/floating/env.d.ts | 24 + src/renderer/floating/index.html | 29 + src/renderer/floating/main.ts | 6 + src/renderer/index.html | 19 + src/renderer/public/sounds/sfx-fc.mp3 | Bin 0 -> 10989 bytes src/renderer/public/sounds/sfx-typing.mp3 | Bin 0 -> 4461 bytes src/renderer/shell/App.vue | 34 + src/renderer/shell/components/AppBar.vue | 579 + .../components/app-bar/AppBarTabItem.vue | 63 + .../shell/components/icons/MaximizeIcon.vue | 15 + .../shell/components/icons/RestoreIcon.vue | 16 + src/renderer/shell/index.html | 19 + src/renderer/shell/main.ts | 27 + src/renderer/shell/stores/tab.ts | 143 + src/renderer/splash/env.d.ts | 8 + src/renderer/splash/index.html | 29 + src/renderer/splash/loading.vue | 160 + src/renderer/splash/main.ts | 6 + src/renderer/src/App.vue | 345 + src/renderer/src/assets/geist.ttf | Bin 0 -> 148832 bytes src/renderer/src/assets/images/dify.png | Bin 0 -> 3963 bytes src/renderer/src/assets/images/fastgpt.png | Bin 0 -> 1828 bytes src/renderer/src/assets/images/ragflow.png | Bin 0 -> 5576 bytes src/renderer/src/assets/llm-icons/302ai.svg | 23 + .../src/assets/llm-icons/adobe-color.svg | 1 + .../src/assets/llm-icons/adobe-text.svg | 1 + src/renderer/src/assets/llm-icons/adobe.svg | 1 + .../assets/llm-icons/adobefirefly-color.svg | 1 + .../assets/llm-icons/adobefirefly-text.svg | 1 + .../src/assets/llm-icons/adobefirefly.svg | 1 + .../src/assets/llm-icons/ai21-brand-color.svg | 1 + .../src/assets/llm-icons/ai21-brand.svg | 1 + .../src/assets/llm-icons/ai21-text.svg | 1 + src/renderer/src/assets/llm-icons/ai21.svg | 1 + .../src/assets/llm-icons/ai360-color.svg | 1 + .../src/assets/llm-icons/ai360-text.svg | 1 + src/renderer/src/assets/llm-icons/ai360.svg | 1 + .../src/assets/llm-icons/aihubmix.png | Bin 0 -> 5723 bytes .../src/assets/llm-icons/aimass-color.svg | 1 + .../src/assets/llm-icons/aimass-text.svg | 1 + src/renderer/src/assets/llm-icons/aimass.svg | 1 + .../assets/llm-icons/alibaba-brand-color.svg | 1 + .../src/assets/llm-icons/alibaba-brand.svg | 1 + .../src/assets/llm-icons/alibaba-color.svg | 1 + .../src/assets/llm-icons/alibaba-text-cn.svg | 1 + .../src/assets/llm-icons/alibaba-text.svg | 1 + src/renderer/src/assets/llm-icons/alibaba.svg | 1 + .../assets/llm-icons/alibabacloud-color.svg | 1 + .../assets/llm-icons/alibabacloud-text-cn.svg | 1 + .../assets/llm-icons/alibabacloud-text.svg | 1 + .../src/assets/llm-icons/alibabacloud.svg | 1 + .../assets/llm-icons/antgroup-brand-color.svg | 1 + .../src/assets/llm-icons/antgroup-brand.svg | 1 + .../src/assets/llm-icons/antgroup-color.svg | 1 + .../src/assets/llm-icons/antgroup-text-cn.svg | 1 + .../src/assets/llm-icons/antgroup-text.svg | 1 + .../src/assets/llm-icons/antgroup.svg | 1 + .../src/assets/llm-icons/anthropic-text.svg | 1 + .../src/assets/llm-icons/anthropic.svg | 1 + .../src/assets/llm-icons/automatic-color.svg | 1 + .../src/assets/llm-icons/automatic-text.svg | 1 + .../src/assets/llm-icons/automatic.svg | 1 + .../src/assets/llm-icons/aws-bedrock.svg | 5 + .../src/assets/llm-icons/aws-brand-color.svg | 1 + .../src/assets/llm-icons/aws-brand.svg | 1 + .../src/assets/llm-icons/aws-color.svg | 1 + .../src/assets/llm-icons/aws-text.svg | 1 + src/renderer/src/assets/llm-icons/aws.svg | 1 + .../src/assets/llm-icons/aya-color.svg | 1 + .../src/assets/llm-icons/aya-text.svg | 1 + src/renderer/src/assets/llm-icons/aya.svg | 1 + .../src/assets/llm-icons/azure-color.svg | 1 + .../src/assets/llm-icons/azure-text.svg | 1 + src/renderer/src/assets/llm-icons/azure.svg | 1 + .../src/assets/llm-icons/azureai-color.svg | 1 + .../src/assets/llm-icons/azureai-text.svg | 1 + src/renderer/src/assets/llm-icons/azureai.svg | 1 + .../src/assets/llm-icons/baichuan-color.svg | 1 + .../src/assets/llm-icons/baichuan-text.svg | 1 + .../src/assets/llm-icons/baichuan.svg | 1 + .../assets/llm-icons/baidu-brand-color.svg | 1 + .../src/assets/llm-icons/baidu-brand.svg | 1 + .../src/assets/llm-icons/baidu-color.svg | 1 + .../src/assets/llm-icons/baidu-text-cn.svg | 1 + .../src/assets/llm-icons/baidu-text.svg | 1 + src/renderer/src/assets/llm-icons/baidu.svg | 1 + .../src/assets/llm-icons/baiducloud-color.svg | 1 + .../src/assets/llm-icons/baiducloud-text.svg | 1 + .../src/assets/llm-icons/baiducloud.svg | 1 + .../src/assets/llm-icons/bedrock-color.svg | 1 + .../src/assets/llm-icons/bedrock-text.svg | 1 + src/renderer/src/assets/llm-icons/bedrock.svg | 1 + .../src/assets/llm-icons/bing-color.svg | 1 + .../src/assets/llm-icons/bing-text.svg | 1 + src/renderer/src/assets/llm-icons/bing.svg | 1 + .../llm-icons/bytedance-brand-color.svg | 1 + .../src/assets/llm-icons/bytedance-brand.svg | 1 + .../src/assets/llm-icons/bytedance-color.svg | 1 + .../assets/llm-icons/bytedance-text-cn.svg | 1 + .../src/assets/llm-icons/bytedance-text.svg | 1 + .../src/assets/llm-icons/bytedance.svg | 1 + .../src/assets/llm-icons/chatglm-color.svg | 1 + .../src/assets/llm-icons/chatglm-text.svg | 1 + src/renderer/src/assets/llm-icons/chatglm.svg | 1 + .../src/assets/llm-icons/civitai-color.svg | 1 + .../assets/llm-icons/civitai-text-color.svg | 1 + .../src/assets/llm-icons/civitai-text.svg | 1 + src/renderer/src/assets/llm-icons/civitai.svg | 1 + .../src/assets/llm-icons/claude-color.svg | 1 + .../src/assets/llm-icons/claude-text.svg | 1 + src/renderer/src/assets/llm-icons/claude.svg | 1 + .../src/assets/llm-icons/clipdrop-text.svg | 1 + .../src/assets/llm-icons/clipdrop.svg | 1 + .../src/assets/llm-icons/cloudflare-color.svg | 1 + .../src/assets/llm-icons/cloudflare-text.svg | 1 + .../src/assets/llm-icons/cloudflare.svg | 1 + .../src/assets/llm-icons/codegeex-color.svg | 1 + .../src/assets/llm-icons/codegeex-text.svg | 1 + .../src/assets/llm-icons/codegeex.svg | 1 + .../src/assets/llm-icons/cogvideo-color.svg | 1 + .../src/assets/llm-icons/cogvideo-text.svg | 1 + .../src/assets/llm-icons/cogvideo.svg | 1 + .../src/assets/llm-icons/cogview-color.svg | 1 + .../src/assets/llm-icons/cogview-text.svg | 1 + src/renderer/src/assets/llm-icons/cogview.svg | 1 + .../src/assets/llm-icons/cohere-color.svg | 1 + .../src/assets/llm-icons/cohere-text.svg | 1 + src/renderer/src/assets/llm-icons/cohere.svg | 1 + .../src/assets/llm-icons/colab-color.svg | 1 + .../src/assets/llm-icons/colab-text.svg | 1 + src/renderer/src/assets/llm-icons/colab.svg | 1 + .../src/assets/llm-icons/comfyui-color.svg | 1 + .../src/assets/llm-icons/comfyui-text.svg | 1 + src/renderer/src/assets/llm-icons/comfyui.svg | 1 + .../src/assets/llm-icons/copilot-color.svg | 1 + .../src/assets/llm-icons/copilot-text.svg | 1 + src/renderer/src/assets/llm-icons/copilot.svg | 1 + .../src/assets/llm-icons/coze-text.svg | 1 + src/renderer/src/assets/llm-icons/coze.svg | 1 + .../src/assets/llm-icons/cursor-text.svg | 1 + src/renderer/src/assets/llm-icons/cursor.svg | 1 + .../src/assets/llm-icons/dalle-color.svg | 1 + .../src/assets/llm-icons/dalle-text.svg | 1 + src/renderer/src/assets/llm-icons/dalle.svg | 1 + .../src/assets/llm-icons/dbrx-brand-color.svg | 1 + .../src/assets/llm-icons/dbrx-brand.svg | 1 + .../src/assets/llm-icons/dbrx-color.svg | 1 + .../src/assets/llm-icons/dbrx-text.svg | 1 + src/renderer/src/assets/llm-icons/dbrx.svg | 1 + .../src/assets/llm-icons/deepai-text.svg | 1 + src/renderer/src/assets/llm-icons/deepai.svg | 1 + .../src/assets/llm-icons/deepmind-color.svg | 1 + .../src/assets/llm-icons/deepmind-text.svg | 1 + .../src/assets/llm-icons/deepmind.svg | 1 + .../src/assets/llm-icons/deepseek-color.svg | 1 + .../src/assets/llm-icons/deepseek-text.svg | 1 + .../src/assets/llm-icons/deepseek.svg | 1 + .../src/assets/llm-icons/dify-color.svg | 1 + .../src/assets/llm-icons/dify-text-color.svg | 1 + .../src/assets/llm-icons/dify-text.svg | 1 + src/renderer/src/assets/llm-icons/dify.svg | 1 + .../src/assets/llm-icons/doubao-color.svg | 1 + .../src/assets/llm-icons/doubao-text.svg | 1 + src/renderer/src/assets/llm-icons/doubao.svg | 1 + .../src/assets/llm-icons/fal-text.svg | 1 + src/renderer/src/assets/llm-icons/fal.svg | 1 + .../src/assets/llm-icons/fireworks-color.svg | 1 + .../src/assets/llm-icons/fireworks-text.svg | 1 + .../src/assets/llm-icons/fireworks.svg | 1 + .../src/assets/llm-icons/fishaudio-text.svg | 1 + .../src/assets/llm-icons/fishaudio.svg | 1 + .../src/assets/llm-icons/flux-text.svg | 1 + src/renderer/src/assets/llm-icons/flux.svg | 1 + .../assets/llm-icons/gemini-brand-color.svg | 1 + .../src/assets/llm-icons/gemini-brand.svg | 1 + .../src/assets/llm-icons/gemini-color.svg | 1 + .../src/assets/llm-icons/gemini-text.svg | 1 + src/renderer/src/assets/llm-icons/gemini.svg | 1 + .../src/assets/llm-icons/gemma-color.svg | 1 + .../src/assets/llm-icons/gemma-text.svg | 1 + src/renderer/src/assets/llm-icons/gemma.svg | 1 + .../src/assets/llm-icons/giteeai-text.svg | 1 + src/renderer/src/assets/llm-icons/giteeai.svg | 1 + .../src/assets/llm-icons/github-text.svg | 1 + src/renderer/src/assets/llm-icons/github.svg | 1 + .../assets/llm-icons/githubcopilot-text.svg | 1 + .../src/assets/llm-icons/githubcopilot.svg | 1 + .../src/assets/llm-icons/glif-text.svg | 1 + src/renderer/src/assets/llm-icons/glif.svg | 1 + .../assets/llm-icons/google-brand-color.svg | 1 + .../src/assets/llm-icons/google-brand.svg | 1 + .../src/assets/llm-icons/google-color.svg | 1 + src/renderer/src/assets/llm-icons/google.svg | 1 + .../src/assets/llm-icons/grok-text.svg | 1 + src/renderer/src/assets/llm-icons/grok.svg | 1 + .../src/assets/llm-icons/groq-text.svg | 1 + src/renderer/src/assets/llm-icons/groq.svg | 25 + .../src/assets/llm-icons/hailuo-color.svg | 1 + .../src/assets/llm-icons/hailuo-text.svg | 1 + src/renderer/src/assets/llm-icons/hailuo.svg | 1 + .../src/assets/llm-icons/haiper-text.svg | 1 + src/renderer/src/assets/llm-icons/haiper.svg | 1 + .../src/assets/llm-icons/hedra-text.svg | 1 + src/renderer/src/assets/llm-icons/hedra.svg | 1 + .../src/assets/llm-icons/higress-color.svg | 1 + .../src/assets/llm-icons/higress-text.svg | 1 + src/renderer/src/assets/llm-icons/higress.svg | 1 + .../assets/llm-icons/huggingface-color.svg | 1 + .../src/assets/llm-icons/huggingface-text.svg | 1 + .../src/assets/llm-icons/huggingface.svg | 1 + .../src/assets/llm-icons/hunyuan-color.svg | 1 + .../src/assets/llm-icons/hunyuan-text.svg | 1 + src/renderer/src/assets/llm-icons/hunyuan.svg | 1 + .../src/assets/llm-icons/ideogram-text.svg | 1 + .../src/assets/llm-icons/ideogram.svg | 1 + .../src/assets/llm-icons/internlm-color.svg | 1 + .../src/assets/llm-icons/internlm-text.svg | 1 + .../src/assets/llm-icons/internlm.svg | 1 + .../src/assets/llm-icons/jina-color.svg | 1 + .../src/assets/llm-icons/jina-text.svg | 1 + src/renderer/src/assets/llm-icons/jina.svg | 1 + .../src/assets/llm-icons/kimi-color.svg | 1 + .../src/assets/llm-icons/kimi-text.svg | 1 + src/renderer/src/assets/llm-icons/kimi.svg | 1 + .../src/assets/llm-icons/kling-color.svg | 1 + .../src/assets/llm-icons/kling-text.svg | 1 + src/renderer/src/assets/llm-icons/kling.svg | 1 + .../src/assets/llm-icons/langchain-color.svg | 1 + .../src/assets/llm-icons/langchain-text.svg | 1 + .../src/assets/llm-icons/langchain.svg | 1 + .../src/assets/llm-icons/langfuse-color.svg | 1 + .../src/assets/llm-icons/langfuse-text.svg | 1 + .../src/assets/llm-icons/langfuse.svg | 1 + .../src/assets/llm-icons/lightricks-text.svg | 1 + .../src/assets/llm-icons/lightricks.svg | 1 + .../src/assets/llm-icons/livekit-color.svg | 1 + .../src/assets/llm-icons/livekit-text.svg | 1 + src/renderer/src/assets/llm-icons/livekit.svg | 1 + .../src/assets/llm-icons/llava-color.svg | 1 + .../src/assets/llm-icons/llava-text.svg | 1 + src/renderer/src/assets/llm-icons/llava.svg | 1 + .../src/assets/llm-icons/lmstudio-text.svg | 1 + .../src/assets/llm-icons/lmstudio.svg | 1 + .../src/assets/llm-icons/lobehub-color.svg | 1 + .../src/assets/llm-icons/lobehub-text.svg | 1 + src/renderer/src/assets/llm-icons/lobehub.svg | 1 + .../src/assets/llm-icons/luma-color.svg | 1 + .../src/assets/llm-icons/luma-text.svg | 1 + src/renderer/src/assets/llm-icons/luma.svg | 1 + .../src/assets/llm-icons/magic-text.svg | 1 + src/renderer/src/assets/llm-icons/magic.svg | 1 + .../src/assets/llm-icons/meta-brand-color.svg | 1 + .../src/assets/llm-icons/meta-brand.svg | 1 + .../src/assets/llm-icons/meta-color.svg | 1 + .../src/assets/llm-icons/meta-text.svg | 1 + src/renderer/src/assets/llm-icons/meta.svg | 1 + .../src/assets/llm-icons/midjourney-text.svg | 1 + .../src/assets/llm-icons/midjourney.svg | 1 + .../src/assets/llm-icons/minimax-color.svg | 1 + .../src/assets/llm-icons/minimax-text.svg | 1 + src/renderer/src/assets/llm-icons/minimax.svg | 1 + .../src/assets/llm-icons/mistral-color.svg | 1 + .../src/assets/llm-icons/mistral-text.svg | 1 + src/renderer/src/assets/llm-icons/mistral.svg | 1 + .../src/assets/llm-icons/modelscope-color.svg | 1 + .../src/assets/llm-icons/modelscope-text.svg | 1 + .../src/assets/llm-icons/modelscope.svg | 1 + .../src/assets/llm-icons/moonshot-text.svg | 1 + .../src/assets/llm-icons/moonshot.svg | 1 + .../src/assets/llm-icons/myshell-color.svg | 1 + .../src/assets/llm-icons/myshell-text.svg | 1 + src/renderer/src/assets/llm-icons/myshell.svg | 1 + .../src/assets/llm-icons/notion-text.svg | 1 + src/renderer/src/assets/llm-icons/notion.svg | 1 + .../src/assets/llm-icons/nova-color.svg | 1 + .../src/assets/llm-icons/nova-text.svg | 1 + src/renderer/src/assets/llm-icons/nova.svg | 1 + .../src/assets/llm-icons/novita-color.svg | 1 + .../src/assets/llm-icons/novita-text.svg | 1 + src/renderer/src/assets/llm-icons/novita.svg | 1 + .../src/assets/llm-icons/nvidia-color.svg | 1 + .../src/assets/llm-icons/nvidia-text.svg | 1 + src/renderer/src/assets/llm-icons/nvidia.svg | 1 + .../src/assets/llm-icons/ollama-text.svg | 1 + src/renderer/src/assets/llm-icons/ollama.svg | 1 + .../src/assets/llm-icons/openai-text.svg | 1 + src/renderer/src/assets/llm-icons/openai.svg | 1 + .../src/assets/llm-icons/openchat-color.svg | 1 + .../src/assets/llm-icons/openchat-text.svg | 1 + .../src/assets/llm-icons/openchat.svg | 1 + .../src/assets/llm-icons/openrouter-text.svg | 1 + .../src/assets/llm-icons/openrouter.svg | 1 + .../src/assets/llm-icons/palm-color.svg | 1 + .../src/assets/llm-icons/palm-text.svg | 1 + src/renderer/src/assets/llm-icons/palm.svg | 1 + .../src/assets/llm-icons/perplexity-color.svg | 1 + .../src/assets/llm-icons/perplexity-text.svg | 1 + .../src/assets/llm-icons/perplexity.svg | 1 + .../src/assets/llm-icons/pika-text.svg | 1 + src/renderer/src/assets/llm-icons/pika.svg | 1 + .../src/assets/llm-icons/pixverse-color.svg | 1 + .../src/assets/llm-icons/pixverse-text.svg | 1 + .../src/assets/llm-icons/pixverse.svg | 1 + .../src/assets/llm-icons/poe-color.svg | 1 + .../src/assets/llm-icons/poe-text.svg | 1 + src/renderer/src/assets/llm-icons/poe.svg | 1 + .../assets/llm-icons/pollinations-text.svg | 1 + .../src/assets/llm-icons/pollinations.svg | 1 + .../src/assets/llm-icons/ppio-brand-color.svg | 1 + .../src/assets/llm-icons/ppio-brand.svg | 1 + .../src/assets/llm-icons/ppio-color.svg | 1 + .../src/assets/llm-icons/ppio-text-cn.svg | 1 + .../src/assets/llm-icons/ppio-text.svg | 1 + src/renderer/src/assets/llm-icons/ppio.svg | 1 + .../src/assets/llm-icons/qingyan-color.svg | 1 + .../src/assets/llm-icons/qingyan-text.svg | 1 + src/renderer/src/assets/llm-icons/qingyan.svg | 1 + src/renderer/src/assets/llm-icons/qiniu.svg | 3 + .../src/assets/llm-icons/qwen-color.svg | 1 + .../src/assets/llm-icons/qwen-text.svg | 1 + src/renderer/src/assets/llm-icons/qwen.svg | 1 + .../src/assets/llm-icons/recraft-text.svg | 1 + src/renderer/src/assets/llm-icons/recraft.svg | 1 + .../src/assets/llm-icons/replicate-brand.svg | 1 + .../src/assets/llm-icons/replicate.svg | 1 + .../src/assets/llm-icons/replit-color.svg | 1 + .../src/assets/llm-icons/replit-text.svg | 1 + src/renderer/src/assets/llm-icons/replit.svg | 1 + .../src/assets/llm-icons/runway-text.svg | 1 + src/renderer/src/assets/llm-icons/runway.svg | 1 + .../src/assets/llm-icons/rwkv-color.svg | 1 + .../src/assets/llm-icons/rwkv-text.svg | 1 + src/renderer/src/assets/llm-icons/rwkv.svg | 1 + .../llm-icons/sensenova-brand-color.svg | 1 + .../src/assets/llm-icons/sensenova-brand.svg | 1 + .../src/assets/llm-icons/sensenova-color.svg | 1 + .../src/assets/llm-icons/sensenova-text.svg | 1 + .../src/assets/llm-icons/sensenova.svg | 1 + .../assets/llm-icons/siliconcloud-color.svg | 13 + .../assets/llm-icons/siliconcloud-text.svg | 1 + .../src/assets/llm-icons/siliconcloud.svg | 20 + .../src/assets/llm-icons/spark-color.svg | 1 + .../src/assets/llm-icons/spark-text.svg | 1 + src/renderer/src/assets/llm-icons/spark.svg | 1 + .../llm-icons/stability-brand-color.svg | 1 + .../src/assets/llm-icons/stability-brand.svg | 1 + .../src/assets/llm-icons/stability-color.svg | 1 + .../src/assets/llm-icons/stability-text.svg | 1 + .../src/assets/llm-icons/stability.svg | 1 + .../src/assets/llm-icons/stepfun-color.svg | 1 + .../src/assets/llm-icons/stepfun-text.svg | 1 + src/renderer/src/assets/llm-icons/stepfun.svg | 1 + .../src/assets/llm-icons/suno-text.svg | 1 + src/renderer/src/assets/llm-icons/suno.svg | 1 + .../src/assets/llm-icons/sync-text.svg | 1 + src/renderer/src/assets/llm-icons/sync.svg | 1 + .../assets/llm-icons/tencent-brand-color.svg | 1 + .../src/assets/llm-icons/tencent-brand.svg | 1 + .../src/assets/llm-icons/tencent-color.svg | 1 + .../src/assets/llm-icons/tencent-text-cn.svg | 1 + .../src/assets/llm-icons/tencent-text.svg | 1 + src/renderer/src/assets/llm-icons/tencent.svg | 1 + .../assets/llm-icons/tencentcloud-color.svg | 1 + .../assets/llm-icons/tencentcloud-text.svg | 1 + .../src/assets/llm-icons/tencentcloud.svg | 1 + .../src/assets/llm-icons/tiangong-color.svg | 1 + .../src/assets/llm-icons/tiangong-text.svg | 1 + .../src/assets/llm-icons/tiangong.svg | 1 + .../src/assets/llm-icons/tii-color.svg | 1 + .../src/assets/llm-icons/tii-text.svg | 1 + src/renderer/src/assets/llm-icons/tii.svg | 1 + .../assets/llm-icons/together-brand-color.svg | 1 + .../src/assets/llm-icons/together-brand.svg | 1 + .../src/assets/llm-icons/together-color.svg | 1 + .../src/assets/llm-icons/together-text.svg | 1 + .../src/assets/llm-icons/together.svg | 1 + .../src/assets/llm-icons/tokenflux-color.svg | 93 + .../src/assets/llm-icons/tripo-color.svg | 1 + .../src/assets/llm-icons/tripo-text.svg | 1 + src/renderer/src/assets/llm-icons/tripo.svg | 1 + .../src/assets/llm-icons/udio-color.svg | 1 + .../src/assets/llm-icons/udio-text.svg | 1 + src/renderer/src/assets/llm-icons/udio.svg | 1 + .../src/assets/llm-icons/upstage-color.svg | 1 + .../src/assets/llm-icons/upstage-text.svg | 1 + src/renderer/src/assets/llm-icons/upstage.svg | 1 + src/renderer/src/assets/llm-icons/v0.svg | 1 + .../src/assets/llm-icons/vercel-text.svg | 1 + src/renderer/src/assets/llm-icons/vercel.svg | 1 + .../src/assets/llm-icons/vertexai-color.svg | 1 + .../src/assets/llm-icons/vertexai-text.svg | 1 + .../src/assets/llm-icons/vertexai.svg | 1 + .../src/assets/llm-icons/vidu-color.svg | 1 + .../src/assets/llm-icons/vidu-text.svg | 1 + src/renderer/src/assets/llm-icons/vidu.svg | 1 + .../src/assets/llm-icons/viggle-text.svg | 1 + src/renderer/src/assets/llm-icons/viggle.svg | 1 + .../src/assets/llm-icons/vllm-color.svg | 1 + .../src/assets/llm-icons/vllm-text.svg | 1 + src/renderer/src/assets/llm-icons/vllm.svg | 1 + .../src/assets/llm-icons/volcengine-color.svg | 1 + .../src/assets/llm-icons/volcengine-text.svg | 1 + .../src/assets/llm-icons/volcengine.svg | 1 + .../src/assets/llm-icons/wenxin-color.svg | 1 + .../src/assets/llm-icons/wenxin-text.svg | 1 + src/renderer/src/assets/llm-icons/wenxin.svg | 1 + .../src/assets/llm-icons/workersai-color.svg | 1 + .../src/assets/llm-icons/workersai-text.svg | 1 + .../src/assets/llm-icons/workersai.svg | 1 + .../src/assets/llm-icons/xai-text.svg | 1 + src/renderer/src/assets/llm-icons/xai.svg | 1 + .../src/assets/llm-icons/xuanyuan-color.svg | 1 + .../src/assets/llm-icons/xuanyuan-text.svg | 1 + .../src/assets/llm-icons/xuanyuan.svg | 1 + .../src/assets/llm-icons/yi-color.svg | 1 + src/renderer/src/assets/llm-icons/yi-text.svg | 1 + src/renderer/src/assets/llm-icons/yi.svg | 1 + .../src/assets/llm-icons/zeabur-color.svg | 1 + .../src/assets/llm-icons/zeabur-text.svg | 1 + src/renderer/src/assets/llm-icons/zeabur.svg | 1 + .../src/assets/llm-icons/zeroone-text.svg | 1 + src/renderer/src/assets/llm-icons/zeroone.svg | 1 + .../src/assets/llm-icons/zhipu-color.svg | 1 + .../src/assets/llm-icons/zhipu-text.svg | 1 + src/renderer/src/assets/llm-icons/zhipu.svg | 1 + src/renderer/src/assets/logo-dark.png | Bin 0 -> 23218 bytes src/renderer/src/assets/logo.png | Bin 0 -> 53089 bytes src/renderer/src/assets/main.css | 1 + .../src/assets/mcp-icons/higress.avif | Bin 0 -> 1455 bytes src/renderer/src/assets/style.css | 153 + src/renderer/src/components/ChatConfig.vue | 739 + src/renderer/src/components/ChatInput.vue | 1444 ++ src/renderer/src/components/ChatView.vue | 123 + src/renderer/src/components/FileItem.vue | 104 + .../components/MessageNavigationSidebar.vue | 361 + src/renderer/src/components/ModelSelect.vue | 129 + src/renderer/src/components/NewThread.vue | 454 + .../src/components/ScrollablePopover.vue | 34 + .../src/components/SearchResultsDrawer.vue | 71 + src/renderer/src/components/SideBar.vue | 123 + src/renderer/src/components/ThreadItem.vue | 164 + src/renderer/src/components/ThreadsView.vue | 378 + src/renderer/src/components/TitleView.vue | 546 + .../components/artifacts/ArtifactBlock.vue | 90 + .../components/artifacts/ArtifactDialog.vue | 565 + .../components/artifacts/ArtifactPreview.vue | 362 + .../components/artifacts/ArtifactThinking.vue | 29 + .../src/components/artifacts/CodeArtifact.vue | 256 + .../src/components/artifacts/HTMLArtifact.vue | 640 + .../components/artifacts/MarkdownArtifact.vue | 40 + .../components/artifacts/MermaidArtifact.vue | 118 + .../components/artifacts/ReactArtifact.vue | 38 + .../src/components/artifacts/ReactTemplate.ts | 46 + .../src/components/artifacts/SvgArtifact.vue | 130 + .../components/artifacts/ToolCallPreview.vue | 114 + .../components/editor/mention/MentionList.vue | 307 + .../editor/mention/PromptParamsDialog.vue | 126 + .../src/components/editor/mention/mention.ts | 41 + .../components/editor/mention/suggestion.ts | 156 + .../src/components/icons/ModelIcon.vue | 190 + .../src/components/json-viewer/JsonArray.ts | 76 + .../src/components/json-viewer/JsonObject.ts | 91 + .../src/components/json-viewer/JsonValue.ts | 56 + .../src/components/json-viewer/index.ts | 3 + .../components/markdown/MarkdownRenderer.vue | 80 + .../src/components/markdown/ReferenceNode.vue | 61 + .../mcp-config/components/McpJsonViewer.vue | 248 + .../mcp-config/components/McpPromptPanel.vue | 416 + .../components/McpResourceViewer.vue | 309 + .../mcp-config/components/McpServerCard.vue | 304 + .../mcp-config/components/McpServers.vue | 449 + .../mcp-config/components/McpTabHeader.vue | 65 + .../mcp-config/components/McpToolPanel.vue | 457 + .../components/mcp-config/components/index.ts | 7 + .../src/components/mcp-config/const.ts | 2 + .../src/components/mcp-config/index.ts | 5 + .../components/mcp-config/mcpServerForm.vue | 1193 ++ src/renderer/src/components/mcpToolsList.vue | 230 + .../components/message/MessageBlockAction.vue | 156 + .../message/MessageBlockContent.vue | 118 + .../components/message/MessageBlockError.vue | 44 + .../components/message/MessageBlockImage.vue | 111 + .../message/MessageBlockPermissionRequest.vue | 260 + .../components/message/MessageBlockSearch.vue | 90 + .../components/message/MessageBlockThink.vue | 84 + .../message/MessageBlockToolCall.vue | 217 + .../src/components/message/MessageContent.vue | 75 + .../src/components/message/MessageInfo.vue | 29 + .../message/MessageItemAssistant.vue | 325 + .../components/message/MessageItemUser.vue | 193 + .../src/components/message/MessageList.vue | 486 + .../components/message/MessageTextContent.vue | 11 + .../src/components/message/MessageToolbar.vue | 313 + .../components/message/ReferencePreview.vue | 79 + .../message/SelectedTextContextMenu.vue | 39 + .../src/components/popup/TranslatePopup.vue | 159 + .../components/settings/AboutUsSettings.vue | 243 + .../settings/AddCustomProviderDialog.vue | 221 + .../AnthropicProviderSettingsDetail.vue | 730 + .../settings/AzureProviderConfig.vue | 54 + .../BedrockProviderSettingsDetail.vue | 324 + .../settings/BuiltinKnowledgeSettings.vue | 1071 + .../components/settings/CommonSettings.vue | 846 + .../src/components/settings/DataSettings.vue | 342 + .../settings/DifyKnowledgeSettings.vue | 439 + .../components/settings/DisplaySettings.vue | 252 + .../settings/FastGptKnowledgeSettings.vue | 438 + .../settings/GeminiSafetyConfig.vue | 121 + .../settings/GitHubCopilotOAuth.vue | 270 + .../settings/KnowledgeBaseSettings.vue | 204 + .../src/components/settings/KnowledgeFile.vue | 442 + .../components/settings/KnowledgeFileItem.vue | 182 + .../components/settings/McpBuiltinMarket.vue | 313 + .../src/components/settings/McpSettings.vue | 458 + .../components/settings/ModelCheckDialog.vue | 206 + .../components/settings/ModelConfigDialog.vue | 913 + .../components/settings/ModelConfigItem.vue | 125 + .../settings/ModelProviderSettings.vue | 349 + .../settings/ModelProviderSettingsDetail.vue | 388 + .../components/settings/ModelScopeMcpSync.vue | 167 + .../settings/OllamaProviderSettingsDetail.vue | 923 + .../src/components/settings/PromptSetting.vue | 1368 ++ .../components/settings/ProviderApiConfig.vue | 233 + .../settings/ProviderDialogContainer.vue | 157 + .../components/settings/ProviderModelList.vue | 272 + .../settings/ProviderModelManager.vue | 96 + .../settings/ProviderRateLimitConfig.vue | 283 + .../settings/RagflowKnowledgeSettings.vue | 456 + .../components/settings/ShortcutSettings.vue | 452 + .../src/components/ui/MessageDialog.vue | 99 + .../src/components/ui/UpdateDialog.vue | 90 + .../src/components/ui/accordion/Accordion.vue | 19 + .../ui/accordion/AccordionContent.vue | 26 + .../components/ui/accordion/AccordionItem.vue | 26 + .../ui/accordion/AccordionTrigger.vue | 41 + .../src/components/ui/accordion/index.ts | 4 + .../ui/alert-dialog/AlertDialog.vue | 14 + .../ui/alert-dialog/AlertDialogAction.vue | 20 + .../ui/alert-dialog/AlertDialogCancel.vue | 27 + .../ui/alert-dialog/AlertDialogContent.vue | 42 + .../alert-dialog/AlertDialogDescription.vue | 25 + .../ui/alert-dialog/AlertDialogFooter.vue | 21 + .../ui/alert-dialog/AlertDialogHeader.vue | 16 + .../ui/alert-dialog/AlertDialogTitle.vue | 22 + .../ui/alert-dialog/AlertDialogTrigger.vue | 11 + .../src/components/ui/alert-dialog/index.ts | 9 + .../src/components/ui/alert/Alert.vue | 16 + .../components/ui/alert/AlertDescription.vue | 14 + .../src/components/ui/alert/AlertTitle.vue | 14 + src/renderer/src/components/ui/alert/index.ts | 23 + .../ui/aspect-ratio/AspectRatio.vue | 11 + .../src/components/ui/aspect-ratio/index.ts | 1 + .../src/components/ui/avatar/Avatar.vue | 21 + .../components/ui/avatar/AvatarFallback.vue | 11 + .../src/components/ui/avatar/AvatarImage.vue | 9 + .../src/components/ui/avatar/index.ts | 24 + .../src/components/ui/badge/Badge.vue | 16 + src/renderer/src/components/ui/badge/index.ts | 23 + .../components/ui/breadcrumb/Breadcrumb.vue | 13 + .../ui/breadcrumb/BreadcrumbEllipsis.vue | 22 + .../ui/breadcrumb/BreadcrumbItem.vue | 16 + .../ui/breadcrumb/BreadcrumbLink.vue | 19 + .../ui/breadcrumb/BreadcrumbList.vue | 16 + .../ui/breadcrumb/BreadcrumbPage.vue | 19 + .../ui/breadcrumb/BreadcrumbSeparator.vue | 21 + .../src/components/ui/breadcrumb/index.ts | 7 + .../src/components/ui/button/Button.vue | 36 + .../src/components/ui/button/index.ts | 33 + src/renderer/src/components/ui/card/Card.vue | 21 + .../src/components/ui/card/CardContent.vue | 14 + .../components/ui/card/CardDescription.vue | 14 + .../src/components/ui/card/CardFooter.vue | 14 + .../src/components/ui/card/CardHeader.vue | 14 + .../src/components/ui/card/CardTitle.vue | 18 + src/renderer/src/components/ui/card/index.ts | 6 + .../src/components/ui/checkbox/Checkbox.vue | 33 + .../src/components/ui/checkbox/index.ts | 1 + .../components/ui/collapsible/collapsible.ts | 119 + .../src/components/ui/collapsible/index.ts | 3 + .../ui/context-menu/ContextMenu.vue | 15 + .../context-menu/ContextMenuCheckboxItem.vue | 40 + .../ui/context-menu/ContextMenuContent.vue | 36 + .../ui/context-menu/ContextMenuGroup.vue | 11 + .../ui/context-menu/ContextMenuItem.vue | 34 + .../ui/context-menu/ContextMenuLabel.vue | 25 + .../ui/context-menu/ContextMenuPortal.vue | 11 + .../ui/context-menu/ContextMenuRadioGroup.vue | 19 + .../ui/context-menu/ContextMenuRadioItem.vue | 40 + .../ui/context-menu/ContextMenuSeparator.vue | 20 + .../ui/context-menu/ContextMenuShortcut.vue | 14 + .../ui/context-menu/ContextMenuSub.vue | 19 + .../ui/context-menu/ContextMenuSubContent.vue | 35 + .../ui/context-menu/ContextMenuSubTrigger.vue | 34 + .../ui/context-menu/ContextMenuTrigger.vue | 13 + .../src/components/ui/context-menu/index.ts | 14 + .../src/components/ui/dialog/Dialog.vue | 14 + .../src/components/ui/dialog/DialogClose.vue | 11 + .../components/ui/dialog/DialogContent.vue | 51 + .../ui/dialog/DialogDescription.vue | 24 + .../src/components/ui/dialog/DialogFooter.vue | 19 + .../src/components/ui/dialog/DialogHeader.vue | 16 + .../ui/dialog/DialogScrollContent.vue | 49 + .../src/components/ui/dialog/DialogTitle.vue | 29 + .../components/ui/dialog/DialogTrigger.vue | 11 + .../src/components/ui/dialog/index.ts | 9 + .../ui/dropdown-menu/DropdownMenu.vue | 14 + .../DropdownMenuCheckboxItem.vue | 40 + .../ui/dropdown-menu/DropdownMenuContent.vue | 38 + .../ui/dropdown-menu/DropdownMenuGroup.vue | 11 + .../ui/dropdown-menu/DropdownMenuItem.vue | 28 + .../ui/dropdown-menu/DropdownMenuLabel.vue | 24 + .../dropdown-menu/DropdownMenuRadioGroup.vue | 19 + .../dropdown-menu/DropdownMenuRadioItem.vue | 41 + .../dropdown-menu/DropdownMenuSeparator.vue | 22 + .../ui/dropdown-menu/DropdownMenuShortcut.vue | 14 + .../ui/dropdown-menu/DropdownMenuSub.vue | 19 + .../dropdown-menu/DropdownMenuSubContent.vue | 30 + .../dropdown-menu/DropdownMenuSubTrigger.vue | 33 + .../ui/dropdown-menu/DropdownMenuTrigger.vue | 13 + .../src/components/ui/dropdown-menu/index.ts | 16 + .../ui/emoji-picker/EmojiPicker.vue | 505 + .../src/components/ui/emoji-picker/index.ts | 4 + .../components/ui/hover-card/HoverCard.vue | 14 + .../ui/hover-card/HoverCardContent.vue | 41 + .../ui/hover-card/HoverCardTrigger.vue | 11 + .../src/components/ui/hover-card/index.ts | 3 + .../src/components/ui/input/Input.vue | 39 + src/renderer/src/components/ui/input/index.ts | 1 + .../src/components/ui/label/Label.vue | 27 + src/renderer/src/components/ui/label/index.ts | 1 + .../src/components/ui/menubar/Menubar.vue | 35 + .../ui/menubar/MenubarCheckboxItem.vue | 40 + .../components/ui/menubar/MenubarContent.vue | 43 + .../components/ui/menubar/MenubarGroup.vue | 11 + .../src/components/ui/menubar/MenubarItem.vue | 35 + .../components/ui/menubar/MenubarLabel.vue | 13 + .../src/components/ui/menubar/MenubarMenu.vue | 11 + .../ui/menubar/MenubarRadioGroup.vue | 20 + .../ui/menubar/MenubarRadioItem.vue | 40 + .../ui/menubar/MenubarSeparator.vue | 19 + .../components/ui/menubar/MenubarShortcut.vue | 14 + .../src/components/ui/menubar/MenubarSub.vue | 19 + .../ui/menubar/MenubarSubContent.vue | 39 + .../ui/menubar/MenubarSubTrigger.vue | 30 + .../components/ui/menubar/MenubarTrigger.vue | 29 + .../src/components/ui/menubar/index.ts | 15 + .../ui/navigation-menu/NavigationMenu.vue | 33 + .../navigation-menu/NavigationMenuContent.vue | 34 + .../NavigationMenuIndicator.vue | 24 + .../ui/navigation-menu/NavigationMenuItem.vue | 11 + .../ui/navigation-menu/NavigationMenuLink.vue | 19 + .../ui/navigation-menu/NavigationMenuList.vue | 29 + .../navigation-menu/NavigationMenuTrigger.vue | 34 + .../NavigationMenuViewport.vue | 33 + .../components/ui/navigation-menu/index.ts | 14 + .../ui/number-field/NumberField.vue | 23 + .../ui/number-field/NumberFieldContent.vue | 14 + .../ui/number-field/NumberFieldDecrement.vue | 25 + .../ui/number-field/NumberFieldIncrement.vue | 25 + .../ui/number-field/NumberFieldInput.vue | 16 + .../src/components/ui/number-field/index.ts | 5 + .../src/components/ui/popover/Popover.vue | 15 + .../components/ui/popover/PopoverContent.vue | 46 + .../components/ui/popover/PopoverTrigger.vue | 12 + .../src/components/ui/popover/index.ts | 4 + .../src/components/ui/progress/Progress.vue | 41 + .../src/components/ui/progress/index.ts | 1 + .../components/ui/radio-group/RadioGroup.vue | 27 + .../ui/radio-group/RadioGroupItem.vue | 39 + .../src/components/ui/radio-group/index.ts | 2 + .../components/ui/scroll-area/ScrollArea.vue | 35 + .../components/ui/scroll-area/ScrollBar.vue | 30 + .../src/components/ui/scroll-area/index.ts | 2 + .../src/components/ui/select/Select.vue | 15 + .../components/ui/select/SelectContent.vue | 53 + .../src/components/ui/select/SelectGroup.vue | 19 + .../src/components/ui/select/SelectItem.vue | 44 + .../components/ui/select/SelectItemText.vue | 11 + .../src/components/ui/select/SelectLabel.vue | 13 + .../ui/select/SelectScrollDownButton.vue | 24 + .../ui/select/SelectScrollUpButton.vue | 24 + .../components/ui/select/SelectSeparator.vue | 17 + .../components/ui/select/SelectTrigger.vue | 31 + .../src/components/ui/select/SelectValue.vue | 11 + .../src/components/ui/select/index.ts | 11 + .../src/components/ui/separator/Separator.vue | 40 + .../src/components/ui/separator/index.ts | 1 + .../src/components/ui/sheet/Sheet.vue | 14 + .../src/components/ui/sheet/SheetClose.vue | 11 + .../src/components/ui/sheet/SheetContent.vue | 56 + .../components/ui/sheet/SheetDescription.vue | 22 + .../src/components/ui/sheet/SheetFooter.vue | 19 + .../src/components/ui/sheet/SheetHeader.vue | 16 + .../src/components/ui/sheet/SheetTitle.vue | 22 + .../src/components/ui/sheet/SheetTrigger.vue | 11 + src/renderer/src/components/ui/sheet/index.ts | 31 + .../src/components/ui/sidebar/Sidebar.vue | 85 + .../components/ui/sidebar/SidebarContent.vue | 17 + .../components/ui/sidebar/SidebarFooter.vue | 17 + .../components/ui/sidebar/SidebarGroup.vue | 17 + .../ui/sidebar/SidebarGroupAction.vue | 27 + .../ui/sidebar/SidebarGroupContent.vue | 17 + .../ui/sidebar/SidebarGroupLabel.vue | 24 + .../components/ui/sidebar/SidebarHeader.vue | 17 + .../components/ui/sidebar/SidebarInput.vue | 21 + .../components/ui/sidebar/SidebarInset.vue | 20 + .../src/components/ui/sidebar/SidebarMenu.vue | 17 + .../ui/sidebar/SidebarMenuAction.vue | 34 + .../ui/sidebar/SidebarMenuBadge.vue | 25 + .../ui/sidebar/SidebarMenuButton.vue | 49 + .../ui/sidebar/SidebarMenuButtonChild.vue | 33 + .../components/ui/sidebar/SidebarMenuItem.vue | 17 + .../ui/sidebar/SidebarMenuSkeleton.vue | 33 + .../components/ui/sidebar/SidebarMenuSub.vue | 21 + .../ui/sidebar/SidebarMenuSubButton.vue | 35 + .../ui/sidebar/SidebarMenuSubItem.vue | 9 + .../components/ui/sidebar/SidebarProvider.vue | 80 + .../src/components/ui/sidebar/SidebarRail.vue | 32 + .../ui/sidebar/SidebarSeparator.vue | 18 + .../components/ui/sidebar/SidebarTrigger.vue | 26 + .../src/components/ui/sidebar/index.ts | 59 + .../src/components/ui/sidebar/utils.ts | 19 + .../src/components/ui/skeleton/Skeleton.vue | 14 + .../src/components/ui/skeleton/index.ts | 1 + .../src/components/ui/slider/Slider.vue | 41 + .../src/components/ui/slider/index.ts | 1 + .../src/components/ui/switch/Switch.vue | 41 + .../src/components/ui/switch/index.ts | 1 + src/renderer/src/components/ui/tabs/Tab.vue | 39 + .../src/components/ui/tabs/TabGroup.vue | 29 + .../src/components/ui/tabs/TabList.vue | 9 + .../src/components/ui/tabs/TabPanel.vue | 20 + .../src/components/ui/tabs/TabPanels.vue | 5 + src/renderer/src/components/ui/tabs/index.ts | 7 + .../src/components/ui/textarea/Textarea.vue | 44 + .../src/components/ui/textarea/index.ts | 1 + .../src/components/ui/toast/Toast.vue | 30 + .../src/components/ui/toast/ToastAction.vue | 21 + .../src/components/ui/toast/ToastClose.vue | 24 + .../components/ui/toast/ToastDescription.vue | 21 + .../src/components/ui/toast/ToastProvider.vue | 12 + .../src/components/ui/toast/ToastTitle.vue | 21 + .../src/components/ui/toast/ToastViewport.vue | 19 + .../src/components/ui/toast/Toaster.vue | 30 + src/renderer/src/components/ui/toast/index.ts | 39 + .../src/components/ui/toast/use-toast.ts | 165 + .../src/components/ui/toggle/Toggle.vue | 38 + .../src/components/ui/toggle/index.ts | 28 + .../src/components/ui/tooltip/Tooltip.vue | 14 + .../components/ui/tooltip/TooltipContent.vue | 48 + .../components/ui/tooltip/TooltipProvider.vue | 11 + .../components/ui/tooltip/TooltipTrigger.vue | 11 + .../src/components/ui/tooltip/index.ts | 4 + src/renderer/src/composables/useArtifacts.ts | 478 + .../src/composables/usePageCapture.example.ts | 196 + .../src/composables/usePageCapture.ts | 472 + src/renderer/src/composables/usePresenter.ts | 105 + src/renderer/src/env.d.ts | 18 + src/renderer/src/events.ts | 153 + src/renderer/src/i18n/en-US/about.json | 19 + src/renderer/src/i18n/en-US/artifacts.json | 48 + src/renderer/src/i18n/en-US/chat.json | 69 + src/renderer/src/i18n/en-US/common.json | 77 + src/renderer/src/i18n/en-US/components.json | 36 + src/renderer/src/i18n/en-US/contextMenu.json | 17 + src/renderer/src/i18n/en-US/dialog.json | 44 + src/renderer/src/i18n/en-US/index.ts | 54 + src/renderer/src/i18n/en-US/mcp.json | 271 + src/renderer/src/i18n/en-US/model.json | 30 + src/renderer/src/i18n/en-US/newThread.json | 4 + .../src/i18n/en-US/promptSetting.json | 92 + src/renderer/src/i18n/en-US/routes.json | 15 + src/renderer/src/i18n/en-US/settings.json | 834 + src/renderer/src/i18n/en-US/sync.json | 17 + src/renderer/src/i18n/en-US/thread.json | 35 + src/renderer/src/i18n/en-US/toolCall.json | 12 + src/renderer/src/i18n/en-US/update.json | 16 + src/renderer/src/i18n/en-US/welcome.json | 37 + src/renderer/src/i18n/fa-IR/about.json | 19 + src/renderer/src/i18n/fa-IR/artifacts.json | 49 + src/renderer/src/i18n/fa-IR/chat.json | 69 + src/renderer/src/i18n/fa-IR/common.json | 77 + src/renderer/src/i18n/fa-IR/components.json | 36 + src/renderer/src/i18n/fa-IR/contextMenu.json | 17 + src/renderer/src/i18n/fa-IR/dialog.json | 44 + src/renderer/src/i18n/fa-IR/index.ts | 53 + src/renderer/src/i18n/fa-IR/mcp.json | 271 + src/renderer/src/i18n/fa-IR/model.json | 30 + src/renderer/src/i18n/fa-IR/newThread.json | 4 + .../src/i18n/fa-IR/promptSetting.json | 92 + src/renderer/src/i18n/fa-IR/routes.json | 15 + src/renderer/src/i18n/fa-IR/settings.json | 834 + src/renderer/src/i18n/fa-IR/sync.json | 17 + src/renderer/src/i18n/fa-IR/thread.json | 35 + src/renderer/src/i18n/fa-IR/toolCall.json | 12 + src/renderer/src/i18n/fa-IR/update.json | 16 + src/renderer/src/i18n/fa-IR/welcome.json | 37 + src/renderer/src/i18n/fr-FR/about.json | 19 + src/renderer/src/i18n/fr-FR/artifacts.json | 49 + src/renderer/src/i18n/fr-FR/chat.json | 69 + src/renderer/src/i18n/fr-FR/common.json | 77 + src/renderer/src/i18n/fr-FR/components.json | 36 + src/renderer/src/i18n/fr-FR/contextMenu.json | 17 + src/renderer/src/i18n/fr-FR/dialog.json | 44 + src/renderer/src/i18n/fr-FR/index.ts | 54 + src/renderer/src/i18n/fr-FR/mcp.json | 271 + src/renderer/src/i18n/fr-FR/model.json | 30 + src/renderer/src/i18n/fr-FR/newThread.json | 4 + .../src/i18n/fr-FR/promptSetting.json | 92 + src/renderer/src/i18n/fr-FR/routes.json | 15 + src/renderer/src/i18n/fr-FR/settings.json | 834 + src/renderer/src/i18n/fr-FR/sync.json | 17 + src/renderer/src/i18n/fr-FR/thread.json | 35 + src/renderer/src/i18n/fr-FR/toolCall.json | 12 + src/renderer/src/i18n/fr-FR/update.json | 16 + src/renderer/src/i18n/fr-FR/welcome.json | 37 + src/renderer/src/i18n/index.ts | 26 + src/renderer/src/i18n/ja-JP/about.json | 19 + src/renderer/src/i18n/ja-JP/artifacts.json | 49 + src/renderer/src/i18n/ja-JP/chat.json | 69 + src/renderer/src/i18n/ja-JP/common.json | 77 + src/renderer/src/i18n/ja-JP/components.json | 36 + src/renderer/src/i18n/ja-JP/contextMenu.json | 17 + src/renderer/src/i18n/ja-JP/dialog.json | 44 + src/renderer/src/i18n/ja-JP/index.ts | 53 + src/renderer/src/i18n/ja-JP/mcp.json | 271 + src/renderer/src/i18n/ja-JP/model.json | 30 + src/renderer/src/i18n/ja-JP/newThread.json | 4 + .../src/i18n/ja-JP/promptSetting.json | 92 + src/renderer/src/i18n/ja-JP/routes.json | 15 + src/renderer/src/i18n/ja-JP/settings.json | 834 + src/renderer/src/i18n/ja-JP/sync.json | 17 + src/renderer/src/i18n/ja-JP/thread.json | 35 + src/renderer/src/i18n/ja-JP/toolCall.json | 12 + src/renderer/src/i18n/ja-JP/update.json | 16 + src/renderer/src/i18n/ja-JP/welcome.json | 37 + src/renderer/src/i18n/ko-KR/about.json | 19 + src/renderer/src/i18n/ko-KR/artifacts.json | 48 + src/renderer/src/i18n/ko-KR/chat.json | 69 + src/renderer/src/i18n/ko-KR/common.json | 77 + src/renderer/src/i18n/ko-KR/components.json | 36 + src/renderer/src/i18n/ko-KR/contextMenu.json | 17 + src/renderer/src/i18n/ko-KR/dialog.json | 44 + src/renderer/src/i18n/ko-KR/index.ts | 54 + src/renderer/src/i18n/ko-KR/mcp.json | 271 + src/renderer/src/i18n/ko-KR/model.json | 30 + src/renderer/src/i18n/ko-KR/newThread.json | 4 + .../src/i18n/ko-KR/promptSetting.json | 92 + src/renderer/src/i18n/ko-KR/routes.json | 15 + src/renderer/src/i18n/ko-KR/settings.json | 834 + src/renderer/src/i18n/ko-KR/sync.json | 17 + src/renderer/src/i18n/ko-KR/thread.json | 35 + src/renderer/src/i18n/ko-KR/toolCall.json | 12 + src/renderer/src/i18n/ko-KR/update.json | 16 + src/renderer/src/i18n/ko-KR/welcome.json | 37 + src/renderer/src/i18n/ru-RU/about.json | 19 + src/renderer/src/i18n/ru-RU/artifacts.json | 49 + src/renderer/src/i18n/ru-RU/chat.json | 69 + src/renderer/src/i18n/ru-RU/common.json | 77 + src/renderer/src/i18n/ru-RU/components.json | 36 + src/renderer/src/i18n/ru-RU/contextMenu.json | 17 + src/renderer/src/i18n/ru-RU/dialog.json | 44 + src/renderer/src/i18n/ru-RU/index.ts | 52 + src/renderer/src/i18n/ru-RU/mcp.json | 271 + src/renderer/src/i18n/ru-RU/model.json | 30 + src/renderer/src/i18n/ru-RU/newThread.json | 4 + .../src/i18n/ru-RU/promptSetting.json | 92 + src/renderer/src/i18n/ru-RU/routes.json | 15 + src/renderer/src/i18n/ru-RU/settings.json | 834 + src/renderer/src/i18n/ru-RU/sync.json | 17 + src/renderer/src/i18n/ru-RU/thread.json | 35 + src/renderer/src/i18n/ru-RU/toolCall.json | 12 + src/renderer/src/i18n/ru-RU/update.json | 16 + src/renderer/src/i18n/ru-RU/welcome.json | 37 + src/renderer/src/i18n/zh-CN/about.json | 19 + src/renderer/src/i18n/zh-CN/artifacts.json | 48 + src/renderer/src/i18n/zh-CN/chat.json | 69 + src/renderer/src/i18n/zh-CN/common.json | 77 + src/renderer/src/i18n/zh-CN/components.json | 36 + src/renderer/src/i18n/zh-CN/contextMenu.json | 17 + src/renderer/src/i18n/zh-CN/dialog.json | 44 + src/renderer/src/i18n/zh-CN/index.ts | 55 + src/renderer/src/i18n/zh-CN/mcp.json | 271 + src/renderer/src/i18n/zh-CN/model.json | 30 + src/renderer/src/i18n/zh-CN/newThread.json | 4 + .../src/i18n/zh-CN/promptSetting.json | 92 + src/renderer/src/i18n/zh-CN/routes.json | 15 + src/renderer/src/i18n/zh-CN/settings.json | 834 + src/renderer/src/i18n/zh-CN/sync.json | 17 + src/renderer/src/i18n/zh-CN/thread.json | 35 + src/renderer/src/i18n/zh-CN/toolCall.json | 12 + src/renderer/src/i18n/zh-CN/update.json | 16 + src/renderer/src/i18n/zh-CN/welcome.json | 37 + src/renderer/src/i18n/zh-HK/about.json | 19 + src/renderer/src/i18n/zh-HK/artifacts.json | 48 + src/renderer/src/i18n/zh-HK/chat.json | 69 + src/renderer/src/i18n/zh-HK/common.json | 77 + src/renderer/src/i18n/zh-HK/components.json | 36 + src/renderer/src/i18n/zh-HK/contextMenu.json | 17 + src/renderer/src/i18n/zh-HK/dialog.json | 44 + src/renderer/src/i18n/zh-HK/index.ts | 53 + src/renderer/src/i18n/zh-HK/mcp.json | 271 + src/renderer/src/i18n/zh-HK/model.json | 30 + src/renderer/src/i18n/zh-HK/newThread.json | 4 + .../src/i18n/zh-HK/promptSetting.json | 92 + src/renderer/src/i18n/zh-HK/routes.json | 15 + src/renderer/src/i18n/zh-HK/settings.json | 834 + src/renderer/src/i18n/zh-HK/sync.json | 17 + src/renderer/src/i18n/zh-HK/thread.json | 35 + src/renderer/src/i18n/zh-HK/toolCall.json | 12 + src/renderer/src/i18n/zh-HK/update.json | 16 + src/renderer/src/i18n/zh-HK/welcome.json | 37 + src/renderer/src/i18n/zh-TW/about.json | 19 + src/renderer/src/i18n/zh-TW/artifacts.json | 48 + src/renderer/src/i18n/zh-TW/chat.json | 69 + src/renderer/src/i18n/zh-TW/common.json | 77 + src/renderer/src/i18n/zh-TW/components.json | 36 + src/renderer/src/i18n/zh-TW/contextMenu.json | 17 + src/renderer/src/i18n/zh-TW/dialog.json | 44 + src/renderer/src/i18n/zh-TW/index.ts | 53 + src/renderer/src/i18n/zh-TW/mcp.json | 271 + src/renderer/src/i18n/zh-TW/model.json | 30 + src/renderer/src/i18n/zh-TW/newThread.json | 4 + .../src/i18n/zh-TW/promptSetting.json | 92 + src/renderer/src/i18n/zh-TW/routes.json | 15 + src/renderer/src/i18n/zh-TW/settings.json | 834 + src/renderer/src/i18n/zh-TW/sync.json | 17 + src/renderer/src/i18n/zh-TW/thread.json | 35 + src/renderer/src/i18n/zh-TW/toolCall.json | 12 + src/renderer/src/i18n/zh-TW/update.json | 16 + src/renderer/src/i18n/zh-TW/welcome.json | 37 + src/renderer/src/lib/float.cursor.ts | 2 + src/renderer/src/lib/gemini.ts | 52 + src/renderer/src/lib/image.ts | 105 + src/renderer/src/lib/sanitizeText.ts | 28 + src/renderer/src/lib/searchHistory.ts | 82 + src/renderer/src/lib/utils.ts | 66 + src/renderer/src/main.ts | 30 + src/renderer/src/router/index.ts | 133 + src/renderer/src/stores/artifact.ts | 57 + src/renderer/src/stores/chat.ts | 1242 ++ src/renderer/src/stores/dialog.ts | 117 + src/renderer/src/stores/floatingButton.ts | 58 + src/renderer/src/stores/language.ts | 61 + src/renderer/src/stores/mcp.ts | 667 + src/renderer/src/stores/modelCheck.ts | 24 + src/renderer/src/stores/prompts.ts | 71 + src/renderer/src/stores/reference.ts | 28 + src/renderer/src/stores/settings.ts | 1785 ++ src/renderer/src/stores/shortcutKey.ts | 45 + src/renderer/src/stores/sound.ts | 57 + src/renderer/src/stores/sync.ts | 136 + src/renderer/src/stores/theme.ts | 87 + src/renderer/src/stores/upgrade.ts | 256 + src/renderer/src/views/ChatTabView.vue | 268 + src/renderer/src/views/SettingsTabView.vue | 84 + src/renderer/src/views/WelcomeView.vue | 447 + src/shared/chat.d.ts | 160 + src/shared/config.dict.ts | 4 + src/shared/dialog.ts | 21 + src/shared/i18n.ts | 371 + src/shared/lifecycle.ts | 10 + src/shared/logger.ts | 101 + src/shared/model.ts | 9 + src/shared/presenter.d.ts | 2 + src/shared/provider-operations.ts | 67 + src/shared/types/core/agent-events.ts | 33 + src/shared/types/core/chat.ts | 95 + src/shared/types/core/llm-events.ts | 148 + src/shared/types/core/mcp.ts | 71 + src/shared/types/core/usage.ts | 16 + src/shared/types/index.d.ts | 2 + src/shared/types/presenters/index.d.ts | 37 + .../types/presenters/legacy.presenters.d.ts | 1883 ++ .../presenters/llmprovider.presenter.d.ts | 215 + .../types/presenters/thread.presenter.d.ts | 222 + .../types/presenters/window.presenter.d.ts | 1 + src/types/electron-store.d.ts | 10 + src/types/i18n.d.ts | 1199 ++ tailwind.config.js | 136 + test/README.md | 213 + test/main/eventbus/eventbus.test.ts | 327 + test/main/presenter/FilePresenter.test.ts | 223 + .../presenter/FileValidationService.test.ts | 366 + .../main/presenter/KnowledgePresenter.test.ts | 271 + test/main/presenter/filesystem.test.ts | 510 + .../presenter/llmProviderPresenter.test.ts | 522 + .../llmProviderPresenter/coreEvents.test.ts | 166 + test/main/presenter/mcpClient.test.ts | 393 + test/main/presenter/modelConfig.test.ts | 379 + test/mocks/electron-toolkit-utils.ts | 3 + test/mocks/electron.ts | 71 + .../messageBlockSnapshot.test.ts.snap | 190 + .../message/eventMappingTable.test.ts | 613 + .../message/messageBlockSnapshot.test.ts | 337 + .../message/performanceEvaluation.test.ts | 424 + .../renderer/message/rendererContract.test.ts | 596 + test/renderer/shell/main.test.ts | 126 + test/setup.renderer.ts | 108 + test/setup.ts | 78 + tsconfig.json | 4 + tsconfig.node.json | 24 + tsconfig.web.json | 38 + vitest.config.renderer.ts | 47 + vitest.config.ts | 50 + 1253 files changed, 157662 insertions(+), 61 deletions(-) create mode 100644 .claude/agents/electron-architecture-agent.md create mode 100644 .claude/agents/i18n-code-reviewer.md create mode 100644 .claude/agents/llm-provider-agent.md create mode 100644 .claude/agents/mcp-integration-agent.md create mode 100644 .cursor/rules/development-setup.mdc create mode 100644 .cursor/rules/electron-best-practices.mdc create mode 100644 .cursor/rules/error-logging.mdc create mode 100644 .cursor/rules/i18n.mdc create mode 100644 .cursor/rules/llm-agent-loop.mdc create mode 100644 .cursor/rules/performance.mdc create mode 100644 .cursor/rules/pinia-best-practices.mdc create mode 100644 .cursor/rules/project-structure.mdc create mode 100644 .cursor/rules/provider-guidelines.mdc create mode 100644 .cursor/rules/vue-best-practices.mdc create mode 100644 .cursor/rules/vue-router-best-practices.mdc create mode 100644 .cursor/rules/vue-shadcn.mdc create mode 100644 .cursorignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/deploycdn.yml create mode 100644 .github/workflows/prcheck.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .oxlintrc.json create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTING.zh.md create mode 100644 Dockerfile.build.linux create mode 100644 LICENSE delete mode 100644 README.en.md create mode 100644 README.jp.md create mode 100644 README.zh.md create mode 100644 brand-config.example-banana.json create mode 100644 brand-config.template.json create mode 100644 build/VERSION-README.md create mode 100644 build/entitlements.mac.plist create mode 100644 build/generate-version-files.mjs create mode 100644 build/icnsmaker.sh create mode 100644 build/icon.icns create mode 100644 build/icon.ico create mode 100644 build/icon.png create mode 100644 build/nsis-installer.nsh create mode 100644 components.json create mode 100644 docs/agent/implementation-tasks.md create mode 100644 docs/agent/message-architecture.md create mode 100644 docs/agent/presenter-split-plan.md create mode 100644 docs/app-lifecycle.md create mode 100644 docs/config-presenter-architecture.md create mode 100644 docs/deepchat-architecture-overview.md create mode 100644 docs/deeplinks.md create mode 100644 docs/developer-guide.md create mode 100644 docs/i18n-review-progress.md create mode 100644 docs/ipc/eventbus-usage.md create mode 100644 docs/ipc/ipc-architecture-complete.md create mode 100644 docs/ipc/optimization-recommendations.md create mode 100644 docs/knowledge-presenter-complete.md create mode 100644 docs/linux-build-guide.md create mode 100644 docs/markdown-support.md create mode 100644 docs/mcp-architecture.md create mode 100644 docs/provider-optimization-summary.md create mode 100644 docs/rebrand-guide.md create mode 100644 docs/tool-calling-system.md create mode 100644 docs/user-guide.md create mode 100644 electron-builder-macx64.yml create mode 100644 electron-builder.yml create mode 100644 electron.vite.config.ts create mode 100644 package.json create mode 100644 pnpm-workspace.yaml create mode 100644 resources/blankSearch.html create mode 100644 resources/cdn/Recharts.js create mode 100644 resources/cdn/babel.min.js create mode 100644 resources/cdn/lucide.js create mode 100644 resources/cdn/prop-types.min.js create mode 100644 resources/cdn/react-dom.production.min.js create mode 100644 resources/cdn/react.production.min.js create mode 100644 resources/cdn/tailwind.min.css create mode 100644 resources/icon.ico create mode 100644 resources/icon.png create mode 100644 resources/linux_tray.png create mode 100644 resources/macTrayTemplate.png create mode 100644 resources/win_tray.ico create mode 100644 scripts/afterPack.js create mode 100644 scripts/brand-assets/.gitkeep create mode 100644 scripts/generate-i18n-types.js create mode 100644 scripts/install-sharp-for-platform.js create mode 100644 scripts/installVss.js create mode 100644 scripts/notarize.js create mode 100644 scripts/rebrand.js create mode 100644 scripts/verify-commit.js create mode 100644 src/main/contextMenuHelper.ts create mode 100644 src/main/env.d.ts create mode 100644 src/main/eventbus.ts create mode 100644 src/main/events.ts create mode 100644 src/main/index.ts create mode 100644 src/main/lib/scrollCapture.ts create mode 100644 src/main/lib/svgSanitizer.ts create mode 100644 src/main/lib/system.ts create mode 100644 src/main/lib/textsplitters/document/document.ts create mode 100644 src/main/lib/textsplitters/document/index.ts create mode 100644 src/main/lib/textsplitters/index.ts create mode 100644 src/main/lib/textsplitters/text_splitter.ts create mode 100644 src/main/lib/watermark.ts create mode 100644 src/main/presenter/anthropicOAuth.ts create mode 100644 src/main/presenter/configPresenter/aes.ts create mode 100644 src/main/presenter/configPresenter/index.ts create mode 100644 src/main/presenter/configPresenter/knowledgeConfHelper.ts create mode 100644 src/main/presenter/configPresenter/mcpConfHelper.ts create mode 100644 src/main/presenter/configPresenter/modelConfig.ts create mode 100644 src/main/presenter/configPresenter/modelDefaultSettings.ts create mode 100644 src/main/presenter/configPresenter/providerModelSettings.ts create mode 100644 src/main/presenter/configPresenter/providers.ts create mode 100644 src/main/presenter/configPresenter/shortcutKeySettings.ts create mode 100644 src/main/presenter/deeplinkPresenter/index.ts create mode 100644 src/main/presenter/devicePresenter/index.ts create mode 100644 src/main/presenter/dialogPresenter/index.ts create mode 100644 src/main/presenter/filePresenter/AudioFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/BaseFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/CodeFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/CsvFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/DirectoryAdapter.ts create mode 100644 src/main/presenter/filePresenter/DocFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/ExcelFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/FileAdapterConstructor.ts create mode 100644 src/main/presenter/filePresenter/FilePresenter.ts create mode 100644 src/main/presenter/filePresenter/FileValidationService.ts create mode 100644 src/main/presenter/filePresenter/ImageFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/PdfFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/PptFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/TextFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/UnsupportFileAdapter.ts create mode 100644 src/main/presenter/filePresenter/mime.ts create mode 100644 src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts create mode 100644 src/main/presenter/floatingButtonPresenter/index.ts create mode 100644 src/main/presenter/floatingButtonPresenter/types.ts create mode 100644 src/main/presenter/githubCopilotDeviceFlow.ts create mode 100644 src/main/presenter/githubCopilotOAuth.ts create mode 100644 src/main/presenter/index.ts create mode 100644 src/main/presenter/knowledgePresenter/database/duckdbPresenter.ts create mode 100644 src/main/presenter/knowledgePresenter/index.ts create mode 100644 src/main/presenter/knowledgePresenter/knowledgeStorePresenter.ts create mode 100644 src/main/presenter/knowledgePresenter/knowledgeTaskPresenter.ts create mode 100644 src/main/presenter/lifecyclePresenter/DatabaseInitializer.ts create mode 100644 src/main/presenter/lifecyclePresenter/SplashWindowManager.ts create mode 100644 src/main/presenter/lifecyclePresenter/coreHooks.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/after-start/traySetupHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/after-start/windowCreationHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/beforeQuit/builtinKnowledgeDestroyHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/beforeQuit/floatingDestroyHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/beforeQuit/presenterDestroyHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/beforeQuit/trayDestroyHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/beforeQuit/windowQuittingHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/beforeStart/protocolRegistrationHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/index.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/init/configInitHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/init/databaseInitHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/ready/eventListenerSetupHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/hooks/ready/presenterInitHook.ts create mode 100644 src/main/presenter/lifecyclePresenter/index.ts create mode 100644 src/main/presenter/lifecyclePresenter/types.ts create mode 100644 src/main/presenter/llamaCppPresenter/index.ts create mode 100644 src/main/presenter/llamaCppPresenter/llama.ts create mode 100644 src/main/presenter/llmProviderPresenter/baseProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/index.ts create mode 100644 src/main/presenter/llmProviderPresenter/oauthHelper.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/_302AIProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/aihubmixProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/anthropicProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/awsBedrockProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/dashscopeProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/deepseekProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/doubaoProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/geminiProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/githubCopilotProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/githubProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/grokProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/groqProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/lmstudioProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/minimaxProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/ollamaProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/openAIProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/openRouterProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/ppioProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/siliconcloudProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/togetherProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/tokenfluxProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/vercelAIGatewayProvider.ts create mode 100644 src/main/presenter/llmProviderPresenter/providers/zhipuProvider.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/appleServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/artifactsServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/autoPromptingServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/bochaSearchServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/braveSearchServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/builder.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/builtinKnowledgeServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/deepResearchServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/difyKnowledgeServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/fastGptKnowledgeServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/filesystem.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/ragflowKnowledgeServer.ts create mode 100644 src/main/presenter/mcpPresenter/index.ts create mode 100644 src/main/presenter/mcpPresenter/mcpClient.ts create mode 100644 src/main/presenter/mcpPresenter/mcprouterManager.ts create mode 100644 src/main/presenter/mcpPresenter/serverManager.ts create mode 100644 src/main/presenter/mcpPresenter/toolManager.ts create mode 100644 src/main/presenter/notifactionPresenter.ts create mode 100644 src/main/presenter/oauthPresenter.ts create mode 100644 src/main/presenter/proxyConfig.ts create mode 100644 src/main/presenter/shortcutPresenter.ts create mode 100644 src/main/presenter/sqlitePresenter/importData.ts create mode 100644 src/main/presenter/sqlitePresenter/index.ts create mode 100644 src/main/presenter/sqlitePresenter/tables/attachments.ts create mode 100644 src/main/presenter/sqlitePresenter/tables/baseTable.ts create mode 100644 src/main/presenter/sqlitePresenter/tables/conversations.ts create mode 100644 src/main/presenter/sqlitePresenter/tables/messageAttachments.ts create mode 100644 src/main/presenter/sqlitePresenter/tables/messages.ts create mode 100644 src/main/presenter/syncPresenter/index.ts create mode 100644 src/main/presenter/tabPresenter.ts create mode 100644 src/main/presenter/threadPresenter/const.ts create mode 100644 src/main/presenter/threadPresenter/contentEnricher.ts create mode 100644 src/main/presenter/threadPresenter/fileContext.ts create mode 100644 src/main/presenter/threadPresenter/index.ts create mode 100644 src/main/presenter/threadPresenter/messageManager.ts create mode 100644 src/main/presenter/threadPresenter/searchManager.ts create mode 100644 src/main/presenter/trayPresenter.ts create mode 100644 src/main/presenter/upgradePresenter/index.ts create mode 100644 src/main/presenter/windowPresenter/FloatingChatWindow.ts create mode 100644 src/main/presenter/windowPresenter/index.ts create mode 100644 src/main/utils/index.ts create mode 100644 src/main/utils/strings.ts create mode 100644 src/main/utils/vector.ts create mode 100644 src/preload/floating-preload.ts create mode 100644 src/preload/index.d.ts create mode 100644 src/preload/index.ts create mode 100644 src/renderer/floating/FloatingButton.vue create mode 100644 src/renderer/floating/env.d.ts create mode 100644 src/renderer/floating/index.html create mode 100644 src/renderer/floating/main.ts create mode 100644 src/renderer/index.html create mode 100644 src/renderer/public/sounds/sfx-fc.mp3 create mode 100644 src/renderer/public/sounds/sfx-typing.mp3 create mode 100644 src/renderer/shell/App.vue create mode 100644 src/renderer/shell/components/AppBar.vue create mode 100644 src/renderer/shell/components/app-bar/AppBarTabItem.vue create mode 100644 src/renderer/shell/components/icons/MaximizeIcon.vue create mode 100644 src/renderer/shell/components/icons/RestoreIcon.vue create mode 100644 src/renderer/shell/index.html create mode 100644 src/renderer/shell/main.ts create mode 100644 src/renderer/shell/stores/tab.ts create mode 100644 src/renderer/splash/env.d.ts create mode 100644 src/renderer/splash/index.html create mode 100644 src/renderer/splash/loading.vue create mode 100644 src/renderer/splash/main.ts create mode 100644 src/renderer/src/App.vue create mode 100644 src/renderer/src/assets/geist.ttf create mode 100644 src/renderer/src/assets/images/dify.png create mode 100644 src/renderer/src/assets/images/fastgpt.png create mode 100644 src/renderer/src/assets/images/ragflow.png create mode 100644 src/renderer/src/assets/llm-icons/302ai.svg create mode 100644 src/renderer/src/assets/llm-icons/adobe-color.svg create mode 100644 src/renderer/src/assets/llm-icons/adobe-text.svg create mode 100644 src/renderer/src/assets/llm-icons/adobe.svg create mode 100644 src/renderer/src/assets/llm-icons/adobefirefly-color.svg create mode 100644 src/renderer/src/assets/llm-icons/adobefirefly-text.svg create mode 100644 src/renderer/src/assets/llm-icons/adobefirefly.svg create mode 100644 src/renderer/src/assets/llm-icons/ai21-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/ai21-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/ai21-text.svg create mode 100644 src/renderer/src/assets/llm-icons/ai21.svg create mode 100644 src/renderer/src/assets/llm-icons/ai360-color.svg create mode 100644 src/renderer/src/assets/llm-icons/ai360-text.svg create mode 100644 src/renderer/src/assets/llm-icons/ai360.svg create mode 100644 src/renderer/src/assets/llm-icons/aihubmix.png create mode 100644 src/renderer/src/assets/llm-icons/aimass-color.svg create mode 100644 src/renderer/src/assets/llm-icons/aimass-text.svg create mode 100644 src/renderer/src/assets/llm-icons/aimass.svg create mode 100644 src/renderer/src/assets/llm-icons/alibaba-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/alibaba-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/alibaba-color.svg create mode 100644 src/renderer/src/assets/llm-icons/alibaba-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/alibaba-text.svg create mode 100644 src/renderer/src/assets/llm-icons/alibaba.svg create mode 100644 src/renderer/src/assets/llm-icons/alibabacloud-color.svg create mode 100644 src/renderer/src/assets/llm-icons/alibabacloud-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/alibabacloud-text.svg create mode 100644 src/renderer/src/assets/llm-icons/alibabacloud.svg create mode 100644 src/renderer/src/assets/llm-icons/antgroup-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/antgroup-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/antgroup-color.svg create mode 100644 src/renderer/src/assets/llm-icons/antgroup-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/antgroup-text.svg create mode 100644 src/renderer/src/assets/llm-icons/antgroup.svg create mode 100644 src/renderer/src/assets/llm-icons/anthropic-text.svg create mode 100644 src/renderer/src/assets/llm-icons/anthropic.svg create mode 100644 src/renderer/src/assets/llm-icons/automatic-color.svg create mode 100644 src/renderer/src/assets/llm-icons/automatic-text.svg create mode 100644 src/renderer/src/assets/llm-icons/automatic.svg create mode 100644 src/renderer/src/assets/llm-icons/aws-bedrock.svg create mode 100644 src/renderer/src/assets/llm-icons/aws-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/aws-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/aws-color.svg create mode 100644 src/renderer/src/assets/llm-icons/aws-text.svg create mode 100644 src/renderer/src/assets/llm-icons/aws.svg create mode 100644 src/renderer/src/assets/llm-icons/aya-color.svg create mode 100644 src/renderer/src/assets/llm-icons/aya-text.svg create mode 100644 src/renderer/src/assets/llm-icons/aya.svg create mode 100644 src/renderer/src/assets/llm-icons/azure-color.svg create mode 100644 src/renderer/src/assets/llm-icons/azure-text.svg create mode 100644 src/renderer/src/assets/llm-icons/azure.svg create mode 100644 src/renderer/src/assets/llm-icons/azureai-color.svg create mode 100644 src/renderer/src/assets/llm-icons/azureai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/azureai.svg create mode 100644 src/renderer/src/assets/llm-icons/baichuan-color.svg create mode 100644 src/renderer/src/assets/llm-icons/baichuan-text.svg create mode 100644 src/renderer/src/assets/llm-icons/baichuan.svg create mode 100644 src/renderer/src/assets/llm-icons/baidu-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/baidu-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/baidu-color.svg create mode 100644 src/renderer/src/assets/llm-icons/baidu-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/baidu-text.svg create mode 100644 src/renderer/src/assets/llm-icons/baidu.svg create mode 100644 src/renderer/src/assets/llm-icons/baiducloud-color.svg create mode 100644 src/renderer/src/assets/llm-icons/baiducloud-text.svg create mode 100644 src/renderer/src/assets/llm-icons/baiducloud.svg create mode 100644 src/renderer/src/assets/llm-icons/bedrock-color.svg create mode 100644 src/renderer/src/assets/llm-icons/bedrock-text.svg create mode 100644 src/renderer/src/assets/llm-icons/bedrock.svg create mode 100644 src/renderer/src/assets/llm-icons/bing-color.svg create mode 100644 src/renderer/src/assets/llm-icons/bing-text.svg create mode 100644 src/renderer/src/assets/llm-icons/bing.svg create mode 100644 src/renderer/src/assets/llm-icons/bytedance-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/bytedance-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/bytedance-color.svg create mode 100644 src/renderer/src/assets/llm-icons/bytedance-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/bytedance-text.svg create mode 100644 src/renderer/src/assets/llm-icons/bytedance.svg create mode 100644 src/renderer/src/assets/llm-icons/chatglm-color.svg create mode 100644 src/renderer/src/assets/llm-icons/chatglm-text.svg create mode 100644 src/renderer/src/assets/llm-icons/chatglm.svg create mode 100644 src/renderer/src/assets/llm-icons/civitai-color.svg create mode 100644 src/renderer/src/assets/llm-icons/civitai-text-color.svg create mode 100644 src/renderer/src/assets/llm-icons/civitai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/civitai.svg create mode 100644 src/renderer/src/assets/llm-icons/claude-color.svg create mode 100644 src/renderer/src/assets/llm-icons/claude-text.svg create mode 100644 src/renderer/src/assets/llm-icons/claude.svg create mode 100644 src/renderer/src/assets/llm-icons/clipdrop-text.svg create mode 100644 src/renderer/src/assets/llm-icons/clipdrop.svg create mode 100644 src/renderer/src/assets/llm-icons/cloudflare-color.svg create mode 100644 src/renderer/src/assets/llm-icons/cloudflare-text.svg create mode 100644 src/renderer/src/assets/llm-icons/cloudflare.svg create mode 100644 src/renderer/src/assets/llm-icons/codegeex-color.svg create mode 100644 src/renderer/src/assets/llm-icons/codegeex-text.svg create mode 100644 src/renderer/src/assets/llm-icons/codegeex.svg create mode 100644 src/renderer/src/assets/llm-icons/cogvideo-color.svg create mode 100644 src/renderer/src/assets/llm-icons/cogvideo-text.svg create mode 100644 src/renderer/src/assets/llm-icons/cogvideo.svg create mode 100644 src/renderer/src/assets/llm-icons/cogview-color.svg create mode 100644 src/renderer/src/assets/llm-icons/cogview-text.svg create mode 100644 src/renderer/src/assets/llm-icons/cogview.svg create mode 100644 src/renderer/src/assets/llm-icons/cohere-color.svg create mode 100644 src/renderer/src/assets/llm-icons/cohere-text.svg create mode 100644 src/renderer/src/assets/llm-icons/cohere.svg create mode 100644 src/renderer/src/assets/llm-icons/colab-color.svg create mode 100644 src/renderer/src/assets/llm-icons/colab-text.svg create mode 100644 src/renderer/src/assets/llm-icons/colab.svg create mode 100644 src/renderer/src/assets/llm-icons/comfyui-color.svg create mode 100644 src/renderer/src/assets/llm-icons/comfyui-text.svg create mode 100644 src/renderer/src/assets/llm-icons/comfyui.svg create mode 100644 src/renderer/src/assets/llm-icons/copilot-color.svg create mode 100644 src/renderer/src/assets/llm-icons/copilot-text.svg create mode 100644 src/renderer/src/assets/llm-icons/copilot.svg create mode 100644 src/renderer/src/assets/llm-icons/coze-text.svg create mode 100644 src/renderer/src/assets/llm-icons/coze.svg create mode 100644 src/renderer/src/assets/llm-icons/cursor-text.svg create mode 100644 src/renderer/src/assets/llm-icons/cursor.svg create mode 100644 src/renderer/src/assets/llm-icons/dalle-color.svg create mode 100644 src/renderer/src/assets/llm-icons/dalle-text.svg create mode 100644 src/renderer/src/assets/llm-icons/dalle.svg create mode 100644 src/renderer/src/assets/llm-icons/dbrx-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/dbrx-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/dbrx-color.svg create mode 100644 src/renderer/src/assets/llm-icons/dbrx-text.svg create mode 100644 src/renderer/src/assets/llm-icons/dbrx.svg create mode 100644 src/renderer/src/assets/llm-icons/deepai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/deepai.svg create mode 100644 src/renderer/src/assets/llm-icons/deepmind-color.svg create mode 100644 src/renderer/src/assets/llm-icons/deepmind-text.svg create mode 100644 src/renderer/src/assets/llm-icons/deepmind.svg create mode 100644 src/renderer/src/assets/llm-icons/deepseek-color.svg create mode 100644 src/renderer/src/assets/llm-icons/deepseek-text.svg create mode 100644 src/renderer/src/assets/llm-icons/deepseek.svg create mode 100644 src/renderer/src/assets/llm-icons/dify-color.svg create mode 100644 src/renderer/src/assets/llm-icons/dify-text-color.svg create mode 100644 src/renderer/src/assets/llm-icons/dify-text.svg create mode 100644 src/renderer/src/assets/llm-icons/dify.svg create mode 100644 src/renderer/src/assets/llm-icons/doubao-color.svg create mode 100644 src/renderer/src/assets/llm-icons/doubao-text.svg create mode 100644 src/renderer/src/assets/llm-icons/doubao.svg create mode 100644 src/renderer/src/assets/llm-icons/fal-text.svg create mode 100644 src/renderer/src/assets/llm-icons/fal.svg create mode 100644 src/renderer/src/assets/llm-icons/fireworks-color.svg create mode 100644 src/renderer/src/assets/llm-icons/fireworks-text.svg create mode 100644 src/renderer/src/assets/llm-icons/fireworks.svg create mode 100644 src/renderer/src/assets/llm-icons/fishaudio-text.svg create mode 100644 src/renderer/src/assets/llm-icons/fishaudio.svg create mode 100644 src/renderer/src/assets/llm-icons/flux-text.svg create mode 100644 src/renderer/src/assets/llm-icons/flux.svg create mode 100644 src/renderer/src/assets/llm-icons/gemini-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/gemini-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/gemini-color.svg create mode 100644 src/renderer/src/assets/llm-icons/gemini-text.svg create mode 100644 src/renderer/src/assets/llm-icons/gemini.svg create mode 100644 src/renderer/src/assets/llm-icons/gemma-color.svg create mode 100644 src/renderer/src/assets/llm-icons/gemma-text.svg create mode 100644 src/renderer/src/assets/llm-icons/gemma.svg create mode 100644 src/renderer/src/assets/llm-icons/giteeai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/giteeai.svg create mode 100644 src/renderer/src/assets/llm-icons/github-text.svg create mode 100644 src/renderer/src/assets/llm-icons/github.svg create mode 100644 src/renderer/src/assets/llm-icons/githubcopilot-text.svg create mode 100644 src/renderer/src/assets/llm-icons/githubcopilot.svg create mode 100644 src/renderer/src/assets/llm-icons/glif-text.svg create mode 100644 src/renderer/src/assets/llm-icons/glif.svg create mode 100644 src/renderer/src/assets/llm-icons/google-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/google-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/google-color.svg create mode 100644 src/renderer/src/assets/llm-icons/google.svg create mode 100644 src/renderer/src/assets/llm-icons/grok-text.svg create mode 100644 src/renderer/src/assets/llm-icons/grok.svg create mode 100644 src/renderer/src/assets/llm-icons/groq-text.svg create mode 100644 src/renderer/src/assets/llm-icons/groq.svg create mode 100644 src/renderer/src/assets/llm-icons/hailuo-color.svg create mode 100644 src/renderer/src/assets/llm-icons/hailuo-text.svg create mode 100644 src/renderer/src/assets/llm-icons/hailuo.svg create mode 100644 src/renderer/src/assets/llm-icons/haiper-text.svg create mode 100644 src/renderer/src/assets/llm-icons/haiper.svg create mode 100644 src/renderer/src/assets/llm-icons/hedra-text.svg create mode 100644 src/renderer/src/assets/llm-icons/hedra.svg create mode 100644 src/renderer/src/assets/llm-icons/higress-color.svg create mode 100644 src/renderer/src/assets/llm-icons/higress-text.svg create mode 100644 src/renderer/src/assets/llm-icons/higress.svg create mode 100644 src/renderer/src/assets/llm-icons/huggingface-color.svg create mode 100644 src/renderer/src/assets/llm-icons/huggingface-text.svg create mode 100644 src/renderer/src/assets/llm-icons/huggingface.svg create mode 100644 src/renderer/src/assets/llm-icons/hunyuan-color.svg create mode 100644 src/renderer/src/assets/llm-icons/hunyuan-text.svg create mode 100644 src/renderer/src/assets/llm-icons/hunyuan.svg create mode 100644 src/renderer/src/assets/llm-icons/ideogram-text.svg create mode 100644 src/renderer/src/assets/llm-icons/ideogram.svg create mode 100644 src/renderer/src/assets/llm-icons/internlm-color.svg create mode 100644 src/renderer/src/assets/llm-icons/internlm-text.svg create mode 100644 src/renderer/src/assets/llm-icons/internlm.svg create mode 100644 src/renderer/src/assets/llm-icons/jina-color.svg create mode 100644 src/renderer/src/assets/llm-icons/jina-text.svg create mode 100644 src/renderer/src/assets/llm-icons/jina.svg create mode 100644 src/renderer/src/assets/llm-icons/kimi-color.svg create mode 100644 src/renderer/src/assets/llm-icons/kimi-text.svg create mode 100644 src/renderer/src/assets/llm-icons/kimi.svg create mode 100644 src/renderer/src/assets/llm-icons/kling-color.svg create mode 100644 src/renderer/src/assets/llm-icons/kling-text.svg create mode 100644 src/renderer/src/assets/llm-icons/kling.svg create mode 100644 src/renderer/src/assets/llm-icons/langchain-color.svg create mode 100644 src/renderer/src/assets/llm-icons/langchain-text.svg create mode 100644 src/renderer/src/assets/llm-icons/langchain.svg create mode 100644 src/renderer/src/assets/llm-icons/langfuse-color.svg create mode 100644 src/renderer/src/assets/llm-icons/langfuse-text.svg create mode 100644 src/renderer/src/assets/llm-icons/langfuse.svg create mode 100644 src/renderer/src/assets/llm-icons/lightricks-text.svg create mode 100644 src/renderer/src/assets/llm-icons/lightricks.svg create mode 100644 src/renderer/src/assets/llm-icons/livekit-color.svg create mode 100644 src/renderer/src/assets/llm-icons/livekit-text.svg create mode 100644 src/renderer/src/assets/llm-icons/livekit.svg create mode 100644 src/renderer/src/assets/llm-icons/llava-color.svg create mode 100644 src/renderer/src/assets/llm-icons/llava-text.svg create mode 100644 src/renderer/src/assets/llm-icons/llava.svg create mode 100644 src/renderer/src/assets/llm-icons/lmstudio-text.svg create mode 100644 src/renderer/src/assets/llm-icons/lmstudio.svg create mode 100644 src/renderer/src/assets/llm-icons/lobehub-color.svg create mode 100644 src/renderer/src/assets/llm-icons/lobehub-text.svg create mode 100644 src/renderer/src/assets/llm-icons/lobehub.svg create mode 100644 src/renderer/src/assets/llm-icons/luma-color.svg create mode 100644 src/renderer/src/assets/llm-icons/luma-text.svg create mode 100644 src/renderer/src/assets/llm-icons/luma.svg create mode 100644 src/renderer/src/assets/llm-icons/magic-text.svg create mode 100644 src/renderer/src/assets/llm-icons/magic.svg create mode 100644 src/renderer/src/assets/llm-icons/meta-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/meta-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/meta-color.svg create mode 100644 src/renderer/src/assets/llm-icons/meta-text.svg create mode 100644 src/renderer/src/assets/llm-icons/meta.svg create mode 100644 src/renderer/src/assets/llm-icons/midjourney-text.svg create mode 100644 src/renderer/src/assets/llm-icons/midjourney.svg create mode 100644 src/renderer/src/assets/llm-icons/minimax-color.svg create mode 100644 src/renderer/src/assets/llm-icons/minimax-text.svg create mode 100644 src/renderer/src/assets/llm-icons/minimax.svg create mode 100644 src/renderer/src/assets/llm-icons/mistral-color.svg create mode 100644 src/renderer/src/assets/llm-icons/mistral-text.svg create mode 100644 src/renderer/src/assets/llm-icons/mistral.svg create mode 100644 src/renderer/src/assets/llm-icons/modelscope-color.svg create mode 100644 src/renderer/src/assets/llm-icons/modelscope-text.svg create mode 100644 src/renderer/src/assets/llm-icons/modelscope.svg create mode 100644 src/renderer/src/assets/llm-icons/moonshot-text.svg create mode 100644 src/renderer/src/assets/llm-icons/moonshot.svg create mode 100644 src/renderer/src/assets/llm-icons/myshell-color.svg create mode 100644 src/renderer/src/assets/llm-icons/myshell-text.svg create mode 100644 src/renderer/src/assets/llm-icons/myshell.svg create mode 100644 src/renderer/src/assets/llm-icons/notion-text.svg create mode 100644 src/renderer/src/assets/llm-icons/notion.svg create mode 100644 src/renderer/src/assets/llm-icons/nova-color.svg create mode 100644 src/renderer/src/assets/llm-icons/nova-text.svg create mode 100644 src/renderer/src/assets/llm-icons/nova.svg create mode 100644 src/renderer/src/assets/llm-icons/novita-color.svg create mode 100644 src/renderer/src/assets/llm-icons/novita-text.svg create mode 100644 src/renderer/src/assets/llm-icons/novita.svg create mode 100644 src/renderer/src/assets/llm-icons/nvidia-color.svg create mode 100644 src/renderer/src/assets/llm-icons/nvidia-text.svg create mode 100644 src/renderer/src/assets/llm-icons/nvidia.svg create mode 100644 src/renderer/src/assets/llm-icons/ollama-text.svg create mode 100644 src/renderer/src/assets/llm-icons/ollama.svg create mode 100644 src/renderer/src/assets/llm-icons/openai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/openai.svg create mode 100644 src/renderer/src/assets/llm-icons/openchat-color.svg create mode 100644 src/renderer/src/assets/llm-icons/openchat-text.svg create mode 100644 src/renderer/src/assets/llm-icons/openchat.svg create mode 100644 src/renderer/src/assets/llm-icons/openrouter-text.svg create mode 100644 src/renderer/src/assets/llm-icons/openrouter.svg create mode 100644 src/renderer/src/assets/llm-icons/palm-color.svg create mode 100644 src/renderer/src/assets/llm-icons/palm-text.svg create mode 100644 src/renderer/src/assets/llm-icons/palm.svg create mode 100644 src/renderer/src/assets/llm-icons/perplexity-color.svg create mode 100644 src/renderer/src/assets/llm-icons/perplexity-text.svg create mode 100644 src/renderer/src/assets/llm-icons/perplexity.svg create mode 100644 src/renderer/src/assets/llm-icons/pika-text.svg create mode 100644 src/renderer/src/assets/llm-icons/pika.svg create mode 100644 src/renderer/src/assets/llm-icons/pixverse-color.svg create mode 100644 src/renderer/src/assets/llm-icons/pixverse-text.svg create mode 100644 src/renderer/src/assets/llm-icons/pixverse.svg create mode 100644 src/renderer/src/assets/llm-icons/poe-color.svg create mode 100644 src/renderer/src/assets/llm-icons/poe-text.svg create mode 100644 src/renderer/src/assets/llm-icons/poe.svg create mode 100644 src/renderer/src/assets/llm-icons/pollinations-text.svg create mode 100644 src/renderer/src/assets/llm-icons/pollinations.svg create mode 100644 src/renderer/src/assets/llm-icons/ppio-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/ppio-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/ppio-color.svg create mode 100644 src/renderer/src/assets/llm-icons/ppio-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/ppio-text.svg create mode 100644 src/renderer/src/assets/llm-icons/ppio.svg create mode 100644 src/renderer/src/assets/llm-icons/qingyan-color.svg create mode 100644 src/renderer/src/assets/llm-icons/qingyan-text.svg create mode 100644 src/renderer/src/assets/llm-icons/qingyan.svg create mode 100644 src/renderer/src/assets/llm-icons/qiniu.svg create mode 100644 src/renderer/src/assets/llm-icons/qwen-color.svg create mode 100644 src/renderer/src/assets/llm-icons/qwen-text.svg create mode 100644 src/renderer/src/assets/llm-icons/qwen.svg create mode 100644 src/renderer/src/assets/llm-icons/recraft-text.svg create mode 100644 src/renderer/src/assets/llm-icons/recraft.svg create mode 100644 src/renderer/src/assets/llm-icons/replicate-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/replicate.svg create mode 100644 src/renderer/src/assets/llm-icons/replit-color.svg create mode 100644 src/renderer/src/assets/llm-icons/replit-text.svg create mode 100644 src/renderer/src/assets/llm-icons/replit.svg create mode 100644 src/renderer/src/assets/llm-icons/runway-text.svg create mode 100644 src/renderer/src/assets/llm-icons/runway.svg create mode 100644 src/renderer/src/assets/llm-icons/rwkv-color.svg create mode 100644 src/renderer/src/assets/llm-icons/rwkv-text.svg create mode 100644 src/renderer/src/assets/llm-icons/rwkv.svg create mode 100644 src/renderer/src/assets/llm-icons/sensenova-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/sensenova-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/sensenova-color.svg create mode 100644 src/renderer/src/assets/llm-icons/sensenova-text.svg create mode 100644 src/renderer/src/assets/llm-icons/sensenova.svg create mode 100644 src/renderer/src/assets/llm-icons/siliconcloud-color.svg create mode 100644 src/renderer/src/assets/llm-icons/siliconcloud-text.svg create mode 100644 src/renderer/src/assets/llm-icons/siliconcloud.svg create mode 100644 src/renderer/src/assets/llm-icons/spark-color.svg create mode 100644 src/renderer/src/assets/llm-icons/spark-text.svg create mode 100644 src/renderer/src/assets/llm-icons/spark.svg create mode 100644 src/renderer/src/assets/llm-icons/stability-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/stability-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/stability-color.svg create mode 100644 src/renderer/src/assets/llm-icons/stability-text.svg create mode 100644 src/renderer/src/assets/llm-icons/stability.svg create mode 100644 src/renderer/src/assets/llm-icons/stepfun-color.svg create mode 100644 src/renderer/src/assets/llm-icons/stepfun-text.svg create mode 100644 src/renderer/src/assets/llm-icons/stepfun.svg create mode 100644 src/renderer/src/assets/llm-icons/suno-text.svg create mode 100644 src/renderer/src/assets/llm-icons/suno.svg create mode 100644 src/renderer/src/assets/llm-icons/sync-text.svg create mode 100644 src/renderer/src/assets/llm-icons/sync.svg create mode 100644 src/renderer/src/assets/llm-icons/tencent-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tencent-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/tencent-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tencent-text-cn.svg create mode 100644 src/renderer/src/assets/llm-icons/tencent-text.svg create mode 100644 src/renderer/src/assets/llm-icons/tencent.svg create mode 100644 src/renderer/src/assets/llm-icons/tencentcloud-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tencentcloud-text.svg create mode 100644 src/renderer/src/assets/llm-icons/tencentcloud.svg create mode 100644 src/renderer/src/assets/llm-icons/tiangong-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tiangong-text.svg create mode 100644 src/renderer/src/assets/llm-icons/tiangong.svg create mode 100644 src/renderer/src/assets/llm-icons/tii-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tii-text.svg create mode 100644 src/renderer/src/assets/llm-icons/tii.svg create mode 100644 src/renderer/src/assets/llm-icons/together-brand-color.svg create mode 100644 src/renderer/src/assets/llm-icons/together-brand.svg create mode 100644 src/renderer/src/assets/llm-icons/together-color.svg create mode 100644 src/renderer/src/assets/llm-icons/together-text.svg create mode 100644 src/renderer/src/assets/llm-icons/together.svg create mode 100644 src/renderer/src/assets/llm-icons/tokenflux-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tripo-color.svg create mode 100644 src/renderer/src/assets/llm-icons/tripo-text.svg create mode 100644 src/renderer/src/assets/llm-icons/tripo.svg create mode 100644 src/renderer/src/assets/llm-icons/udio-color.svg create mode 100644 src/renderer/src/assets/llm-icons/udio-text.svg create mode 100644 src/renderer/src/assets/llm-icons/udio.svg create mode 100644 src/renderer/src/assets/llm-icons/upstage-color.svg create mode 100644 src/renderer/src/assets/llm-icons/upstage-text.svg create mode 100644 src/renderer/src/assets/llm-icons/upstage.svg create mode 100644 src/renderer/src/assets/llm-icons/v0.svg create mode 100644 src/renderer/src/assets/llm-icons/vercel-text.svg create mode 100644 src/renderer/src/assets/llm-icons/vercel.svg create mode 100644 src/renderer/src/assets/llm-icons/vertexai-color.svg create mode 100644 src/renderer/src/assets/llm-icons/vertexai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/vertexai.svg create mode 100644 src/renderer/src/assets/llm-icons/vidu-color.svg create mode 100644 src/renderer/src/assets/llm-icons/vidu-text.svg create mode 100644 src/renderer/src/assets/llm-icons/vidu.svg create mode 100644 src/renderer/src/assets/llm-icons/viggle-text.svg create mode 100644 src/renderer/src/assets/llm-icons/viggle.svg create mode 100644 src/renderer/src/assets/llm-icons/vllm-color.svg create mode 100644 src/renderer/src/assets/llm-icons/vllm-text.svg create mode 100644 src/renderer/src/assets/llm-icons/vllm.svg create mode 100644 src/renderer/src/assets/llm-icons/volcengine-color.svg create mode 100644 src/renderer/src/assets/llm-icons/volcengine-text.svg create mode 100644 src/renderer/src/assets/llm-icons/volcengine.svg create mode 100644 src/renderer/src/assets/llm-icons/wenxin-color.svg create mode 100644 src/renderer/src/assets/llm-icons/wenxin-text.svg create mode 100644 src/renderer/src/assets/llm-icons/wenxin.svg create mode 100644 src/renderer/src/assets/llm-icons/workersai-color.svg create mode 100644 src/renderer/src/assets/llm-icons/workersai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/workersai.svg create mode 100644 src/renderer/src/assets/llm-icons/xai-text.svg create mode 100644 src/renderer/src/assets/llm-icons/xai.svg create mode 100644 src/renderer/src/assets/llm-icons/xuanyuan-color.svg create mode 100644 src/renderer/src/assets/llm-icons/xuanyuan-text.svg create mode 100644 src/renderer/src/assets/llm-icons/xuanyuan.svg create mode 100644 src/renderer/src/assets/llm-icons/yi-color.svg create mode 100644 src/renderer/src/assets/llm-icons/yi-text.svg create mode 100644 src/renderer/src/assets/llm-icons/yi.svg create mode 100644 src/renderer/src/assets/llm-icons/zeabur-color.svg create mode 100644 src/renderer/src/assets/llm-icons/zeabur-text.svg create mode 100644 src/renderer/src/assets/llm-icons/zeabur.svg create mode 100644 src/renderer/src/assets/llm-icons/zeroone-text.svg create mode 100644 src/renderer/src/assets/llm-icons/zeroone.svg create mode 100644 src/renderer/src/assets/llm-icons/zhipu-color.svg create mode 100644 src/renderer/src/assets/llm-icons/zhipu-text.svg create mode 100644 src/renderer/src/assets/llm-icons/zhipu.svg create mode 100644 src/renderer/src/assets/logo-dark.png create mode 100644 src/renderer/src/assets/logo.png create mode 100644 src/renderer/src/assets/main.css create mode 100644 src/renderer/src/assets/mcp-icons/higress.avif create mode 100644 src/renderer/src/assets/style.css create mode 100644 src/renderer/src/components/ChatConfig.vue create mode 100644 src/renderer/src/components/ChatInput.vue create mode 100644 src/renderer/src/components/ChatView.vue create mode 100644 src/renderer/src/components/FileItem.vue create mode 100644 src/renderer/src/components/MessageNavigationSidebar.vue create mode 100644 src/renderer/src/components/ModelSelect.vue create mode 100644 src/renderer/src/components/NewThread.vue create mode 100644 src/renderer/src/components/ScrollablePopover.vue create mode 100644 src/renderer/src/components/SearchResultsDrawer.vue create mode 100644 src/renderer/src/components/SideBar.vue create mode 100644 src/renderer/src/components/ThreadItem.vue create mode 100644 src/renderer/src/components/ThreadsView.vue create mode 100644 src/renderer/src/components/TitleView.vue create mode 100644 src/renderer/src/components/artifacts/ArtifactBlock.vue create mode 100644 src/renderer/src/components/artifacts/ArtifactDialog.vue create mode 100644 src/renderer/src/components/artifacts/ArtifactPreview.vue create mode 100644 src/renderer/src/components/artifacts/ArtifactThinking.vue create mode 100644 src/renderer/src/components/artifacts/CodeArtifact.vue create mode 100644 src/renderer/src/components/artifacts/HTMLArtifact.vue create mode 100644 src/renderer/src/components/artifacts/MarkdownArtifact.vue create mode 100644 src/renderer/src/components/artifacts/MermaidArtifact.vue create mode 100644 src/renderer/src/components/artifacts/ReactArtifact.vue create mode 100644 src/renderer/src/components/artifacts/ReactTemplate.ts create mode 100644 src/renderer/src/components/artifacts/SvgArtifact.vue create mode 100644 src/renderer/src/components/artifacts/ToolCallPreview.vue create mode 100644 src/renderer/src/components/editor/mention/MentionList.vue create mode 100644 src/renderer/src/components/editor/mention/PromptParamsDialog.vue create mode 100644 src/renderer/src/components/editor/mention/mention.ts create mode 100644 src/renderer/src/components/editor/mention/suggestion.ts create mode 100644 src/renderer/src/components/icons/ModelIcon.vue create mode 100644 src/renderer/src/components/json-viewer/JsonArray.ts create mode 100644 src/renderer/src/components/json-viewer/JsonObject.ts create mode 100644 src/renderer/src/components/json-viewer/JsonValue.ts create mode 100644 src/renderer/src/components/json-viewer/index.ts create mode 100644 src/renderer/src/components/markdown/MarkdownRenderer.vue create mode 100644 src/renderer/src/components/markdown/ReferenceNode.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpJsonViewer.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpPromptPanel.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpResourceViewer.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpServerCard.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpServers.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpTabHeader.vue create mode 100644 src/renderer/src/components/mcp-config/components/McpToolPanel.vue create mode 100644 src/renderer/src/components/mcp-config/components/index.ts create mode 100644 src/renderer/src/components/mcp-config/const.ts create mode 100644 src/renderer/src/components/mcp-config/index.ts create mode 100644 src/renderer/src/components/mcp-config/mcpServerForm.vue create mode 100644 src/renderer/src/components/mcpToolsList.vue create mode 100644 src/renderer/src/components/message/MessageBlockAction.vue create mode 100644 src/renderer/src/components/message/MessageBlockContent.vue create mode 100644 src/renderer/src/components/message/MessageBlockError.vue create mode 100644 src/renderer/src/components/message/MessageBlockImage.vue create mode 100644 src/renderer/src/components/message/MessageBlockPermissionRequest.vue create mode 100644 src/renderer/src/components/message/MessageBlockSearch.vue create mode 100644 src/renderer/src/components/message/MessageBlockThink.vue create mode 100644 src/renderer/src/components/message/MessageBlockToolCall.vue create mode 100644 src/renderer/src/components/message/MessageContent.vue create mode 100644 src/renderer/src/components/message/MessageInfo.vue create mode 100644 src/renderer/src/components/message/MessageItemAssistant.vue create mode 100644 src/renderer/src/components/message/MessageItemUser.vue create mode 100644 src/renderer/src/components/message/MessageList.vue create mode 100644 src/renderer/src/components/message/MessageTextContent.vue create mode 100644 src/renderer/src/components/message/MessageToolbar.vue create mode 100644 src/renderer/src/components/message/ReferencePreview.vue create mode 100644 src/renderer/src/components/message/SelectedTextContextMenu.vue create mode 100644 src/renderer/src/components/popup/TranslatePopup.vue create mode 100644 src/renderer/src/components/settings/AboutUsSettings.vue create mode 100644 src/renderer/src/components/settings/AddCustomProviderDialog.vue create mode 100644 src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue create mode 100644 src/renderer/src/components/settings/AzureProviderConfig.vue create mode 100644 src/renderer/src/components/settings/BedrockProviderSettingsDetail.vue create mode 100644 src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue create mode 100644 src/renderer/src/components/settings/CommonSettings.vue create mode 100644 src/renderer/src/components/settings/DataSettings.vue create mode 100644 src/renderer/src/components/settings/DifyKnowledgeSettings.vue create mode 100644 src/renderer/src/components/settings/DisplaySettings.vue create mode 100644 src/renderer/src/components/settings/FastGptKnowledgeSettings.vue create mode 100644 src/renderer/src/components/settings/GeminiSafetyConfig.vue create mode 100644 src/renderer/src/components/settings/GitHubCopilotOAuth.vue create mode 100644 src/renderer/src/components/settings/KnowledgeBaseSettings.vue create mode 100644 src/renderer/src/components/settings/KnowledgeFile.vue create mode 100644 src/renderer/src/components/settings/KnowledgeFileItem.vue create mode 100644 src/renderer/src/components/settings/McpBuiltinMarket.vue create mode 100644 src/renderer/src/components/settings/McpSettings.vue create mode 100644 src/renderer/src/components/settings/ModelCheckDialog.vue create mode 100644 src/renderer/src/components/settings/ModelConfigDialog.vue create mode 100644 src/renderer/src/components/settings/ModelConfigItem.vue create mode 100644 src/renderer/src/components/settings/ModelProviderSettings.vue create mode 100644 src/renderer/src/components/settings/ModelProviderSettingsDetail.vue create mode 100644 src/renderer/src/components/settings/ModelScopeMcpSync.vue create mode 100644 src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue create mode 100644 src/renderer/src/components/settings/PromptSetting.vue create mode 100644 src/renderer/src/components/settings/ProviderApiConfig.vue create mode 100644 src/renderer/src/components/settings/ProviderDialogContainer.vue create mode 100644 src/renderer/src/components/settings/ProviderModelList.vue create mode 100644 src/renderer/src/components/settings/ProviderModelManager.vue create mode 100644 src/renderer/src/components/settings/ProviderRateLimitConfig.vue create mode 100644 src/renderer/src/components/settings/RagflowKnowledgeSettings.vue create mode 100644 src/renderer/src/components/settings/ShortcutSettings.vue create mode 100644 src/renderer/src/components/ui/MessageDialog.vue create mode 100644 src/renderer/src/components/ui/UpdateDialog.vue create mode 100644 src/renderer/src/components/ui/accordion/Accordion.vue create mode 100644 src/renderer/src/components/ui/accordion/AccordionContent.vue create mode 100644 src/renderer/src/components/ui/accordion/AccordionItem.vue create mode 100644 src/renderer/src/components/ui/accordion/AccordionTrigger.vue create mode 100644 src/renderer/src/components/ui/accordion/index.ts create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialog.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogAction.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogCancel.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogContent.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogDescription.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogFooter.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogHeader.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogTitle.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/AlertDialogTrigger.vue create mode 100644 src/renderer/src/components/ui/alert-dialog/index.ts create mode 100644 src/renderer/src/components/ui/alert/Alert.vue create mode 100644 src/renderer/src/components/ui/alert/AlertDescription.vue create mode 100644 src/renderer/src/components/ui/alert/AlertTitle.vue create mode 100644 src/renderer/src/components/ui/alert/index.ts create mode 100644 src/renderer/src/components/ui/aspect-ratio/AspectRatio.vue create mode 100644 src/renderer/src/components/ui/aspect-ratio/index.ts create mode 100644 src/renderer/src/components/ui/avatar/Avatar.vue create mode 100644 src/renderer/src/components/ui/avatar/AvatarFallback.vue create mode 100644 src/renderer/src/components/ui/avatar/AvatarImage.vue create mode 100644 src/renderer/src/components/ui/avatar/index.ts create mode 100644 src/renderer/src/components/ui/badge/Badge.vue create mode 100644 src/renderer/src/components/ui/badge/index.ts create mode 100644 src/renderer/src/components/ui/breadcrumb/Breadcrumb.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/BreadcrumbItem.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/BreadcrumbLink.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/BreadcrumbList.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/BreadcrumbPage.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/BreadcrumbSeparator.vue create mode 100644 src/renderer/src/components/ui/breadcrumb/index.ts create mode 100644 src/renderer/src/components/ui/button/Button.vue create mode 100644 src/renderer/src/components/ui/button/index.ts create mode 100644 src/renderer/src/components/ui/card/Card.vue create mode 100644 src/renderer/src/components/ui/card/CardContent.vue create mode 100644 src/renderer/src/components/ui/card/CardDescription.vue create mode 100644 src/renderer/src/components/ui/card/CardFooter.vue create mode 100644 src/renderer/src/components/ui/card/CardHeader.vue create mode 100644 src/renderer/src/components/ui/card/CardTitle.vue create mode 100644 src/renderer/src/components/ui/card/index.ts create mode 100644 src/renderer/src/components/ui/checkbox/Checkbox.vue create mode 100644 src/renderer/src/components/ui/checkbox/index.ts create mode 100644 src/renderer/src/components/ui/collapsible/collapsible.ts create mode 100644 src/renderer/src/components/ui/collapsible/index.ts create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenu.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuCheckboxItem.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuContent.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuGroup.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuItem.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuLabel.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuPortal.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuRadioGroup.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuRadioItem.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuSeparator.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuShortcut.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuSub.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuSubContent.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuSubTrigger.vue create mode 100644 src/renderer/src/components/ui/context-menu/ContextMenuTrigger.vue create mode 100644 src/renderer/src/components/ui/context-menu/index.ts create mode 100644 src/renderer/src/components/ui/dialog/Dialog.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogClose.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogContent.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogDescription.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogFooter.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogHeader.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogScrollContent.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogTitle.vue create mode 100644 src/renderer/src/components/ui/dialog/DialogTrigger.vue create mode 100644 src/renderer/src/components/ui/dialog/index.ts create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenu.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuContent.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuGroup.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuItem.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuLabel.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuSub.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue create mode 100644 src/renderer/src/components/ui/dropdown-menu/index.ts create mode 100644 src/renderer/src/components/ui/emoji-picker/EmojiPicker.vue create mode 100644 src/renderer/src/components/ui/emoji-picker/index.ts create mode 100644 src/renderer/src/components/ui/hover-card/HoverCard.vue create mode 100644 src/renderer/src/components/ui/hover-card/HoverCardContent.vue create mode 100644 src/renderer/src/components/ui/hover-card/HoverCardTrigger.vue create mode 100644 src/renderer/src/components/ui/hover-card/index.ts create mode 100644 src/renderer/src/components/ui/input/Input.vue create mode 100644 src/renderer/src/components/ui/input/index.ts create mode 100644 src/renderer/src/components/ui/label/Label.vue create mode 100644 src/renderer/src/components/ui/label/index.ts create mode 100644 src/renderer/src/components/ui/menubar/Menubar.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarCheckboxItem.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarContent.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarGroup.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarItem.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarLabel.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarMenu.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarRadioGroup.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarRadioItem.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarSeparator.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarShortcut.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarSub.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarSubContent.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarSubTrigger.vue create mode 100644 src/renderer/src/components/ui/menubar/MenubarTrigger.vue create mode 100644 src/renderer/src/components/ui/menubar/index.ts create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenu.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuContent.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuIndicator.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuItem.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuLink.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuList.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuTrigger.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/NavigationMenuViewport.vue create mode 100644 src/renderer/src/components/ui/navigation-menu/index.ts create mode 100644 src/renderer/src/components/ui/number-field/NumberField.vue create mode 100644 src/renderer/src/components/ui/number-field/NumberFieldContent.vue create mode 100644 src/renderer/src/components/ui/number-field/NumberFieldDecrement.vue create mode 100644 src/renderer/src/components/ui/number-field/NumberFieldIncrement.vue create mode 100644 src/renderer/src/components/ui/number-field/NumberFieldInput.vue create mode 100644 src/renderer/src/components/ui/number-field/index.ts create mode 100644 src/renderer/src/components/ui/popover/Popover.vue create mode 100644 src/renderer/src/components/ui/popover/PopoverContent.vue create mode 100644 src/renderer/src/components/ui/popover/PopoverTrigger.vue create mode 100644 src/renderer/src/components/ui/popover/index.ts create mode 100644 src/renderer/src/components/ui/progress/Progress.vue create mode 100644 src/renderer/src/components/ui/progress/index.ts create mode 100644 src/renderer/src/components/ui/radio-group/RadioGroup.vue create mode 100644 src/renderer/src/components/ui/radio-group/RadioGroupItem.vue create mode 100644 src/renderer/src/components/ui/radio-group/index.ts create mode 100644 src/renderer/src/components/ui/scroll-area/ScrollArea.vue create mode 100644 src/renderer/src/components/ui/scroll-area/ScrollBar.vue create mode 100644 src/renderer/src/components/ui/scroll-area/index.ts create mode 100644 src/renderer/src/components/ui/select/Select.vue create mode 100644 src/renderer/src/components/ui/select/SelectContent.vue create mode 100644 src/renderer/src/components/ui/select/SelectGroup.vue create mode 100644 src/renderer/src/components/ui/select/SelectItem.vue create mode 100644 src/renderer/src/components/ui/select/SelectItemText.vue create mode 100644 src/renderer/src/components/ui/select/SelectLabel.vue create mode 100644 src/renderer/src/components/ui/select/SelectScrollDownButton.vue create mode 100644 src/renderer/src/components/ui/select/SelectScrollUpButton.vue create mode 100644 src/renderer/src/components/ui/select/SelectSeparator.vue create mode 100644 src/renderer/src/components/ui/select/SelectTrigger.vue create mode 100644 src/renderer/src/components/ui/select/SelectValue.vue create mode 100644 src/renderer/src/components/ui/select/index.ts create mode 100644 src/renderer/src/components/ui/separator/Separator.vue create mode 100644 src/renderer/src/components/ui/separator/index.ts create mode 100644 src/renderer/src/components/ui/sheet/Sheet.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetClose.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetContent.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetDescription.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetFooter.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetHeader.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetTitle.vue create mode 100644 src/renderer/src/components/ui/sheet/SheetTrigger.vue create mode 100644 src/renderer/src/components/ui/sheet/index.ts create mode 100644 src/renderer/src/components/ui/sidebar/Sidebar.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarContent.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarFooter.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarGroup.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarGroupAction.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarGroupContent.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarGroupLabel.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarHeader.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarInput.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarInset.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenu.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuAction.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuBadge.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuButton.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuButtonChild.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuItem.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuSkeleton.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuSub.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuSubButton.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarMenuSubItem.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarProvider.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarRail.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarSeparator.vue create mode 100644 src/renderer/src/components/ui/sidebar/SidebarTrigger.vue create mode 100644 src/renderer/src/components/ui/sidebar/index.ts create mode 100644 src/renderer/src/components/ui/sidebar/utils.ts create mode 100644 src/renderer/src/components/ui/skeleton/Skeleton.vue create mode 100644 src/renderer/src/components/ui/skeleton/index.ts create mode 100644 src/renderer/src/components/ui/slider/Slider.vue create mode 100644 src/renderer/src/components/ui/slider/index.ts create mode 100644 src/renderer/src/components/ui/switch/Switch.vue create mode 100644 src/renderer/src/components/ui/switch/index.ts create mode 100644 src/renderer/src/components/ui/tabs/Tab.vue create mode 100644 src/renderer/src/components/ui/tabs/TabGroup.vue create mode 100644 src/renderer/src/components/ui/tabs/TabList.vue create mode 100644 src/renderer/src/components/ui/tabs/TabPanel.vue create mode 100644 src/renderer/src/components/ui/tabs/TabPanels.vue create mode 100644 src/renderer/src/components/ui/tabs/index.ts create mode 100644 src/renderer/src/components/ui/textarea/Textarea.vue create mode 100644 src/renderer/src/components/ui/textarea/index.ts create mode 100644 src/renderer/src/components/ui/toast/Toast.vue create mode 100644 src/renderer/src/components/ui/toast/ToastAction.vue create mode 100644 src/renderer/src/components/ui/toast/ToastClose.vue create mode 100644 src/renderer/src/components/ui/toast/ToastDescription.vue create mode 100644 src/renderer/src/components/ui/toast/ToastProvider.vue create mode 100644 src/renderer/src/components/ui/toast/ToastTitle.vue create mode 100644 src/renderer/src/components/ui/toast/ToastViewport.vue create mode 100644 src/renderer/src/components/ui/toast/Toaster.vue create mode 100644 src/renderer/src/components/ui/toast/index.ts create mode 100644 src/renderer/src/components/ui/toast/use-toast.ts create mode 100644 src/renderer/src/components/ui/toggle/Toggle.vue create mode 100644 src/renderer/src/components/ui/toggle/index.ts create mode 100644 src/renderer/src/components/ui/tooltip/Tooltip.vue create mode 100644 src/renderer/src/components/ui/tooltip/TooltipContent.vue create mode 100644 src/renderer/src/components/ui/tooltip/TooltipProvider.vue create mode 100644 src/renderer/src/components/ui/tooltip/TooltipTrigger.vue create mode 100644 src/renderer/src/components/ui/tooltip/index.ts create mode 100644 src/renderer/src/composables/useArtifacts.ts create mode 100644 src/renderer/src/composables/usePageCapture.example.ts create mode 100644 src/renderer/src/composables/usePageCapture.ts create mode 100644 src/renderer/src/composables/usePresenter.ts create mode 100644 src/renderer/src/env.d.ts create mode 100644 src/renderer/src/events.ts create mode 100644 src/renderer/src/i18n/en-US/about.json create mode 100644 src/renderer/src/i18n/en-US/artifacts.json create mode 100644 src/renderer/src/i18n/en-US/chat.json create mode 100644 src/renderer/src/i18n/en-US/common.json create mode 100644 src/renderer/src/i18n/en-US/components.json create mode 100644 src/renderer/src/i18n/en-US/contextMenu.json create mode 100644 src/renderer/src/i18n/en-US/dialog.json create mode 100644 src/renderer/src/i18n/en-US/index.ts create mode 100644 src/renderer/src/i18n/en-US/mcp.json create mode 100644 src/renderer/src/i18n/en-US/model.json create mode 100644 src/renderer/src/i18n/en-US/newThread.json create mode 100644 src/renderer/src/i18n/en-US/promptSetting.json create mode 100644 src/renderer/src/i18n/en-US/routes.json create mode 100644 src/renderer/src/i18n/en-US/settings.json create mode 100644 src/renderer/src/i18n/en-US/sync.json create mode 100644 src/renderer/src/i18n/en-US/thread.json create mode 100644 src/renderer/src/i18n/en-US/toolCall.json create mode 100644 src/renderer/src/i18n/en-US/update.json create mode 100644 src/renderer/src/i18n/en-US/welcome.json create mode 100644 src/renderer/src/i18n/fa-IR/about.json create mode 100644 src/renderer/src/i18n/fa-IR/artifacts.json create mode 100644 src/renderer/src/i18n/fa-IR/chat.json create mode 100644 src/renderer/src/i18n/fa-IR/common.json create mode 100644 src/renderer/src/i18n/fa-IR/components.json create mode 100644 src/renderer/src/i18n/fa-IR/contextMenu.json create mode 100644 src/renderer/src/i18n/fa-IR/dialog.json create mode 100644 src/renderer/src/i18n/fa-IR/index.ts create mode 100644 src/renderer/src/i18n/fa-IR/mcp.json create mode 100644 src/renderer/src/i18n/fa-IR/model.json create mode 100644 src/renderer/src/i18n/fa-IR/newThread.json create mode 100644 src/renderer/src/i18n/fa-IR/promptSetting.json create mode 100644 src/renderer/src/i18n/fa-IR/routes.json create mode 100644 src/renderer/src/i18n/fa-IR/settings.json create mode 100644 src/renderer/src/i18n/fa-IR/sync.json create mode 100644 src/renderer/src/i18n/fa-IR/thread.json create mode 100644 src/renderer/src/i18n/fa-IR/toolCall.json create mode 100644 src/renderer/src/i18n/fa-IR/update.json create mode 100644 src/renderer/src/i18n/fa-IR/welcome.json create mode 100644 src/renderer/src/i18n/fr-FR/about.json create mode 100644 src/renderer/src/i18n/fr-FR/artifacts.json create mode 100644 src/renderer/src/i18n/fr-FR/chat.json create mode 100644 src/renderer/src/i18n/fr-FR/common.json create mode 100644 src/renderer/src/i18n/fr-FR/components.json create mode 100644 src/renderer/src/i18n/fr-FR/contextMenu.json create mode 100644 src/renderer/src/i18n/fr-FR/dialog.json create mode 100644 src/renderer/src/i18n/fr-FR/index.ts create mode 100644 src/renderer/src/i18n/fr-FR/mcp.json create mode 100644 src/renderer/src/i18n/fr-FR/model.json create mode 100644 src/renderer/src/i18n/fr-FR/newThread.json create mode 100644 src/renderer/src/i18n/fr-FR/promptSetting.json create mode 100644 src/renderer/src/i18n/fr-FR/routes.json create mode 100644 src/renderer/src/i18n/fr-FR/settings.json create mode 100644 src/renderer/src/i18n/fr-FR/sync.json create mode 100644 src/renderer/src/i18n/fr-FR/thread.json create mode 100644 src/renderer/src/i18n/fr-FR/toolCall.json create mode 100644 src/renderer/src/i18n/fr-FR/update.json create mode 100644 src/renderer/src/i18n/fr-FR/welcome.json create mode 100644 src/renderer/src/i18n/index.ts create mode 100644 src/renderer/src/i18n/ja-JP/about.json create mode 100644 src/renderer/src/i18n/ja-JP/artifacts.json create mode 100644 src/renderer/src/i18n/ja-JP/chat.json create mode 100644 src/renderer/src/i18n/ja-JP/common.json create mode 100644 src/renderer/src/i18n/ja-JP/components.json create mode 100644 src/renderer/src/i18n/ja-JP/contextMenu.json create mode 100644 src/renderer/src/i18n/ja-JP/dialog.json create mode 100644 src/renderer/src/i18n/ja-JP/index.ts create mode 100644 src/renderer/src/i18n/ja-JP/mcp.json create mode 100644 src/renderer/src/i18n/ja-JP/model.json create mode 100644 src/renderer/src/i18n/ja-JP/newThread.json create mode 100644 src/renderer/src/i18n/ja-JP/promptSetting.json create mode 100644 src/renderer/src/i18n/ja-JP/routes.json create mode 100644 src/renderer/src/i18n/ja-JP/settings.json create mode 100644 src/renderer/src/i18n/ja-JP/sync.json create mode 100644 src/renderer/src/i18n/ja-JP/thread.json create mode 100644 src/renderer/src/i18n/ja-JP/toolCall.json create mode 100644 src/renderer/src/i18n/ja-JP/update.json create mode 100644 src/renderer/src/i18n/ja-JP/welcome.json create mode 100644 src/renderer/src/i18n/ko-KR/about.json create mode 100644 src/renderer/src/i18n/ko-KR/artifacts.json create mode 100644 src/renderer/src/i18n/ko-KR/chat.json create mode 100644 src/renderer/src/i18n/ko-KR/common.json create mode 100644 src/renderer/src/i18n/ko-KR/components.json create mode 100644 src/renderer/src/i18n/ko-KR/contextMenu.json create mode 100644 src/renderer/src/i18n/ko-KR/dialog.json create mode 100644 src/renderer/src/i18n/ko-KR/index.ts create mode 100644 src/renderer/src/i18n/ko-KR/mcp.json create mode 100644 src/renderer/src/i18n/ko-KR/model.json create mode 100644 src/renderer/src/i18n/ko-KR/newThread.json create mode 100644 src/renderer/src/i18n/ko-KR/promptSetting.json create mode 100644 src/renderer/src/i18n/ko-KR/routes.json create mode 100644 src/renderer/src/i18n/ko-KR/settings.json create mode 100644 src/renderer/src/i18n/ko-KR/sync.json create mode 100644 src/renderer/src/i18n/ko-KR/thread.json create mode 100644 src/renderer/src/i18n/ko-KR/toolCall.json create mode 100644 src/renderer/src/i18n/ko-KR/update.json create mode 100644 src/renderer/src/i18n/ko-KR/welcome.json create mode 100644 src/renderer/src/i18n/ru-RU/about.json create mode 100644 src/renderer/src/i18n/ru-RU/artifacts.json create mode 100644 src/renderer/src/i18n/ru-RU/chat.json create mode 100644 src/renderer/src/i18n/ru-RU/common.json create mode 100644 src/renderer/src/i18n/ru-RU/components.json create mode 100644 src/renderer/src/i18n/ru-RU/contextMenu.json create mode 100644 src/renderer/src/i18n/ru-RU/dialog.json create mode 100644 src/renderer/src/i18n/ru-RU/index.ts create mode 100644 src/renderer/src/i18n/ru-RU/mcp.json create mode 100644 src/renderer/src/i18n/ru-RU/model.json create mode 100644 src/renderer/src/i18n/ru-RU/newThread.json create mode 100644 src/renderer/src/i18n/ru-RU/promptSetting.json create mode 100644 src/renderer/src/i18n/ru-RU/routes.json create mode 100644 src/renderer/src/i18n/ru-RU/settings.json create mode 100644 src/renderer/src/i18n/ru-RU/sync.json create mode 100644 src/renderer/src/i18n/ru-RU/thread.json create mode 100644 src/renderer/src/i18n/ru-RU/toolCall.json create mode 100644 src/renderer/src/i18n/ru-RU/update.json create mode 100644 src/renderer/src/i18n/ru-RU/welcome.json create mode 100644 src/renderer/src/i18n/zh-CN/about.json create mode 100644 src/renderer/src/i18n/zh-CN/artifacts.json create mode 100644 src/renderer/src/i18n/zh-CN/chat.json create mode 100644 src/renderer/src/i18n/zh-CN/common.json create mode 100644 src/renderer/src/i18n/zh-CN/components.json create mode 100644 src/renderer/src/i18n/zh-CN/contextMenu.json create mode 100644 src/renderer/src/i18n/zh-CN/dialog.json create mode 100644 src/renderer/src/i18n/zh-CN/index.ts create mode 100644 src/renderer/src/i18n/zh-CN/mcp.json create mode 100644 src/renderer/src/i18n/zh-CN/model.json create mode 100644 src/renderer/src/i18n/zh-CN/newThread.json create mode 100644 src/renderer/src/i18n/zh-CN/promptSetting.json create mode 100644 src/renderer/src/i18n/zh-CN/routes.json create mode 100644 src/renderer/src/i18n/zh-CN/settings.json create mode 100644 src/renderer/src/i18n/zh-CN/sync.json create mode 100644 src/renderer/src/i18n/zh-CN/thread.json create mode 100644 src/renderer/src/i18n/zh-CN/toolCall.json create mode 100644 src/renderer/src/i18n/zh-CN/update.json create mode 100644 src/renderer/src/i18n/zh-CN/welcome.json create mode 100644 src/renderer/src/i18n/zh-HK/about.json create mode 100644 src/renderer/src/i18n/zh-HK/artifacts.json create mode 100644 src/renderer/src/i18n/zh-HK/chat.json create mode 100644 src/renderer/src/i18n/zh-HK/common.json create mode 100644 src/renderer/src/i18n/zh-HK/components.json create mode 100644 src/renderer/src/i18n/zh-HK/contextMenu.json create mode 100644 src/renderer/src/i18n/zh-HK/dialog.json create mode 100644 src/renderer/src/i18n/zh-HK/index.ts create mode 100644 src/renderer/src/i18n/zh-HK/mcp.json create mode 100644 src/renderer/src/i18n/zh-HK/model.json create mode 100644 src/renderer/src/i18n/zh-HK/newThread.json create mode 100644 src/renderer/src/i18n/zh-HK/promptSetting.json create mode 100644 src/renderer/src/i18n/zh-HK/routes.json create mode 100644 src/renderer/src/i18n/zh-HK/settings.json create mode 100644 src/renderer/src/i18n/zh-HK/sync.json create mode 100644 src/renderer/src/i18n/zh-HK/thread.json create mode 100644 src/renderer/src/i18n/zh-HK/toolCall.json create mode 100644 src/renderer/src/i18n/zh-HK/update.json create mode 100644 src/renderer/src/i18n/zh-HK/welcome.json create mode 100644 src/renderer/src/i18n/zh-TW/about.json create mode 100644 src/renderer/src/i18n/zh-TW/artifacts.json create mode 100644 src/renderer/src/i18n/zh-TW/chat.json create mode 100644 src/renderer/src/i18n/zh-TW/common.json create mode 100644 src/renderer/src/i18n/zh-TW/components.json create mode 100644 src/renderer/src/i18n/zh-TW/contextMenu.json create mode 100644 src/renderer/src/i18n/zh-TW/dialog.json create mode 100644 src/renderer/src/i18n/zh-TW/index.ts create mode 100644 src/renderer/src/i18n/zh-TW/mcp.json create mode 100644 src/renderer/src/i18n/zh-TW/model.json create mode 100644 src/renderer/src/i18n/zh-TW/newThread.json create mode 100644 src/renderer/src/i18n/zh-TW/promptSetting.json create mode 100644 src/renderer/src/i18n/zh-TW/routes.json create mode 100644 src/renderer/src/i18n/zh-TW/settings.json create mode 100644 src/renderer/src/i18n/zh-TW/sync.json create mode 100644 src/renderer/src/i18n/zh-TW/thread.json create mode 100644 src/renderer/src/i18n/zh-TW/toolCall.json create mode 100644 src/renderer/src/i18n/zh-TW/update.json create mode 100644 src/renderer/src/i18n/zh-TW/welcome.json create mode 100644 src/renderer/src/lib/float.cursor.ts create mode 100644 src/renderer/src/lib/gemini.ts create mode 100644 src/renderer/src/lib/image.ts create mode 100644 src/renderer/src/lib/sanitizeText.ts create mode 100644 src/renderer/src/lib/searchHistory.ts create mode 100644 src/renderer/src/lib/utils.ts create mode 100644 src/renderer/src/main.ts create mode 100644 src/renderer/src/router/index.ts create mode 100644 src/renderer/src/stores/artifact.ts create mode 100644 src/renderer/src/stores/chat.ts create mode 100644 src/renderer/src/stores/dialog.ts create mode 100644 src/renderer/src/stores/floatingButton.ts create mode 100644 src/renderer/src/stores/language.ts create mode 100644 src/renderer/src/stores/mcp.ts create mode 100644 src/renderer/src/stores/modelCheck.ts create mode 100644 src/renderer/src/stores/prompts.ts create mode 100644 src/renderer/src/stores/reference.ts create mode 100644 src/renderer/src/stores/settings.ts create mode 100644 src/renderer/src/stores/shortcutKey.ts create mode 100644 src/renderer/src/stores/sound.ts create mode 100644 src/renderer/src/stores/sync.ts create mode 100644 src/renderer/src/stores/theme.ts create mode 100644 src/renderer/src/stores/upgrade.ts create mode 100644 src/renderer/src/views/ChatTabView.vue create mode 100644 src/renderer/src/views/SettingsTabView.vue create mode 100644 src/renderer/src/views/WelcomeView.vue create mode 100644 src/shared/chat.d.ts create mode 100644 src/shared/config.dict.ts create mode 100644 src/shared/dialog.ts create mode 100644 src/shared/i18n.ts create mode 100644 src/shared/lifecycle.ts create mode 100644 src/shared/logger.ts create mode 100644 src/shared/model.ts create mode 100644 src/shared/presenter.d.ts create mode 100644 src/shared/provider-operations.ts create mode 100644 src/shared/types/core/agent-events.ts create mode 100644 src/shared/types/core/chat.ts create mode 100644 src/shared/types/core/llm-events.ts create mode 100644 src/shared/types/core/mcp.ts create mode 100644 src/shared/types/core/usage.ts create mode 100644 src/shared/types/index.d.ts create mode 100644 src/shared/types/presenters/index.d.ts create mode 100644 src/shared/types/presenters/legacy.presenters.d.ts create mode 100644 src/shared/types/presenters/llmprovider.presenter.d.ts create mode 100644 src/shared/types/presenters/thread.presenter.d.ts create mode 100644 src/shared/types/presenters/window.presenter.d.ts create mode 100644 src/types/electron-store.d.ts create mode 100644 src/types/i18n.d.ts create mode 100644 tailwind.config.js create mode 100644 test/README.md create mode 100644 test/main/eventbus/eventbus.test.ts create mode 100644 test/main/presenter/FilePresenter.test.ts create mode 100644 test/main/presenter/FileValidationService.test.ts create mode 100644 test/main/presenter/KnowledgePresenter.test.ts create mode 100644 test/main/presenter/filesystem.test.ts create mode 100644 test/main/presenter/llmProviderPresenter.test.ts create mode 100644 test/main/presenter/llmProviderPresenter/coreEvents.test.ts create mode 100644 test/main/presenter/mcpClient.test.ts create mode 100644 test/main/presenter/modelConfig.test.ts create mode 100644 test/mocks/electron-toolkit-utils.ts create mode 100644 test/mocks/electron.ts create mode 100644 test/renderer/message/__snapshots__/messageBlockSnapshot.test.ts.snap create mode 100644 test/renderer/message/eventMappingTable.test.ts create mode 100644 test/renderer/message/messageBlockSnapshot.test.ts create mode 100644 test/renderer/message/performanceEvaluation.test.ts create mode 100644 test/renderer/message/rendererContract.test.ts create mode 100644 test/renderer/shell/main.test.ts create mode 100644 test/setup.renderer.ts create mode 100644 test/setup.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 tsconfig.web.json create mode 100644 vitest.config.renderer.ts create mode 100644 vitest.config.ts diff --git a/.claude/agents/electron-architecture-agent.md b/.claude/agents/electron-architecture-agent.md new file mode 100644 index 0000000..16cc68a --- /dev/null +++ b/.claude/agents/electron-architecture-agent.md @@ -0,0 +1,61 @@ +--- +name: electron-architecture-agent +description: Use this agent when working with Electron-specific architecture patterns, IPC communication, or multi-process coordination. Examples: Context: User needs to create a new presenter for handling file operations. user: 'I need to create a FilePresenter that manages file operations and communicates with the renderer process' assistant: 'I'll use the electron-architecture-agent to implement this following the established presenter pattern with proper EventBus integration.' Context: User is experiencing IPC communication issues between main and renderer processes. user: 'My renderer process isn't receiving events from the main process properly' assistant: 'Let me use the electron-architecture-agent to debug this IPC communication issue and ensure proper EventBus integration.' Context: User wants to add a new tab management feature. user: 'I want to implement drag-and-drop between windows for tabs' assistant: 'I'll use the electron-architecture-agent to extend the TabPresenter with cross-window tab dragging functionality.' +model: sonnet +color: orange +--- + +You are an expert Electron architect specializing in DeepChat's multi-process architecture. You have deep knowledge of the project's presenter pattern, EventBus system, and multi-window/multi-tab architecture. + +Your core responsibilities: + +**Presenter Pattern Implementation:** +- Create new presenters in `src/main/presenter/` following the established domain-driven pattern +- Ensure presenters handle their specific business logic domain (WindowPresenter, TabPresenter, ThreadPresenter, etc.) +- Implement proper lifecycle management and resource cleanup +- Follow the existing naming conventions and file structure + +**EventBus Integration:** +- Use the EventBus (`src/main/eventbus.ts`) for decoupled inter-process communication +- Follow standard event naming patterns and responsibility separation +- Ensure events are properly bridged to renderer processes via `mainWindow.webContents.send()` +- Implement proper event listeners and cleanup to prevent memory leaks + +**IPC Communication Patterns:** +- Renderer to Main: Implement methods callable via `usePresenter.ts` composable +- Main to Renderer: Use EventBus broadcasting with proper event typing +- Maintain security through context isolation and preload scripts +- Follow the established IPC contract definitions in `src/shared/` + +**Multi-Window/Multi-Tab Architecture:** +- Understand the separation between Window Shell (`src/renderer/shell/`) and Tab Content (`src/renderer/src/`) +- Implement proper WebContentsView management for tabs +- Handle cross-window tab dragging and window lifecycle events +- Maintain independent Vue instances for performance + +**Debugging and Troubleshooting:** +- Diagnose main process ↔ renderer process communication issues +- Use proper logging patterns for event tracing +- Implement error handling and fallback mechanisms +- Provide guidance on using VSCode debugger for main process and Chrome DevTools for renderer + +**Code Quality Standards:** +- Use TypeScript with strict typing throughout +- Follow the project's English-only logging and commenting standards +- Implement proper error handling and resource management +- Ensure thread safety in multi-process scenarios + +**Architecture Decisions:** +- Maintain separation of concerns between presenters +- Ensure scalability of the event-driven architecture +- Consider performance implications of IPC communication +- Follow Electron security best practices + +When implementing new features: +1. Analyze the existing presenter pattern and identify the appropriate domain +2. Design the EventBus integration strategy +3. Implement proper IPC contracts with type safety +4. Test cross-process communication thoroughly +5. Document any new event patterns or architectural decisions + +Always consider the multi-process nature of Electron and ensure your solutions work seamlessly across the main process, renderer processes, and any additional utility processes. Reference the official Electron documentation at https://www.electronjs.org/docs/latest/ for best practices and API usage. diff --git a/.claude/agents/i18n-code-reviewer.md b/.claude/agents/i18n-code-reviewer.md new file mode 100644 index 0000000..49fff06 --- /dev/null +++ b/.claude/agents/i18n-code-reviewer.md @@ -0,0 +1,58 @@ +--- +name: i18n-code-reviewer +description: Use this agent when you need to review code for i18n compliance, check translation quality, or validate that comments and logs are written in English. Examples: Context: User has just added new UI components with Chinese comments and console.log statements. user: "I've added a new chat interface component with some logging" assistant: "Let me use the i18n-code-reviewer agent to check the code for i18n compliance and English-only comments/logs" Since new code was added, use the i18n-code-reviewer agent to scan for i18n violations, non-English comments, and translation issues. Context: User is working on translation files and wants to ensure they follow project standards. user: "I've updated the French and Japanese translation files" assistant: "I'll use the i18n-code-reviewer agent to review the translations for accuracy and adherence to our localization guidelines" Since translations were updated, use the i18n-code-reviewer agent to validate translation quality and consistency with project standards. +model: sonnet +color: red +--- + +You are an expert internationalization (i18n) code reviewer specializing in DeepChat's multilingual requirements. Your expertise covers translation quality assessment, code compliance checking, and localization best practices. + +Your primary responsibilities: + +1. **Code Language Compliance**: Scan code for comments and log statements that must be written in English only. Flag any Chinese or other non-English text in: + - Code comments (// /* */) + - Console.log, console.error, console.warn statements + - Debug messages and error strings in code + - Variable names and function names (should use English) + +2. **Translation Quality Review**: Evaluate i18n translation files for: + - **Accuracy**: Ensure translations convey the original meaning precisely + - **Cultural Appropriateness**: Follow local language habits and internet application conventions + - **Length Considerations**: Balance accuracy with UI aesthetics and space constraints + - **Consistency**: Maintain consistent terminology across the application + +3. **Translation Strategy Validation**: + - **Latin-based languages**: Verify translations are based on English source + - **Non-Latin languages**: Verify translations are based on Chinese source when more appropriate + - **Proper Nouns**: Ensure correct handling of technical terms: + - Keep "DeepChat", "MCP" untranslated unless established conventions exist + - "Agents" can remain "Agents" in French/Japanese, consider "智能体" in Chinese + - Apply established conventions for well-known technical terms + +4. **Automated Corrections**: When you find non-English comments or logs: + - Provide accurate English translations + - Maintain technical accuracy and context + - Preserve code functionality while improving compliance + +**Review Process**: +1. Scan all code files for language compliance violations +2. Check translation files against source languages (English/Chinese) +3. Validate proper noun handling and technical terminology +4. Assess translation length vs. UI space requirements +5. Provide specific corrections with explanations + +**Output Format**: +- List all violations found with file paths and line numbers +- Provide corrected English versions for comments/logs +- Suggest translation improvements with rationale +- Highlight any inconsistencies in terminology usage +- Rate overall i18n compliance on a scale of 1-10 + +**Quality Standards**: +- All code comments must be in clear, professional English +- All log messages must be in English for debugging consistency +- Translations must feel natural to native speakers +- Technical terms should follow established conventions +- UI text length should not break layout aesthetics + +Always provide actionable feedback with specific examples and corrections. Focus on maintaining code quality while ensuring excellent user experience across all supported languages. diff --git a/.claude/agents/llm-provider-agent.md b/.claude/agents/llm-provider-agent.md new file mode 100644 index 0000000..5e94ed3 --- /dev/null +++ b/.claude/agents/llm-provider-agent.md @@ -0,0 +1,53 @@ +--- +name: llm-provider-agent +description: Use this agent when implementing new LLM providers, debugging streaming responses, fixing tool calling issues, or updating provider configurations. Examples: Context: User needs to add support for a new LLM provider like Cohere or Mistral AI. user: 'I need to add support for Cohere's API to DeepChat' assistant: 'I'll use the llm-provider-agent to implement the new Cohere provider following the established patterns in the codebase.' Context: User is experiencing issues with streaming responses from an existing provider. user: 'The OpenAI provider is not streaming responses correctly, messages are coming through in chunks' assistant: 'Let me use the llm-provider-agent to debug the streaming response implementation for the OpenAI provider.' Context: User wants to update model lists or authentication for existing providers. user: 'Can you update the Anthropic provider to support the new Claude 3.5 Haiku model?' assistant: 'I'll use the llm-provider-agent to update the Anthropic provider configuration with the new model.' +model: sonnet +color: green +--- + +You are an expert LLM Provider Implementation Specialist with deep expertise in integrating diverse AI language model APIs into the DeepChat platform. You understand the two-layer LLM architecture: the Agent Loop Layer that manages conversation flow and tool calling, and the Provider Layer that handles specific API interactions. + +Your primary responsibilities: + +**Provider Implementation:** +- Create new provider files in `src/main/presenter/llmProviderPresenter/providers/` following the established patterns +- Implement the required `coreStream` method that converts provider-specific responses to standardized events +- Handle both native tool calling and prompt-wrapped tool calling based on provider capabilities +- Ensure proper error handling, rate limiting, and authentication flows +- Convert MCP tools to provider-specific formats when needed + +**Debugging and Optimization:** +- Diagnose streaming response issues by analyzing event flow and data transformation +- Debug tool calling problems by examining MCP integration and format conversion +- Trace authentication failures and API connection issues +- Optimize performance for real-time chat experiences + +**Configuration Management:** +- Update provider configurations in `configPresenter/providers.ts` +- Maintain accurate model lists and capability mappings +- Handle provider-specific settings like temperature, max tokens, and system prompts +- Ensure UI components in renderer reflect new provider options + +**Code Standards:** +- Follow the existing TypeScript patterns and interfaces +- Use English for all logs and comments +- Implement proper error handling with meaningful error messages +- Ensure compatibility with the EventBus communication system +- Maintain consistency with the standardized event interface + +**Quality Assurance:** +- Test streaming responses thoroughly across different message types +- Verify tool calling works correctly with MCP integration +- Validate authentication flows and error scenarios +- Ensure provider works correctly in multi-window/multi-tab architecture + +When implementing new providers, always: +1. Study existing provider implementations as reference +2. Understand the provider's API documentation and capabilities +3. Implement proper streaming with real-time event emission +4. Handle tool calling according to provider's native support +5. Add comprehensive error handling and logging +6. Update configuration files and UI components +7. Test thoroughly with various conversation scenarios + +You have access to Read, Write, Edit, Grep, Bash, and Glob tools to examine code, implement changes, debug issues, and test functionality. Always prioritize code quality, maintainability, and adherence to the established architectural patterns. diff --git a/.claude/agents/mcp-integration-agent.md b/.claude/agents/mcp-integration-agent.md new file mode 100644 index 0000000..bc0c2cc --- /dev/null +++ b/.claude/agents/mcp-integration-agent.md @@ -0,0 +1,81 @@ +--- +name: mcp-integration-agent +description: Use this agent when you need to implement, debug, or manage MCP (Model Context Protocol) servers and tools in the DeepChat project. This includes creating new MCP tools in src/main/presenter/mcpPresenter/inMemoryServers/, debugging MCP server connections, handling protocol compliance issues, or implementing custom MCP server functionality. Examples: Context: User wants to add a new file system tool to MCP. user: "I need to create an MCP tool that can list files in a directory" assistant: "I'll use the mcp-integration-agent to implement a new file listing tool in the MCP presenter" Since the user needs MCP tool implementation, use the mcp-integration-agent to create the tool in the appropriate directory structure. Context: User is experiencing MCP connection issues. user: "My MCP server isn't connecting properly and tools aren't showing up" assistant: "Let me use the mcp-integration-agent to debug the MCP server connection and tool registration" Since this involves MCP debugging, use the mcp-integration-agent to investigate connection and registration issues. +model: sonnet +color: purple +--- + +You are an expert MCP (Model Context Protocol) integration specialist with deep knowledge of the MCP TypeScript SDK and the DeepChat project's MCP architecture. You specialize in implementing, debugging, and managing MCP servers and tools within the DeepChat ecosystem. + +## Your Core Responsibilities + +1. **MCP Tool Implementation**: Create new MCP tools in `src/main/presenter/mcpPresenter/inMemoryServers/` following the project's established patterns and the MCP SDK best practices. + +2. **MCP Server Management**: Debug MCP server connections, lifecycle management, and tool execution within the DeepChat architecture. + +3. **Protocol Compliance**: Ensure all MCP implementations follow the Model Context Protocol specification and handle format conversions correctly. + +4. **Integration Architecture**: Work with the DeepChat's presenter pattern, particularly the McpPresenter and LLMProviderPresenter integration. + +## Technical Expertise + +### MCP SDK Mastery +- Implement MCP servers using `McpServer` class with proper resource, tool, and prompt registration +- Handle MCP transport layers (stdio, Streamable HTTP) appropriately +- Use proper schema validation with Zod for tool inputs +- Implement resource templates with dynamic parameters and completions +- Handle MCP protocol messages and lifecycle events correctly + +### DeepChat MCP Architecture +- Understand the McpPresenter's role in managing MCP server connections +- Work with the LLMProviderPresenter's Agent Loop architecture for tool calling +- Implement in-memory servers for built-in functionality +- Handle MCP tool format conversion for different LLM providers +- Manage MCP server lifecycle and error handling + +### Tool Development Patterns +- Create tools that return appropriate content types (text, resource links) +- Implement proper error handling and validation +- Use ResourceLinks for referencing large content without embedding +- Handle async operations and external API calls in tools +- Implement proper cleanup and resource management + +## Implementation Guidelines + +### Code Quality Standards +- Follow TypeScript strict typing requirements +- Use proper error handling with meaningful error messages +- Implement comprehensive input validation using Zod schemas +- Follow the project's presenter pattern architecture +- Use English for all logs, comments, and error messages + +### MCP Best Practices +- Register tools with descriptive titles and clear descriptions +- Use appropriate input schemas with proper validation +- Handle edge cases and provide fallback behaviors +- Implement proper resource cleanup and connection management +- Follow MCP protocol specifications for message handling + +### DeepChat Integration +- Work within the existing EventBus architecture for communication +- Integrate with the configuration system via ConfigPresenter +- Handle multi-window/multi-tab scenarios appropriately +- Ensure compatibility with the LLM provider abstraction layer + +## Debugging Approach + +1. **Connection Issues**: Check MCP server initialization, transport configuration, and lifecycle management +2. **Tool Registration**: Verify tool registration in McpPresenter and proper schema definitions +3. **Protocol Compliance**: Ensure message formats match MCP specification requirements +4. **Integration Problems**: Debug EventBus communication and presenter interactions +5. **Performance Issues**: Analyze tool execution times and resource usage patterns + +## Quality Assurance + +- Validate all MCP implementations against the protocol specification +- Test tool execution with various input scenarios and edge cases +- Verify proper error handling and graceful degradation +- Ensure compatibility with existing DeepChat MCP infrastructure +- Test integration with different LLM providers through the abstraction layer + +When implementing MCP functionality, always consider the broader DeepChat architecture, follow established patterns, and ensure robust error handling. Your implementations should be production-ready, well-documented, and maintainable within the existing codebase structure. diff --git a/.cursor/rules/development-setup.mdc b/.cursor/rules/development-setup.mdc new file mode 100644 index 0000000..012c888 --- /dev/null +++ b/.cursor/rules/development-setup.mdc @@ -0,0 +1,26 @@ +--- +description: +globs: +alwaysApply: true +--- +# 开发环境设置指南 +- 使用 OxLint 进行代码检查 +- 使用 pnpm 包管理 +- Log和注释使用英文书写 +- Node.js >= 22 +- pnpm >= 9 + +# install +`pnpm install` + +# lint +`pnpm run lint` + +# format +`pnpm run format` + +# dev +`pnpm run dev` + +# build +`pnpm run build` diff --git a/.cursor/rules/electron-best-practices.mdc b/.cursor/rules/electron-best-practices.mdc new file mode 100644 index 0000000..0869f76 --- /dev/null +++ b/.cursor/rules/electron-best-practices.mdc @@ -0,0 +1,18 @@ +--- +description: Best practices for Electron applications,explain the IPC communication event mechanism between the main and renderer process of this electron project. +globs: src/main/presenter/**/*.ts,src/renderer/stores/**/*.ts,src/shared/*.d.ts +alwaysApply: false +--- +This is an Electron project. The renderer process is mainly used for UI rendering, while the main process is primarily responsible for data and logic processing. The shared/*.d.ts is used to define the types of objects exposed by the main process to the renderer process. + +The IPC in the renderer process is implemented in [usePresenter.ts](mdc:src/renderer/src/composables/usePresenter.ts) , allowing direct calls to the presenter-related interfaces exposed by the main process. + +[eventbus.ts](mdc:src/main/eventbus.ts) is primarily used for intercommunication between main processes and decouples modules with events. + +The IPC messages from the main process to notify the view mainly rely on the EventBus [index.ts](mdc:src/main/presenter/index.ts) to listen for events that need to be notified and then send them to the renderer through the mainWindow. + +- Use context isolation for improved security +- Implement proper inter-process communication (IPC) patterns +- Use Electron's built-in APIs for file system and native dialogs +- Optimize application startup time with lazy loading +- Implement proper error handling and logging for debugging \ No newline at end of file diff --git a/.cursor/rules/error-logging.mdc b/.cursor/rules/error-logging.mdc new file mode 100644 index 0000000..04c040e --- /dev/null +++ b/.cursor/rules/error-logging.mdc @@ -0,0 +1,93 @@ +--- +description: +globs: +alwaysApply: false +--- +# 错误处理和日志指南 + +## 错误处理规范 + +1. 错误类型 +- 用户错误:用户输入或操作导致的错误 +- 系统错误:程序运行时的错误 +- 网络错误:API 调用或网络请求错误 +- 业务错误:业务逻辑相关的错误 + +2. 错误处理原则 +- 始终使用 try-catch 处理可能的错误 +- 提供有意义的错误信息 +- 记录详细的错误日志 +- 优雅降级处理 + +3. 错误处理示例: +```typescript +try { + await someOperation() +} catch (error) { + if (error instanceof UserError) { + // 处理用户错误 + showUserFriendlyMessage(error.message) + } else if (error instanceof NetworkError) { + // 处理网络错误 + handleNetworkError(error) + } else { + // 处理未知错误 + logError(error) + showGenericErrorMessage() + } +} +``` + +## 日志规范 + +1. 日志级别 +- ERROR:错误信息 +- WARN:警告信息 +- INFO:一般信息 +- DEBUG:调试信息 + +2. 日志内容 +- 时间戳 +- 日志级别 +- 错误代码 +- 错误描述 +- 堆栈跟踪(如果适用) +- 相关上下文信息 + +3. 日志记录示例: +```typescript +import { logger } from '@/utils/logger' + +// 错误日志 +logger.error('Failed to save data', { + error: error, + context: { userId, operation } +}) + +// 信息日志 +logger.info('User action completed', { + action: 'save', + userId, + timestamp: new Date() +}) +``` + +## 最佳实践 + +1. 错误处理 +- 不要吞掉错误 +- 提供用户友好的错误信息 +- 实现错误重试机制 +- 使用错误边界捕获渲染错误 + +2. 日志记录 +- 避免记录敏感信息 +- 使用结构化日志 +- 实现日志轮转 +- 设置适当的日志级别 + +3. 监控和告警 +- 设置错误监控 +- 配置关键错误告警 +- 定期检查错误日志 +- 分析错误模式 diff --git a/.cursor/rules/i18n.mdc b/.cursor/rules/i18n.mdc new file mode 100644 index 0000000..7236338 --- /dev/null +++ b/.cursor/rules/i18n.mdc @@ -0,0 +1,74 @@ +--- +description: Internationalization for renderer +globs: src/renderer/src/** +alwaysApply: false +--- +# Internationalization +i18n: + framework: 'vue-i18n' + location: 'src/renderer/src/i18n' + requirement: 'all user-facing strings must use i18n keys' + locales: ['zh-CN','en-US','ko-KR','ru-RU','zh-HK','fr-FR','fa-IR'] + +# 国际化开发指南 +R +## 多语言支持 + +本项目支持多语言,包括: +- 中文(简体) +- 英文 +- 日文 +- 韩文 +- 俄文 +- 繁体中文 +- 法文 +- 波斯文(伊朗) + +## 技术实现 + +- 框架:vue-i18n +- 位置:src/renderer/src/i18n +- 要求:所有面向用户的字符串必须使用 i18n 键 + +## 文件结构 + +- 语言文件位于 `src/renderer/src/i18n/` 目录 +- 每种语言都有独立的 JSON 文件 +- 共享的翻译键值放在 `common.json` 中 + +## 使用规范 + +1. 翻译键命名规范: +- 使用点号分隔的层级结构 +- 使用小写字母 +- 使用有意义的描述性名称 +- 例如:`common.button.submit` + +2. 添加新翻译: +- 在 `common.json` 中添加共享翻译 +- 在语言特定文件中添加特定翻译 +- 保持所有语言文件的键值一致 + +3. 在代码中使用: +```typescript +import { useI18n } from 'vue-i18n' + +const { t } = useI18n() +// 使用翻译 +const message = t('common.button.submit') +``` + +4. 动态切换语言: +```typescript +const { locale } = useI18n() +// 切换语言 +locale.value = 'zh-CN' +``` + +## 最佳实践 + +1. 避免硬编码文本 +2. 使用有意义的键名 +3. 保持翻译文件的结构一致 +4. 定期检查未使用的翻译键 +5. 确保所有用户可见的文本都使用翻译系统 diff --git a/.cursor/rules/llm-agent-loop.mdc b/.cursor/rules/llm-agent-loop.mdc new file mode 100644 index 0000000..3380c70 --- /dev/null +++ b/.cursor/rules/llm-agent-loop.mdc @@ -0,0 +1,91 @@ +--- +description: +globs: src/main/presenter/llmProviderPresenter/index.ts,src/main/presenter/llmProviderPresenter/baseProvider.ts +alwaysApply: false +--- +# LLM Agent Loop 和 Provider 架构 + +本文档概述了处理 LLM 流式补全的架构,特别关注涉及工具调用的 Agent 循环。 + +## 核心原则 + +1. **关注点分离 (Separation of Concerns):** + * `src/main/presenter/llmProviderPresenter/index.ts`: 管理整体 Agent 循环、对话历史、通过 `McpPresenter` 执行工具、并通过 `eventBus` 与前端通信。 + * `src/main/presenter/llmProviderPresenter/providers/*.ts`: 每个 Provider 文件负责与特定的 LLM API 交互,处理特定于 Provider 的请求/响应格式,转换工具定义,管理原生与非原生工具调用机制(Prompt 包装),并将输出流标准化为通用事件格式。 + +2. **标准化流事件 (Standardized Stream Events):** Provider 实现 (`coreStream` 方法) 使用标准化接口 `yield` 事件,以将主循环与 Provider 的具体细节解耦。 + +3. **Provider 中的单次流式传输 (Single-Pass Streaming in Providers):** 每个 Provider 中的核心流式方法 (`coreStream`) 应为对话的每一轮执行*单次*流式 API 请求。它不应包含多轮工具调用的循环逻辑。 + +## 架构细节 + +### `llmProviderPresenter/index.ts` (`startStreamCompletion`) + +* **Agent 循环:** 包含主要的 `while` 循环,管理对话流程,包括在需要时进行多轮 LLM 调用和工具使用。 +* **状态管理:** 在循环迭代中维护 `conversationMessages` 历史记录。 +* **Provider 交互:** 在每个循环迭代中调用 `provider.coreStream()` 方法。 +* **事件处理:** + * 监听由 `coreStream` `yield` 的标准化事件。 + * 缓冲文本内容 (`currentContent`)。 + * 处理 `tool_call_start/chunk/end` 事件: + * 收集完整的工具调用详细信息(id, name, arguments)。 + * 调用 `presenter.mcpPresenter.callTool` 执行工具。 + * 向前端发送带有工具调用状态(`tool_call: 'start' | 'end' | 'error'`)的 `STREAM_EVENTS.RESPONSE` 事件。 + * 将工具结果格式化为适合*下一次* LLM 调用的消息(例如,角色为 'tool' 或附加到用户消息)。 + * 设置 `needContinueConversation = true`。 + * 处理 `reasoning` 事件并通过 `STREAM_EVENTS.RESPONSE` 发送它们。 + * 处理 `text` 事件并通过 `STREAM_EVENTS.RESPONSE` 发送它们。 + * 处理 `image_data` 事件并通过 `STREAM_EVENTS.RESPONSE` 发送它们。 + * 处理 `usage` 事件并聚合它们。 + * 处理 `stop` 事件: + * 如果 `stop_reason: 'tool_use'`,则添加缓冲的助手消息并准备下一次循环迭代。 + * 否则,添加最终的助手消息并跳出循环。 +* **循环控制:** 使用 `needContinueConversation` 和 `toolCallCount`(与 `MAX_TOOL_CALLS` 比较)来管理循环。 +* **前端通信:** 通过 `eventBus` 发送标准化的 `STREAM_EVENTS`(`RESPONSE`, `END`, `ERROR`)。 + +### Provider 实现 (`src/main/presenter/llmProviderPresenter/providers/*.ts`) + +* **`coreStream(messages, modelId, temperature, maxTokens)` 方法:** + * **输入:** 接收当前对话消息(根据 Provider 的需求格式化,可能包含上一轮的工具结果)和生成参数。 + * **工具处理:** + * **原生支持:** 如果 Provider/模型支持原生函数调用,则将 MCP 工具转换为 Provider 的格式 (`convertToProviderTools`) 并包含在 API 请求中。 + * **无原生支持:** 如果不支持原生 FC,则在进行 API 调用之前使用 Prompt 包装 (`prepareFunctionCallPrompt`) 准备消息。 + * **API 调用:** 向 LLM Provider 发出*单次*流式 API 调用。 + * **流处理:** 迭代 Provider 的原生流数据块。 + * **事件标准化:** 解析特定于 Provider 的数据块,并 `yield` 符合 **标准化流事件接口** 的事件。 + * 解析文本、思考(`` 或原生)、工具调用(原生或 ``)、使用情况、错误、停止原因和图像数据。 + * **输出:** 异步 `yield` `StandardizedStreamEvent` 对象。 +* **辅助方法:** 包含特定于 Provider 的辅助函数,如 `formatMessages`, `convertToProviderTools`, `parseFunctionCalls`, `prepareFunctionCallPrompt` 等。 + +### 标准化流事件接口 (`LLMCoreStreamEvent`) + +```typescript +// 建议定义在例如 src/main/presenter/llmProviderPresenter/streamEvents.ts +interface LLMCoreStreamEvent { + type: 'text' | 'reasoning' | 'tool_call_start' | 'tool_call_chunk' | 'tool_call_end' | 'error' | 'usage' | 'stop' | 'image_data'; + content?: string; // 用于 type 'text' + reasoning_content?: string; // 用于 type 'reasoning' + tool_call_id?: string; // 用于 tool_call_* 类型 + tool_call_name?: string; // 用于 tool_call_start + tool_call_arguments_chunk?: string; // 用于 tool_call_chunk (流式参数) + tool_call_arguments_complete?: string; // 用于 tool_call_end (可选,如果一次性可用) + error_message?: string; // 用于 type 'error' + usage?: { // 用于 type 'usage' + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; + stop_reason?: 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error' | 'complete'; // 用于 type 'stop' + image_data?: { // 用于 type 'image_data' + data: string; // Base64 编码的图像数据 + mimeType: string; + }; +} +``` + +## 优点 + +* **减少代码重复:** Agent 循环逻辑集中在 `index.ts`。 +* **提高可维护性:** Provider 实现更简单,专注于 API 交互和事件标准化。 +* **更容易添加新 Provider:** 添加新 Provider 只需根据标准化接口实现 `coreStream` 方法,无需复制复杂的 Agent 循环。 +* **行为一致性:** 确保工具处理、思考内容解析和事件发送在所有 Provider 之间保持一致。 diff --git a/.cursor/rules/performance.mdc b/.cursor/rules/performance.mdc new file mode 100644 index 0000000..b93c988 --- /dev/null +++ b/.cursor/rules/performance.mdc @@ -0,0 +1,5 @@ +--- +description: +globs: +alwaysApply: false +--- diff --git a/.cursor/rules/pinia-best-practices.mdc b/.cursor/rules/pinia-best-practices.mdc new file mode 100644 index 0000000..b103aa0 --- /dev/null +++ b/.cursor/rules/pinia-best-practices.mdc @@ -0,0 +1,10 @@ +--- +description: Best practices for state management with Pinia +globs: src/renderer/src/stores/**/*.{vue,ts,tsx,js,jsx} +alwaysApply: false +--- +- Use modules to organize related state and actions +- Implement proper state persistence for maintaining data across sessions +- Use getters for computed state properties +- Utilize actions for side effects and asynchronous operations +- Keep the store focused on global state, not component-specific data \ No newline at end of file diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 0000000..bfcd71c --- /dev/null +++ b/.cursor/rules/project-structure.mdc @@ -0,0 +1,32 @@ +--- +description: +globs: +alwaysApply: false +--- +# 项目结构指南 + +本项目是一个基于 Electron + Vue 3 的桌面应用程序。主要目录结构如下: + +- `src/`: 源代码目录 + - `main/`: Electron 主进程代码 + - `renderer/`: 渲染进程代码(Vue 3 应用) + - `shared/`: 主进程和渲染进程共享的类型定义和工具 + +- `resources/`: 应用程序资源文件 +- `build/`: 构建相关配置 +- `scripts/`: 构建和开发脚本 +- `docs/`: 项目文档 +- `tests/`: 测试文件 + +主要配置文件: +- [electron.vite.config.ts](mdc:electron.vite.config.ts): Vite 构建配置 +- [electron-builder.yml](mdc:electron-builder.yml): Electron Builder 配置 +- [tsconfig.json](mdc:tsconfig.json): TypeScript 配置 +- [tailwind.config.js](mdc:tailwind.config.js): Tailwind CSS 配置 + +开发规范: +1. 所有新功能应该在 `src` 目录下开发 +2. 共享类型定义放在 `shared` 目录 +3. 主进程代码放在 `src/main` +4. 渲染进程代码放在 `src/renderer` +5. 测试文件放在 `tests` 目录 diff --git a/.cursor/rules/provider-guidelines.mdc b/.cursor/rules/provider-guidelines.mdc new file mode 100644 index 0000000..7d8ed9f --- /dev/null +++ b/.cursor/rules/provider-guidelines.mdc @@ -0,0 +1,40 @@ +--- +description: Provider Implementation Guidelines - Strong Typed CoreEvent, Tool Call Sequences, Usage and Rate Limit Specifications +--- + +## Provider Implementation Guidelines (Strong Typed) + +- Reference message specification: [LLM Agent Message Architecture](mdc:docs/agent/message-architecture.md) +- Related implementation entry: [llmProviderPresenter](mdc:src/main/presenter/llmProviderPresenter/index.ts) + +### Strong Typed Events +- Only output discriminated union `LLMCoreStreamEvent`, do not use "single interface + optional fields". +- Use factory methods `createStreamEvent.*` to construct events, avoid field pollution. + +### Event Sequence Conventions +- Text: Multiple `text` chunks emitted in arrival order. +- Reasoning: `reasoning` is optional; if provided, suggest containing complete chain. +- Tool calls: Strictly follow `tool_call_start → tool_call_chunk* → tool_call_end`, `tool_call_id` is required and stable. +- Stop: Emit `stop` at stream end, `stop_reason ∈ { 'tool_use','max_tokens','stop_sequence','error','complete' }`. + +### Statistics and Rate Limiting +- Usage: Send `usage` once before or at end (`prompt_tokens/completion_tokens/total_tokens`). +- Rate limit: Send `rate_limit` when reaching limit threshold (`providerId/qpsLimit/currentQps/queueLength/estimatedWaitTime?`), do not block event channel. + +### Errors +- Use `error` event uniformly to carry error messages, avoid mixing into other event fields. +- Once fatal error occurs, emit `stop` as needed and terminate stream. + +### Images +- `image_data` event must provide `data(Base64)` and `mimeType`; control single frame size and frequency to avoid blocking. + +### What Not to Do +- Do not emit `AssistantMessageBlock` or any UI types to UI layer. +- Do not introduce renderer dependencies inside Provider. + +### Quality Thresholds (Self-Check) +- Every event construction uses factory functions. +- Tool call IDs are globally unique and stable, chunks arrive strictly in order. +- Error scenarios have corresponding stop_reason and error message. +- At least one usage (if provider has statistics capability). +- Provide rate limit events (if rate limiter is configured). diff --git a/.cursor/rules/vue-best-practices.mdc b/.cursor/rules/vue-best-practices.mdc new file mode 100644 index 0000000..a3b1cce --- /dev/null +++ b/.cursor/rules/vue-best-practices.mdc @@ -0,0 +1,10 @@ +--- +description: Best practices for Vue.js applications +globs: src/renderer/src/**/*.{vue,ts,tsx,js,jsx} +alwaysApply: false +--- +- Use the Composition API for better code organization and reusability +- Implement proper state management with Pinia +- Utilize Vue Router for navigation and route management +- Leverage Vue's built-in reactivity system for efficient data handling +- Use scoped styles to prevent CSS conflicts between components \ No newline at end of file diff --git a/.cursor/rules/vue-router-best-practices.mdc b/.cursor/rules/vue-router-best-practices.mdc new file mode 100644 index 0000000..9c490c9 --- /dev/null +++ b/.cursor/rules/vue-router-best-practices.mdc @@ -0,0 +1,10 @@ +--- +description: Best practices for Vue Router +globs: src/renderer/src/router/**/*.{vue,ts,tsx,js,jsx} +alwaysApply: false +--- +- Use named routes for easier navigation and maintenance +- Implement route-level code splitting for better performance +- Use route meta fields for attaching additional data to routes +- Implement proper navigation guards for authentication and authorization +- Use dynamic routing for handling variable route segments \ No newline at end of file diff --git a/.cursor/rules/vue-shadcn.mdc b/.cursor/rules/vue-shadcn.mdc new file mode 100644 index 0000000..f48184d --- /dev/null +++ b/.cursor/rules/vue-shadcn.mdc @@ -0,0 +1,79 @@ +--- +description: +globs: src/renderer/*,src/renderer/**/*,srr/renderer/src/**/* +alwaysApply: false +--- + + You are an expert in TypeScript, Node.js, NuxtJS, Vue 3, Shadcn Vue, Radix Vue, VueUse, and Tailwind. + + Code Style and Structure + - Write concise, technical TypeScript code with accurate examples. + - Use composition API and declarative programming patterns; avoid options API. + - Prefer iteration and modularization over code duplication. + - Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError). + - Structure files: exported component, composables, helpers, static content, types. + + Naming Conventions + - Use lowercase with dashes for directories (e.g., components/auth-wizard). + - Use PascalCase for component names (e.g., AuthWizard.vue). + - Use camelCase for composables (e.g., useAuthState.ts). + + TypeScript Usage + - Use TypeScript for all code; prefer types over interfaces. + - Avoid enums; use const objects instead. + - Use Vue 3 with TypeScript, leveraging defineComponent and PropType. + + Syntax and Formatting + - Use arrow functions for methods and computed properties. + - Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements. + - Use template syntax for declarative rendering. + + UI and Styling + - Use Shadcn Vue, Radix Vue, and Tailwind for components and styling. + - Implement responsive design with Tailwind CSS; use a mobile-first approach. + + Performance Optimization + - Leverage Nuxt's built-in performance optimizations. + - Use Suspense for asynchronous components. + - Implement lazy loading for routes and components. + - Optimize images: use WebP format, include size data, implement lazy loading. + + Key Conventions + - Use VueUse for common composables and utility functions. + - Use Pinia for state management. + - Optimize Web Vitals (LCP, CLS, FID). + - Utilize Nuxt's auto-imports feature for components and composables. + + Nuxt-specific Guidelines + - Follow Nuxt 3 directory structure (e.g., pages/, components/, composables/). + - Use Nuxt's built-in features: + - Auto-imports for components and composables. + - File-based routing in the pages/ directory. + - Server routes in the server/ directory. + - Leverage Nuxt plugins for global functionality. + - Use useFetch and useAsyncData for data fetching. + - Implement SEO best practices using Nuxt's useHead and useSeoMeta. + + Vue 3 and Composition API Best Practices + - Use + + + `) + + // Trigger callback handling + this.handleServerCallback(code, null) + } else if (error) { + // Error page + res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }) + res.end(` + + + + + Authorization Failed + + + +

❌ Authorization Failed

+

An error occurred during authorization: ${error}

+

You can close this window and try again.

+ + + `) + + this.handleServerCallback(null, error) + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end('Invalid callback request') + } + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }) + res.end('Not found') + } + }) + + this.callbackServer.listen(this.callbackPort, 'localhost', () => { + console.log(`OAuth callback server started on http://localhost:${this.callbackPort}`) + resolve() + }) + + this.callbackServer.on('error', (error) => { + console.error('Callback server error:', error) + reject(error) + }) + }) + } + + /** + * Stop callback server + */ + private stopCallbackServer(): void { + if (this.callbackServer) { + this.callbackServer.close() + this.callbackServer = null + console.log('OAuth callback server stopped') + } + } + + // Callback handling resolve and reject functions + private callbackResolve: ((value: string) => void) | null = null + private callbackReject: ((reason?: Error) => void) | null = null + + /** + * Handle server callback + */ + private handleServerCallback(code: string | null, error: string | null): void { + if (error) { + console.error('OAuth server callback error:', error) + this.callbackReject?.(new Error(`OAuth authorization failed: ${error}`)) + } else if (code) { + console.log('OAuth server callback success, received code:', code) + this.callbackResolve?.(code) + } + + // Clean up callback functions + this.callbackResolve = null + this.callbackReject = null + } + + /** + * Start OAuth flow + */ + private async startOAuthFlow(config: OAuthConfig): Promise { + return new Promise((resolve, reject) => { + // Save callback functions + this.callbackResolve = resolve + this.callbackReject = reject + + // Create authorization window + this.authWindow = new BrowserWindow({ + width: 500, + height: 700, + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + }, + autoHideMenuBar: true, + title: 'GitHub Authorization Login' + }) + + // Build authorization URL + const authUrl = this.buildAuthUrl(config) + console.log('Opening OAuth URL:', authUrl) + + // Load authorization page + this.authWindow.loadURL(authUrl) + this.authWindow.show() + + // Handle window close + this.authWindow.on('closed', () => { + this.authWindow = null + if (this.callbackReject) { + this.callbackReject(new Error('User cancelled login')) + this.callbackReject = null + this.callbackResolve = null + } + }) + + // Handle loading errors + this.authWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription) => { + console.error('OAuth page load failed:', errorCode, errorDescription) + this.closeAuthWindow() + if (this.callbackReject) { + this.callbackReject(new Error(`Failed to load authorization page: ${errorDescription}`)) + this.callbackReject = null + this.callbackResolve = null + } + }) + + // Monitor page navigation to check if callback page is reached + this.authWindow.webContents.on('did-navigate', (_event, navigationUrl) => { + console.log('OAuth window navigated to:', navigationUrl) + // If navigated to our callback page, authorization flow is complete + if (navigationUrl.includes('deepchatai.cn/auth/github/callback')) { + // Close authorization window as callback server handles remaining logic + setTimeout(() => { + this.closeAuthWindow() + }, 2000) // Close after 2 seconds to let user see success page + } + }) + }) + } + + /** + * Build authorization URL + */ + private buildAuthUrl(config: OAuthConfig): string { + const params = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: config.redirectUri, + response_type: config.responseType, + scope: config.scope + }) + + return `${config.authUrl}?${params.toString()}` + } + + /** + * Exchange authorization code for access token + */ + private async exchangeCodeForToken(code: string, config: OAuthConfig): Promise { + const response = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'User-Agent': 'DeepChat/1.0.0' + }, + body: JSON.stringify({ + client_id: config.clientId, + client_secret: config.clientSecret || process.env.GITHUB_CLIENT_SECRET, + code: code, + redirect_uri: config.redirectUri + }) + }) + + if (!response.ok) { + throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`) + } + + const data = (await response.json()) as { + access_token: string + error?: string + error_description?: string + } + + if (data.error) { + throw new Error(`Token exchange error: ${data.error_description || data.error}`) + } + + return data.access_token + } + + /** + * Close authorization window + */ + private closeAuthWindow(): void { + if (this.authWindow && !this.authWindow.isDestroyed()) { + this.authWindow.close() + this.authWindow = null + } + } +} + +// GitHub Copilot OAuth configuration +export const GITHUB_COPILOT_OAUTH_CONFIG: OAuthConfig = { + authUrl: 'https://github.com/login/oauth/authorize', + redirectUri: + import.meta.env.VITE_GITHUB_REDIRECT_URI || 'https://deepchatai.cn/auth/github/callback', + clientId: import.meta.env.VITE_GITHUB_CLIENT_ID, + clientSecret: import.meta.env.VITE_GITHUB_CLIENT_SECRET, + scope: 'read:user read:org', + responseType: 'code' +} diff --git a/src/main/presenter/proxyConfig.ts b/src/main/presenter/proxyConfig.ts new file mode 100644 index 0000000..82cedd2 --- /dev/null +++ b/src/main/presenter/proxyConfig.ts @@ -0,0 +1,209 @@ +import { session } from 'electron' +import { Agent, EnvHttpProxyAgent, setGlobalDispatcher } from 'undici' +import { eventBus } from '@/eventbus' +import { CONFIG_EVENTS } from '@/events' + +// 先简单处理,用系统代理 +export enum ProxyMode { + SYSTEM = 'system', + NONE = 'none', + CUSTOM = 'custom' +} +export const NO_PROXY = + 'localhost, 127.0.0.1, ::1, 192.168.*.*, 10.*.*.*, *.local, host.docker.internal' +// const NO_PROXY = '' + +// 合并系统和自定义的 no_proxy 设置 +function mergeNoProxy(defaultNoProxy: string): string { + const systemNoProxy = process.env.no_proxy || process.env.NO_PROXY || '' + console.log('systemNoProxy', systemNoProxy) + if (!systemNoProxy) { + return defaultNoProxy + } + // 将两个 no_proxy 字符串分割成数组,去重,然后重新组合 + const noProxySet = new Set( + [ + ...defaultNoProxy.split(',').map((item) => item.trim()), + ...systemNoProxy.split(',').map((item) => item.trim()) + ].filter(Boolean) + ) // 过滤掉空字符串 + + return Array.from(noProxySet).join(', ') +} + +export class ProxyConfig { + private proxyUrl: string | null = null + private mode: ProxyMode = ProxyMode.SYSTEM + private customProxyUrl: string = '' + + constructor() { + this.mode = ProxyMode.SYSTEM + + // 监听代理模式变更事件 + eventBus.on(CONFIG_EVENTS.PROXY_MODE_CHANGED, (mode: string) => { + this.setProxyMode(mode as ProxyMode) + this.resolveProxy() + }) + + // 监听自定义代理地址变更事件 + eventBus.on(CONFIG_EVENTS.CUSTOM_PROXY_URL_CHANGED, (url: string) => { + this.setCustomProxyUrl(url) + if (this.mode === ProxyMode.CUSTOM) { + this.resolveProxy() + } + }) + } + + async resolveProxy(): Promise { + try { + // 根据不同的代理模式设置 + if (this.mode === ProxyMode.NONE) { + this.clearProxy() + console.log('clear proxy') + return + } else if (this.mode === ProxyMode.CUSTOM && this.customProxyUrl) { + console.log('proxy url', this.customProxyUrl) + this.setCustomProxy(this.customProxyUrl) + return + } + + // 系统代理模式 + session.defaultSession.setProxy({ mode: 'system' }) + const proxyString = await session.defaultSession.resolveProxy('https://www.google.com') + const [protocol, address] = proxyString.split(';')[0].split(' ') + console.log('proxy url', protocol, address) + this.proxyUrl = protocol === 'PROXY' ? `http://${address}` : null + + if (this.proxyUrl) { + process.env.http_proxy = this.proxyUrl + process.env.https_proxy = this.proxyUrl + process.env.HTTP_PROXY = this.proxyUrl + process.env.HTTPS_PROXY = this.proxyUrl + process.env.GRPC_PROXY = this.proxyUrl + process.env.grpc_proxy = this.proxyUrl + const mergedNoProxy = mergeNoProxy(NO_PROXY) + process.env.no_proxy = mergedNoProxy + process.env.NO_PROXY = mergedNoProxy + setGlobalDispatcher( + new EnvHttpProxyAgent({ + httpProxy: this.proxyUrl, + httpsProxy: this.proxyUrl, + noProxy: mergedNoProxy + }) + ) + } + eventBus.sendToMain(CONFIG_EVENTS.PROXY_RESOLVED) + } catch (error) { + console.error('Failed to resolve proxy:', error) + return + } + } + + private clearProxy(): void { + this.proxyUrl = null + delete process.env.http_proxy + delete process.env.https_proxy + delete process.env.HTTP_PROXY + delete process.env.HTTPS_PROXY + delete process.env.GRPC_PROXY + delete process.env.grpc_proxy + delete process.env.no_proxy + delete process.env.NO_PROXY + session.defaultSession.setProxy({ mode: 'direct' }) + setGlobalDispatcher(new Agent()) + } + + private setCustomProxy(proxyUrl: string): void { + this.proxyUrl = proxyUrl + process.env.http_proxy = proxyUrl + process.env.https_proxy = proxyUrl + process.env.HTTP_PROXY = proxyUrl + process.env.HTTPS_PROXY = proxyUrl + process.env.GRPC_PROXY = proxyUrl + process.env.grpc_proxy = proxyUrl + const mergedNoProxy = mergeNoProxy(NO_PROXY) + process.env.no_proxy = mergedNoProxy + process.env.NO_PROXY = mergedNoProxy + session.defaultSession.setProxy({ proxyRules: proxyUrl }) + setGlobalDispatcher( + new EnvHttpProxyAgent({ + httpProxy: proxyUrl, + httpsProxy: proxyUrl, + noProxy: mergedNoProxy + }) + ) + } + + /** + * 验证代理URL是否有效 + * @param url 要验证的代理URL + * @returns 是否是有效的代理URL + */ + isValidProxyUrl(url: string): boolean { + if (!url || url.trim() === '') { + return false + } + + try { + // 检查URL格式,确保开头是http://或https:// + const urlPattern = + /^(http|https):\/\/(?:([^:@/]+)(?::([^@/]*))?@)?([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(:[0-9]+)?(\/[^\s]*)?$/ + if (!urlPattern.test(url)) { + return false + } + + // 尝试解析URL + const parsedUrl = new URL(url) + // 确保端口号是有效的数字(如果有指定端口) + if (parsedUrl.port && isNaN(parseInt(parsedUrl.port))) { + return false + } + + return true + } catch (error) { + console.error('Invalid proxy URL:', error) + return false + } + } + + getProxyUrl(): string | null { + return this.proxyUrl + } + + getProxyMode(): ProxyMode { + return this.mode + } + + setProxyMode(mode: ProxyMode): void { + this.mode = mode + } + + getCustomProxyUrl(): string { + return this.customProxyUrl + } + + setCustomProxyUrl(url: string): void { + // 只设置有效的URL,否则保留原有值 + if (this.isValidProxyUrl(url) || url.trim() === '') { + this.customProxyUrl = url + } else { + console.warn('Invalid proxy URL format:', url) + } + } + + // 从配置初始化代理设置 + initFromConfig(mode: ProxyMode, customUrl: string): void { + this.mode = mode + // 如果是自定义模式,验证URL有效性 + if (mode === ProxyMode.CUSTOM && customUrl) { + if (this.isValidProxyUrl(customUrl)) { + this.customProxyUrl = customUrl + } else { + console.warn('Invalid custom proxy URL in config, fallback to system proxy mode') + this.mode = ProxyMode.SYSTEM + } + } + this.resolveProxy() + } +} +export const proxyConfig = new ProxyConfig() diff --git a/src/main/presenter/shortcutPresenter.ts b/src/main/presenter/shortcutPresenter.ts new file mode 100644 index 0000000..6d529a6 --- /dev/null +++ b/src/main/presenter/shortcutPresenter.ts @@ -0,0 +1,278 @@ +import { app, globalShortcut } from 'electron' + +import { presenter } from '.' +import { SHORTCUT_EVENTS, TRAY_EVENTS } from '../events' +import { eventBus, SendTarget } from '../eventbus' +import { + CommandKey, + defaultShortcutKey, + ShortcutKeySetting +} from './configPresenter/shortcutKeySettings' +import { IConfigPresenter, IShortcutPresenter } from '@shared/presenter' + +export class ShortcutPresenter implements IShortcutPresenter { + private isActive: boolean = false + private configPresenter: IConfigPresenter + private shortcutKeys: ShortcutKeySetting = { + ...defaultShortcutKey + } + + /** + * 创建一个新的 ShortcutPresenter 实例 + * @param shortKey 可选的自定义快捷键设置 + */ + constructor(configPresenter: IConfigPresenter) { + this.configPresenter = configPresenter + } + + registerShortcuts(): void { + if (this.isActive) return + console.log('reg shortcuts') + + this.shortcutKeys = { + ...defaultShortcutKey, + ...this.configPresenter.getShortcutKey() + } + + // Command+N 或 Ctrl+N 创建新会话 + if (this.shortcutKeys.NewConversation) { + globalShortcut.register(this.shortcutKeys.NewConversation, async () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + presenter.windowPresenter.sendToActiveTab( + focusedWindow.id, + SHORTCUT_EVENTS.CREATE_NEW_CONVERSATION + ) + } + }) + } + + // Command+Shift+N 或 Ctrl+Shift+N 创建新窗口 + if (this.shortcutKeys.NewWindow) { + globalShortcut.register(this.shortcutKeys.NewWindow, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + eventBus.sendToMain(SHORTCUT_EVENTS.CREATE_NEW_WINDOW) + } + }) + } + + // Command+T 或 Ctrl+T 在当前窗口创建新标签页 + if (this.shortcutKeys.NewTab) { + globalShortcut.register(this.shortcutKeys.NewTab, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + eventBus.sendToMain(SHORTCUT_EVENTS.CREATE_NEW_TAB, focusedWindow.id) + } + }) + } + + // Command+W 或 Ctrl+W 关闭当前标签页 + if (this.shortcutKeys.CloseTab) { + globalShortcut.register(this.shortcutKeys.CloseTab, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + eventBus.sendToMain(SHORTCUT_EVENTS.CLOSE_CURRENT_TAB, focusedWindow.id) + } + }) + } + + // Command+Q 或 Ctrl+Q 退出程序 + if (this.shortcutKeys.Quit) { + globalShortcut.register(this.shortcutKeys.Quit, () => { + app.quit() // Exit trigger: shortcut key + }) + } + + // Command+= 或 Ctrl+= 放大字体 + if (this.shortcutKeys.ZoomIn) { + globalShortcut.register(this.shortcutKeys.ZoomIn, () => { + eventBus.send(SHORTCUT_EVENTS.ZOOM_IN, SendTarget.ALL_WINDOWS) + }) + } + + // Command+- 或 Ctrl+- 缩小字体 + if (this.shortcutKeys.ZoomOut) { + globalShortcut.register(this.shortcutKeys.ZoomOut, () => { + eventBus.send(SHORTCUT_EVENTS.ZOOM_OUT, SendTarget.ALL_WINDOWS) + }) + } + + // Command+0 或 Ctrl+0 重置字体大小 + if (this.shortcutKeys.ZoomResume) { + globalShortcut.register(this.shortcutKeys.ZoomResume, () => { + eventBus.send(SHORTCUT_EVENTS.ZOOM_RESUME, SendTarget.ALL_WINDOWS) + }) + } + + // Command+, 或 Ctrl+, 打开设置 + if (this.shortcutKeys.GoSettings) { + globalShortcut.register(this.shortcutKeys.GoSettings, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + presenter.windowPresenter.sendToActiveTab(focusedWindow.id, SHORTCUT_EVENTS.GO_SETTINGS) + } + }) + } + + // Command+L 或 Ctrl+L 清除聊天历史 + if (this.shortcutKeys.CleanChatHistory) { + globalShortcut.register(this.shortcutKeys.CleanChatHistory, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + presenter.windowPresenter.sendToActiveTab( + focusedWindow.id, + SHORTCUT_EVENTS.CLEAN_CHAT_HISTORY + ) + } + }) + } + + // Command+D 或 Ctrl+D 清除聊天历史 + if (this.shortcutKeys.DeleteConversation) { + globalShortcut.register(this.shortcutKeys.DeleteConversation, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + presenter.windowPresenter.sendToActiveTab( + focusedWindow.id, + SHORTCUT_EVENTS.DELETE_CONVERSATION + ) + } + }) + } + + // 添加标签页切换相关快捷键 + + // Command+Tab 或 Ctrl+Tab 切换到下一个标签页 + if (this.shortcutKeys.SwitchNextTab) { + globalShortcut.register(this.shortcutKeys.SwitchNextTab, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + this.switchToNextTab(focusedWindow.id) + } + }) + } + + // Ctrl+Shift+Tab 切换到上一个标签页 + if (this.shortcutKeys.SwitchPrevTab) { + globalShortcut.register(this.shortcutKeys.SwitchPrevTab, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + this.switchToPreviousTab(focusedWindow.id) + } + }) + } + + // 注册标签页数字快捷键 (1-8) + for (let i = 1; i <= 8; i++) { + globalShortcut.register(`${CommandKey}+${i}`, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + this.switchToTabByIndex(focusedWindow.id, i - 1) // 索引从0开始 + } + }) + } + + // Command+9 或 Ctrl+9 切换到最后一个标签页 + if (this.shortcutKeys.SwtichToLastTab) { + globalShortcut.register(this.shortcutKeys.SwtichToLastTab, () => { + const focusedWindow = presenter.windowPresenter.getFocusedWindow() + if (focusedWindow?.isFocused()) { + this.switchToLastTab(focusedWindow.id) + } + }) + } + + this.showHideWindow() + + this.isActive = true + } + + // 切换到下一个标签页 + private async switchToNextTab(windowId: number): Promise { + try { + const tabsData = await presenter.tabPresenter.getWindowTabsData(windowId) + if (!tabsData || tabsData.length <= 1) return // 只有一个或没有标签页时不执行切换 + + // 找到当前活动标签的索引 + const activeTabIndex = tabsData.findIndex((tab) => tab.isActive) + if (activeTabIndex === -1) return + + // 计算下一个标签页的索引(循环到第一个) + const nextTabIndex = (activeTabIndex + 1) % tabsData.length + + // 切换到下一个标签页 + await presenter.tabPresenter.switchTab(tabsData[nextTabIndex].id) + } catch (error) { + console.error('Failed to switch to next tab:', error) + } + } + + // 切换到上一个标签页 + private async switchToPreviousTab(windowId: number): Promise { + try { + const tabsData = await presenter.tabPresenter.getWindowTabsData(windowId) + if (!tabsData || tabsData.length <= 1) return // 只有一个或没有标签页时不执行切换 + + // 找到当前活动标签的索引 + const activeTabIndex = tabsData.findIndex((tab) => tab.isActive) + if (activeTabIndex === -1) return + + // 计算上一个标签页的索引(循环到最后一个) + const previousTabIndex = (activeTabIndex - 1 + tabsData.length) % tabsData.length + + // 切换到上一个标签页 + await presenter.tabPresenter.switchTab(tabsData[previousTabIndex].id) + } catch (error) { + console.error('Failed to switch to previous tab:', error) + } + } + + // 切换到指定索引的标签页 + private async switchToTabByIndex(windowId: number, index: number): Promise { + try { + const tabsData = await presenter.tabPresenter.getWindowTabsData(windowId) + if (!tabsData || index >= tabsData.length) return // 索引超出范围 + + // 切换到指定索引的标签页 + await presenter.tabPresenter.switchTab(tabsData[index].id) + } catch (error) { + console.error(`Failed to switch to tab at index ${index}:`, error) + } + } + + // 切换到最后一个标签页 + private async switchToLastTab(windowId: number): Promise { + try { + const tabsData = await presenter.tabPresenter.getWindowTabsData(windowId) + if (!tabsData || tabsData.length === 0) return + + // 切换到最后一个标签页 + await presenter.tabPresenter.switchTab(tabsData[tabsData.length - 1].id) + } catch (error) { + console.error('Failed to switch to last tab:', error) + } + } + + // Command+O 或 Ctrl+O 显示/隐藏窗口 + private async showHideWindow() { + // Command+O 或 Ctrl+O 显示/隐藏窗口 + if (this.shortcutKeys.ShowHideWindow) { + globalShortcut.register(this.shortcutKeys.ShowHideWindow, () => { + eventBus.sendToMain(TRAY_EVENTS.SHOW_HIDDEN_WINDOW) + }) + } + } + + unregisterShortcuts(): void { + console.log('unreg shortcuts') + globalShortcut.unregisterAll() + + this.showHideWindow() + this.isActive = false + } + + destroy(): void { + this.unregisterShortcuts() + } +} diff --git a/src/main/presenter/sqlitePresenter/importData.ts b/src/main/presenter/sqlitePresenter/importData.ts new file mode 100644 index 0000000..507786b --- /dev/null +++ b/src/main/presenter/sqlitePresenter/importData.ts @@ -0,0 +1,452 @@ +import Database from 'better-sqlite3-multiple-ciphers' +import { nanoid } from 'nanoid' +import path from 'path' + +/** + * 数据导入类 + * 用于从外部SQLite数据库导入数据到当前数据库 + */ +export class DataImporter { + private sourceDb: Database.Database + private targetDb: Database.Database + private idMappings: { + conversations: Map + messages: Map + attachments: Map + } + + /** + * 构造函数 + * @param sourcePath 源数据库路径 + * @param targetDbOrPath 目标数据库实例或路径 + * @param sourcePassword 源数据库密码(如果有) + * @param targetPassword 目标数据库密码(如果有) + */ + constructor( + sourcePath: string, + targetDbOrPath: Database.Database | string, + sourcePassword?: string, + targetPassword?: string + ) { + // 初始化源数据库连接 + this.sourceDb = new Database(sourcePath) + this.sourceDb.pragma('journal_mode = WAL') + + // 如果有密码,设置加密 + if (sourcePassword) { + this.sourceDb.pragma(`cipher='sqlcipher'`) + this.sourceDb.pragma(`key='${sourcePassword}'`) + } + + // 设置目标数据库 + if (typeof targetDbOrPath === 'string') { + // 如果传入的是路径字符串,创建新的数据库连接 + this.targetDb = new Database(targetDbOrPath) + this.targetDb.pragma('journal_mode = WAL') + + // 如果有目标数据库密码,设置加密 + if (targetPassword) { + this.targetDb.pragma(`cipher='sqlcipher'`) + this.targetDb.pragma(`key='${targetPassword}'`) + } + } else { + // 如果传入的是数据库实例,直接使用 + this.targetDb = targetDbOrPath + } + + // 初始化ID映射 + this.idMappings = { + conversations: new Map(), + messages: new Map(), + attachments: new Map() + } + } + + /** + * 开始导入数据 + * @returns 导入的会话数量 + */ + public async importData(): Promise { + // 获取所有会话 - 兼容不同版本的数据库schema + let conversations: any[] + + try { + // 尝试使用包含所有新字段的查询 + conversations = this.sourceDb + .prepare( + `SELECT + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, + COALESCE(is_pinned, 0) as is_pinned, + COALESCE(is_new, 0) as is_new, + COALESCE(artifacts, 0) as artifacts, + enabled_mcp_tools, + thinking_budget, + reasoning_effort, + verbosity, + enable_search, + forced_search, + search_strategy + FROM conversations` + ) + .all() as any[] + } catch { + // 如果失败,使用基础字段查询(兼容旧版本数据库) + try { + conversations = this.sourceDb + .prepare( + `SELECT + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, + COALESCE(is_pinned, 0) as is_pinned, + COALESCE(is_new, 0) as is_new, + COALESCE(artifacts, 0) as artifacts + FROM conversations` + ) + .all() as any[] + + // 为缺失的字段设置默认值 + conversations = conversations.map((conv) => ({ + ...conv, + enabled_mcp_tools: null, + thinking_budget: null, + reasoning_effort: null, + verbosity: null, + enable_search: null, + forced_search: null, + search_strategy: null + })) + } catch (fallbackError) { + throw new Error( + `Failed to query conversations: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}` + ) + } + } + + // 使用better-sqlite3的transaction API来处理事务 + const importTransaction = this.targetDb.transaction(() => { + let importedCount = 0 + for (const conv of conversations) { + // 如果是增量导入模式,检查会话是否已存在 + const existingConv = this.targetDb + .prepare('SELECT conv_id FROM conversations WHERE conv_id = ?') + .get(conv.conv_id) + if (existingConv) { + continue // 跳过已存在的会话 + } + + this.importConversation(conv) + importedCount++ + } + return importedCount + }) + + try { + // 执行事务并返回导入的会话数量 + return importTransaction() + } catch (transactionError) { + // 事务会自动回滚,抛出详细错误 + throw new Error( + `Failed to import data: ${transactionError instanceof Error ? transactionError.message : String(transactionError)}` + ) + } + } + + /** + * 导入单个会话及其相关数据 + * @param conv 会话数据 + */ + private importConversation(conv: any): void { + // 为会话生成新ID + // const newConvId = nanoid() + // this.idMappings.conversations.set(conv.conv_id, newConvId) + + try { + // 首先尝试使用包含所有新字段的INSERT语句 + this.targetDb + .prepare( + `INSERT INTO conversations ( + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, is_pinned, is_new, artifacts, enabled_mcp_tools, + thinking_budget, reasoning_effort, verbosity, + enable_search, forced_search, search_strategy + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + conv.conv_id, + conv.title, + conv.created_at, + conv.updated_at, + conv.system_prompt, + conv.temperature, + conv.context_length, + conv.max_tokens, + conv.provider_id, + conv.model_id, + conv.is_pinned || 0, + conv.is_new || 0, + conv.artifacts || 0, + conv.enabled_mcp_tools || null, + conv.thinking_budget || null, + conv.reasoning_effort || null, + conv.verbosity || null, + conv.enable_search ?? null, + conv.forced_search ?? null, + conv.search_strategy ?? null + ) + } catch { + // 如果失败,使用基础字段的INSERT语句(兼容旧版本目标数据库) + try { + this.targetDb + .prepare( + `INSERT INTO conversations ( + conv_id, title, created_at, updated_at, system_prompt, + temperature, context_length, max_tokens, provider_id, + model_id, is_pinned, is_new, artifacts + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + conv.conv_id, + conv.title, + conv.created_at, + conv.updated_at, + conv.system_prompt, + conv.temperature, + conv.context_length, + conv.max_tokens, + conv.provider_id, + conv.model_id, + conv.is_pinned || 0, + conv.is_new || 0, + conv.artifacts || 0 + ) + } catch (fallbackError) { + throw new Error( + `Failed to insert conversation ${conv.conv_id}: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}` + ) + } + } + + // 导入该会话的所有消息 + try { + this.importMessages(conv.conv_id) + } catch (messageError) { + throw new Error( + `Failed to import messages for conversation ${conv.conv_id}: ${messageError instanceof Error ? messageError.message : String(messageError)}` + ) + } + } + + /** + * 导入会话的所有消息 + * @param oldConvId 原会话ID + */ + private importMessages(oldConvId: string): void { + // 获取会话的所有消息 + const messages = this.sourceDb + .prepare( + `SELECT + msg_id, parent_id, role, content, created_at, + order_seq, token_count, status, metadata, + is_context_edge, is_variant + FROM messages + WHERE conversation_id = ? + ORDER BY order_seq` + ) + .all(oldConvId) as any[] + + // 逐个导入消息 + for (const msg of messages) { + const newMsgId = nanoid() + this.idMappings.messages.set(msg.msg_id, newMsgId) + + // 处理父消息ID映射 + let newParentId = '' + if (msg.parent_id && msg.parent_id !== '') { + newParentId = this.idMappings.messages.get(msg.parent_id) || '' + } + + try { + // 插入消息 + this.targetDb + .prepare( + `INSERT INTO messages ( + msg_id, conversation_id, parent_id, role, content, + created_at, order_seq, token_count, status, metadata, + is_context_edge, is_variant + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + newMsgId, + oldConvId, + newParentId, + msg.role, + msg.content, + msg.created_at, + msg.order_seq, + msg.token_count || 0, + msg.status || 'sent', + msg.metadata || null, + msg.is_context_edge || 0, + msg.is_variant || 0 + ) + + // 导入消息的附件 + this.importAttachments(msg.msg_id, newMsgId) + this.importMessageAttachments(msg.msg_id, newMsgId) + } catch (msgError) { + throw new Error( + `Failed to insert message ${msg.msg_id}: ${msgError instanceof Error ? msgError.message : String(msgError)}` + ) + } + } + } + + /** + * 导入消息的附件 + * @param oldMsgId 原消息ID + * @param newMsgId 新消息ID + */ + private importAttachments(oldMsgId: string, newMsgId: string): void { + // 获取消息的所有附件 + const attachments = this.sourceDb + .prepare( + `SELECT + attach_id, attachment_type, file_name, file_size, + storage_type, storage_path, thumbnail, vectorized, + data_summary, mime_type, created_at + FROM attachments + WHERE message_id = ?` + ) + .all(oldMsgId) as any[] + + // 逐个导入附件 + for (const attachment of attachments) { + const newAttachId = nanoid() + this.idMappings.attachments.set(attachment.attach_id, newAttachId) + + // 处理存储路径 + let storagePath = attachment.storage_path + if (storagePath && attachment.storage_type === 'path') { + // 如果是文件路径,可能需要复制文件或调整路径 + // 这里简单处理,实际应用中可能需要更复杂的逻辑 + storagePath = path.basename(storagePath) + } + + // 插入附件 + this.targetDb + .prepare( + `INSERT INTO attachments ( + attach_id, message_id, attachment_type, file_name, + file_size, storage_type, storage_path, thumbnail, + vectorized, data_summary, mime_type, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + newAttachId, + newMsgId, + attachment.attachment_type, + attachment.file_name, + attachment.file_size || 0, + attachment.storage_type, + storagePath, + attachment.thumbnail, + attachment.vectorized || 0, + attachment.data_summary, + attachment.mime_type, + attachment.created_at + ) + } + } + + /** + * 导入消息的附件(message_attachments表) + * @param oldMsgId 原消息ID + * @param newMsgId 新消息ID + */ + private importMessageAttachments(oldMsgId: string, newMsgId: string): void { + // 获取消息的所有message_attachments - 兼容不同的schema版本 + let messageAttachments: any[] + + try { + // 首先尝试包含metadata字段的查询 + messageAttachments = this.sourceDb + .prepare( + `SELECT + attachment_id, type, content, created_at, metadata + FROM message_attachments + WHERE message_id = ?` + ) + .all(oldMsgId) as any[] + } catch { + // 如果失败,使用不包含metadata的查询(兼容新版本schema) + messageAttachments = this.sourceDb + .prepare( + `SELECT + attachment_id, type, content, created_at + FROM message_attachments + WHERE message_id = ?` + ) + .all(oldMsgId) as any[] + + // 为缺失的字段设置默认值 + messageAttachments = messageAttachments.map((attachment) => ({ + ...attachment, + metadata: null + })) + } + + // 逐个导入message_attachments + for (const attachment of messageAttachments) { + const newAttachmentId = nanoid() + + try { + // 首先尝试包含metadata字段的INSERT + this.targetDb + .prepare( + `INSERT INTO message_attachments ( + attachment_id, message_id, type, content, created_at, metadata + ) VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + newAttachmentId, + newMsgId, + attachment.type, + attachment.content, + attachment.created_at, + attachment.metadata + ) + } catch { + // 如果失败,使用不包含metadata的INSERT(兼容新版本schema) + this.targetDb + .prepare( + `INSERT INTO message_attachments ( + attachment_id, message_id, type, content, created_at + ) VALUES (?, ?, ?, ?, ?)` + ) + .run( + newAttachmentId, + newMsgId, + attachment.type, + attachment.content, + attachment.created_at + ) + } + } + } + + /** + * 关闭数据库连接 + */ + public close(): void { + if (this.sourceDb) { + this.sourceDb.close() + } + if (this.targetDb) { + this.targetDb.close() + } + } +} diff --git a/src/main/presenter/sqlitePresenter/index.ts b/src/main/presenter/sqlitePresenter/index.ts new file mode 100644 index 0000000..5c95967 --- /dev/null +++ b/src/main/presenter/sqlitePresenter/index.ts @@ -0,0 +1,356 @@ +import Database from 'better-sqlite3-multiple-ciphers' +import path from 'path' +import fs from 'fs' +import { ConversationsTable } from './tables/conversations' +import { MessagesTable } from './tables/messages' +import { AttachmentsTable } from './tables/attachments' +import { + ISQLitePresenter, + SQLITE_MESSAGE, + CONVERSATION, + CONVERSATION_SETTINGS +} from '@shared/presenter' +import { MessageAttachmentsTable } from './tables/messageAttachments' + +/** + * 导入模式枚举 + */ +export enum ImportMode { + INCREMENT = 'increment', // 增量导入 + OVERWRITE = 'overwrite' // 覆盖导入 +} + +export class SQLitePresenter implements ISQLitePresenter { + private db!: Database.Database + private conversationsTable!: ConversationsTable + private messagesTable!: MessagesTable + private attachmentsTable!: AttachmentsTable + private messageAttachmentsTable!: MessageAttachmentsTable + private currentVersion: number = 0 + private dbPath: string + + constructor(dbPath: string, password?: string) { + this.dbPath = dbPath + try { + // 确保数据库目录存在 + const dbDir = path.dirname(dbPath) + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }) + } + + // 初始化数据库连接 + this.db = new Database(dbPath) + this.db.pragma('journal_mode = WAL') + + if (password) { + this.db.pragma(`cipher='sqlcipher'`) + this.db.pragma(`key='${password}'`) + } + + // 尝试执行一个简单的查询来验证数据库是否正常 + this.db.prepare('SELECT 1').get() + + // 初始化所有表 + this.initTables() + + // 初始化版本表 + this.initVersionTable() + + // 执行迁移 + this.migrate() + } catch (error) { + console.error('Database initialization failed:', error) + + // 如果数据库已经打开,先关闭它 + if (this.db) { + try { + this.db.close() + } catch (closeError) { + console.error('Error closing database:', closeError) + } + } + + // 备份现有的损坏数据库 + this.backupDatabase() + + // 删除现有的数据库文件和相关的 WAL/SHM 文件 + this.cleanupDatabaseFiles() + + // 重新创建一个新的数据库 + this.db = new Database(dbPath) + this.db.pragma('journal_mode = WAL') + + if (password) { + this.db.pragma(`cipher='sqlcipher'`) + this.db.pragma(`key='${password}'`) + } + + // 重新初始化数据库 + this.initTables() + this.initVersionTable() + this.migrate() + } + } + async deleteAllMessagesInConversation(conversationId: string): Promise { + return this.messagesTable.deleteAllInConversation(conversationId) + } + + private backupDatabase(): void { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + const backupPath = `${this.dbPath}.${timestamp}.bak` + + try { + if (fs.existsSync(this.dbPath)) { + fs.copyFileSync(this.dbPath, backupPath) + console.log(`Database backed up to: ${backupPath}`) + } + } catch (error) { + console.error('Error creating database backup:', error) + } + } + + private cleanupDatabaseFiles(): void { + const filesToDelete = [this.dbPath, `${this.dbPath}-wal`, `${this.dbPath}-shm`] + + for (const file of filesToDelete) { + try { + if (fs.existsSync(file)) { + fs.unlinkSync(file) + console.log(`Deleted file: ${file}`) + } + } catch (error) { + console.error(`Error deleting file ${file}:`, error) + } + } + } + + renameConversation(conversationId: string, title: string): Promise { + this.conversationsTable.rename(conversationId, title) + return this.getConversation(conversationId) + } + + private initTables() { + this.conversationsTable = new ConversationsTable(this.db) + this.messagesTable = new MessagesTable(this.db) + this.attachmentsTable = new AttachmentsTable(this.db) + this.messageAttachmentsTable = new MessageAttachmentsTable(this.db) + + // 创建所有表 + this.conversationsTable.createTable() + this.messagesTable.createTable() + this.attachmentsTable.createTable() + this.messageAttachmentsTable.createTable() + } + + private initVersionTable() { + this.db.exec(` + CREATE TABLE IF NOT EXISTS schema_versions ( + version INTEGER PRIMARY KEY, + applied_at INTEGER NOT NULL + ) + `) + + const result = this.db.prepare('SELECT MAX(version) as version FROM schema_versions').get() as { + version: number + applied_at: number + } + this.currentVersion = result?.version || 0 + } + + private migrate() { + // 获取所有表的迁移脚本 + const migrations = new Map() + const tables = [ + this.conversationsTable, + this.messagesTable, + this.attachmentsTable, + this.messageAttachmentsTable + ] + + // 获取最新的迁移版本 + const latestVersion = tables.reduce((maxVersion, table) => { + const tableMaxVersion = table.getLatestVersion?.() || 0 + return Math.max(maxVersion, tableMaxVersion) + }, 0) + + // 只迁移未执行的版本 + tables.forEach((table) => { + for (let version = this.currentVersion + 1; version <= latestVersion; version++) { + const sql = table.getMigrationSQL?.(version) + if (sql) { + if (!migrations.has(version)) { + migrations.set(version, []) + } + migrations.get(version)?.push(sql) + } + } + }) + + // 按版本号顺序执行迁移 + const versions = Array.from(migrations.keys()).sort((a, b) => a - b) + + for (const version of versions) { + const migrationSQLs = migrations.get(version) || [] + if (migrationSQLs.length > 0) { + console.log(`Executing migration version ${version}`) + this.db.transaction(() => { + migrationSQLs.forEach((sql) => { + console.log(`Executing SQL: ${sql}`) + this.db.exec(sql) + }) + this.db + .prepare('INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)') + .run(version, Date.now()) + })() + } + } + } + + // 关闭数据库连接 + public close() { + this.db.close() + } + + // 创建新对话 + public async createConversation( + title: string, + settings: Partial = {} + ): Promise { + return this.conversationsTable.create(title, settings) + } + + // 获取对话信息 + public async getConversation(conversationId: string): Promise { + return this.conversationsTable.get(conversationId) + } + + // 更新对话信息 + public async updateConversation( + conversationId: string, + data: Partial + ): Promise { + return this.conversationsTable.update(conversationId, data) + } + + // 获取对话列表 + public async getConversationList( + page: number, + pageSize: number + ): Promise<{ total: number; list: CONVERSATION[] }> { + return this.conversationsTable.list(page, pageSize) + } + + // 获取对话总数 + public async getConversationCount(): Promise { + return this.conversationsTable.count() + } + + // 删除对话 + public async deleteConversation(conversationId: string): Promise { + return this.conversationsTable.delete(conversationId) + } + + // 插入消息 + public async insertMessage( + conversationId: string, + content: string, + role: string, + parentId: string, + metadata: string = '{}', + orderSeq: number = 0, + tokenCount: number = 0, + status: string = 'pending', + isContextEdge: number = 0, + isVariant: number = 0 + ): Promise { + return this.messagesTable.insert( + conversationId, + content, + role, + parentId, + metadata, + orderSeq, + tokenCount, + status, + isContextEdge, + isVariant + ) + } + + // 查询消息 + public async queryMessages(conversationId: string): Promise { + return this.messagesTable.query(conversationId) + } + + // 更新消息 + public async updateMessage( + messageId: string, + data: { + content?: string + status?: string + metadata?: string + isContextEdge?: number + tokenCount?: number + } + ): Promise { + return this.messagesTable.update(messageId, data) + } + + // 删除消息 + public async deleteMessage(messageId: string): Promise { + return this.messagesTable.delete(messageId) + } + + // 获取单条消息 + public async getMessage(messageId: string): Promise { + return this.messagesTable.get(messageId) + } + + // 获取消息变体 + public async getMessageVariants(messageId: string): Promise { + return this.messagesTable.getVariants(messageId) + } + + // 获取会话的最大消息序号 + public async getMaxOrderSeq(conversationId: string): Promise { + return this.messagesTable.getMaxOrderSeq(conversationId) + } + + // 删除所有消息 + public async deleteAllMessages(): Promise { + return this.messagesTable.deleteAll() + } + + // 执行事务 + public async runTransaction(operations: () => void): Promise { + await this.db.transaction(operations)() + } + + public async getLastUserMessage(conversationId: string): Promise { + return this.messagesTable.getLastUserMessage(conversationId) + } + + public async getMainMessageByParentId( + conversationId: string, + parentId: string + ): Promise { + return this.messagesTable.getMainMessageByParentId(conversationId, parentId) + } + + // 添加消息附件 + public async addMessageAttachment( + messageId: string, + attachmentType: string, + attachmentData: string + ): Promise { + return this.messageAttachmentsTable.add(messageId, attachmentType, attachmentData) + } + + // 获取消息附件 + public async getMessageAttachments( + messageId: string, + type: string + ): Promise<{ content: string }[]> { + return this.messageAttachmentsTable.get(messageId, type) + } +} diff --git a/src/main/presenter/sqlitePresenter/tables/attachments.ts b/src/main/presenter/sqlitePresenter/tables/attachments.ts new file mode 100644 index 0000000..3993bc9 --- /dev/null +++ b/src/main/presenter/sqlitePresenter/tables/attachments.ts @@ -0,0 +1,39 @@ +import { BaseTable } from './baseTable' +import type Database from 'better-sqlite3-multiple-ciphers' + +export class AttachmentsTable extends BaseTable { + constructor(db: Database.Database) { + super(db, 'attachments') + } + + getCreateTableSQL(): string { + return ` + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + attach_id TEXT UNIQUE NOT NULL, + message_id TEXT NOT NULL, + attachment_type TEXT NOT NULL CHECK(attachment_type IN ('file', 'image', 'code', 'audio', 'video')), + file_name TEXT NOT NULL, + file_size INTEGER, + storage_type TEXT NOT NULL CHECK(storage_type IN ('path', 'blob', 'cloud')), + storage_path TEXT, + thumbnail BLOB, + vectorized INTEGER DEFAULT 0, + data_summary TEXT, + mime_type TEXT, + created_at INTEGER NOT NULL, + FOREIGN KEY(message_id) REFERENCES messages(id) ON DELETE CASCADE + ); + CREATE INDEX idx_attachment_type ON attachments(attachment_type); + CREATE INDEX idx_attachment_vector ON attachments(vectorized); + ` + } + + getMigrationSQL(): string | null { + return null + } + + getLatestVersion(): number { + return 0 + } +} diff --git a/src/main/presenter/sqlitePresenter/tables/baseTable.ts b/src/main/presenter/sqlitePresenter/tables/baseTable.ts new file mode 100644 index 0000000..72884fc --- /dev/null +++ b/src/main/presenter/sqlitePresenter/tables/baseTable.ts @@ -0,0 +1,36 @@ +import Database from 'better-sqlite3-multiple-ciphers' + +export abstract class BaseTable { + protected db: Database.Database + protected tableName: string + + constructor(db: Database.Database, tableName: string) { + this.db = db + this.tableName = tableName + } + + // 获取表创建SQL + abstract getCreateTableSQL(): string + + // 获取表升级SQL (如果有的话) + abstract getMigrationSQL?(version: number): string | null + + // 获取最新的迁移版本号 + abstract getLatestVersion(): number + + // 检查表是否存在 + protected tableExists(): boolean { + const result = this.db + .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`) + .get(this.tableName) as { name: string } | undefined + + return !!result + } + + // 执行表创建 + public createTable(): void { + if (!this.tableExists()) { + this.db.exec(this.getCreateTableSQL()) + } + } +} diff --git a/src/main/presenter/sqlitePresenter/tables/conversations.ts b/src/main/presenter/sqlitePresenter/tables/conversations.ts new file mode 100644 index 0000000..5e88009 --- /dev/null +++ b/src/main/presenter/sqlitePresenter/tables/conversations.ts @@ -0,0 +1,428 @@ +import { BaseTable } from './baseTable' +import type Database from 'better-sqlite3-multiple-ciphers' +import { CONVERSATION, CONVERSATION_SETTINGS } from '@shared/presenter' +import { nanoid } from 'nanoid' + +type ConversationRow = { + id: string + title: string + createdAt: number + updatedAt: number + systemPrompt: string + temperature: number + contextLength: number + maxTokens: number + providerId: string + modelId: string + artifacts: number + is_new: number + is_pinned: number + enabled_mcp_tools: string | null + thinking_budget: number | null + reasoning_effort: string | null + verbosity: string | null + enable_search: number | null + forced_search: number | null + search_strategy: string | null +} + +// 解析 JSON 字段 +function getJsonField(val: string | null | undefined, fallback: T): T { + try { + return val ? JSON.parse(val) : fallback + } catch { + return fallback + } +} + +export class ConversationsTable extends BaseTable { + constructor(db: Database.Database) { + super(db, 'conversations') + } + + getCreateTableSQL(): string { + return ` + CREATE TABLE IF NOT EXISTS conversations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conv_id TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + user_id INTEGER DEFAULT 0, + is_pinned INTEGER DEFAULT 0, + model_id TEXT DEFAULT 'gpt-4', + provider_id TEXT DEFAULT 'openai', + context_length INTEGER DEFAULT 10, + max_tokens INTEGER DEFAULT 2000, + temperature REAL DEFAULT 0.7, + system_prompt TEXT DEFAULT '', + context_chain TEXT DEFAULT '[]' + ); + CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); + CREATE INDEX idx_conversations_pinned ON conversations(is_pinned); + ` + } + getMigrationSQL(version: number): string | null { + if (version === 1) { + return ` + -- 添加 is_new 字段 + ALTER TABLE conversations ADD COLUMN is_new INTEGER DEFAULT 1; + + -- 移除 user_id 字段 + ALTER TABLE conversations DROP COLUMN user_id; + + -- 更新所有现有会话的 is_new 为 0 + UPDATE conversations SET is_new = 0; + ` + } + if (version === 2) { + return ` + -- 添加 artifacts 开关 + ALTER TABLE conversations ADD COLUMN artifacts INTEGER DEFAULT 0; + UPDATE conversations SET artifacts = 0; + ` + } + if (version === 3) { + return ` + --- 添加 enabled_mcp_tools 字段 + ALTER TABLE conversations ADD COLUMN enabled_mcp_tools TEXT DEFAULT '[]'; + ` + } + if (version === 4) { + return ` + -- 添加 thinking_budget 字段 + ALTER TABLE conversations ADD COLUMN thinking_budget INTEGER DEFAULT NULL; + ` + } + if (version === 5) { + return ` + -- 回滚脏数据 enabled_mcp_tools + UPDATE conversations SET enabled_mcp_tools = NULL WHERE enabled_mcp_tools = '[]'; + ` + } + if (version === 6) { + return ` + -- 添加 reasoning_effort 字段 + ALTER TABLE conversations ADD COLUMN reasoning_effort TEXT DEFAULT NULL; + + -- 添加 verbosity 字段 + ALTER TABLE conversations ADD COLUMN verbosity TEXT DEFAULT NULL; + ` + } + if (version === 7) { + return ` + -- 添加搜索相关字段 + ALTER TABLE conversations ADD COLUMN enable_search INTEGER DEFAULT NULL; + ALTER TABLE conversations ADD COLUMN forced_search INTEGER DEFAULT NULL; + ALTER TABLE conversations ADD COLUMN search_strategy TEXT DEFAULT NULL; + ` + } + + return null + } + + getLatestVersion(): number { + return 7 + } + + async create(title: string, settings: Partial = {}): Promise { + const insert = this.db.prepare(` + INSERT INTO conversations ( + conv_id, + title, + created_at, + updated_at, + system_prompt, + temperature, + context_length, + max_tokens, + provider_id, + model_id, + is_new, + artifacts, + is_pinned, + enabled_mcp_tools, + thinking_budget, + reasoning_effort, + verbosity, + enable_search, + forced_search, + search_strategy + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + const conv_id = nanoid() + const now = Date.now() + insert.run( + conv_id, + title, + now, + now, + settings.systemPrompt || '', + settings.temperature || 0.7, + settings.contextLength || 4000, + settings.maxTokens || 2000, + settings.providerId || 'openai', + settings.modelId || 'gpt-4', + 1, + settings.artifacts || 0, + 0, // Default is_pinned to 0 + settings.enabledMcpTools ? JSON.stringify(settings.enabledMcpTools) : 'NULL', + settings.thinkingBudget !== undefined ? settings.thinkingBudget : null, + settings.reasoningEffort !== undefined ? settings.reasoningEffort : null, + settings.verbosity !== undefined ? settings.verbosity : null, + settings.enableSearch !== undefined ? (settings.enableSearch ? 1 : 0) : null, + settings.forcedSearch !== undefined ? (settings.forcedSearch ? 1 : 0) : null, + settings.searchStrategy !== undefined ? settings.searchStrategy : null + ) + return conv_id + } + + async get(conversationId: string): Promise { + const result = this.db + .prepare( + ` + SELECT + conv_id as id, + title, + created_at as createdAt, + updated_at as updatedAt, + system_prompt as systemPrompt, + temperature, + context_length as contextLength, + max_tokens as maxTokens, + provider_id as providerId, + model_id as modelId, + is_new, + artifacts, + is_pinned, + enabled_mcp_tools, + thinking_budget, + reasoning_effort, + verbosity, + enable_search, + forced_search, + search_strategy + FROM conversations + WHERE conv_id = ? + ` + ) + .get(conversationId) as ConversationRow & { is_pinned: number } + + if (!result) { + throw new Error(`Conversation ${conversationId} not found`) + } + + return { + id: result.id, + title: result.title, + createdAt: result.createdAt, + updatedAt: result.updatedAt, + is_new: result.is_new, + is_pinned: result.is_pinned, + settings: { + systemPrompt: result.systemPrompt, + temperature: result.temperature, + contextLength: result.contextLength, + maxTokens: result.maxTokens, + providerId: result.providerId, + modelId: result.modelId, + artifacts: result.artifacts as 0 | 1, + enabledMcpTools: getJsonField(result.enabled_mcp_tools, undefined), + thinkingBudget: result.thinking_budget !== null ? result.thinking_budget : undefined, + reasoningEffort: result.reasoning_effort + ? (result.reasoning_effort as 'minimal' | 'low' | 'medium' | 'high') + : undefined, + verbosity: result.verbosity ? (result.verbosity as 'low' | 'medium' | 'high') : undefined, + enableSearch: result.enable_search !== null ? Boolean(result.enable_search) : undefined, + forcedSearch: result.forced_search !== null ? Boolean(result.forced_search) : undefined, + searchStrategy: result.search_strategy + ? (result.search_strategy as 'turbo' | 'max') + : undefined + } + } + } + + async update(conversationId: string, data: Partial): Promise { + const updates: string[] = [] + const params: (string | number)[] = [] + + if (data.title !== undefined) { + updates.push('title = ?') + params.push(data.title) + } + + if (data.is_new !== undefined) { + updates.push('is_new = ?') + params.push(data.is_new) + } + + if (data.is_pinned !== undefined) { + updates.push('is_pinned = ?') + params.push(data.is_pinned) + } + + if (data.settings) { + if (data.settings.systemPrompt !== undefined) { + updates.push('system_prompt = ?') + params.push(data.settings.systemPrompt) + } + if (data.settings.temperature !== undefined) { + updates.push('temperature = ?') + params.push(data.settings.temperature) + } + if (data.settings.contextLength !== undefined) { + updates.push('context_length = ?') + params.push(data.settings.contextLength) + } + if (data.settings.maxTokens !== undefined) { + updates.push('max_tokens = ?') + params.push(data.settings.maxTokens) + } + if (data.settings.providerId !== undefined) { + updates.push('provider_id = ?') + params.push(data.settings.providerId) + } + if (data.settings.modelId !== undefined) { + updates.push('model_id = ?') + params.push(data.settings.modelId) + } + if (data.settings.artifacts !== undefined) { + updates.push('artifacts = ?') + params.push(data.settings.artifacts) + } + if (data.settings.enabledMcpTools !== undefined) { + updates.push('enabled_mcp_tools = ?') + params.push(JSON.stringify(data.settings.enabledMcpTools)) + } + if (data.settings.thinkingBudget !== undefined) { + updates.push('thinking_budget = ?') + params.push(data.settings.thinkingBudget) + } + if (data.settings.reasoningEffort !== undefined) { + updates.push('reasoning_effort = ?') + params.push(data.settings.reasoningEffort) + } + if (data.settings.verbosity !== undefined) { + updates.push('verbosity = ?') + params.push(data.settings.verbosity) + } + if (data.settings.enableSearch !== undefined) { + updates.push('enable_search = ?') + params.push(data.settings.enableSearch ? 1 : 0) + } + if (data.settings.forcedSearch !== undefined) { + updates.push('forced_search = ?') + params.push(data.settings.forcedSearch ? 1 : 0) + } + if (data.settings.searchStrategy !== undefined) { + updates.push('search_strategy = ?') + params.push(data.settings.searchStrategy) + } + } + if (updates.length > 0 || data.updatedAt) { + updates.push('updated_at = ?') + params.push(data.updatedAt || Date.now()) + + const updateStmt = this.db.prepare(` + UPDATE conversations + SET ${updates.join(', ')} + WHERE conv_id = ? + `) + params.push(conversationId) + updateStmt.run(...params) + } + } + + async delete(conversationId: string): Promise { + const deleteStmt = this.db.prepare('DELETE FROM conversations WHERE conv_id = ?') + deleteStmt.run(conversationId) + } + + async list(page: number, pageSize: number): Promise<{ total: number; list: CONVERSATION[] }> { + const offset = (page - 1) * pageSize + + const totalResult = this.db.prepare('SELECT COUNT(*) as count FROM conversations').get() as { + count: number + } + + const results = this.db + .prepare( + ` + SELECT + conv_id as id, + title, + created_at as createdAt, + updated_at as updatedAt, + system_prompt as systemPrompt, + temperature, + context_length as contextLength, + max_tokens as maxTokens, + provider_id as providerId, + model_id as modelId, + is_new, + artifacts, + is_pinned, + enabled_mcp_tools, + thinking_budget, + reasoning_effort, + verbosity, + enable_search, + forced_search, + search_strategy + FROM conversations + ORDER BY updated_at DESC + LIMIT ? OFFSET ? + ` + ) + .all(pageSize, offset) as ConversationRow[] + + return { + total: totalResult.count, + list: results.map((row) => ({ + id: row.id, + title: row.title, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + is_new: row.is_new, + is_pinned: row.is_pinned, + settings: { + systemPrompt: row.systemPrompt, + temperature: row.temperature, + contextLength: row.contextLength, + maxTokens: row.maxTokens, + providerId: row.providerId, + modelId: row.modelId, + artifacts: row.artifacts as 0 | 1, + enabledMcpTools: getJsonField(row.enabled_mcp_tools, undefined), + thinkingBudget: row.thinking_budget !== null ? row.thinking_budget : undefined, + reasoningEffort: row.reasoning_effort + ? (row.reasoning_effort as 'minimal' | 'low' | 'medium' | 'high') + : undefined, + verbosity: row.verbosity ? (row.verbosity as 'low' | 'medium' | 'high') : undefined, + enableSearch: row.enable_search !== null ? Boolean(row.enable_search) : undefined, + forcedSearch: row.forced_search !== null ? Boolean(row.forced_search) : undefined, + searchStrategy: row.search_strategy ? (row.search_strategy as 'turbo' | 'max') : undefined + } + })) + } + } + + async rename(conversationId: string, title: string): Promise { + // 新增 updatedAt 更新 + const updateStmt = this.db.prepare(` + UPDATE conversations + SET title = ?, is_new = 0, updated_at = ? + WHERE conv_id = ? + `) + // 传入当前时间 + updateStmt.run(title, Date.now(), conversationId) + } + + async count(): Promise { + const result = this.db.prepare('SELECT COUNT(*) as count FROM conversations').get() as { + count: number + } + return result.count + } +} diff --git a/src/main/presenter/sqlitePresenter/tables/messageAttachments.ts b/src/main/presenter/sqlitePresenter/tables/messageAttachments.ts new file mode 100644 index 0000000..63d3329 --- /dev/null +++ b/src/main/presenter/sqlitePresenter/tables/messageAttachments.ts @@ -0,0 +1,61 @@ +import { Database } from 'better-sqlite3-multiple-ciphers' +import { BaseTable } from './baseTable' +import { nanoid } from 'nanoid' + +export class MessageAttachmentsTable extends BaseTable { + constructor(db: Database) { + super(db, 'message_attachments') + } + + getCreateTableSQL(): string { + return ` + CREATE TABLE IF NOT EXISTS message_attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + attachment_id TEXT UNIQUE NOT NULL, + message_id TEXT NOT NULL, + type TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL, + FOREIGN KEY(message_id) REFERENCES messages(msg_id) ON DELETE CASCADE + ); + CREATE INDEX idx_message_attachments_message ON message_attachments(message_id); + CREATE INDEX idx_message_attachments_type ON message_attachments(type); + ` + } + + getMigrationSQL(_version: number): string | null { + return null + } + + getLatestVersion(): number { + return 0 + } + + async add(messageId: string, attachmentType: string, attachmentData: string): Promise { + const attachmentId = nanoid() + const insert = this.db.prepare(` + INSERT INTO message_attachments ( + attachment_id, + message_id, + type, + content, + created_at + ) + VALUES (?, ?, ?, ?, ?) + `) + insert.run(attachmentId, messageId, attachmentType, attachmentData, Date.now()) + } + + async get(messageId: string, type: string): Promise<{ content: string }[]> { + return this.db + .prepare( + ` + SELECT content + FROM message_attachments + WHERE message_id = ? AND type = ? + ORDER BY created_at ASC + ` + ) + .all(messageId, type) as { content: string }[] + } +} diff --git a/src/main/presenter/sqlitePresenter/tables/messages.ts b/src/main/presenter/sqlitePresenter/tables/messages.ts new file mode 100644 index 0000000..5860dd3 --- /dev/null +++ b/src/main/presenter/sqlitePresenter/tables/messages.ts @@ -0,0 +1,361 @@ +import { Database } from 'better-sqlite3-multiple-ciphers' +import { BaseTable } from './baseTable' +import { SQLITE_MESSAGE } from '@shared/presenter' +import { nanoid } from 'nanoid' + +export class MessagesTable extends BaseTable { + constructor(db: Database) { + super(db, 'messages') + } + + getCreateTableSQL(): string { + return ` + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + msg_id TEXT UNIQUE NOT NULL, + conversation_id TEXT NOT NULL, + parent_id TEXT DEFAULT '', + role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'function')), + content TEXT NOT NULL, + created_at INTEGER NOT NULL, + order_seq INTEGER NOT NULL DEFAULT 0, + token_count INTEGER DEFAULT 0, + status TEXT DEFAULT 'sent' CHECK(status IN ('sent', 'pending', 'error')), + metadata TEXT, + is_context_edge INTEGER DEFAULT 0, + is_variant INTEGER DEFAULT 0, + FOREIGN KEY(conversation_id) REFERENCES conversations(conv_id) ON DELETE CASCADE + ); + CREATE INDEX idx_messages_session ON messages(conversation_id, order_seq); + CREATE INDEX idx_message_timeline ON messages(created_at DESC); + CREATE INDEX idx_message_context_edge ON messages(is_context_edge); + ` + } + + getMigrationSQL(_version: number): string | null { + return null + } + + getLatestVersion(): number { + return 0 + } + + createTable(): void { + if (!this.tableExists()) { + this.db.exec(this.getCreateTableSQL()) + } + } + + async insert( + conversationId: string, + content: string, + role: string, + parentId: string, + metadata: string = '{}', + orderSeq: number = 0, + tokenCount: number = 0, + status: string = 'pending', + isContextEdge: number = 0, + isVariant: number = 0 + ): Promise { + const insert = this.db.prepare(` + INSERT INTO messages ( + msg_id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `) + const msgId = nanoid() + insert.run( + msgId, + conversationId, + parentId, + content, + role, + Date.now(), + orderSeq, + tokenCount, + status, + metadata, + isContextEdge, + isVariant + ) + return msgId + } + + async update( + messageId: string, + data: { + content?: string + status?: string + metadata?: string + isContextEdge?: number + tokenCount?: number + } + ): Promise { + const updates: string[] = [] + const params: (string | number)[] = [] + + if (data.content !== undefined) { + updates.push('content = ?') + params.push(data.content) + } + if (data.status !== undefined) { + updates.push('status = ?') + params.push(data.status) + } + if (data.metadata !== undefined) { + updates.push('metadata = ?') + params.push(data.metadata) + } + if (data.isContextEdge !== undefined) { + updates.push('is_context_edge = ?') + params.push(data.isContextEdge) + } + if (data.tokenCount !== undefined) { + updates.push('token_count = ?') + params.push(data.tokenCount) + } + + if (updates.length > 0) { + const updateStmt = this.db.prepare(` + UPDATE messages + SET ${updates.join(', ')} + WHERE msg_id = ? + `) + params.push(messageId) + updateStmt.run(...params) + } + } + + async delete(messageId: string): Promise { + const deleteStmt = this.db.prepare('DELETE FROM messages WHERE msg_id = ?') + deleteStmt.run(messageId) + } + + async deleteAll(): Promise { + const deleteStmt = this.db.prepare('DELETE FROM messages') + deleteStmt.run() + } + + async deleteAllInConversation(conversationId: string): Promise { + const deleteStmt = this.db.prepare('DELETE FROM messages WHERE conversation_id = ?') + deleteStmt.run(conversationId) + } + + async get(messageId: string): Promise { + return this.db + .prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE msg_id = ? + ` + ) + .get(messageId) as SQLITE_MESSAGE | null + } + + async getVariants(messageId: string): Promise { + return this.db + .prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE parent_id = ? + ORDER BY created_at ASC + ` + ) + .all(messageId) as SQLITE_MESSAGE[] + } + + async getMaxOrderSeq(conversationId: string): Promise { + const result = this.db + .prepare('SELECT MAX(order_seq) as maxSeq FROM messages WHERE conversation_id = ?') + .get(conversationId) as { maxSeq: number } + return result.maxSeq || 0 + } + + async getLastUserMessage(conversationId: string): Promise { + return this.db + .prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE conversation_id = ? AND role = 'user' + ORDER BY created_at DESC + LIMIT 1 + ` + ) + .get(conversationId) as SQLITE_MESSAGE | null + } + + async getMainMessageByParentId( + conversationId: string, + parentId: string + ): Promise { + const mainMessage = this.db + .prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE conversation_id = ? + AND parent_id = ? + AND is_variant = 0 + ORDER BY created_at ASC + LIMIT 1 + ` + ) + .get(conversationId, parentId) as SQLITE_MESSAGE | null + + if (mainMessage) { + const variants = this.db + .prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE conversation_id = ? + AND parent_id = ? + AND is_variant = 1 + ORDER BY created_at ASC + ` + ) + .all(conversationId, parentId) as SQLITE_MESSAGE[] + + mainMessage.variants = variants + } + + return mainMessage + } + + async query(conversationId: string): Promise { + // 首先获取所有非变体消息 + const mainMessages = this.db + .prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE conversation_id = ? AND is_variant != 1 + ORDER BY created_at ASC, order_seq ASC + ` + ) + .all(conversationId) as SQLITE_MESSAGE[] + + // 对于每个助手消息,获取其变体 + const getVariants = this.db.prepare( + ` + SELECT + msg_id as id, + conversation_id, + parent_id, + content, + role, + created_at, + order_seq, + token_count, + status, + metadata, + is_context_edge, + is_variant + FROM messages + WHERE parent_id = ? AND is_variant = 1 + ORDER BY created_at ASC + ` + ) + + // 为每个助手消息添加变体 + return mainMessages.map((msg) => { + if (msg.role === 'assistant' && msg.parent_id !== '') { + const variants = getVariants.all(msg.parent_id) as SQLITE_MESSAGE[] + if (variants.length > 0) { + return { + ...msg, + variants + } + } + } + return msg + }) + } +} diff --git a/src/main/presenter/syncPresenter/index.ts b/src/main/presenter/syncPresenter/index.ts new file mode 100644 index 0000000..f4422b5 --- /dev/null +++ b/src/main/presenter/syncPresenter/index.ts @@ -0,0 +1,543 @@ +import { app, shell } from 'electron' +import path from 'path' +import fs from 'fs' +import { ISyncPresenter, IConfigPresenter, ISQLitePresenter } from '@shared/presenter' +import { eventBus, SendTarget } from '@/eventbus' +import { SYNC_EVENTS } from '@/events' +import { DataImporter } from '../sqlitePresenter/importData' +import { ImportMode } from '../sqlitePresenter/index' + +// 为配置文件定义接口 +interface AppSettings { + syncEnabled?: boolean + syncFolderPath?: string + lastSyncTime?: number + [key: string]: unknown +} + +export class SyncPresenter implements ISyncPresenter { + private configPresenter: IConfigPresenter + private sqlitePresenter: ISQLitePresenter + private isBackingUp: boolean = false + private backupTimer: NodeJS.Timeout | null = null + private readonly BACKUP_DELAY = 60 * 1000 // 60秒无变更后触发备份 + private readonly APP_SETTINGS_PATH = path.join(app.getPath('userData'), 'app-settings.json') + private readonly MCP_SETTINGS_PATH = path.join(app.getPath('userData'), 'mcp-settings.json') + private readonly PROVIDER_MODELS_DIR_PATH = path.join(app.getPath('userData'), 'provider_models') + private readonly DB_PATH = path.join(app.getPath('userData'), 'app_db', 'chat.db') + private readonly MODEL_CONFIG_PATH = path.join(app.getPath('userData'), 'model-config.json') + + constructor(configPresenter: IConfigPresenter, sqlitePresenter: ISQLitePresenter) { + this.configPresenter = configPresenter + this.sqlitePresenter = sqlitePresenter + this.init() + } + + public init(): void { + // 监听数据变更事件,触发备份计划 + this.listenForChanges() + } + + public destroy(): void { + // 清理定时器 + if (this.backupTimer) { + clearTimeout(this.backupTimer) + this.backupTimer = null + } + } + + /** + * 检查同步文件夹状态 + */ + public async checkSyncFolder(): Promise<{ exists: boolean; path: string }> { + const syncFolderPath = this.configPresenter.getSyncFolderPath() + const exists = fs.existsSync(syncFolderPath) + + return { exists, path: syncFolderPath } + } + + /** + * 打开同步文件夹 + */ + public async openSyncFolder(): Promise { + const { exists, path: syncFolderPath } = await this.checkSyncFolder() + + // 如果文件夹不存在,先创建它 + if (!exists) { + fs.mkdirSync(syncFolderPath, { recursive: true }) + } + + // 打开文件夹 + shell.openPath(syncFolderPath) + } + + /** + * 获取备份状态 + */ + public async getBackupStatus(): Promise<{ isBackingUp: boolean; lastBackupTime: number }> { + const lastBackupTime = this.configPresenter.getLastSyncTime() + return { isBackingUp: this.isBackingUp, lastBackupTime } + } + + /** + * 手动触发备份 + */ + public async startBackup(): Promise { + if (this.isBackingUp) { + return + } + + // 检查同步功能是否启用 + if (!this.configPresenter.getSyncEnabled()) { + throw new Error('sync.error.notEnabled') + } + + try { + await this.performBackup() + } catch (error: unknown) { + console.error('备份失败:', error) + eventBus.send( + SYNC_EVENTS.BACKUP_ERROR, + SendTarget.ALL_WINDOWS, + (error as Error).message || 'sync.error.unknown' + ) + throw error + } + } + + /** + * 取消备份操作 + */ + public async cancelBackup(): Promise { + if (this.backupTimer) { + clearTimeout(this.backupTimer) + this.backupTimer = null + } + this.isBackingUp = false + } + + /** + * 从同步文件夹导入数据 + */ + public async importFromSync( + importMode: ImportMode = ImportMode.INCREMENT + ): Promise<{ success: boolean; message: string }> { + // 检查同步文件夹是否存在 + const { exists, path: syncFolderPath } = await this.checkSyncFolder() + if (!exists) { + return { success: false, message: 'sync.error.folderNotExists' } + } + + // 检查是否有备份文件 + const dbBackupPath = path.join(syncFolderPath, 'chat.db') + const appSettingsBackupPath = path.join(syncFolderPath, 'app-settings.json') + const providerModelsBackupPath = path.join(syncFolderPath, 'provider_models') + const modelConfigBackupPath = path.join(syncFolderPath, 'model-config.json') + + if (!fs.existsSync(dbBackupPath) || !fs.existsSync(appSettingsBackupPath)) { + return { success: false, message: 'sync.error.noValidBackup' } + } + + // 发出导入开始事件 + eventBus.send(SYNC_EVENTS.IMPORT_STARTED, SendTarget.ALL_WINDOWS) + + try { + // 关闭数据库连接 + this.sqlitePresenter.close() + + // 备份当前文件 + const tempDbPath = path.join(app.getPath('temp'), `chat_${Date.now()}.db`) + const tempAppSettingsPath = path.join(app.getPath('temp'), `app_settings_${Date.now()}.json`) + const tempProviderModelsPath = path.join(app.getPath('temp'), `provider_models_${Date.now()}`) + const tempMcpSettingsPath = path.join(app.getPath('temp'), `mcp_settings_${Date.now()}.json`) + const tempModelConfigPath = path.join(app.getPath('temp'), `model_config_${Date.now()}.json`) + // 创建临时备份 + if (fs.existsSync(this.DB_PATH)) { + fs.copyFileSync(this.DB_PATH, tempDbPath) + } + + if (fs.existsSync(this.APP_SETTINGS_PATH)) { + fs.copyFileSync(this.APP_SETTINGS_PATH, tempAppSettingsPath) + } + + if (fs.existsSync(this.MCP_SETTINGS_PATH)) { + fs.copyFileSync(this.MCP_SETTINGS_PATH, tempMcpSettingsPath) + } + + // 备份模型配置文件 + if (fs.existsSync(this.MODEL_CONFIG_PATH)) { + fs.copyFileSync(this.MODEL_CONFIG_PATH, tempModelConfigPath) + } + + // 如果 provider_models 目录存在,备份整个目录 + if (fs.existsSync(this.PROVIDER_MODELS_DIR_PATH)) { + this.copyDirectory(this.PROVIDER_MODELS_DIR_PATH, tempProviderModelsPath) + } + + try { + if (importMode === ImportMode.OVERWRITE) { + fs.copyFileSync(dbBackupPath, this.DB_PATH) + } else { + // 使用 DataImporter 导入数据 + const importer = new DataImporter(dbBackupPath, this.DB_PATH) + const importedCount = await importer.importData() + console.log(`成功导入 ${importedCount} 个会话`) + importer.close() + } + // 合并 app-settings.json 文件 (排除同步相关的设置) + if (fs.existsSync(appSettingsBackupPath)) { + // 读取当前的 app-settings + let currentSettings: AppSettings = {} + if (fs.existsSync(this.APP_SETTINGS_PATH)) { + const currentContent = fs.readFileSync(this.APP_SETTINGS_PATH, 'utf-8') + currentSettings = JSON.parse(currentContent) + } + + // 读取备份的 app-settings + const backupContent = fs.readFileSync(appSettingsBackupPath, 'utf-8') + const backupSettings = JSON.parse(backupContent) + + // 保留当前的同步相关设置 + const syncSettings: AppSettings = { + syncEnabled: currentSettings.syncEnabled, + syncFolderPath: currentSettings.syncFolderPath, + lastSyncTime: currentSettings.lastSyncTime + } + + // 合并设置: 使用备份的设置,但保留同步相关设置 + const mergedSettings = { ...backupSettings, ...syncSettings } + + // 保存合并后的设置 + fs.writeFileSync(this.APP_SETTINGS_PATH, JSON.stringify(mergedSettings, null, 2), 'utf-8') + } + + // 如果存在 provider_models 备份,复制整个目录(直接覆盖) + if (fs.existsSync(providerModelsBackupPath)) { + // 清空当前 provider_models 目录 + if (fs.existsSync(this.PROVIDER_MODELS_DIR_PATH)) { + this.removeDirectory(this.PROVIDER_MODELS_DIR_PATH) + } + // 确保目标目录存在 + fs.mkdirSync(this.PROVIDER_MODELS_DIR_PATH, { recursive: true }) + // 复制备份目录到应用目录 + this.copyDirectory(providerModelsBackupPath, this.PROVIDER_MODELS_DIR_PATH) + } + + // 导入模型配置文件 + if (fs.existsSync(modelConfigBackupPath)) { + fs.copyFileSync(modelConfigBackupPath, this.MODEL_CONFIG_PATH) + } + + eventBus.send(SYNC_EVENTS.IMPORT_COMPLETED, SendTarget.ALL_WINDOWS) + return { success: true, message: 'sync.success.importComplete' } + } catch (error: unknown) { + console.error('导入文件失败,恢复备份:', error) + + // 恢复备份 + if (fs.existsSync(tempDbPath)) { + fs.copyFileSync(tempDbPath, this.DB_PATH) + } + + if (fs.existsSync(tempAppSettingsPath)) { + fs.copyFileSync(tempAppSettingsPath, this.APP_SETTINGS_PATH) + } + + if (fs.existsSync(tempMcpSettingsPath)) { + fs.copyFileSync(tempMcpSettingsPath, this.MCP_SETTINGS_PATH) + } + + if (fs.existsSync(tempProviderModelsPath)) { + if (fs.existsSync(this.PROVIDER_MODELS_DIR_PATH)) { + this.removeDirectory(this.PROVIDER_MODELS_DIR_PATH) + } + this.copyDirectory(tempProviderModelsPath, this.PROVIDER_MODELS_DIR_PATH) + } + + // 恢复模型配置文件 + if (fs.existsSync(tempModelConfigPath)) { + fs.copyFileSync(tempModelConfigPath, this.MODEL_CONFIG_PATH) + } + + eventBus.send( + SYNC_EVENTS.IMPORT_ERROR, + SendTarget.ALL_WINDOWS, + (error as Error).message || 'sync.error.unknown' + ) + return { success: false, message: 'sync.error.importFailed' } + } finally { + // 清理临时文件 + if (fs.existsSync(tempDbPath)) { + fs.unlinkSync(tempDbPath) + } + + if (fs.existsSync(tempAppSettingsPath)) { + fs.unlinkSync(tempAppSettingsPath) + } + + if (fs.existsSync(tempMcpSettingsPath)) { + fs.unlinkSync(tempMcpSettingsPath) + } + + if (fs.existsSync(tempProviderModelsPath)) { + this.removeDirectory(tempProviderModelsPath) + } + + // 清理模型配置临时文件 + if (fs.existsSync(tempModelConfigPath)) { + fs.unlinkSync(tempModelConfigPath) + } + } + } catch (error: unknown) { + console.error('导入过程出错:', error) + eventBus.send( + SYNC_EVENTS.IMPORT_ERROR, + SendTarget.ALL_WINDOWS, + (error as Error).message || 'sync.error.unknown' + ) + return { success: false, message: 'sync.error.importProcess' } + } + } + + /** + * 执行实际的备份操作 + */ + private async performBackup(): Promise { + // 标记备份开始 + this.isBackingUp = true + eventBus.send(SYNC_EVENTS.BACKUP_STARTED, SendTarget.ALL_WINDOWS) + + try { + const syncFolderPath = this.configPresenter.getSyncFolderPath() + + // 确保同步文件夹存在 + if (!fs.existsSync(syncFolderPath)) { + fs.mkdirSync(syncFolderPath, { recursive: true }) + } + + // 生成临时备份文件路径(防止导入过程中的文件冲突) + const tempDbBackupPath = path.join(syncFolderPath, `chat_${Date.now()}.db.tmp`) + const tempAppSettingsBackupPath = path.join( + syncFolderPath, + `app_settings_${Date.now()}.json.tmp` + ) + const tempProviderModelsBackupPath = path.join( + syncFolderPath, + `provider_models_${Date.now()}.tmp` + ) + const tempMcpSettingsBackupPath = path.join( + syncFolderPath, + `mcp_settings_${Date.now()}.json.tmp` + ) + const tempModelConfigBackupPath = path.join( + syncFolderPath, + `model_config_${Date.now()}.json.tmp` + ) + + const finalDbBackupPath = path.join(syncFolderPath, 'chat.db') + const finalAppSettingsBackupPath = path.join(syncFolderPath, 'app-settings.json') + const finalProviderModelsBackupPath = path.join(syncFolderPath, 'provider_models') + const finalMcpSettingsBackupPath = path.join(syncFolderPath, 'mcp-settings.json') + const finalModelConfigBackupPath = path.join(syncFolderPath, 'model-config.json') + + // 确保数据库文件存在 + if (!fs.existsSync(this.DB_PATH)) { + console.warn('数据库文件不存在:', this.DB_PATH) + throw new Error('sync.error.dbNotExists') + } + + // 确保配置文件存在 + if (!fs.existsSync(this.APP_SETTINGS_PATH)) { + console.warn('配置文件不存在:', this.APP_SETTINGS_PATH) + throw new Error('sync.error.configNotExists') + } + + // 备份数据库 + fs.copyFileSync(this.DB_PATH, tempDbBackupPath) + + // 备份配置文件(过滤掉同步相关的设置) + if (fs.existsSync(this.APP_SETTINGS_PATH)) { + const appSettingsContent = fs.readFileSync(this.APP_SETTINGS_PATH, 'utf-8') + const appSettings = JSON.parse(appSettingsContent) + + // 创建配置副本,不包含同步相关的设置 + const filteredSettings = { ...appSettings } + // 删除同步相关的设置 + delete filteredSettings.syncEnabled + delete filteredSettings.syncFolderPath + delete filteredSettings.lastSyncTime + + fs.writeFileSync( + tempAppSettingsBackupPath, + JSON.stringify(filteredSettings, null, 2), + 'utf-8' + ) + } + + // 备份 MCP 设置 + if (fs.existsSync(this.MCP_SETTINGS_PATH)) { + fs.copyFileSync(this.MCP_SETTINGS_PATH, tempMcpSettingsBackupPath) + } + + // 备份模型配置文件 + if (fs.existsSync(this.MODEL_CONFIG_PATH)) { + fs.copyFileSync(this.MODEL_CONFIG_PATH, tempModelConfigBackupPath) + } + + // 备份 provider_models 目录 + if (fs.existsSync(this.PROVIDER_MODELS_DIR_PATH)) { + // 确保临时目录存在 + fs.mkdirSync(tempProviderModelsBackupPath, { recursive: true }) + // 复制整个 provider_models 目录 + this.copyDirectory(this.PROVIDER_MODELS_DIR_PATH, tempProviderModelsBackupPath) + } + + // 检查临时文件是否成功创建 + if (!fs.existsSync(tempDbBackupPath)) { + throw new Error('sync.error.tempDbFailed') + } + + if (!fs.existsSync(tempAppSettingsBackupPath)) { + throw new Error('sync.error.tempConfigFailed') + } + + if (!fs.existsSync(tempMcpSettingsBackupPath)) { + throw new Error('sync.error.tempMcpSettingsFailed') + } + + // 重命名临时文件为最终文件 + if (fs.existsSync(finalDbBackupPath)) { + fs.unlinkSync(finalDbBackupPath) + } + + if (fs.existsSync(finalAppSettingsBackupPath)) { + fs.unlinkSync(finalAppSettingsBackupPath) + } + + // 如果存在之前的 provider_models 备份目录,删除它 + if (fs.existsSync(finalProviderModelsBackupPath)) { + this.removeDirectory(finalProviderModelsBackupPath) + } + + if (fs.existsSync(finalMcpSettingsBackupPath)) { + fs.unlinkSync(finalMcpSettingsBackupPath) + } + + // 清理之前的模型配置文件备份 + if (fs.existsSync(finalModelConfigBackupPath)) { + fs.unlinkSync(finalModelConfigBackupPath) + } + + // 确保临时文件存在后再执行重命名 + fs.renameSync(tempDbBackupPath, finalDbBackupPath) + fs.renameSync(tempAppSettingsBackupPath, finalAppSettingsBackupPath) + fs.renameSync(tempMcpSettingsBackupPath, finalMcpSettingsBackupPath) + + // 重命名模型配置文件 + if (fs.existsSync(tempModelConfigBackupPath)) { + fs.renameSync(tempModelConfigBackupPath, finalModelConfigBackupPath) + } + + // 重命名 provider_models 临时目录 + if (fs.existsSync(tempProviderModelsBackupPath)) { + fs.renameSync(tempProviderModelsBackupPath, finalProviderModelsBackupPath) + } + + // 更新最后备份时间 + const now = Date.now() + this.configPresenter.setLastSyncTime(now) + + // 发送备份完成事件 + eventBus.send(SYNC_EVENTS.BACKUP_COMPLETED, SendTarget.ALL_WINDOWS, now) + } catch (error: unknown) { + console.error('备份过程出错:', error) + eventBus.send( + SYNC_EVENTS.BACKUP_ERROR, + SendTarget.ALL_WINDOWS, + (error as Error).message || 'sync.error.unknown' + ) + throw error + } finally { + // 标记备份结束 + this.isBackingUp = false + } + } + + /** + * 监听数据变更事件,触发备份计划 + */ + private listenForChanges(): void { + // 监听多种数据变更事件,使用防抖逻辑触发备份 + const scheduleBackup = () => { + // 如果同步功能未启用,不执行备份 + if (!this.configPresenter.getSyncEnabled()) { + return + } + + // 清除现有定时器 + if (this.backupTimer) { + clearTimeout(this.backupTimer) + } + + // 设置新的定时器,延迟执行备份 + this.backupTimer = setTimeout(async () => { + if (!this.isBackingUp) { + try { + await this.performBackup() + } catch (error) { + console.error('自动备份失败:', error) + } + } + }, this.BACKUP_DELAY) + } + + // 监听消息相关变更 + eventBus.on(SYNC_EVENTS.DATA_CHANGED, scheduleBackup) + } + + /** + * 辅助方法:复制目录 + */ + private copyDirectory(source: string, target: string): void { + // 确保目标目录存在 + if (!fs.existsSync(target)) { + fs.mkdirSync(target, { recursive: true }) + } + + // 读取源目录 + const entries = fs.readdirSync(source, { withFileTypes: true }) + + // 复制每个文件和子目录 + for (const entry of entries) { + const srcPath = path.join(source, entry.name) + const destPath = path.join(target, entry.name) + + if (entry.isDirectory()) { + // 递归复制子目录 + this.copyDirectory(srcPath, destPath) + } else { + // 复制文件 + fs.copyFileSync(srcPath, destPath) + } + } + } + + /** + * 辅助方法:删除目录及其内容 + */ + private removeDirectory(dirPath: string): void { + if (fs.existsSync(dirPath)) { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name) + if (entry.isDirectory()) { + this.removeDirectory(fullPath) + } else { + fs.unlinkSync(fullPath) + } + } + + fs.rmdirSync(dirPath) + } + } +} diff --git a/src/main/presenter/tabPresenter.ts b/src/main/presenter/tabPresenter.ts new file mode 100644 index 0000000..cc35a09 --- /dev/null +++ b/src/main/presenter/tabPresenter.ts @@ -0,0 +1,989 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { eventBus } from '@/eventbus' +import { WINDOW_EVENTS, CONFIG_EVENTS, SYSTEM_EVENTS, TAB_EVENTS } from '@/events' +import { is } from '@electron-toolkit/utils' +import { ITabPresenter, TabCreateOptions, IWindowPresenter, TabData } from '@shared/presenter' +import { BrowserWindow, WebContentsView, shell, nativeImage } from 'electron' +import { join } from 'path' +import contextMenu from '@/contextMenuHelper' +import { getContextMenuLabels } from '@shared/i18n' +import { app } from 'electron' +import { addWatermarkToNativeImage } from '@/lib/watermark' +import { stitchImagesVertically } from '@/lib/scrollCapture' +import { presenter } from './' + +export class TabPresenter implements ITabPresenter { + // 全局标签页实例存储 + private tabs: Map = new Map() + + // 存储标签页状态 + private tabState: Map = new Map() + + // 窗口ID到其包含的标签页ID列表的映射 + private windowTabs: Map = new Map() + + // 标签页ID到其当前所属窗口ID的映射 + private tabWindowMap: Map = new Map() + + // 存储每个标签页的右键菜单处理器 + private tabContextMenuDisposers: Map void> = new Map() + + // WebContents ID 到 Tab ID 的映射 (用于IPC调用来源识别) + private webContentsToTabId: Map = new Map() + + private windowPresenter: IWindowPresenter // 窗口管理器实例 + + constructor(windowPresenter: IWindowPresenter) { + this.windowPresenter = windowPresenter // 注入窗口管理器 + this.initBusHandlers() + } + private onWindowSizeChange(windowId: number) { + const views = this.windowTabs.get(windowId) + const window = BrowserWindow.fromId(windowId) + if (window) { + views?.forEach((view) => { + const tabView = this.tabs.get(view) + if (tabView) { + this.updateViewBounds(window, tabView) + } + }) + } + } + // 初始化事件总线处理器 + private initBusHandlers(): void { + // 窗口尺寸变化,更新视图 bounds + eventBus.on(WINDOW_EVENTS.WINDOW_RESIZE, (windowId: number) => + this.onWindowSizeChange(windowId) + ) + eventBus.on(WINDOW_EVENTS.WINDOW_MAXIMIZED, (windowId: number) => { + setTimeout(() => { + this.onWindowSizeChange(windowId) + }, 100) + }) + eventBus.on(WINDOW_EVENTS.WINDOW_UNMAXIMIZED, (windowId: number) => { + setTimeout(() => { + this.onWindowSizeChange(windowId) + }, 100) + }) + + // 窗口关闭,分离包含的视图 + eventBus.on(WINDOW_EVENTS.WINDOW_CLOSED, (windowId: number) => { + const views = this.windowTabs.get(windowId) + const window = BrowserWindow.fromId(windowId) + if (window) { + views?.forEach((viewId) => { + const view = this.tabs.get(viewId) + if (view) { + this.detachViewFromWindow(window, view) + } + }) + } + }) + + // 语言设置改变,更新所有标签页右键菜单 + eventBus.on(CONFIG_EVENTS.SETTING_CHANGED, async (key) => { + if (key === 'language') { + // 为所有活动的标签页更新右键菜单 + for (const [tabId] of this.tabWindowMap.entries()) { + await this.setupTabContextMenu(tabId) + } + } + }) + + // 系统主题更新,通知所有标签页 + eventBus.on(SYSTEM_EVENTS.SYSTEM_THEME_UPDATED, (isDark: boolean) => { + // 向所有标签页广播主题更新 + for (const [, view] of this.tabs.entries()) { + if (!view.webContents.isDestroyed()) { + view.webContents.send('system-theme-updated', isDark) + } + } + }) + } + + /** + * 创建新标签页并添加到指定窗口 + */ + async createTab( + windowId: number, + url: string, + options: TabCreateOptions = {} + ): Promise { + console.log('createTab', windowId, url, options) + const window = BrowserWindow.fromId(windowId) + if (!window) return null + + // 创建新的WebContentsView + const view = new WebContentsView({ + webPreferences: { + preload: join(__dirname, '../preload/index.mjs'), + sandbox: false, + devTools: is.dev + } + }) + + view.setBorderRadius(8) + view.setBackgroundColor('#00ffffff') + + // 加载内容 + if (url.startsWith('local://')) { + const viewType = url.replace('local://', '') + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + view.webContents.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/${viewType}`) + } else { + view.webContents.loadFile(join(__dirname, '../renderer/index.html'), { + hash: `/${viewType}` + }) + } + } else { + view.webContents.loadURL(url) + } + + if (is.dev) { + view.webContents.openDevTools({ mode: 'detach' }) + } + + // 存储标签信息 + const tabId = view.webContents.id + this.tabs.set(tabId, view) + this.tabState.set(tabId, { + id: tabId, + title: url, + isActive: options.active ?? true, + url: url, + closable: true, + position: options?.position ?? 0 + }) + + // 建立 WebContents ID 到 Tab ID 的映射 + this.webContentsToTabId.set(view.webContents.id, tabId) + + // 更新窗口-标签映射 + if (!this.windowTabs.has(windowId)) { + this.windowTabs.set(windowId, []) + } + + const tabs = this.windowTabs.get(windowId)! + const insertIndex = options.position !== undefined ? options.position : tabs.length + tabs.splice(insertIndex, 0, tabId) + + this.tabWindowMap.set(tabId, windowId) + + // 添加到窗口 + this.attachViewToWindow(window, view) + + // 如果需要激活,设置为活动标签 + if (options.active ?? true) { + await this.activateTab(tabId) + } + + // 在创建标签页后设置右键菜单 + await this.setupTabContextMenu(tabId) + + // 监听标签页相关事件 + this.setupWebContentsListeners(view.webContents, tabId, windowId) + + // 通知渲染进程更新标签列表 + await this.notifyWindowTabsUpdate(windowId) + + return tabId + } + + /** + * 销毁标签页 + */ + async closeTab(tabId: number): Promise { + return await this.destroyTab(tabId) + } + + /** + * 销毁标签页 + */ + async closeTabs(windowId: number): Promise { + const tabs = [...(this.windowTabs.get(windowId) ?? [])] + tabs.forEach((t) => this.closeTab(t)) + } + + /** + * 激活标签页 + */ + async switchTab(tabId: number): Promise { + return await this.activateTab(tabId) + } + + /** + * 获取标签页实例 + */ + async getTab(tabId: number): Promise { + return this.tabs.get(tabId) + } + + /** + * 销毁标签页 + */ + private async destroyTab(tabId: number): Promise { + // 清理右键菜单 + this.cleanupTabContextMenu(tabId) + + const view = this.tabs.get(tabId) + if (!view) return false + + const windowId = this.tabWindowMap.get(tabId) + if (!windowId) return false + + const window = BrowserWindow.fromId(windowId) + if (window) { + // 从窗口中移除视图 + this.detachViewFromWindow(window, view) + } + + // 移除事件监听 + this.removeWebContentsListeners(view.webContents) + + // 从数据结构中移除 + this.tabs.delete(tabId) + this.tabState.delete(tabId) + this.tabWindowMap.delete(tabId) + + // 广播Tab关闭事件 + eventBus.sendToMain(TAB_EVENTS.CLOSED, tabId) + + // 清除 WebContents 映射 + if (view) { + this.webContentsToTabId.delete(view.webContents.id) + } + + if (this.windowTabs.has(windowId)) { + const tabs = this.windowTabs.get(windowId)! + const index = tabs.indexOf(tabId) + if (index !== -1) { + tabs.splice(index, 1) + + // 如果还有其他标签并且关闭的是活动标签,激活相邻标签 + if (tabs.length > 0) { + const newActiveIndex = Math.min(index, tabs.length - 1) + await this.activateTab(tabs[newActiveIndex]) + } + } + + // 通知渲染进程更新标签列表 + await this.notifyWindowTabsUpdate(windowId) + } + + // 销毁视图 + view.webContents.close() + // Note: view.destroy() is also an option depending on Electron version/behavior + return true + } + + /** + * 激活标签页 + */ + private async activateTab(tabId: number): Promise { + const view = this.tabs.get(tabId) + if (!view) return false + + const windowId = this.tabWindowMap.get(tabId) + if (!windowId) return false + + const window = BrowserWindow.fromId(windowId) + if (!window) return false + + // 获取窗口中的所有标签 + const tabs = this.windowTabs.get(windowId) || [] + + // 更新所有标签的活动状态并处理视图显示/隐藏 + for (const id of tabs) { + const state = this.tabState.get(id) + const tabView = this.tabs.get(id) + if (state && tabView) { + state.isActive = id === tabId + tabView.setVisible(id === tabId) // 根据活动状态设置视图可见性 + } + } + + // 确保活动视图可见并位于最前 + this.bringViewToFront(window, view) + + // 通知渲染进程更新标签列表 + await this.notifyWindowTabsUpdate(windowId) + + // 通知渲染进程切换活动标签 + window.webContents.send('setActiveTab', windowId, tabId) + + return true + } + + /** + * 从当前窗口分离标签页(不销毁) + */ + async detachTab(tabId: number): Promise { + const view = this.tabs.get(tabId) + if (!view) return false + + const windowId = this.tabWindowMap.get(tabId) + if (!windowId) return false + + const window = BrowserWindow.fromId(windowId) + if (window) { + // 从窗口中移除视图 + this.detachViewFromWindow(window, view) + } + + // 从窗口标签列表中移除 + if (this.windowTabs.has(windowId)) { + const tabs = this.windowTabs.get(windowId)! + const index = tabs.indexOf(tabId) + if (index !== -1) { + tabs.splice(index, 1) + } + + // 通知渲染进程更新标签列表 + await this.notifyWindowTabsUpdate(windowId) + + // 如果窗口还有其他标签,激活一个 + if (tabs.length > 0) { + await this.activateTab(tabs[Math.min(index, tabs.length - 1)]) + } + } + + // 标记为已分离 + this.tabWindowMap.delete(tabId) + + return true + } + + /** + * 将标签页附加到目标窗口 + */ + async attachTab(tabId: number, targetWindowId: number, index?: number): Promise { + const view = this.tabs.get(tabId) + if (!view) return false + + const window = BrowserWindow.fromId(targetWindowId) + if (!window) return false + + // 确保目标窗口有标签列表 + if (!this.windowTabs.has(targetWindowId)) { + this.windowTabs.set(targetWindowId, []) + } + + // 添加到目标窗口的标签列表 + const tabs = this.windowTabs.get(targetWindowId)! + const insertIndex = index !== undefined ? index : tabs.length + tabs.splice(insertIndex, 0, tabId) + + // 更新标签所属窗口 + this.tabWindowMap.set(tabId, targetWindowId) + + // 将视图添加到窗口 + this.attachViewToWindow(window, view) + + // 激活标签 + await this.activateTab(tabId) + + // 通知渲染进程更新标签列表 + await this.notifyWindowTabsUpdate(targetWindowId) + + return true + } + + /** + * 将标签页从源窗口移动到目标窗口 + */ + async moveTab(tabId: number, targetWindowId: number, index?: number): Promise { + const windowId = this.tabWindowMap.get(tabId) + + // 如果已经在目标窗口中,仅调整位置 + if (windowId === targetWindowId) { + if (index !== undefined && this.windowTabs.has(windowId)) { + const tabs = this.windowTabs.get(windowId)! + const currentIndex = tabs.indexOf(tabId) + if (currentIndex !== -1 && currentIndex !== index) { + // 移除当前位置 + tabs.splice(currentIndex, 1) + + // 计算新的插入位置(考虑到移除元素后的索引变化) + const newIndex = index > currentIndex ? index - 1 : index + + // 插入到新位置 + tabs.splice(newIndex, 0, tabId) + // 通知渲染进程更新标签列表 + await this.notifyWindowTabsUpdate(windowId) + return true + } + } + return false + } + + // 从源窗口分离 + const detached = await this.detachTab(tabId) + if (!detached) return false + + // 附加到目标窗口 + return await this.attachTab(tabId, targetWindowId, index) + } + + /** + * 获取指定窗口中当前活动标签页的 ID。 + * 此方法位于 TabPresenter 中,因为它维护着 isActive 状态。 + * @param windowId 窗口 ID。 + * @returns 当前活动标签页的 ID;如果未找到活动标签页或窗口无效,则返回 undefined。 + */ + async getActiveTabId(windowId: number): Promise { + // 获取窗口对应的标签页 ID 列表 + const tabsInWindow = this.windowTabs.get(windowId) + if (!tabsInWindow) { + console.warn( + `TabPresenter: No tab list found for window ${windowId} when getting active tab ID.` + ) + return undefined + } + + // 遍历标签页列表,查找第一个标记为活动的标签页 + for (const tabId of tabsInWindow) { + const state = this.tabState.get(tabId) + // 检查状态是否存在且 isActive 为 true + if (state?.isActive) { + return tabId // 返回活动标签页 ID + } + } + + // 未找到活动标签页 + console.log(`TabPresenter: No active tab found for window ${windowId}.`) + return undefined + } + + /** + * 获取窗口的所有标签数据 + */ + async getWindowTabsData(windowId: number): Promise { + const tabsInWindow = this.windowTabs.get(windowId) || [] + return tabsInWindow.map((tabId) => { + const state = this.tabState.get(tabId) || ({} as TabData) + return state + }) + } + + /** + * 根据 WebContents ID 获取对应的 Tab ID + * @param webContentsId WebContents ID + * @returns Tab ID,如果未找到则返回 undefined + */ + getTabIdByWebContentsId(webContentsId: number): number | undefined { + return this.webContentsToTabId.get(webContentsId) + } + + /** + * 根据 WebContents ID 获取对应的窗口ID + * @param webContentsId WebContents ID + * @returns 窗口ID,如果未找到则返回 undefined + */ + getWindowIdByWebContentsId(webContentsId: number): number | undefined { + const tabId = this.getTabIdByWebContentsId(webContentsId) + return tabId ? this.tabWindowMap.get(tabId) : undefined + } + + /** + * 通知渲染进程更新标签列表 + */ + async notifyWindowTabsUpdate(windowId: number): Promise { + const window = BrowserWindow.fromId(windowId) + if (!window || window.isDestroyed()) return + + // Await the internal async call + const tabListData = await this.getWindowTabsData(windowId) + + if (!window.isDestroyed() && window.webContents && !window.webContents.isDestroyed()) { + // Sending IPC is typically synchronous + window.webContents.send('update-window-tabs', windowId, tabListData) + } + } + + /** + * 为WebContents设置事件监听 + */ + private setupWebContentsListeners( + webContents: Electron.WebContents, + tabId: number, + windowId: number + ): void { + // 处理外部链接 + webContents.setWindowOpenHandler(({ url }) => { + // 使用系统默认浏览器打开链接 + shell.openExternal(url) + return { action: 'deny' } + }) + + // 标题变更 + webContents.on('page-title-updated', (_event, title) => { + const state = this.tabState.get(tabId) + if (state) { + state.title = title || state.url || 'Untitled' + // 通知渲染进程标题已更新 + const window = BrowserWindow.fromId(windowId) + if (window && !window.isDestroyed()) { + window.webContents.send(TAB_EVENTS.TITLE_UPDATED, { + tabId, + title: state.title, + windowId + }) + } + this.notifyWindowTabsUpdate(windowId).catch(console.error) // Call async function, handle potential rejection + } + }) + + // 检查是否是窗口的第一个标签页 + const isFirstTab = this.windowTabs.get(windowId)?.length === 1 + + // 页面加载完成 + if (isFirstTab) { + eventBus.sendToMain(WINDOW_EVENTS.READY_TO_SHOW) + // Once did-finish-load happens, emit first content loaded + webContents.once('did-finish-load', () => { + eventBus.sendToMain(WINDOW_EVENTS.FIRST_CONTENT_LOADED, windowId) + setTimeout(() => { + const windowPresenter = presenter.windowPresenter as any + if (windowPresenter && typeof windowPresenter.focusActiveTab === 'function') { + windowPresenter.focusActiveTab(windowId, 'initial') + } + }, 300) + }) + } + + // Favicon变更 + webContents.on('page-favicon-updated', (_event, favicons) => { + if (favicons.length > 0) { + const state = this.tabState.get(tabId) + if (state) { + if (state.icon !== favicons[0]) { + console.log('page-favicon-updated', state.icon, favicons[0]) + state.icon = favicons[0] + this.notifyWindowTabsUpdate(windowId).catch(console.error) // Call async function, handle potential rejection + } + } + } + }) + + // 导航完成 + webContents.on('did-navigate', (_event, url) => { + const state = this.tabState.get(tabId) + if (state) { + state.url = url + // 如果没有标题,使用URL作为标题 + if (!state.title || state.title === 'Untitled') { + state.title = url + const window = BrowserWindow.fromId(windowId) + if (window && !window.isDestroyed()) { + window.webContents.send(TAB_EVENTS.TITLE_UPDATED, { + tabId, + title: state.title, + windowId + }) + } + this.notifyWindowTabsUpdate(windowId).catch(console.error) // Call async function, handle potential rejection + } + } + }) + } + + /** + * 移除WebContents的事件监听 + */ + private removeWebContentsListeners(webContents: Electron.WebContents): void { + webContents.removeAllListeners('page-title-updated') + webContents.removeAllListeners('page-favicon-updated') + webContents.removeAllListeners('did-navigate') + webContents.removeAllListeners('did-finish-load') + webContents.setWindowOpenHandler(() => ({ action: 'allow' })) + } + + /** + * 将视图添加到窗口 + * 注意:实际实现可能需要根据Electron窗口布局策略调整 + */ + private attachViewToWindow(window: BrowserWindow, view: WebContentsView): void { + // 这里需要根据实际窗口结构实现 + // 简单实现可能是: + window.contentView.addChildView(view) + this.updateViewBounds(window, view) + } + + /** + * 从窗口中分离视图 + */ + private detachViewFromWindow(window: BrowserWindow, view: WebContentsView): void { + // 这里需要根据实际窗口结构实现 + window.contentView.removeChildView(view) + } + + /** + * 将视图带到前面(激活) + */ + private bringViewToFront(window: BrowserWindow, view: WebContentsView): void { + // Re-adding ensures it's on top in most view hierarchies + window.contentView.addChildView(view) + this.updateViewBounds(window, view) + if (!view.webContents.isDestroyed()) { + view.webContents.focus() + } + } + + /** + * 更新视图大小以适应窗口 + */ + private updateViewBounds(window: BrowserWindow, view: WebContentsView): void { + // 获取窗口尺寸 + const { width, height } = window.getContentBounds() + + // 设置视图位置大小(留出顶部标签栏空间) + const TAB_BAR_HEIGHT = 40 // 标签栏高度,需要根据实际UI调整 + view.setBounds({ + x: 4, + y: TAB_BAR_HEIGHT, + width: width - 8, + height: height - TAB_BAR_HEIGHT - 4 + }) + } + + /** + * 为标签页设置右键菜单 + */ + private async setupTabContextMenu(tabId: number): Promise { + const view = this.tabs.get(tabId) + if (!view || view.webContents.isDestroyed()) return + + // 如果已存在处理器,先清理 + if (this.tabContextMenuDisposers.has(tabId)) { + this.tabContextMenuDisposers.get(tabId)?.() + this.tabContextMenuDisposers.delete(tabId) + } + + const lang = app.getLocale() + const labels = await getContextMenuLabels(lang) + + const disposer = contextMenu({ + webContents: view.webContents, + labels, + shouldShowMenu() { + return true + } + }) + + this.tabContextMenuDisposers.set(tabId, disposer) + } + + /** + * 清理标签页的右键菜单 + */ + private cleanupTabContextMenu(tabId: number): void { + if (this.tabContextMenuDisposers.has(tabId)) { + this.tabContextMenuDisposers.get(tabId)?.() + this.tabContextMenuDisposers.delete(tabId) + } + } + + // 清理Presenter资源 + public async destroy(): Promise { + // 清理所有标签页的右键菜单 + for (const [tabId] of this.tabContextMenuDisposers) { + this.cleanupTabContextMenu(tabId) + } + this.tabContextMenuDisposers.clear() + + // 销毁所有标签页 + // 使用 `for...of` 循环确保每个 closeTab 调用都被 await + for (const [tabId] of this.tabWindowMap.entries()) { + console.log(`Destroying resources for tab: ${tabId}`) + await this.closeTab(tabId) + } + + // 清理所有映射 + this.tabWindowMap.clear() + this.tabs.clear() + this.tabState.clear() + this.windowTabs.clear() + this.webContentsToTabId.clear() + } + + /** + * 重排序窗口内的标签页 + */ + async reorderTabs(windowId: number, tabIds: number[]): Promise { + console.log('reorderTabs', windowId, tabIds) + + const windowTabs = this.windowTabs.get(windowId) + if (!windowTabs) return false + + for (const tabId of tabIds) { + if (!windowTabs.includes(tabId)) { + console.warn(`Tab ${tabId} does not belong to window ${windowId}`) + return false + } + } + + if (tabIds.length !== windowTabs.length) { + console.warn('Tab count mismatch in reorder operation') + return false + } + + this.windowTabs.set(windowId, [...tabIds]) + + tabIds.forEach((tabId, index) => { + const tabState = this.tabState.get(tabId) + if (tabState) { + tabState.position = index + } + }) + + await this.notifyWindowTabsUpdate(windowId) + + return true + } + + // 将标签页移动到新窗口 + async moveTabToNewWindow(tabId: number, screenX?: number, screenY?: number): Promise { + const tabInfo = this.tabState.get(tabId) + const originalWindowId = this.tabWindowMap.get(tabId) + + if (!tabInfo || originalWindowId === undefined) { + console.error(`moveTabToNewWindow: Tab ${tabId} not found or no window associated.`) + return false + } + + // 1. 从当前窗口分离标签页 + const detached = await this.detachTab(tabId) + if (!detached) { + console.error( + `moveTabToNewWindow: Failed to detach tab ${tabId} from window ${originalWindowId}.` + ) + // Consider reattaching here on failure if that's the desired fallback + // await this.attachTab(tabId, originalWindowId); + return false + } + + // 2. 创建新窗口 + const newWindowOptions: Record = { + forMovedTab: true, + activateTabId: tabId // Pass the tabId to the new window presenter to activate it + } + if (screenX !== undefined && screenY !== undefined) { + newWindowOptions.x = screenX + newWindowOptions.y = screenY + } + + const newWindowId = await this.windowPresenter.createShellWindow(newWindowOptions) + + if (newWindowId === null) { + console.error('moveTabToNewWindow: Failed to create a new window.') + // Reattach to original window if new window creation fails + await this.attachTab(tabId, originalWindowId) + return false + } + + // 3. 将标签页附加到新窗口 + const attached = await this.attachTab(tabId, newWindowId) + if (!attached) { + console.error( + `moveTabToNewWindow: Failed to attach tab ${tabId} to new window ${newWindowId}.` + ) + // Reattach to original window if attaching fails + await this.attachTab(tabId, originalWindowId) + // Optionally close the empty new window here: + // const newBrowserWindow = BrowserWindow.fromId(newWindowId); + // if (newBrowserWindow && !newBrowserWindow.isDestroyed()) newBrowserWindow.close(); + return false + } + + // console.log(`Tab ${tabId} moved from window ${originalWindowId} to new window ${newWindowId}`); // Kept concise log + // 通知原窗口更新标签列表 + await this.notifyWindowTabsUpdate(originalWindowId) + // 通知新窗口更新标签列表 + await this.notifyWindowTabsUpdate(newWindowId) + + return true + } + + /** + * 截取标签页指定区域的简单截图 + * @param tabId 标签页ID + * @param rect 截图区域 + * @returns 返回base64格式的图片数据,失败时返回null + */ + async captureTabArea( + tabId: number, + rect: { x: number; y: number; width: number; height: number } + ): Promise { + try { + const view = this.tabs.get(tabId) + if (!view || view.webContents.isDestroyed()) { + console.error(`captureTabArea: Tab ${tabId} not found or destroyed`) + return null + } + + // 使用Electron的capturePage API进行截图 + const image = await view.webContents.capturePage(rect) + + if (image.isEmpty()) { + console.error('Capture tab area: Captured image is empty') + return null + } + + // 转换为base64格式 + const base64Data = image.toDataURL() + return base64Data + } catch (error) { + console.error('Capture tab area error:', error) + return null + } + } + + /** + * 处理渲染进程标签页就绪事件 + * @param tabId 标签页ID + */ + async onRendererTabReady(tabId: number): Promise { + console.log(`Tab ${tabId} renderer ready`) + // 通过事件总线通知其他模块 + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, tabId) + } + + /** + * 处理渲染进程标签页激活事件 + * @param threadId 会话ID + */ + async onRendererTabActivated(threadId: string): Promise { + console.log(`Thread ${threadId} activated in renderer`) + // 通过事件总线通知其他模块 + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_ACTIVATED, threadId) + } + + /** + * 将多张截图拼接成长图并添加水印 + * @param imageDataList base64格式的图片数据数组 + * @param options 水印选项 + * @returns 返回拼接并添加水印后的base64图片数据,失败时返回null + */ + async stitchImagesWithWatermark( + imageDataList: string[], + options: { + isDark?: boolean + version?: string + texts?: { + brand?: string + time?: string + tip?: string + model?: string + provider?: string + } + } = {} + ): Promise { + try { + if (imageDataList.length === 0) { + console.error('stitchImagesWithWatermark: No images provided') + return null + } + + // 如果只有一张图片,直接添加水印 + if (imageDataList.length === 1) { + const nativeImageInstance = nativeImage.createFromDataURL(imageDataList[0]) + const watermarkedImage = await addWatermarkToNativeImage(nativeImageInstance, options) + return watermarkedImage.toDataURL() + } + + // 将base64图片转换为NativeImage,然后转换为Buffer + const imageBuffers = imageDataList.map((data) => { + const image = nativeImage.createFromDataURL(data) + return image.toPNG() + }) + + // 拼接图片 + const stitchedImage = await stitchImagesVertically(imageBuffers) + + // 添加水印 + const watermarkedImage = await addWatermarkToNativeImage(stitchedImage, options) + + // 转换为base64格式 + const base64Data = watermarkedImage.toDataURL() + + console.log(`Successfully stitched ${imageDataList.length} images with watermark`) + return base64Data + } catch (error) { + console.error('Stitch images with watermark error:', error) + return null + } + } + + /** + * 新增:检查一个Tab是否是其所在窗口的最后一个Tab + */ + async isLastTabInWindow(tabId: number): Promise { + const windowId = this.tabWindowMap.get(tabId) + if (windowId === undefined) return false + const tabsInWindow = this.windowTabs.get(windowId) || [] + return tabsInWindow.length === 1 + } + + /** + * 新增:将指定Tab重置到空白页(新建会话页) + */ + async resetTabToBlank(tabId: number): Promise { + const view = this.tabs.get(tabId) + if (view && !view.webContents.isDestroyed()) { + const url = 'local://chat' + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + view.webContents.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/chat`) + } else { + view.webContents.loadFile(join(__dirname, '../renderer/index.html'), { + hash: `/chat` + }) + } + // 更新 Tab 状态 + const state = this.tabState.get(tabId) + if (state) { + state.title = 'New Chat' + state.url = url + const windowId = this.tabWindowMap.get(tabId) + if (windowId) { + await this.notifyWindowTabsUpdate(windowId) + } + } + } + } + + registerFloatingWindow(webContentsId: number, webContents: Electron.WebContents): void { + try { + console.log(`TabPresenter: Registering floating window as virtual tab, ID: ${webContentsId}`) + if (this.tabs.has(webContentsId)) { + console.warn(`TabPresenter: Tab ${webContentsId} already exists, skipping registration`) + return + } + const virtualView = { + webContents: webContents, + setVisible: () => {}, + setBounds: () => {}, + getBounds: () => ({ x: 0, y: 0, width: 400, height: 600 }) + } as any + this.webContentsToTabId.set(webContentsId, webContentsId) + this.tabs.set(webContentsId, virtualView) + console.log( + `TabPresenter: Virtual tab registered successfully for floating window ${webContentsId}` + ) + } catch (error) { + console.error('TabPresenter: Failed to register floating window:', error) + } + } + + unregisterFloatingWindow(webContentsId: number): void { + try { + console.log(`TabPresenter: Unregistering floating window virtual tab, ID: ${webContentsId}`) + this.webContentsToTabId.delete(webContentsId) + this.tabs.delete(webContentsId) + console.log( + `TabPresenter: Virtual tab unregistered successfully for floating window ${webContentsId}` + ) + } catch (error) { + console.error('TabPresenter: Failed to unregister floating window:', error) + } + } +} diff --git a/src/main/presenter/threadPresenter/const.ts b/src/main/presenter/threadPresenter/const.ts new file mode 100644 index 0000000..10bfc7f --- /dev/null +++ b/src/main/presenter/threadPresenter/const.ts @@ -0,0 +1,120 @@ +import { CONVERSATION_SETTINGS } from '@shared/presenter' + +export const SEARCH_PROMPT_TEMPLATE = ` +# The following content is based on the search results from the user's message: +{{SEARCH_RESULTS}} +In the search results I provided, each result is in the format [webpage X begin]...[webpage X end], where X represents the numerical index of each article. Please reference the context at the end of sentences where appropriate. Use the citation number [X] format to reference the corresponding parts in your answer. If a sentence is derived from multiple contexts, list all relevant citation numbers, such as [3][5]. Be careful not to concentrate the citation numbers at the end of the response, but rather list them in the corresponding parts of the answer. +When answering, please pay attention to the following points: + +- Today is {{CUR_DATE}} +- The language of the answer should be consistent with the language of the user's message, unless the user explicitly indicates a different language for the response. +- Not all content from the search results is closely related to the user's question; you need to discern and filter the search results based on the question. +- For listing questions (e.g., listing all flight information), try to limit the answer to no more than 10 points and inform the user that they can check the search sources for complete information. Prioritize providing the most complete and relevant items; unless necessary, do not proactively inform the user that the search results did not provide certain content. +- For creative questions (e.g., writing an essay), be sure to cite the corresponding reference numbers in the body of the paragraphs, such as [3][5], and not just at the end of the article. You need to interpret and summarize the user's topic requirements, choose an appropriate format, fully utilize the search results, and extract important information to generate answers that meet the user's requirements, are deeply thoughtful, creative, and professional. Your creative length should be as long as possible, and for each point, infer the user's intent, provide as many angles of response as possible, and ensure that the information is rich and the discussion is detailed. +- If the answer is long, try to structure it and summarize it in paragraphs. If you need to answer in points, try to limit it to no more than 5 points and merge related content. +- For objective questions, if the answer to the question is very brief, you can appropriately add one or two sentences of related information to enrich the content. +- You need to choose an appropriate and aesthetically pleasing answer format based on the user's requirements and the content of the answer to ensure strong readability. +- Your answer should synthesize multiple relevant web pages and not repeat citations from a single web page. +- Use markdown to format paragraphs, lists, tables, and citations as much as possible. +- Use markdown code blocks to write code, including syntax-highlighted languages. +- Enclose all mathematical expressions in LaTeX. Always use double dollar signs $$, for example, $$x^4 = x - 3$$. +- Do not include any URLs, only include citations with numbers, such as [1]. +- Do not include references (URLs, sources) at the end. +- Use footnote citations at the end of applicable sentences (e.g., [1][2]). +- Write more than 100 words (2 paragraphs). +- Avoid directly quoting citations in the answer. +- If the meaning is unclear, return the user's original query. +- Every footnote citations used in the answer must correspond to a real search results. Do not invent or hallucinate references. +- If no search results are provided currently, clearly state that no sources were given and do not use any [X] citation format in your answer. + +# The user's message is: +{{USER_QUERY}} + ` + +export const SEARCH_PROMPT_ARTIFACTS_TEMPLATE = ` +# The following content is based on the search results from the user's message: +{{SEARCH_RESULTS}} +In the search results I provided, each result is in the format [webpage X begin]...[webpage X end], where X represents the numerical index of each article. Please reference the context at the end of sentences where appropriate. Use the citation number [X] format to reference the corresponding parts in your answer. If a sentence is derived from multiple contexts, list all relevant citation numbers, such as [3][5]. Be careful not to concentrate the citation numbers at the end of the response, but rather list them in the corresponding parts of the answer. +When answering, please pay attention to the following points: + +- Today is {{CUR_DATE}} +- The language of the answer should be consistent with the language of the user's message, unless the user explicitly indicates a different language for the response. +- Not all content from the search results is closely related to the user's question; you need to discern and filter the search results based on the question. +- For listing questions (e.g., listing all flight information), try to limit the answer to no more than 10 points and inform the user that they can check the search sources for complete information. Prioritize providing the most complete and relevant items; unless necessary, do not proactively inform the user that the search results did not provide certain content. +- For creative questions (e.g., writing an essay), be sure to cite the corresponding reference numbers in the body of the paragraphs, such as [3][5], and not just at the end of the article. You need to interpret and summarize the user's topic requirements, choose an appropriate format, fully utilize the search results, and extract important information to generate answers that meet the user's requirements, are deeply thoughtful, creative, and professional. Your creative length should be as long as possible, and for each point, infer the user's intent, provide as many angles of response as possible, and ensure that the information is rich and the discussion is detailed. +- If the answer is long, try to structure it and summarize it in paragraphs. If you need to answer in points, try to limit it to no more than 5 points and merge related content. +- For objective questions, if the answer to the question is very brief, you can appropriately add one or two sentences of related information to enrich the content. +- You need to choose an appropriate and aesthetically pleasing answer format based on the user's requirements and the content of the answer to ensure strong readability. +- Your answer should synthesize multiple relevant web pages and not repeat citations from a single web page. +- Use markdown to format paragraphs, lists, tables, and citations as much as possible. +- Use markdown code blocks to write code, including syntax-highlighted languages. +- Enclose all mathematical expressions in LaTeX. Always use double dollar signs $$, for example, $$x^4 = x - 3$$. +- Do not include any URLs, only include citations with numbers, such as [1]. +- Do not include references (URLs, sources) at the end. +- Use footnote citations at the end of applicable sentences (e.g., [1][2]). +- Write more than 100 words (2 paragraphs). +- Avoid directly quoting citations in the answer. +- If the meaning is unclear, return the user's original query. +- Every footnote citations used in the answer must correspond to a real search results. Do not invent or hallucinate references. +- If no search results are provided currently, clearly state that no sources were given and do not use any [X] citation format in your answer. + +# Artifacts Support - MANDATORY FOR CERTAIN CONTENT TYPES +You MUST use artifacts for specific types of content. This is not optional. Creating artifacts is required for the following content types: + +## REQUIRED ARTIFACT USE CASES (YOU MUST USE ARTIFACTS FOR THESE): +1. Reports and documents: + - Annual reports, financial analyses, market research + - Academic papers, essays, articles + - Business plans, proposals, executive summaries + - Any document longer than 300 words + - Example requests: "Write a report on...", "Create an analysis of...", "Draft a document about..." + +2. Complete code implementations: + - Full code files or scripts (>15 lines) + - Complete functions or classes + - Configuration files + - Example requests: "Write a program that...", "Create a script for...", "Implement a class that..." + +3. Structured content: + - Tables with multiple rows/columns + - Diagrams, flowcharts, mind maps + - HTML pages or templates + - Example requests: "Create a diagram showing...", "Make a table of...", "Design an HTML page for..." + +## HOW TO CREATE ARTIFACTS: +1. Identify if the user's request matches ANY of the required artifact use cases above +2. Place the ENTIRE content within the artifact - do not split content between artifacts and your main response +3. Use the appropriate artifact type: + - markdown: For reports, documents, articles, essays + - code: For programming code, scripts, configuration files + - HTML: For web pages + - SVG: For vector graphics + - mermaid: For diagrams and charts +4. Give each artifact a clear, descriptive title +5. Include complete content without truncation +6. Still include citations [X] when referencing search results within artifacts + +## IMPORTANT RULES: +- If the user asks for a report, document, essay, analysis, or any substantial written content, YOU MUST use a markdown artifact +- In your main response, briefly introduce the artifact but put ALL the substantial content in the artifact +- DO NOT fragment content between artifacts and your main response +- For code solutions, put the COMPLETE implementation in the artifact +- For documents or reports, the ENTIRE document should be in the artifact + +DO NOT use artifacts for: +- Simple explanations or answers (less than 300 words) +- Short code snippets (<15 lines) +- Brief answers that work better as part of the conversation flow + +# The user's message is: +{{USER_QUERY}} +` +export const DEFAULT_SETTINGS: CONVERSATION_SETTINGS = { + systemPrompt: '', + temperature: 0.7, + contextLength: 1000, + maxTokens: 2000, + providerId: 'deepseek', + modelId: 'deepseek-chat', + artifacts: 0 +} diff --git a/src/main/presenter/threadPresenter/contentEnricher.ts b/src/main/presenter/threadPresenter/contentEnricher.ts new file mode 100644 index 0000000..dd8411b --- /dev/null +++ b/src/main/presenter/threadPresenter/contentEnricher.ts @@ -0,0 +1,383 @@ +import axios from 'axios' +import * as cheerio from 'cheerio' +import { SearchResult } from '../../../shared/presenter' +import { HttpsProxyAgent } from 'https-proxy-agent' +import { proxyConfig } from '@/presenter/proxyConfig' +import { presenter } from '@/presenter' +// 统一的搜索结果类型 + +/** + * 内容丰富工具类,用于处理URL内容提取和丰富 + */ +export class ContentEnricher { + /** + * 从文本中提取并丰富URL内容 + * @param text 包含URL的文本 + * @returns 丰富后的URL结果数组 + */ + static async extractAndEnrichUrls(text: string): Promise { + // 用正则表达式匹配http和https链接 + const urlRegex = /(https?:\/\/[^\s]+)/g + const matches = text.match(urlRegex) + + if (!matches || matches.length === 0) { + return [] + } + + const results: SearchResult[] = [] + + for (const url of matches) { + const result = await this.enrichUrl(url, results.length + 1) + results.push(result as SearchResult) + } + + return results + } + + /** + * 丰富单个URL的内容 + * @param url 需要丰富的URL + * @param rank 结果排名(可选) + * @returns 丰富后的SearchResult对象 + */ + static async enrichUrl(url: string, rank: number = 1): Promise { + const timeout = 5000 // 5秒超时 + + try { + const proxyUrl = proxyConfig.getProxyUrl() + // 使用axios获取页面内容 + const response = await axios.get(url, { + timeout, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + }, + httpAgent: proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined + }) + + const $ = cheerio.load(response.data) + // 移除不需要的元素 + $('script, style, nav, header, footer, iframe, .ad, #ad, .advertisement').remove() + + // 获取页面标题 + const title = $('title').text().trim() || url + + // 尝试获取主要内容 + const mainContent = this.extractMainContent($) + // 获取图标 + const icon = this.extractFavicon($, url) + + // 获取页面描述 + const description = + $('meta[name="description"]').attr('content') || + $('meta[property="og:description"]').attr('content') || + '' + + return { + title, + url, + content: mainContent, + icon, + description, + rank + } + } catch (error: Error | unknown) { + console.error(`提取URL内容失败 ${url}:`, error instanceof Error ? error.message : '') + // 如果获取失败,只添加URL信息 + return { + title: url, + url, + rank, + description: '', + icon: '' + } + } + } + + /** + * 批量丰富搜索结果内容 + * @param results 搜索结果数组 + * @param limit 处理结果的数量限制(可选) + * @returns 丰富后的搜索结果数组 + */ + static async enrichResults(results: SearchResult[], limit?: number): Promise { + const enrichedResults: SearchResult[] = [] + const resultsToProcess = limit ? results.slice(0, limit) : results + + for (const result of resultsToProcess) { + try { + const enrichedResult = await this.enrichUrl(result.url, result.rank) + // 合并原始结果和丰富的结果 + enrichedResults.push({ + ...result, + content: enrichedResult.content || result.description || '', + icon: result.icon || enrichedResult.icon || '' + }) + } catch (error) { + console.error(`Error enriching content for ${result.url}:`, error) + // 获取失败保留原始结果 + enrichedResults.push(result) + } + } + + return enrichedResults + } + + /** + * 从HTML中提取主要内容 + * @param $ cheerio加载的HTML + * @returns 提取的内容文本 + */ + private static extractMainContent($: cheerio.CheerioAPI): string { + // 尝试获取主要内容 + let mainContent = '' + const possibleSelectors = [ + 'article', + 'main', + '.content', + '#content', + '.post-content', + '.article-content', + '.entry-content', + '[role="main"]', + '.container' + ] + + for (const selector of possibleSelectors) { + const element = $(selector) + if (element.length > 0) { + mainContent = element.text() + break + } + } + mainContent = mainContent + .replace(/[\r\n]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .trim() + + // 如果没有找到主要内容,使用body + if (!mainContent) { + mainContent = $('body').text() + } + // 清理文本内容 + mainContent = mainContent + .replace(/[\r\n]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() + + // 获取配置的长度限制,如果获取失败则使用默认值3000 + let lengthLimit = 3000 + try { + const configValue = presenter.configPresenter.getSetting('webContentLengthLimit') + if (configValue && typeof configValue === 'number') { + if (configValue === 0) { + // 0 表示不限制长度 + return mainContent + } else if (configValue > 0 && configValue <= 50000) { + lengthLimit = configValue + } + } + } catch { + // 忽略错误,使用默认值 + } + + // 如果内容长度小于或等于限制,直接返回全部内容 + if (mainContent.length <= lengthLimit) { + return mainContent + } + + // 否则截断到指定长度 + return mainContent.slice(0, lengthLimit) + } + + /** + * 从HTML中提取网站图标 + * @param $ cheerio加载的HTML + * @param url 页面URL + * @returns 图标URL + */ + private static extractFavicon($: cheerio.CheerioAPI, url: string): string { + // 尝试获取网站图标 + let icon = $('link[rel="icon"]').attr('href') || $('link[rel="shortcut icon"]').attr('href') + + // 如果找到了相对路径的图标,转换为绝对路径 + if (icon && !icon.startsWith('http')) { + const urlObj = new URL(url) + icon = icon.startsWith('/') + ? `${urlObj.protocol}//${urlObj.host}${icon}` + : `${urlObj.protocol}//${urlObj.host}/${icon}` + } + + // 如果没有找到图标,使用默认的favicon.ico + if (!icon) { + const urlObj = new URL(url) + icon = `${urlObj.protocol}//${urlObj.host}/favicon.ico` + } + + return icon + } + + /** + * 根据提取的URL内容丰富用户消息 + * @param userText 原始用户消息 + * @param urlResults 提取的URL内容结果 + * @returns 丰富后的用户消息 + */ + static enrichUserMessageWithUrlContent(userText: string, urlResults: SearchResult[]): string { + if (urlResults.length === 0) { + return userText + } + + let enrichedContent = `--- + 如下url-content的标签中包含了用户上述提到的一些链接的具体信息: + + \n + ` + for (let i = 0; i < urlResults.length; i++) { + const result = urlResults[i] + if (result.content) { + enrichedContent += `${result.url}\n${result.content}\n` + } + } + enrichedContent += `` + + return enrichedContent + } + + /** + * 将HTML转换为简洁的Markdown格式 + * 保留链接和结构化数据,减少数据量 + * @param html HTML内容 + * @param baseUrl 基础URL,用于解析相对路径 + * @returns 转换后的Markdown内容 + */ + static convertHtmlToMarkdown(html: string, baseUrl: string): string { + const $ = cheerio.load(html) + + // 移除不需要的元素 + $('script, style, nav, header, footer, iframe, .ad, #ad, .advertisement').remove() + + let markdown = '' + + // 提取页面中可能的搜索结果链接和文本 + const searchResults: { title: string; url: string; text: string }[] = [] + + // 1. 查找明显的搜索结果项 + $('a').each((_, element) => { + const $el = $(element) + const href = $el.attr('href') || '' + const text = $el.text().trim() + + // 跳过空链接和明显的导航链接 + if (!href || href === '#' || text.length < 3 || href.includes('javascript:')) { + return + } + + // 转换为绝对URL + let url = href + try { + url = href.startsWith('http') ? href : new URL(href, baseUrl).toString() + } catch (error) { + // 如果URL构建失败,使用原始href + console.error('构建URL失败:', error) + } + + // 获取周围的文本作为描述 + const parent = $el.parent() + let description = '' + + // 尝试在父元素或祖先元素中寻找描述性文本 + if (parent && parent.children().length > 1) { + // 克隆父元素并移除所有链接,只保留文本内容 + const parentClone = parent.clone() + parentClone.find('a').remove() + description = parentClone.text().trim() + } + + if (!description) { + // 尝试查找相邻的段落元素 + const nextParagraph = $el.next('p, div, span') + if (nextParagraph.length) { + description = nextParagraph.text().trim() + } + } + + // 清理描述文本 + description = description.replace(/\s+/g, ' ').trim().substring(0, 200) // 限制描述长度 + + searchResults.push({ + title: text, + url, + text: description + }) + }) + + // 2. 将提取的搜索结果转换为Markdown格式 + searchResults.forEach((result, index) => { + markdown += `## 结果 ${index + 1}\n` + markdown += `### [${result.title}](${result.url})\n` + markdown += `- URL: ${result.url}\n` + if (result.text) { + markdown += `- 描述: ${result.text}\n` + } + markdown += '\n' + }) + + // 3. 如果没有提取到结构化结果,提取基础HTML结构 + if (searchResults.length === 0) { + // 提取所有可见文本块并保留基本结构 + $('h1, h2, h3, h4, h5, h6, p, div').each((_, element) => { + const $el = $(element) + // 跳过空白或很短的文本块 + const text = $el.text().trim() + if (text.length < 5) return + + // 根据元素类型添加标记 + const tagName = $el.prop('tagName')?.toLowerCase() || '' + if (tagName.startsWith('h')) { + const level = parseInt(tagName.substring(1)) + markdown += `${'#'.repeat(level)} ${text}\n\n` + } else { + markdown += `${text}\n\n` + } + }) + + // 再次提取所有链接 + $('a').each((_, element) => { + const $el = $(element) + const href = $el.attr('href') || '' + const text = $el.text().trim() + + if (href && href !== '#' && text.length > 0) { + let url = href + try { + url = href.startsWith('http') ? href : new URL(href, baseUrl).toString() + } catch { + // 如果URL构建失败,使用原始href + } + markdown += `- [${text}](${url})\n` + } + }) + } + + // 4. 最后,添加所有图片的引用 + $('img').each((_, element) => { + const $el = $(element) + const src = $el.attr('src') || '' + const alt = $el.attr('alt') || '图片' + + if (src) { + let imageUrl = src + try { + imageUrl = src.startsWith('http') ? src : new URL(src, baseUrl).toString() + } catch { + // 如果URL构建失败,使用原始src + } + markdown += `![${alt}](${imageUrl})\n` + } + }) + + return markdown + } +} diff --git a/src/main/presenter/threadPresenter/fileContext.ts b/src/main/presenter/threadPresenter/fileContext.ts new file mode 100644 index 0000000..5983c3d --- /dev/null +++ b/src/main/presenter/threadPresenter/fileContext.ts @@ -0,0 +1,22 @@ +import { MessageFile } from '@shared/chat' + +export const getFileContext = (files: MessageFile[]) => { + return files.length > 0 + ? ` + + + ${files + .map( + (file) => ` + ${file.name} + ${file.mimeType} + ${file.metadata.fileSize} + ${file.path} + ${!file.mimeType.startsWith('image') ? file.content : ''} + ` + ) + .join('\n')} + + ` + : '' +} diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts new file mode 100644 index 0000000..a3f9a3e --- /dev/null +++ b/src/main/presenter/threadPresenter/index.ts @@ -0,0 +1,4359 @@ +import { + IThreadPresenter, + CONVERSATION, + CONVERSATION_SETTINGS, + MESSAGE_ROLE, + MESSAGE_STATUS, + MESSAGE_METADATA, + SearchResult, + MODEL_META, + ISQLitePresenter, + IConfigPresenter, + ILlmProviderPresenter, + MCPToolResponse, + ChatMessage, + ChatMessageContent, + LLMAgentEventData +} from '../../../shared/presenter' +import { presenter } from '@/presenter' +import { MessageManager } from './messageManager' +import { eventBus, SendTarget } from '@/eventbus' +import { + AssistantMessage, + Message, + AssistantMessageBlock, + SearchEngineTemplate, + UserMessage, + MessageFile, + UserMessageContent, + UserMessageTextBlock, + UserMessageMentionBlock, + UserMessageCodeBlock +} from '@shared/chat' +import { ModelType } from '@shared/model' +import { approximateTokenSize } from 'tokenx' +import { generateSearchPrompt, SearchManager } from './searchManager' +import { getFileContext } from './fileContext' +import { ContentEnricher } from './contentEnricher' +import { CONVERSATION_EVENTS, STREAM_EVENTS, TAB_EVENTS } from '@/events' +import { DEFAULT_SETTINGS } from './const' + +interface GeneratingMessageState { + message: AssistantMessage + conversationId: string + startTime: number + firstTokenTime: number | null + promptTokens: number + reasoningStartTime: number | null + reasoningEndTime: number | null + lastReasoningTime: number | null + isSearching?: boolean + isCancelled?: boolean + totalUsage?: { + prompt_tokens: number + completion_tokens: number + total_tokens: number + context_length: number + } + // 统一的自适应内容处理 + adaptiveBuffer?: { + content: string + lastUpdateTime: number + updateCount: number + totalSize: number + isLargeContent: boolean + chunks?: string[] + currentChunkIndex?: number + // 精确追踪已发送内容的位置 + sentPosition: number // 已发送到渲染器的内容位置 + isProcessing?: boolean + } + flushTimeout?: NodeJS.Timeout + throttleTimeout?: NodeJS.Timeout + lastRendererUpdateTime?: number +} + +export class ThreadPresenter implements IThreadPresenter { + private sqlitePresenter: ISQLitePresenter + private messageManager: MessageManager + private llmProviderPresenter: ILlmProviderPresenter + private configPresenter: IConfigPresenter + private searchManager: SearchManager + private generatingMessages: Map = new Map() + public searchAssistantModel: MODEL_META | null = null + public searchAssistantProviderId: string | null = null + private searchingMessages: Set = new Set() + private activeConversationIds: Map = new Map() + private fetchThreadLength: number = 300 + + constructor( + sqlitePresenter: ISQLitePresenter, + llmProviderPresenter: ILlmProviderPresenter, + configPresenter: IConfigPresenter + ) { + this.sqlitePresenter = sqlitePresenter + this.messageManager = new MessageManager(sqlitePresenter) + this.llmProviderPresenter = llmProviderPresenter + this.searchManager = new SearchManager() + this.configPresenter = configPresenter + + // 监听Tab关闭事件,清理绑定关系 + eventBus.on(TAB_EVENTS.CLOSED, (tabId: number) => { + if (this.activeConversationIds.has(tabId)) { + this.activeConversationIds.delete(tabId) + console.log(`ThreadPresenter: Cleaned up conversation binding for closed tab ${tabId}.`) + } + }) + eventBus.on(TAB_EVENTS.RENDERER_TAB_READY, () => { + this.broadcastThreadListUpdate() + }) + + // 初始化时处理所有未完成的消息 + this.messageManager.initializeUnfinishedMessages() + } + + /** + * 新增:查找指定会话ID所在的Tab ID + * @param conversationId 会话ID + * @returns 如果找到,返回tabId,否则返回null + */ + async findTabForConversation(conversationId: string): Promise { + for (const [tabId, activeId] of this.activeConversationIds.entries()) { + if (activeId === conversationId) { + // 验证该tab是否还真实存在 + const tabView = await presenter.tabPresenter.getTab(tabId) + if (tabView && !tabView.webContents.isDestroyed()) { + return tabId + } + } + } + return null + } + + private async getTabWindowType(tabId: number): Promise<'floating' | 'main' | 'unknown'> { + try { + const tabView = await presenter.tabPresenter.getTab(tabId) + if (!tabView) { + return 'unknown' + } + const windowId = presenter.tabPresenter['tabWindowMap'].get(tabId) + return windowId ? 'main' : 'floating' + } catch (error) { + console.error('Error determining tab window type:', error) + return 'unknown' + } + } + + async handleLLMAgentError(msg: LLMAgentEventData) { + const { eventId, error } = msg + const state = this.generatingMessages.get(eventId) + if (state) { + // 刷新剩余缓冲内容 + if (state.adaptiveBuffer) { + await this.flushAdaptiveBuffer(eventId) + } + + // 清理缓冲相关资源 + this.cleanupContentBuffer(state) + + await this.messageManager.handleMessageError(eventId, String(error)) + this.generatingMessages.delete(eventId) + } + eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, msg) + } + + async handleLLMAgentEnd(msg: LLMAgentEventData) { + const { eventId, userStop } = msg + const state = this.generatingMessages.get(eventId) + if (state) { + console.log( + `[ThreadPresenter] Handling LLM agent end for message: ${eventId}, userStop: ${userStop}` + ) + + // 检查是否有未处理的权限请求 + const hasPendingPermissions = state.message.content.some( + (block) => + block.type === 'action' && + block.action_type === 'tool_call_permission' && + block.status === 'pending' + ) + + if (hasPendingPermissions) { + console.log( + `[ThreadPresenter] Message ${eventId} has pending permissions, keeping in generating state` + ) + // 保持消息在generating状态,等待权限响应 + // 但是要更新非权限块为success状态 + state.message.content.forEach((block) => { + if ( + !(block.type === 'action' && block.action_type === 'tool_call_permission') && + block.status === 'loading' + ) { + block.status = 'success' + } + }) + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + return + } + + console.log(`[ThreadPresenter] Finalizing message ${eventId} - no pending permissions`) + + // 正常完成流程 + await this.finalizeMessage(state, eventId, userStop || false) + } + + eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) + } + + // 清理所有缓冲相关资源 + private cleanupContentBuffer(state: GeneratingMessageState): void { + if (state.flushTimeout) { + clearTimeout(state.flushTimeout) + state.flushTimeout = undefined + } + if (state.throttleTimeout) { + clearTimeout(state.throttleTimeout) + state.throttleTimeout = undefined + } + state.adaptiveBuffer = undefined + state.lastRendererUpdateTime = undefined + } + + // 完成消息的通用方法 + private async finalizeMessage( + state: GeneratingMessageState, + eventId: string, + userStop: boolean + ): Promise { + // 将所有块设为success状态,但保留权限块的状态 + state.message.content.forEach((block) => { + if (block.type === 'action' && block.action_type === 'tool_call_permission') { + // 权限块保持其当前状态(granted/denied/error) + return + } + block.status = 'success' + }) + + // 计算completion tokens + let completionTokens = 0 + if (state.totalUsage) { + completionTokens = state.totalUsage.completion_tokens + } else { + for (const block of state.message.content) { + if ( + block.type === 'content' || + block.type === 'reasoning_content' || + block.type === 'tool_call' + ) { + completionTokens += approximateTokenSize(block.content) + } + } + } + + // 检查是否有内容块 + const hasContentBlock = state.message.content.some( + (block) => + block.type === 'content' || + block.type === 'reasoning_content' || + block.type === 'tool_call' || + block.type === 'image' + ) + + // 如果没有内容块,添加错误信息 + if (!hasContentBlock && !userStop) { + state.message.content.push({ + type: 'error', + content: 'common.error.noModelResponse', + status: 'error', + timestamp: Date.now() + }) + } + + const totalTokens = state.promptTokens + completionTokens + const generationTime = Date.now() - (state.firstTokenTime ?? state.startTime) + const tokensPerSecond = completionTokens / (generationTime / 1000) + const contextUsage = state?.totalUsage?.context_length + ? (totalTokens / state.totalUsage.context_length) * 100 + : 0 + + // 如果有reasoning_content,记录结束时间 + const metadata: Partial = { + totalTokens, + inputTokens: state.promptTokens, + outputTokens: completionTokens, + generationTime, + firstTokenTime: state.firstTokenTime ? state.firstTokenTime - state.startTime : 0, + tokensPerSecond, + contextUsage + } + + if (state.reasoningStartTime !== null && state.lastReasoningTime !== null) { + metadata.reasoningStartTime = state.reasoningStartTime - state.startTime + metadata.reasoningEndTime = state.lastReasoningTime - state.startTime + } + + // 刷新剩余缓冲内容 + if (state.adaptiveBuffer) { + await this.flushAdaptiveBuffer(eventId) + } + + // 清理缓冲相关资源 + this.cleanupContentBuffer(state) + + // 更新消息的usage信息 + await this.messageManager.updateMessageMetadata(eventId, metadata) + await this.messageManager.updateMessageStatus(eventId, 'sent') + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + this.generatingMessages.delete(eventId) + + // 处理标题更新和会话更新 + await this.handleConversationUpdates(state) + + // 广播消息生成完成事件 + const finalMessage = await this.messageManager.getMessage(eventId) + if (finalMessage) { + eventBus.sendToMain(CONVERSATION_EVENTS.MESSAGE_GENERATED, { + conversationId: finalMessage.conversationId, + message: finalMessage + }) + } + } + + // 处理会话更新和标题生成 + private async handleConversationUpdates(state: GeneratingMessageState): Promise { + const conversation = await this.sqlitePresenter.getConversation(state.conversationId) + let titleUpdated = false + + if (conversation.is_new === 1) { + try { + this.summaryTitles(undefined, state.conversationId).then((title) => { + if (title) { + this.renameConversation(state.conversationId, title).then(() => { + titleUpdated = true + }) + } + }) + } catch (e) { + console.error('Failed to summarize title in main process:', e) + } + } + + if (!titleUpdated) { + this.sqlitePresenter + .updateConversation(state.conversationId, { + updatedAt: Date.now() + }) + .then(() => { + console.log('updated conv time', state.conversationId) + }) + await this.broadcastThreadListUpdate() + } + } + + // 释放缓冲的内容 + + // 统一的自适应内容刷新 + private async flushAdaptiveBuffer(eventId: string): Promise { + const state = this.generatingMessages.get(eventId) + if (!state?.adaptiveBuffer) return + + const buffer = state.adaptiveBuffer + const now = Date.now() + + // 清理超时 + if (state.flushTimeout) { + clearTimeout(state.flushTimeout) + state.flushTimeout = undefined + } + + // 处理缓冲的内容 - 只发送从 sentPosition 开始的新内容 + if (buffer.content && buffer.sentPosition < buffer.content.length) { + const newContent = buffer.content.slice(buffer.sentPosition) + if (newContent) { + await this.processBufferedContent(eventId, newContent, now) + // 更新已发送位置 + buffer.sentPosition = buffer.content.length + } + } + + // 清理缓冲 + state.adaptiveBuffer = undefined + } + + // 优化的自适应内容处理 - 核心逻辑 (当前未使用) + // private async addToAdaptiveBuffer(eventId: string, content: string): Promise { + // // 方法保留以备将来使用 + // } + + // 分块大内容 - 使用更小的分块避免UI阻塞 + private splitLargeContent(content: string): string[] { + const chunks: string[] = [] + let maxChunkSize = 4096 // 默认4KB + + // 对于图片base64内容,使用非常小的分块 + if (content.includes('data:image/')) { + maxChunkSize = 512 // 图片内容使用512字节分块 + } + + // 对于超长内容,进一步减小分块 + if (content.length > 50000) { + maxChunkSize = Math.min(maxChunkSize, 256) + } + + for (let i = 0; i < content.length; i += maxChunkSize) { + chunks.push(content.slice(i, i + maxChunkSize)) + } + + return chunks + } + + // 智能判断是否需要分块处理 - 优化阈值判断 + private shouldSplitContent(content: string): boolean { + const sizeThreshold = 8192 // 8KB - 适中的阈值 + const hasBase64Image = content.includes('data:image/') && content.includes('base64,') + const hasLargeBase64 = hasBase64Image && content.length > 5120 // 图片内容超过5KB才分块 + + return content.length > sizeThreshold || hasLargeBase64 + } + + // 处理缓冲的内容 - 优化异步处理 + private async processBufferedContent( + eventId: string, + content: string, + currentTime: number + ): Promise { + const state = this.generatingMessages.get(eventId) + if (!state) return + + const buffer = state.adaptiveBuffer + + // 如果是大内容,使用分块处理 + if (buffer?.isLargeContent) { + await this.processLargeContentAsynchronously(eventId, content, currentTime) + return + } + + // 正常内容处理 + await this.processNormalContent(eventId, content, currentTime) + } + + // 异步处理大内容 - 避免阻塞主进程 + private async processLargeContentAsynchronously( + eventId: string, + content: string, + currentTime: number + ): Promise { + const state = this.generatingMessages.get(eventId) + if (!state) return + + const buffer = state.adaptiveBuffer + if (!buffer) return + + // 设置处理状态 + buffer.isProcessing = true + + try { + // 动态分块 - 只处理传入的新增内容 + const chunks = this.splitLargeContent(content) + const totalChunks = chunks.length + + console.log( + `[ThreadPresenter] Processing ${totalChunks} chunks asynchronously for ${content.length} bytes` + ) + + // 初始化或获取内容块 + const lastBlock = state.message.content[state.message.content.length - 1] + let contentBlock: any + + if (lastBlock && lastBlock.type === 'content') { + contentBlock = lastBlock + } else { + this.finalizeLastBlock(state) + contentBlock = { + type: 'content', + content: '', + status: 'loading', + timestamp: currentTime + } + state.message.content.push(contentBlock) + } + + // 批量处理分块,每次处琅5个 + const batchSize = 5 + for (let batchStart = 0; batchStart < chunks.length; batchStart += batchSize) { + const batchEnd = Math.min(batchStart + batchSize, chunks.length) + const batch = chunks.slice(batchStart, batchEnd) + + // 合并当前批次的内容 + const batchContent = batch.join('') + contentBlock.content += batchContent + + // 更新数据库 + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + + // 发送渲染器事件 + const eventData: any = { + eventId, + content: batchContent, + chunkInfo: { + current: batchEnd, + total: totalChunks, + isLargeContent: true, + batchSize: batch.length + } + } + + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, eventData) + + // 每批次之间的延迟,让出event loop + if (batchEnd < chunks.length) { + await new Promise((resolve) => setImmediate(resolve)) + } + } + + console.log(`[ThreadPresenter] Completed processing ${totalChunks} chunks`) + } catch (error) { + console.error('[ThreadPresenter] Error in processLargeContentAsynchronously:', error) + } finally { + // 清理处理状态 + buffer.isProcessing = false + } + } + + // 处理普通内容 + private async processNormalContent( + eventId: string, + content: string, + currentTime: number + ): Promise { + const state = this.generatingMessages.get(eventId) + if (!state) return + + const lastBlock = state.message.content[state.message.content.length - 1] + + if (lastBlock && lastBlock.type === 'content') { + lastBlock.content += content + } else { + this.finalizeLastBlock(state) + state.message.content.push({ + type: 'content', + content: content, + status: 'loading', + timestamp: currentTime + }) + } + + // 只更新数据库,不额外发送到渲染器(避免重复发送) + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + } + + // 完成最后一个块的状态 + private finalizeLastBlock(state: GeneratingMessageState): void { + const lastBlock = + state.message.content.length > 0 + ? state.message.content[state.message.content.length - 1] + : undefined + + if (lastBlock) { + if ( + lastBlock.type === 'action' && + lastBlock.action_type === 'tool_call_permission' && + lastBlock.status === 'pending' + ) { + lastBlock.status = 'granted' + return + } + if (!(lastBlock.type === 'tool_call' && lastBlock.status === 'loading')) { + lastBlock.status = 'success' + } + } + } + + // 统一的数据库和渲染器更新 (当前未使用) + // private async updateMessageAndRenderer(eventId: string, content: string, currentTime: number, chunkInfo?: any): Promise { + // // 方法保留以备将来使用 + // } + + async handleLLMAgentResponse(msg: LLMAgentEventData) { + const currentTime = Date.now() + const { + eventId, + content, + reasoning_content, + tool_call_id, + tool_call_name, + tool_call_params, + tool_call_response, + maximum_tool_calls_reached, + tool_call_server_name, + tool_call_server_icons, + tool_call_server_description, + tool_call_response_raw, + tool_call, + totalUsage, + image_data + } = msg + const state = this.generatingMessages.get(eventId) + if (state) { + // 使用保护逻辑 + const finalizeLastBlock = () => { + const lastBlock = + state.message.content.length > 0 + ? state.message.content[state.message.content.length - 1] + : undefined + if (lastBlock) { + if ( + lastBlock.type === 'action' && + lastBlock.action_type === 'tool_call_permission' && + lastBlock.status === 'pending' + ) { + lastBlock.status = 'granted' + return + } + // 只有当上一个块不是一个正在等待结果的工具调用时,才将其标记为成功 + if (!(lastBlock.type === 'tool_call' && lastBlock.status === 'loading')) { + lastBlock.status = 'success' + } + } + } + + // 记录第一个token的时间 + if (state.firstTokenTime === null && (content || reasoning_content)) { + state.firstTokenTime = currentTime + await this.messageManager.updateMessageMetadata(eventId, { + firstTokenTime: currentTime - state.startTime + }) + } + if (totalUsage) { + state.totalUsage = totalUsage + state.promptTokens = totalUsage.prompt_tokens + } + + // 处理工具调用达到最大次数的情况 + if (maximum_tool_calls_reached) { + finalizeLastBlock() // 使用保护逻辑 + state.message.content.push({ + type: 'action', + content: 'common.error.maximumToolCallsReached', + status: 'success', + timestamp: currentTime, + action_type: 'maximum_tool_calls_reached', + tool_call: { + id: tool_call_id, + name: tool_call_name, + params: tool_call_params, + server_name: tool_call_server_name, + server_icons: tool_call_server_icons, + server_description: tool_call_server_description + }, + extra: { + needContinue: true + } + }) + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + return + } + + // 处理reasoning_content的时间戳 + if (reasoning_content) { + if (state.reasoningStartTime === null) { + state.reasoningStartTime = currentTime + await this.messageManager.updateMessageMetadata(eventId, { + reasoningStartTime: currentTime - state.startTime + }) + } + state.lastReasoningTime = currentTime + } + + const lastBlock = state.message.content[state.message.content.length - 1] + + // 检查tool_call_response_raw中是否包含搜索结果 + if (tool_call_response_raw && tool_call === 'end') { + try { + // 检查返回的内容中是否有deepchat-webpage类型的资源 + // 确保content是数组才调用some方法 + const hasSearchResults = + Array.isArray(tool_call_response_raw.content) && + tool_call_response_raw.content.some( + (item: { type: string; resource?: { mimeType: string } }) => + item?.type === 'resource' && + item?.resource?.mimeType === 'application/deepchat-webpage' + ) + + if (hasSearchResults && Array.isArray(tool_call_response_raw.content)) { + // 解析搜索结果 + const searchResults = tool_call_response_raw.content + .filter( + (item: { + type: string + resource?: { mimeType: string; text: string; uri?: string } + }) => + item.type === 'resource' && + item.resource?.mimeType === 'application/deepchat-webpage' + ) + .map((item: { resource: { text: string; uri?: string } }) => { + try { + const blobContent = JSON.parse(item.resource.text) as { + title?: string + url?: string + content?: string + icon?: string + } + return { + title: blobContent.title || '', + url: blobContent.url || item.resource.uri || '', + content: blobContent.content || '', + description: blobContent.content || '', + icon: blobContent.icon || '' + } + } catch (e) { + console.error('解析搜索结果失败:', e) + return null + } + }) + .filter(Boolean) + + if (searchResults.length > 0) { + // 检查是否已经存在搜索块 + const existingSearchBlock = + state.message.content.length > 0 && state.message.content[0].type === 'search' + ? state.message.content[0] + : null + + if (existingSearchBlock) { + // 如果已经存在搜索块,更新其状态和总数 + existingSearchBlock.status = 'success' + existingSearchBlock.timestamp = currentTime + if (existingSearchBlock.extra) { + // 累加搜索结果数量 + existingSearchBlock.extra.total = + (existingSearchBlock.extra.total || 0) + searchResults.length + } else { + existingSearchBlock.extra = { + total: searchResults.length + } + } + } else { + // 如果不存在搜索块,创建新的并添加到内容的最前面 + const searchBlock: AssistantMessageBlock = { + type: 'search', + content: '', + status: 'success', + timestamp: currentTime, + extra: { + total: searchResults.length + } + } + state.message.content.unshift(searchBlock) + } + + // 保存搜索结果 + for (const result of searchResults) { + await this.sqlitePresenter.addMessageAttachment( + eventId, + 'search_result', + JSON.stringify(result) + ) + } + + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + } + } + } catch (error) { + console.error('处理搜索结果时出错:', error) + } + } + + // 处理工具调用 + if (tool_call) { + if (tool_call === 'start') { + // 创建新的工具调用块 + finalizeLastBlock() // 使用保护逻辑 + state.message.content.push({ + type: 'tool_call', + content: '', + status: 'loading', + timestamp: currentTime, + tool_call: { + id: tool_call_id, + name: tool_call_name, + params: tool_call_params || '', + server_name: tool_call_server_name, + server_icons: tool_call_server_icons, + server_description: tool_call_server_description + } + }) + } else if (tool_call === 'update') { + // 更新工具调用参数 + const toolCallBlock = state.message.content.find( + (block) => + block.type === 'tool_call' && + block.tool_call?.id === tool_call_id && + block.status === 'loading' + ) + + if (toolCallBlock && toolCallBlock.type === 'tool_call' && toolCallBlock.tool_call) { + toolCallBlock.tool_call.params = tool_call_params || '' + } + } else if (tool_call === 'running') { + // 工具调用正在执行 + const toolCallBlock = state.message.content.find( + (block) => + block.type === 'tool_call' && + block.tool_call?.id === tool_call_id && + block.status === 'loading' + ) + + if (toolCallBlock && toolCallBlock.type === 'tool_call') { + // 保持 loading 状态,但更新工具信息 + if (toolCallBlock.tool_call) { + toolCallBlock.tool_call.params = tool_call_params || '' + toolCallBlock.tool_call.server_name = tool_call_server_name + toolCallBlock.tool_call.server_icons = tool_call_server_icons + toolCallBlock.tool_call.server_description = tool_call_server_description + } + } + } else if (tool_call === 'permission-required') { + // 处理权限请求:创建权限请求块 + // 注意:不调用finalizeLastBlock,因为工具调用还没有完成,在等待权限 + + // 从 msg 中获取权限请求信息 + const { permission_request } = msg + + state.message.content.push({ + type: 'action', + action_type: 'tool_call_permission', + content: + typeof tool_call_response === 'string' + ? tool_call_response + : 'Permission required for this operation', + status: 'pending', + timestamp: currentTime, + tool_call: { + id: tool_call_id, + name: tool_call_name, + params: tool_call_params || '', + server_name: tool_call_server_name, + server_icons: tool_call_server_icons, + server_description: tool_call_server_description + }, + extra: { + permissionType: permission_request?.permissionType || 'write', + serverName: permission_request?.serverName || tool_call_server_name || '', + toolName: permission_request?.toolName || tool_call_name || '', + needsUserAction: true, + permissionRequest: JSON.stringify( + permission_request || { + toolName: tool_call_name || '', + serverName: tool_call_server_name || '', + permissionType: 'write' as const, + description: 'Permission required for this operation' + } + ) + } + }) + } else if (tool_call === 'end' || tool_call === 'error') { + // 查找对应的工具调用块 + const toolCallBlock = state.message.content.find( + (block) => + block.type === 'tool_call' && + ((tool_call_id && block.tool_call?.id === tool_call_id) || + block.tool_call?.name === tool_call_name) && + block.status === 'loading' + ) + + if (toolCallBlock && toolCallBlock.type === 'tool_call') { + if (tool_call === 'error') { + toolCallBlock.status = 'error' + if (toolCallBlock.tool_call) { + if (typeof tool_call_response === 'string') { + toolCallBlock.tool_call.response = tool_call_response || '执行失败' + } else { + toolCallBlock.tool_call.response = JSON.stringify(tool_call_response) + } + } + } else { + toolCallBlock.status = 'success' + if (toolCallBlock.tool_call) { + if (typeof tool_call_response === 'string') { + toolCallBlock.tool_call.response = tool_call_response + } else { + toolCallBlock.tool_call.response = JSON.stringify(tool_call_response) + } + } + } + } + } + } else if (image_data) { + // 处理图像数据 + finalizeLastBlock() // 使用保护逻辑 + state.message.content.push({ + type: 'image', + content: 'image', + status: 'success', + timestamp: currentTime, + image_data: image_data + }) + } else if (content) { + // 简化的直接内容处理 + await this.processContentDirectly(state.message.id, content, currentTime) + } + + // 处理推理内容 + if (reasoning_content) { + if (lastBlock && lastBlock.type === 'reasoning_content') { + lastBlock.content += reasoning_content + if (lastBlock.reasoning_time) { + lastBlock.reasoning_time.end = currentTime + } + } else { + finalizeLastBlock() // 使用保护逻辑 + state.message.content.push({ + type: 'reasoning_content', + content: reasoning_content, + status: 'loading', + reasoning_time: { + start: currentTime, + end: currentTime + }, + timestamp: currentTime + }) + } + } + + // 更新消息内容 + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + } + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, msg) + } + + setSearchAssistantModel(model: MODEL_META, providerId: string) { + this.searchAssistantModel = model + this.searchAssistantProviderId = providerId + } + async getSearchEngines(): Promise { + return this.searchManager.getEngines() + } + async getActiveSearchEngine(): Promise { + return this.searchManager.getActiveEngine() + } + async setActiveSearchEngine(engineId: string): Promise { + await this.searchManager.setActiveEngine(engineId) + } + + /** + * 测试当前选择的搜索引擎 + * @param query 测试搜索的关键词,默认为"天气" + * @returns 测试是否成功打开窗口 + */ + async testSearchEngine(query: string = '天气'): Promise { + return await this.searchManager.testSearch(query) + } + + /** + * 设置搜索引擎 + * @param engineId 搜索引擎ID + * @returns 是否设置成功 + */ + async setSearchEngine(engineId: string): Promise { + try { + return await this.searchManager.setActiveEngine(engineId) + } catch (error) { + console.error('设置搜索引擎失败:', error) + return false + } + } + + async renameConversation(conversationId: string, title: string): Promise { + await this.sqlitePresenter.renameConversation(conversationId, title) + await this.broadcastThreadListUpdate() // 必须广播 + + const conversation = await this.getConversation(conversationId) + + // 新增:找到与此 conversationId 关联的 tabId + let tabId: number | undefined + for (const [key, value] of this.activeConversationIds.entries()) { + if (value === conversationId) { + tabId = key + break + } + } + + // 新增:发出事件通知UI更新标题 + if (tabId !== undefined) { + const windowId = presenter.tabPresenter['tabWindowMap'].get(tabId) + eventBus.sendToRenderer(TAB_EVENTS.TITLE_UPDATED, SendTarget.ALL_WINDOWS, { + tabId, + conversationId, + title: conversation.title, + windowId // 附带 windowId + }) + } + + return conversation + } + async createConversation( + title: string, + settings: Partial = {}, + tabId: number, + options: { forceNewAndActivate?: boolean } = {} // 新增参数,允许强制创建新会话 + ): Promise { + console.log('createConversation', title, settings) + + const latestConversation = await this.getLatestConversation() + + // 只有在非强制模式下,才执行空会话的单例检查 + if (!options.forceNewAndActivate) { + if (latestConversation) { + const { list: messages } = await this.getMessages(latestConversation.id, 1, 1) + if (messages.length === 0) { + await this.setActiveConversation(latestConversation.id, tabId) + return latestConversation.id + } + } + } + + let defaultSettings = DEFAULT_SETTINGS + if (latestConversation?.settings) { + defaultSettings = { ...latestConversation.settings } + defaultSettings.systemPrompt = '' + defaultSettings.reasoningEffort = undefined + defaultSettings.enableSearch = undefined + defaultSettings.forcedSearch = undefined + defaultSettings.searchStrategy = undefined + } + Object.keys(settings).forEach((key) => { + if (settings[key] === undefined || settings[key] === null || settings[key] === '') { + delete settings[key] + } + }) + const mergedSettings = { ...defaultSettings, ...settings } + const defaultModelsSettings = this.configPresenter.getModelConfig( + mergedSettings.modelId, + mergedSettings.providerId + ) + if (defaultModelsSettings) { + mergedSettings.maxTokens = defaultModelsSettings.maxTokens + mergedSettings.contextLength = defaultModelsSettings.contextLength + mergedSettings.temperature = defaultModelsSettings.temperature ?? 0.7 + if (settings.thinkingBudget === undefined) { + mergedSettings.thinkingBudget = defaultModelsSettings.thinkingBudget + } + } + if (settings.artifacts) { + mergedSettings.artifacts = settings.artifacts + } + if (settings.maxTokens) { + mergedSettings.maxTokens = settings.maxTokens + } + if (settings.temperature) { + mergedSettings.temperature = settings.temperature + } + if (settings.contextLength) { + mergedSettings.contextLength = settings.contextLength + } + if (settings.systemPrompt) { + mergedSettings.systemPrompt = settings.systemPrompt + } + const conversationId = await this.sqlitePresenter.createConversation(title, mergedSettings) + + // 根据 forceNewAndActivate 标志决定激活行为 + if (options.forceNewAndActivate) { + // 强制模式:直接为当前 tabId 激活新会话,不进行任何检查 + this.activeConversationIds.set(tabId, conversationId) + eventBus.sendToRenderer(CONVERSATION_EVENTS.ACTIVATED, SendTarget.ALL_WINDOWS, { + conversationId, + tabId + }) + } else { + // 默认模式:保持原有的、防止重复打开的激活逻辑 + await this.setActiveConversation(conversationId, tabId) + } + + await this.broadcastThreadListUpdate() // 必须广播 + return conversationId + } + + async deleteConversation(conversationId: string): Promise { + await this.sqlitePresenter.deleteConversation(conversationId) + + // 作为兜底,确保所有与此会话相关的绑定都被移除 + for (const [tabId, activeId] of this.activeConversationIds.entries()) { + if (activeId === conversationId) { + this.activeConversationIds.delete(tabId) + } + } + + await this.broadcastThreadListUpdate() // 必须广播 + } + + async getConversation(conversationId: string): Promise { + return await this.sqlitePresenter.getConversation(conversationId) + } + + async toggleConversationPinned(conversationId: string, pinned: boolean): Promise { + await this.sqlitePresenter.updateConversation(conversationId, { is_pinned: pinned ? 1 : 0 }) + await this.broadcastThreadListUpdate() // 必须广播 + } + + async updateConversationTitle(conversationId: string, title: string): Promise { + await this.sqlitePresenter.updateConversation(conversationId, { title }) + await this.broadcastThreadListUpdate() // 必须广播 + } + + async updateConversationSettings( + conversationId: string, + settings: Partial + ): Promise { + const conversation = await this.getConversation(conversationId) + const mergedSettings = { ...conversation.settings } + for (const key in settings) { + if (settings[key] !== undefined) { + mergedSettings[key] = settings[key] + } + } + console.log('updateConversationSettings', mergedSettings) + // 检查是否有 modelId 的变化 + if (settings.modelId && settings.modelId !== conversation.settings.modelId) { + // 获取模型配置 + const modelConfig = this.configPresenter.getModelConfig( + mergedSettings.modelId, + mergedSettings.providerId + ) + console.log('check model default config', modelConfig) + if (modelConfig) { + // 如果当前设置小于推荐值,则使用推荐值 + mergedSettings.maxTokens = modelConfig.maxTokens + mergedSettings.contextLength = modelConfig.contextLength + } + } + + await this.sqlitePresenter.updateConversation(conversationId, { settings: mergedSettings }) + await this.broadcastThreadListUpdate() // 必须广播 + } + + async getConversationList( + page: number, + pageSize: number + ): Promise<{ total: number; list: CONVERSATION[] }> { + return await this.sqlitePresenter.getConversationList(page, pageSize) + } + + async loadMoreThreads(): Promise<{ hasMore: boolean; total: number }> { + // 获取会话总数 + const total = await this.sqlitePresenter.getConversationCount() + + // 检查是否还有更多会话可以加载 + const hasMore = this.fetchThreadLength < total + + if (hasMore) { + // 增加 fetchThreadLength,每次增加 500 + this.fetchThreadLength = Math.min(this.fetchThreadLength + 300, total) + + // 广播更新的会话列表 + await this.broadcastThreadListUpdate() + } + + return { hasMore: this.fetchThreadLength < total, total } + } + + async setActiveConversation(conversationId: string, tabId: number): Promise { + // 【核心修正】由主进程负责全部决策(防重和自动切换逻辑) + const existingTabId = await this.findTabForConversation(conversationId) + + // 如果会话已在其他Tab打开,并且不是当前Tab,则切换到那个Tab + if (existingTabId !== null && existingTabId !== tabId) { + console.log( + `Conversation ${conversationId} is already open in tab ${existingTabId}. Switching to it.` + ) + // 命令TabPresenter切换到已存在的Tab + const currentTabType = await this.getTabWindowType(tabId) + const existingTabType = await this.getTabWindowType(existingTabId) + if (currentTabType !== existingTabType) { + this.activeConversationIds.delete(existingTabId) + eventBus.sendToRenderer(CONVERSATION_EVENTS.DEACTIVATED, SendTarget.ALL_WINDOWS, { + tabId: existingTabId + }) + this.activeConversationIds.set(tabId, conversationId) + eventBus.sendToRenderer(CONVERSATION_EVENTS.ACTIVATED, SendTarget.ALL_WINDOWS, { + conversationId, + tabId + }) + return + } else { + await presenter.tabPresenter.switchTab(existingTabId) + // 注意:这里不应该再为 requesting tab (即 tabId) 设置 activeConversationId + // 也不需要发送ACTIVATED事件,因为tab-session的绑定关系没有改变。 + // switchTab 自身会处理UI的激活。 + return + } + } + + // 如果会话未在其他Tab打开,或者是请求激活当前Tab已绑定的会话,则正常执行绑定 + const conversation = await this.getConversation(conversationId) + if (conversation) { + // 检查当前Tab是否已经绑定了这个会话,避免不必要的事件广播 + if (this.activeConversationIds.get(tabId) === conversationId) { + return // 状态未改变,无需操作 + } + + this.activeConversationIds.set(tabId, conversationId) + // 广播事件,通知所有渲染进程UI更新 + eventBus.sendToRenderer(CONVERSATION_EVENTS.ACTIVATED, SendTarget.ALL_WINDOWS, { + conversationId, + tabId + }) + } else { + throw new Error(`Conversation ${conversationId} not found`) + } + } + + async getActiveConversation(tabId: number): Promise { + const conversationId = this.activeConversationIds.get(tabId) + if (!conversationId) { + return null + } + return this.getConversation(conversationId) + } + + async getMessages( + conversationId: string, + page: number, + pageSize: number + ): Promise<{ total: number; list: Message[] }> { + return await this.messageManager.getMessageThread(conversationId, page, pageSize) + } + + async getContextMessages(conversationId: string): Promise { + const conversation = await this.getConversation(conversationId) + // 计算需要获取的消息数量(假设每条消息平均300字) + let messageCount = Math.ceil(conversation.settings.contextLength / 300) + if (messageCount < 2) { + messageCount = 2 + } + const messages = await this.messageManager.getContextMessages(conversationId, messageCount) + + // 确保消息列表以用户消息开始 + while (messages.length > 0 && messages[0].role !== 'user') { + messages.shift() + } + + return messages.map((msg) => { + if (msg.role === 'user') { + const newMsg = { ...msg } + const msgContent = newMsg.content as UserMessageContent + if (msgContent.content) { + ;(newMsg.content as UserMessageContent).text = this.formatUserMessageContent( + msgContent.content + ) + } + return newMsg + } else { + return msg + } + }) + } + + private formatUserMessageContent( + msgContentBlock: (UserMessageTextBlock | UserMessageMentionBlock | UserMessageCodeBlock)[] + ) { + return msgContentBlock + .map((block) => { + if (block.type === 'mention') { + if (block.category === 'resources') { + return `@${block.content}` + } else if (block.category === 'tools') { + return `@${block.id}` + } else if (block.category === 'files') { + return `@${block.id}` + } else if (block.category === 'prompts') { + try { + // 尝试解析prompt内容 + const promptData = JSON.parse(block.content) + // 如果包含messages数组,尝试提取其中的文本内容 + if (promptData && Array.isArray(promptData.messages)) { + const messageTexts = promptData.messages + .map((msg) => { + if (typeof msg.content === 'string') { + return msg.content + } else if (msg.content && msg.content.type === 'text') { + return msg.content.text + } else { + // 对于其他类型的内容(如图片等),返回空字符串或特定标记 + return `[${msg.content?.type || 'content'}]` + } + }) + .filter(Boolean) + .join('\n') + return `@${block.id} ${messageTexts || block.content}` + } + } catch (e) { + // 如果解析失败,直接返回原始内容 + console.log('解析prompt内容失败:', e) + } + // 默认返回原内容 + return `@${block.id} ${block.content}` + } + return `@${block.id}` + } else if (block.type === 'text') { + return block.content + } else if (block.type === 'code') { + return `\`\`\`${block.content}\`\`\`` + } + return '' + }) + .join('') + } + + async clearContext(conversationId: string): Promise { + await this.sqlitePresenter.runTransaction(async () => { + const conversation = await this.getConversation(conversationId) + if (conversation) { + await this.sqlitePresenter.deleteAllMessages() + } + }) + } + /** + * + * @param conversationId + * @param content + * @param role + * @returns 如果是user的消息,返回ai生成的message,否则返回空 + */ + async sendMessage( + conversationId: string, + content: string, + role: MESSAGE_ROLE + ): Promise { + const conversation = await this.getConversation(conversationId) + const { providerId, modelId } = conversation.settings + console.log('sendMessage', conversation) + const message = await this.messageManager.sendMessage( + conversationId, + content, + role, + '', + false, + { + contextUsage: 0, + totalTokens: 0, + generationTime: 0, + firstTokenTime: 0, + tokensPerSecond: 0, + inputTokens: 0, + outputTokens: 0, + model: modelId, + provider: providerId + } + ) + if (role === 'user') { + const assistantMessage = await this.generateAIResponse(conversationId, message.id) + this.generatingMessages.set(assistantMessage.id, { + message: assistantMessage, + conversationId, + startTime: Date.now(), + firstTokenTime: null, + promptTokens: 0, + reasoningStartTime: null, + reasoningEndTime: null, + lastReasoningTime: null + }) + + // 检查是否是新会话的第一条消息 + const { list: messages } = await this.getMessages(conversationId, 1, 2) + if (messages.length === 1) { + // 更新会话的 is_new 标志位 + await this.sqlitePresenter.updateConversation(conversationId, { + is_new: 0, + updatedAt: Date.now() + }) + } else { + await this.sqlitePresenter.updateConversation(conversationId, { + updatedAt: Date.now() + }) + } + + // 因为handleLLMAgentEnd会处理会话列表广播,所以此处不用广播 + + return assistantMessage + } + + return null + } + + private async generateAIResponse(conversationId: string, userMessageId: string) { + try { + const triggerMessage = await this.messageManager.getMessage(userMessageId) + if (!triggerMessage) { + throw new Error('找不到触发消息') + } + + await this.messageManager.updateMessageStatus(userMessageId, 'sent') + + const conversation = await this.getConversation(conversationId) + const { providerId, modelId } = conversation.settings + const assistantMessage = (await this.messageManager.sendMessage( + conversationId, + JSON.stringify([]), + 'assistant', + userMessageId, + false, + { + contextUsage: 0, + totalTokens: 0, + generationTime: 0, + firstTokenTime: 0, + tokensPerSecond: 0, + inputTokens: 0, + outputTokens: 0, + model: modelId, + provider: providerId + } + )) as AssistantMessage + + return assistantMessage + } catch (error) { + await this.messageManager.updateMessageStatus(userMessageId, 'error') + console.error('生成 AI 响应失败:', error) + throw error + } + } + + async getMessage(messageId: string): Promise { + return await this.messageManager.getMessage(messageId) + } + + /** + * 获取指定消息之前的历史消息 + * @param messageId 消息ID + * @param limit 限制返回的消息数量 + * @returns 历史消息列表,按时间正序排列 + */ + private async getMessageHistory(messageId: string, limit: number = 100): Promise { + const message = await this.messageManager.getMessage(messageId) + if (!message) { + throw new Error('找不到指定的消息') + } + + const { list: messages } = await this.messageManager.getMessageThread( + message.conversationId, + 1, + limit * 2 + ) + + // 找到目标消息在列表中的位置 + const targetIndex = messages.findIndex((msg) => msg.id === messageId) + if (targetIndex === -1) { + return [message] + } + + // 返回目标消息之前的消息(包括目标消息) + return messages.slice(Math.max(0, targetIndex - limit + 1), targetIndex + 1) + } + + private async rewriteUserSearchQuery( + query: string, + contextMessages: string, + conversationId: string, + searchEngine: string + ): Promise { + const rewritePrompt = ` + 你非常擅长于使用搜索引擎去获取最新的数据,你的目标是在充分理解用户的问题后,进行全面的网络搜索搜集必要的信息,首先你要提取并优化搜索的查询内容 + + 现在时间:${new Date().toISOString()} + 正在使用的搜索引擎:${searchEngine} + + 请遵循以下规则重写搜索查询: + 1. 根据用户的问题和上下文,重写应该进行搜索的关键词 + 2. 如果需要使用时间,则根据当前时间给出需要查询的具体时间日期信息 + 3. 生成的查询关键词要选择合适的语言,考虑用户的问题类型使用最适合的语言进行搜索,例如某些问题应该保持用户的问题语言,而有一些则更适合翻译成英语或其他语言 + 4. 保持查询简洁,通常不超过3个关键词, 最多不要超过5个关键词,参考当前搜索引擎的查询习惯重写关键字 + + 直接返回优化后的搜索词,不要有任何额外说明。 + 如果你觉得用户的问题不需要进行搜索,请直接返回"无须搜索"。 + + 如下是之前对话的上下文: + + ${contextMessages} + + 如下是用户的问题: + + ${query} + + ` + const conversation = await this.getConversation(conversationId) + if (!conversation) { + return query + } + console.log('rewriteUserSearchQuery', query, contextMessages, conversation.id) + const { providerId, modelId } = conversation.settings + try { + const rewrittenQuery = await this.llmProviderPresenter.generateCompletion( + this.searchAssistantProviderId || providerId, + [ + { + role: 'user', + content: rewritePrompt + } + ], + this.searchAssistantModel?.id || modelId + ) + return rewrittenQuery.trim() || query + } catch (error) { + console.error('重写搜索查询失败:', error) + return query + } + } + + /** + * 检查消息是否已被取消 + * @param messageId 消息ID + * @returns 是否已被取消 + */ + private isMessageCancelled(messageId: string): boolean { + const state = this.generatingMessages.get(messageId) + return !state || state.isCancelled === true + } + + /** + * 如果消息已被取消,则抛出错误 + * @param messageId 消息ID + */ + private throwIfCancelled(messageId: string): void { + if (this.isMessageCancelled(messageId)) { + throw new Error('common.error.userCanceledGeneration') + } + } + + private async startStreamSearch( + conversationId: string, + messageId: string, + query: string + ): Promise { + const state = this.generatingMessages.get(messageId) + if (!state) { + throw new Error('找不到生成状态') + } + + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + // 添加搜索加载状态 + const searchBlock: AssistantMessageBlock = { + type: 'search', + content: '', + status: 'loading', + timestamp: Date.now(), + extra: { + total: 0 + } + } + state.message.content.unshift(searchBlock) + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + // 标记消息为搜索状态 + state.isSearching = true + this.searchingMessages.add(messageId) + try { + // 获取历史消息用于上下文 + const contextMessages = await this.getContextMessages(conversationId) + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + const formattedContext = contextMessages + .map((msg) => { + if (msg.role === 'user') { + const content = msg.content as UserMessageContent + return `user: ${content.text}${getFileContext(content.files)}` + } else if (msg.role === 'assistant') { + let finalContent = 'assistant: ' + const content = msg.content as AssistantMessageBlock[] + content.forEach((block) => { + if (block.type === 'content') { + finalContent += block.content + '\n' + } + if (block.type === 'search') { + finalContent += `search-result: ${JSON.stringify(block.extra)}` + } + if (block.type === 'tool_call') { + finalContent += `tool_call: ${JSON.stringify(block.tool_call)}` + } + if (block.type === 'image') { + finalContent += `image: ${block.image_data?.data}` + } + }) + return finalContent + } else { + return JSON.stringify(msg.content) + } + }) + .join('\n') + + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + // 重写搜索查询 + searchBlock.status = 'optimizing' + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + console.log('optimizing') + + const optimizedQuery = await this.rewriteUserSearchQuery( + query, + formattedContext, + conversationId, + this.searchManager.getActiveEngine().name + ).catch((err) => { + console.error('重写搜索查询失败:', err) + return query + }) + + // 如果不需要搜索,直接返回空结果 + if (optimizedQuery.includes('无须搜索')) { + searchBlock.status = 'success' + searchBlock.content = '' + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + state.isSearching = false + this.searchingMessages.delete(messageId) + return [] + } + + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + // 更新搜索状态为阅读中 + searchBlock.status = 'reading' + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + + // 开始搜索 + const results = await this.searchManager.search(conversationId, optimizedQuery) + + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + searchBlock.status = 'loading' + searchBlock.extra = { + total: results.length + } + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + + // 保存搜索结果 + for (const result of results) { + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + await this.sqlitePresenter.addMessageAttachment( + messageId, + 'search_result', + JSON.stringify({ + title: result.title, + url: result.url, + content: result.content || '', + description: result.description || '', + icon: result.icon || '' + }) + ) + } + + // 检查是否已被取消 + this.throwIfCancelled(messageId) + + // 更新搜索状态为成功 + searchBlock.status = 'success' + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + + // 标记消息搜索完成 + state.isSearching = false + this.searchingMessages.delete(messageId) + + return results + } catch (error) { + // 标记消息搜索完成 + state.isSearching = false + this.searchingMessages.delete(messageId) + + // 更新搜索状态为错误 + searchBlock.status = 'error' + searchBlock.content = String(error) + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + + if (String(error).includes('userCanceledGeneration')) { + // 如果是取消操作导致的错误,确保搜索窗口关闭 + this.searchManager.stopSearch(state.conversationId) + } + + return [] + } + } + + private async getLastUserMessage(conversationId: string): Promise { + return await this.messageManager.getLastUserMessage(conversationId) + } + + // 从数据库获取搜索结果 + async getSearchResults(messageId: string): Promise { + const results = await this.sqlitePresenter.getMessageAttachments(messageId, 'search_result') + return results.map((result) => JSON.parse(result.content) as SearchResult) ?? [] + } + + async startStreamCompletion(conversationId: string, queryMsgId?: string) { + const state = this.findGeneratingState(conversationId) + if (!state) { + console.warn('未找到状态,conversationId:', conversationId) + return + } + try { + // 设置消息未取消 + state.isCancelled = false + + // 1. 获取上下文信息 + const { conversation, userMessage, contextMessages } = await this.prepareConversationContext( + conversationId, + queryMsgId + ) + + const { providerId, modelId } = conversation.settings + const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + const { vision } = modelConfig || {} + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + + // 2. 处理用户消息内容 + const { userContent, urlResults, imageFiles } = await this.processUserMessageContent( + userMessage as UserMessage + ) + + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + + // 3. 处理搜索(如果需要) + let searchResults: SearchResult[] | null = null + if ((userMessage.content as UserMessageContent).search) { + try { + searchResults = await this.startStreamSearch( + conversationId, + state.message.id, + userContent + ) + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + } catch (error) { + // 如果是用户取消导致的错误,不继续后续步骤 + if (String(error).includes('userCanceledGeneration')) { + return + } + // 其他错误继续处理(搜索失败不应影响生成) + console.error('搜索过程中出错:', error) + } + } + + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + + // 4. 准备提示内容 + const { finalContent, promptTokens } = await this.preparePromptContent( + conversation, + userContent, + contextMessages, + searchResults, + urlResults, + userMessage, + vision, + vision ? imageFiles : [], + modelConfig.functionCall + ) + + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + + // 5. 更新生成状态 + await this.updateGenerationState(state, promptTokens) + + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + // 6. 启动流式生成 + + // 重新获取最新的会话设置,以防在之前的 await 期间发生变化 + const currentConversation = await this.getConversation(conversationId) + const { + providerId: currentProviderId, + modelId: currentModelId, + temperature: currentTemperature, + maxTokens: currentMaxTokens, + enabledMcpTools: currentEnabledMcpTools, + thinkingBudget: currentThinkingBudget, + reasoningEffort: currentReasoningEffort, + verbosity: currentVerbosity, + enableSearch: currentEnableSearch, + forcedSearch: currentForcedSearch, + searchStrategy: currentSearchStrategy + } = currentConversation.settings + const stream = this.llmProviderPresenter.startStreamCompletion( + currentProviderId, // 使用最新的设置 + finalContent, + currentModelId, // 使用最新的设置 + state.message.id, + currentTemperature, // 使用最新的设置 + currentMaxTokens, // 使用最新的设置 + currentEnabledMcpTools, + currentThinkingBudget, + currentReasoningEffort, + currentVerbosity, + currentEnableSearch, + currentForcedSearch, + currentSearchStrategy + ) + for await (const event of stream) { + const msg = event.data + if (event.type === 'response') { + await this.handleLLMAgentResponse(msg) + } else if (event.type === 'error') { + await this.handleLLMAgentError(msg) + } else if (event.type === 'end') { + await this.handleLLMAgentEnd(msg) + } + } + } catch (error) { + // 检查是否是取消错误 + if (String(error).includes('userCanceledGeneration')) { + console.log('消息生成已被用户取消') + return + } + + console.error('流式生成过程中出错:', error) + await this.messageManager.handleMessageError(state.message.id, String(error)) + throw error + } + } + async continueStreamCompletion(conversationId: string, queryMsgId: string) { + const state = this.findGeneratingState(conversationId) + if (!state) { + console.warn('未找到状态,conversationId:', conversationId) + return + } + + try { + // 设置消息未取消 + state.isCancelled = false + + // 1. 获取需要继续的消息 + const queryMessage = await this.messageManager.getMessage(queryMsgId) + if (!queryMessage) { + throw new Error('找不到指定的消息') + } + + // 2. 解析最后一个 action block + const content = queryMessage.content as AssistantMessageBlock[] + const lastActionBlock = content.filter((block) => block.type === 'action').pop() + + if (!lastActionBlock || lastActionBlock.type !== 'action') { + throw new Error('找不到最后的 action block') + } + + // 3. 检查是否是 maximum_tool_calls_reached + let toolCallResponse: { content: string; rawData: MCPToolResponse } | null = null + const toolCall = lastActionBlock.tool_call + + if (lastActionBlock.action_type === 'maximum_tool_calls_reached' && toolCall) { + // 设置 needContinue 为 0(false) + if (lastActionBlock.extra) { + lastActionBlock.extra = { + ...lastActionBlock.extra, + needContinue: false + } + } + await this.messageManager.editMessage(queryMsgId, JSON.stringify(content)) + + // 4. 检查工具调用参数 + if (!toolCall.id || !toolCall.name || !toolCall.params) { + // 参数不完整就跳过,然后继续执行即可 + console.warn('工具调用参数不完整') + } else { + // 5. 调用工具获取结果 + toolCallResponse = await presenter.mcpPresenter.callTool({ + id: toolCall.id, + type: 'function', + function: { + name: toolCall.name, + arguments: toolCall.params + }, + server: { + name: toolCall.server_name || '', + icons: toolCall.server_icons || '', + description: toolCall.server_description || '' + } + }) + } + } + + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + + // 6. 获取上下文信息 + const { conversation, contextMessages, userMessage } = await this.prepareConversationContext( + conversationId, + state.message.id + ) + + // 检查是否已被取消 + this.throwIfCancelled(state.message.id) + + // 7. 准备提示内容 + const { + providerId, + modelId, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + } = conversation.settings + const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + + const { finalContent, promptTokens } = await this.preparePromptContent( + conversation, + 'continue', + contextMessages, + null, // 不进行搜索 + [], // 没有 URL 结果 + userMessage, + false, + [], // 没有图片文件 + modelConfig.functionCall + ) + + // 8. 更新生成状态 + await this.updateGenerationState(state, promptTokens) + + // 9. 如果有工具调用结果,发送工具调用结果事件 + if (toolCallResponse && toolCall) { + // console.log('toolCallResponse', toolCallResponse) + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { + eventId: state.message.id, + content: '', + tool_call: 'start', + tool_call_id: toolCall.id, + tool_call_name: toolCall.name, + tool_call_params: toolCall.params, + tool_call_response: toolCallResponse.content, + tool_call_server_name: toolCall.server_name, + tool_call_server_icons: toolCall.server_icons, + tool_call_server_description: toolCall.server_description + }) + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { + eventId: state.message.id, + content: '', + tool_call: 'running', + tool_call_id: toolCall.id, + tool_call_name: toolCall.name, + tool_call_params: toolCall.params, + tool_call_response: toolCallResponse.content, + tool_call_server_name: toolCall.server_name, + tool_call_server_icons: toolCall.server_icons, + tool_call_server_description: toolCall.server_description + }) + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { + eventId: state.message.id, + content: '', + tool_call: 'end', + tool_call_id: toolCall.id, + tool_call_response: toolCallResponse.content, + tool_call_name: toolCall.name, + tool_call_params: toolCall.params, + tool_call_server_name: toolCall.server_name, + tool_call_server_icons: toolCall.server_icons, + tool_call_server_description: toolCall.server_description, + tool_call_response_raw: toolCallResponse.rawData + }) + } + + // 10. 启动流式生成 + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + finalContent, + modelId, + state.message.id, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + ) + for await (const event of stream) { + const msg = event.data + if (event.type === 'response') { + await this.handleLLMAgentResponse(msg) + } else if (event.type === 'error') { + await this.handleLLMAgentError(msg) + } else if (event.type === 'end') { + await this.handleLLMAgentEnd(msg) + } + } + } catch (error) { + // 检查是否是取消错误 + if (String(error).includes('userCanceledGeneration')) { + console.log('消息生成已被用户取消') + return + } + + console.error('继续生成过程中出错:', error) + await this.messageManager.handleMessageError(state.message.id, String(error)) + throw error + } + } + + // 查找特定会话的生成状态 + private findGeneratingState(conversationId: string): GeneratingMessageState | null { + return ( + Array.from(this.generatingMessages.values()).find( + (state) => state.conversationId === conversationId + ) || null + ) + } + + // 准备会话上下文 + private async prepareConversationContext( + conversationId: string, + queryMsgId?: string + ): Promise<{ + conversation: CONVERSATION + userMessage: Message + contextMessages: Message[] + }> { + const conversation = await this.getConversation(conversationId) + let contextMessages: Message[] = [] + let userMessage: Message | null = null + + if (queryMsgId) { + // 处理指定消息ID的情况 + const queryMessage = await this.getMessage(queryMsgId) + if (!queryMessage) { + throw new Error('找不到指定的消息') + } + + // 修复:根据消息类型确定如何获取用户消息 + if (queryMessage.role === 'user') { + // 如果 queryMessage 就是用户消息,直接使用 + userMessage = queryMessage + } else if (queryMessage.role === 'assistant') { + // 如果 queryMessage 是助手消息,获取它的 parentId(用户消息) + if (!queryMessage.parentId) { + throw new Error('助手消息缺少 parentId') + } + userMessage = await this.getMessage(queryMessage.parentId) + if (!userMessage) { + throw new Error('找不到触发消息') + } + } else { + throw new Error('不支持的消息类型') + } + + contextMessages = await this.getMessageHistory( + userMessage.id, + conversation.settings.contextLength + ) + } else { + // 获取最新的用户消息 + userMessage = await this.getLastUserMessage(conversationId) + if (!userMessage) { + throw new Error('找不到用户消息') + } + contextMessages = await this.getContextMessages(conversationId) + } + + // 处理 UserMessageMentionBlock + if (userMessage.role === 'user') { + const msgContent = userMessage.content as UserMessageContent + if (msgContent.content && !msgContent.text) { + msgContent.text = this.formatUserMessageContent(msgContent.content) + } + } + + // 任何情况都使用最新配置 + const webSearchEnabled = this.configPresenter.getSetting('input_webSearch') as boolean + const thinkEnabled = this.configPresenter.getSetting('input_deepThinking') as boolean + ;(userMessage.content as UserMessageContent).search = webSearchEnabled + ;(userMessage.content as UserMessageContent).think = thinkEnabled + return { conversation, userMessage, contextMessages } + } + + // 处理用户消息内容 + private async processUserMessageContent(userMessage: UserMessage): Promise<{ + userContent: string + urlResults: SearchResult[] + imageFiles: MessageFile[] // 图片文件列表 + }> { + // 处理文本内容 + const userContent = ` + ${ + userMessage.content.content + ? this.formatUserMessageContent(userMessage.content.content) + : userMessage.content.text + } + ${getFileContext(userMessage.content.files)} + ` + + // 从用户消息中提取并丰富URL内容 + const urlResults = await ContentEnricher.extractAndEnrichUrls(userMessage.content.text) + + // 提取图片文件 + + const imageFiles = + userMessage.content.files?.filter((file) => { + // 根据文件类型、MIME类型或扩展名过滤图片文件 + const isImage = + file.mimeType.startsWith('data:image') || + /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name || '') + return isImage + }) || [] + + return { userContent, urlResults, imageFiles } + } + + // 准备提示内容 + private async preparePromptContent( + conversation: CONVERSATION, + userContent: string, + contextMessages: Message[], + searchResults: SearchResult[] | null, + urlResults: SearchResult[], + userMessage: Message, + vision: boolean, + imageFiles: MessageFile[], + supportsFunctionCall: boolean, + modelType?: ModelType + ): Promise<{ + finalContent: ChatMessage[] + promptTokens: number + }> { + const { systemPrompt, contextLength, artifacts, enabledMcpTools } = conversation.settings + + // 判断是否为图片生成模型 + const isImageGeneration = modelType === ModelType.ImageGeneration + + // 图片生成模型不使用搜索、系统提示词和MCP工具 + const searchPrompt = + !isImageGeneration && searchResults ? generateSearchPrompt(userContent, searchResults) : '' + const enrichedUserMessage = + !isImageGeneration && urlResults.length > 0 + ? '\n\n' + ContentEnricher.enrichUserMessageWithUrlContent(userContent, urlResults) + : '' + + // 处理系统提示词,添加当前时间信息 + const finalSystemPrompt = this.enhanceSystemPromptWithDateTime(systemPrompt, isImageGeneration) + + // 计算token数量(使用处理后的系统提示词) + const searchPromptTokens = searchPrompt ? approximateTokenSize(searchPrompt ?? '') : 0 + const systemPromptTokens = + !isImageGeneration && finalSystemPrompt ? approximateTokenSize(finalSystemPrompt ?? '') : 0 + const userMessageTokens = approximateTokenSize(userContent + enrichedUserMessage) + // 图片生成模型不使用MCP工具 + const mcpTools = !isImageGeneration + ? await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) + : [] + const mcpToolsTokens = mcpTools.reduce( + (acc, tool) => acc + approximateTokenSize(JSON.stringify(tool)), + 0 + ) + // 计算剩余可用的上下文长度 + const reservedTokens = + searchPromptTokens + systemPromptTokens + userMessageTokens + mcpToolsTokens + const remainingContextLength = contextLength - reservedTokens + + // 选择合适的上下文消息 + const selectedContextMessages = this.selectContextMessages( + contextMessages, + userMessage, + remainingContextLength + ) + + // 格式化消息 + const formattedMessages = this.formatMessagesForCompletion( + selectedContextMessages, + isImageGeneration ? '' : finalSystemPrompt, // 图片生成模型不使用系统提示词 + artifacts, + searchPrompt, + userContent, + enrichedUserMessage, + imageFiles, + vision, + supportsFunctionCall + ) + + // 合并连续的相同角色消息 + const mergedMessages = this.mergeConsecutiveMessages(formattedMessages) + + // 计算prompt tokens + let promptTokens = 0 + for (const msg of mergedMessages) { + if (typeof msg.content === 'string') { + promptTokens += approximateTokenSize(msg.content) + } else { + promptTokens += + approximateTokenSize(msg.content?.map((item) => item.text).join('') || '') + + imageFiles.reduce((acc, file) => acc + file.token, 0) + } + } + // console.log('preparePromptContent', mergedMessages, promptTokens) + + return { finalContent: mergedMessages, promptTokens } + } + + // 选择上下文消息 + private selectContextMessages( + contextMessages: Message[], + userMessage: Message, + remainingContextLength: number + ): Message[] { + if (remainingContextLength <= 0) { + return [] + } + + const messages = contextMessages.filter((msg) => msg.id !== userMessage?.id).reverse() + + let currentLength = 0 + const selectedMessages: Message[] = [] + + for (const msg of messages) { + if (msg.status !== 'sent') { + continue + } + const msgContent = msg.role === 'user' ? (msg.content as UserMessageContent) : null + const msgText = msgContent + ? msgContent.text || + (msgContent.content ? this.formatUserMessageContent(msgContent.content) : '') + : '' + + const msgTokens = approximateTokenSize( + msg.role === 'user' + ? `${msgText}${getFileContext(msgContent?.files || [])}` + : JSON.stringify(msg.content) + ) + + if (currentLength + msgTokens <= remainingContextLength) { + // 如果是用户消息且有 content 但没有 text,添加 text + if (msg.role === 'user') { + const userMsgContent = msg.content as UserMessageContent + if (userMsgContent.content && !userMsgContent.text) { + userMsgContent.text = this.formatUserMessageContent(userMsgContent.content) + } + } + + selectedMessages.unshift(msg) + currentLength += msgTokens + } else { + break + } + } + while (selectedMessages.length > 0 && selectedMessages[0].role !== 'user') { + selectedMessages.shift() + } + return selectedMessages + } + + // 格式化消息用于完成 + private formatMessagesForCompletion( + contextMessages: Message[], + systemPrompt: string, + artifacts: number, + searchPrompt: string, + userContent: string, + enrichedUserMessage: string, + imageFiles: MessageFile[], + vision: boolean, + supportsFunctionCall: boolean + ): ChatMessage[] { + const formattedMessages: ChatMessage[] = [] + + // 添加上下文消息 + formattedMessages.push( + ...this.addContextMessages(contextMessages, vision, supportsFunctionCall) + ) + + // 添加系统提示 + if (systemPrompt) { + // formattedMessages.push(...this.addSystemPrompt(formattedMessages, systemPrompt, artifacts)) + formattedMessages.unshift({ + role: 'system', + content: systemPrompt + }) + // console.log('-------------> system prompt \n', systemPrompt, artifacts, formattedMessages) + } + + // 添加当前用户消息 + let finalContent = searchPrompt || userContent + + if (enrichedUserMessage) { + finalContent += enrichedUserMessage + } + + if (artifacts === 1) { + // formattedMessages.push({ + // role: 'user', + // content: ARTIFACTS_PROMPT + // }) + console.log('artifacts目前由mcp提供,此处为兼容性保留') + } + // 没有 vision 就不用塞进去了 + if (vision && imageFiles.length > 0) { + formattedMessages.push(this.addImageFiles(finalContent, imageFiles)) + } else { + formattedMessages.push({ + role: 'user', + content: finalContent.trim() + }) + } + + return formattedMessages + } + + private addImageFiles(finalContent: string, imageFiles: MessageFile[]): ChatMessage { + return { + role: 'user', + content: [ + ...imageFiles.map((file) => ({ + type: 'image_url' as const, + image_url: { url: file.content, detail: 'auto' as const } + })), + { type: 'text' as const, text: finalContent.trim() } + ] + } + } + + // 添加上下文消息 + private addContextMessages( + contextMessages: Message[], + vision: boolean, + supportsFunctionCall: boolean + ): ChatMessage[] { + const resultMessages = [] as ChatMessage[] + + // 对于原生fc模型,支持正确的tool_call response history插入 + if (supportsFunctionCall) { + contextMessages.forEach((msg) => { + if (msg.role === 'user') { + // 处理用户消息 + const msgContent = msg.content as UserMessageContent + const msgText = msgContent.content + ? this.formatUserMessageContent(msgContent.content) + : msgContent.text + const userContent = `${msgText}${getFileContext(msgContent.files)}` + resultMessages.push({ + role: 'user', + content: userContent + }) + } else if (msg.role === 'assistant') { + // 处理助手消息 + let afterSearch = false + const assistantBlocks = msg.content as AssistantMessageBlock[] + for (const subMsg of assistantBlocks) { + if ( + subMsg.type === 'tool_call' && + subMsg?.tool_call?.id?.trim() && + subMsg?.tool_call?.name?.trim() && + subMsg?.tool_call?.params?.trim() && + subMsg?.tool_call?.response?.trim() + ) { + resultMessages.push({ + role: 'assistant', + tool_calls: [ + { + id: subMsg.tool_call.id, + type: 'function', + function: { + name: subMsg.tool_call.name, + arguments: subMsg.tool_call.params + } + } + ] + }) + resultMessages.push({ + role: 'tool', + tool_call_id: subMsg.tool_call.id, + content: subMsg.tool_call.response + }) + } else if (subMsg.type === 'search') { + // 删除强制搜索结果中遗留的[x]引文标记 + afterSearch = true + } else if (subMsg.type === 'content') { + // 删除强制搜索结果中遗留的[x]引文标记 + let content = subMsg.content ?? '' + if (afterSearch) content = content.replace(/\[\d+\]/g, '') + resultMessages.push({ + role: 'assistant', + content: content + }) + afterSearch = false + } + } + } + }) + return resultMessages + } else { + // 对于非原生fc模型,支持规范化prompt实现 + contextMessages.forEach((msg) => { + if (msg.role === 'user') { + // 处理用户消息 + const msgContent = msg.content as UserMessageContent + const msgText = msgContent.content + ? this.formatUserMessageContent(msgContent.content) + : msgContent.text + const userContent = `${msgText}${getFileContext(msgContent.files)}` + resultMessages.push({ + role: 'user', + content: userContent + }) + } else if (msg.role === 'assistant') { + // 处理助手消息 + const assistantBlocks = msg.content as AssistantMessageBlock[] + // 提取文本内容块,同时将工具调用的响应内容提取出来 + let afterSearch = false + const textContent = assistantBlocks + .filter( + (block) => + block.type === 'content' || block.type === 'search' || block.type === 'tool_call' + ) + .map((block) => { + if (block.type === 'search') { + // 删除强制搜索结果中遗留的[x]引文标记 + afterSearch = true + return '' + } else if (block.type === 'content') { + // 删除强制搜索结果中遗留的[x]引文标记 + let content = block.content ?? '' + if (afterSearch) content = content.replace(/\[\d+\]/g, '') + afterSearch = false + return content + } else if ( + block.type === 'tool_call' && + block.tool_call?.response && + block.tool_call?.params + ) { + let parsedParams + let parsedResponse + + try { + parsedParams = JSON.parse(block.tool_call.params) + } catch { + parsedParams = block.tool_call.params // 保留原字符串 + } + + try { + parsedResponse = JSON.parse(block.tool_call.response) + } catch { + parsedResponse = block.tool_call.response // 保留原字符串 + } + + return ( + '' + + JSON.stringify({ + function_call_record: { + name: block.tool_call.name, + arguments: parsedParams, + response: parsedResponse + } + }) + + '' + ) + } else { + return '' // 若 tool_call 或 response、params 是 undefined 返回。只是便于调试而已,可以为空。 + } + }) + .join('\n') + + // 查找图像块 + const imageBlocks = assistantBlocks.filter( + (block) => block.type === 'image' && block.image_data + ) + + // 如果没有任何内容,则跳过此消息 + if (!textContent && imageBlocks.length === 0) { + return + } + + // 如果有图像,则使用复合内容格式 + if (vision && imageBlocks.length > 0) { + const content: ChatMessageContent[] = [] + + // 添加图像内容 + imageBlocks.forEach((block) => { + if (block.image_data) { + content.push({ + type: 'image_url', + image_url: { + url: block.image_data.data, + detail: 'auto' + } + }) + } + }) + + // 添加文本内容 + if (textContent) { + content.push({ + type: 'text', + text: textContent + }) + } + + resultMessages.push({ + role: 'assistant', + content: content + }) + } else { + // 仅有文本内容 + resultMessages.push({ + role: 'assistant', + content: textContent + }) + } + } + }) + + return resultMessages + } + } + + // 合并连续的相同角色的content,但注意assistant下content不能跟tool_calls合并 + private mergeConsecutiveMessages(messages: ChatMessage[]): ChatMessage[] { + if (!messages || messages.length === 0) { + return [] + } + + const mergedResult: ChatMessage[] = [] + // 为第一条消息创建一个深拷贝并添加到结果数组 + mergedResult.push(JSON.parse(JSON.stringify(messages[0]))) + + for (let i = 1; i < messages.length; i++) { + // 为当前消息创建一个深拷贝 + const currentMessage = JSON.parse(JSON.stringify(messages[i])) as ChatMessage + const lastPushedMessage = mergedResult[mergedResult.length - 1] + + let allowMessagePropertiesMerge = false // 标志是否允许消息属性(如content)合并 + + // 步骤 1: 判断消息本身是否允许合并(基于role和tool_calls) + if (lastPushedMessage.role === currentMessage.role) { + if (currentMessage.role === 'assistant') { + // Assistant消息: 仅当两条消息都【不】包含tool_calls时,才允许合并 + if (!lastPushedMessage.tool_calls && !currentMessage.tool_calls) { + allowMessagePropertiesMerge = true + } + } else { + // 其他角色 (user, system): 如果role相同,则允许合并 + allowMessagePropertiesMerge = true + } + } + + if (allowMessagePropertiesMerge) { + // 步骤 2: 如果消息允许合并,尝试合并其 content 字段 + const LMC = lastPushedMessage.content // 上一条已推送消息的内容 + const CMC = currentMessage.content // 当前待处理消息的内容 + + let newCombinedContent: string | ChatMessageContent[] | undefined = undefined + let contentTypesCompatibleForMerging = false + + if (LMC === undefined && CMC === undefined) { + newCombinedContent = undefined + contentTypesCompatibleForMerging = true + } else if (typeof LMC === 'string' && (typeof CMC === 'string' || CMC === undefined)) { + // LMC是string, CMC是string或undefined + const sLMC = LMC || '' + const sCMC = CMC || '' + if (sLMC && sCMC) newCombinedContent = `${sLMC}\n${sCMC}` + else newCombinedContent = sLMC || sCMC // 保留有内容的一方 + if (newCombinedContent === '') newCombinedContent = undefined // 空字符串视为undefined + contentTypesCompatibleForMerging = true + } else if (Array.isArray(LMC) && (Array.isArray(CMC) || CMC === undefined)) { + // LMC是数组, CMC是数组或undefined + const arrLMC = LMC + const arrCMC = CMC || [] // 如果CMC是undefined, 视为空数组进行合并 + newCombinedContent = [...arrLMC, ...arrCMC] + if (newCombinedContent.length === 0) newCombinedContent = undefined // 空数组视为undefined + contentTypesCompatibleForMerging = true + } else if (LMC === undefined && CMC !== undefined) { + // LMC是undefined, CMC有值 (string或array) + newCombinedContent = CMC + contentTypesCompatibleForMerging = true + } else if (LMC !== undefined && CMC === undefined) { + // LMC有值, CMC是undefined -> content保持LMC的值,无需改变 + newCombinedContent = LMC + contentTypesCompatibleForMerging = true // 视为成功合并(当前消息内容被"吸收") + } + // 如果LMC和CMC的类型不兼容 (例如一个是string, 另一个是array), + // contentTypesCompatibleForMerging 将保持 false + + if (contentTypesCompatibleForMerging) { + lastPushedMessage.content = newCombinedContent + // currentMessage 被成功合并,不需单独push + } else { + // 角色和tool_calls条件允许合并,但内容类型不兼容 + // 因此,不合并消息,将 currentMessage 作为新消息加入 + mergedResult.push(currentMessage) + } + } else { + // 角色不同,或者 assistant 消息因 tool_calls 而不允许合并 + // 将 currentMessage 作为新消息加入 + mergedResult.push(currentMessage) + } + } + + return mergedResult + } + + // 更新生成状态 + private async updateGenerationState( + state: GeneratingMessageState, + promptTokens: number + ): Promise { + // 更新生成状态 + this.generatingMessages.set(state.message.id, { + ...state, + startTime: Date.now(), + firstTokenTime: null, + promptTokens + }) + + // 更新消息的usage信息 + await this.messageManager.updateMessageMetadata(state.message.id, { + totalTokens: promptTokens, + generationTime: 0, + firstTokenTime: 0, + tokensPerSecond: 0 + }) + } + + async editMessage(messageId: string, content: string): Promise { + return await this.messageManager.editMessage(messageId, content) + } + + async deleteMessage(messageId: string): Promise { + await this.messageManager.deleteMessage(messageId) + } + + async retryMessage(messageId: string): Promise { + const message = await this.messageManager.getMessage(messageId) + if (message.role !== 'assistant') { + throw new Error('只能重试助手消息') + } + + const userMessage = await this.messageManager.getMessage(message.parentId || '') + if (!userMessage) { + throw new Error('找不到对应的用户消息') + } + const conversation = await this.getConversation(message.conversationId) + const { providerId, modelId } = conversation.settings + const assistantMessage = await this.messageManager.retryMessage(messageId, { + totalTokens: 0, + generationTime: 0, + firstTokenTime: 0, + tokensPerSecond: 0, + contextUsage: 0, + inputTokens: 0, + outputTokens: 0, + model: modelId, + provider: providerId + }) + + // 初始化生成状态 + this.generatingMessages.set(assistantMessage.id, { + message: assistantMessage as AssistantMessage, + conversationId: message.conversationId, + startTime: Date.now(), + firstTokenTime: null, + promptTokens: 0, + reasoningStartTime: null, + reasoningEndTime: null, + lastReasoningTime: null + }) + + return assistantMessage as AssistantMessage + } + + async getMessageVariants(messageId: string): Promise { + return await this.messageManager.getMessageVariants(messageId) + } + + async updateMessageStatus(messageId: string, status: MESSAGE_STATUS): Promise { + await this.messageManager.updateMessageStatus(messageId, status) + } + + async updateMessageMetadata( + messageId: string, + metadata: Partial + ): Promise { + await this.messageManager.updateMessageMetadata(messageId, metadata) + } + + async markMessageAsContextEdge(messageId: string, isEdge: boolean): Promise { + await this.messageManager.markMessageAsContextEdge(messageId, isEdge) + } + + async getActiveConversationId(tabId: number): Promise { + return this.activeConversationIds.get(tabId) || null + } + + private async getLatestConversation(): Promise { + const result = await this.getConversationList(1, 1) + return result.list[0] || null + } + + getGeneratingMessageState(messageId: string): GeneratingMessageState | null { + return this.generatingMessages.get(messageId) || null + } + + getConversationGeneratingMessages(conversationId: string): AssistantMessage[] { + return Array.from(this.generatingMessages.values()) + .filter((state) => state.conversationId === conversationId) + .map((state) => state.message) + } + + async stopMessageGeneration(messageId: string): Promise { + const state = this.generatingMessages.get(messageId) + if (state) { + // 设置统一的取消标志 + state.isCancelled = true + + // 刷新剩余缓冲内容 + if (state.adaptiveBuffer) { + await this.flushAdaptiveBuffer(messageId) + } + + // 清理缓冲相关资源 + this.cleanupContentBuffer(state) + + // 标记消息不再处于搜索状态 + if (state.isSearching) { + this.searchingMessages.delete(messageId) + + // 停止搜索窗口 + await this.searchManager.stopSearch(state.conversationId) + } + + // 添加用户取消的消息块 + state.message.content.forEach((block) => { + if ( + block.status === 'loading' || + block.status === 'reading' || + block.status === 'optimizing' + ) { + block.status = 'success' + } + }) + state.message.content.push({ + type: 'error', + content: 'common.error.userCanceledGeneration', + status: 'cancel', + timestamp: Date.now() + }) + + // 更新消息状态和内容 + await this.messageManager.updateMessageStatus(messageId, 'error') + await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) + + // 停止流式生成 + await this.llmProviderPresenter.stopStream(messageId) + + // 清理生成状态 + this.generatingMessages.delete(messageId) + } + } + + async stopConversationGeneration(conversationId: string): Promise { + const messageIds = Array.from(this.generatingMessages.entries()) + .filter(([, state]) => state.conversationId === conversationId) + .map(([messageId]) => messageId) + + await Promise.all(messageIds.map((messageId) => this.stopMessageGeneration(messageId))) + } + + async summaryTitles(tabId?: number, conversationId?: string): Promise { + const targetConversationId = + conversationId ?? (tabId !== undefined ? this.activeConversationIds.get(tabId) : undefined) + if (!targetConversationId) { + throw new Error('找不到当前对话') + } + const conversation = await this.getConversation(targetConversationId) + if (!conversation) { + throw new Error('找不到当前对话') + } + let summaryProviderId = conversation.settings.providerId + const modelId = this.searchAssistantModel?.id + summaryProviderId = this.searchAssistantProviderId || conversation.settings.providerId + const messages = await this.getContextMessages(conversation.id) + const messagesWithLength = messages + .map((msg) => { + if (msg.role === 'user') { + return { + message: msg, + length: + `${(msg.content as UserMessageContent).text}${getFileContext((msg.content as UserMessageContent).files)}` + .length, + formattedMessage: { + role: 'user' as const, + content: `${(msg.content as UserMessageContent).text}${getFileContext((msg.content as UserMessageContent).files)}` + } + } + } else { + const content = (msg.content as AssistantMessageBlock[]) + .filter((block) => block.type === 'content') + .map((block) => block.content) + .join('\n') + return { + message: msg, + length: content.length, + formattedMessage: { + role: 'assistant' as const, + content: content + } + } + } + }) + .filter((item) => item.formattedMessage.content.length > 0) + const title = await this.llmProviderPresenter.summaryTitles( + messagesWithLength.map((item) => item.formattedMessage), + summaryProviderId || conversation.settings.providerId, + modelId || conversation.settings.modelId + ) + console.log('-------------> title \n', title) + let cleanedTitle = title.replace(/.*?<\/think>/g, '').trim() + cleanedTitle = cleanedTitle.replace(/^/, '').trim() + console.log('-------------> cleanedTitle \n', cleanedTitle) + return cleanedTitle + } + + async clearActiveThread(tabId: number): Promise { + this.activeConversationIds.delete(tabId) + eventBus.sendToRenderer(CONVERSATION_EVENTS.DEACTIVATED, SendTarget.ALL_WINDOWS, { tabId }) + } + + async clearAllMessages(conversationId: string): Promise { + await this.messageManager.clearAllMessages(conversationId) + // 检查所有 tab 中的活跃会话 + for (const [, activeId] of this.activeConversationIds.entries()) { + if (activeId === conversationId) { + // 停止所有正在生成的消息 + await this.stopConversationGeneration(conversationId) + } + } + } + + async getMessageExtraInfo(messageId: string, type: string): Promise[]> { + const attachments = await this.sqlitePresenter.getMessageAttachments(messageId, type) + return attachments.map((attachment) => JSON.parse(attachment.content)) + } + + async getMainMessageByParentId( + conversationId: string, + parentId: string + ): Promise { + const message = await this.messageManager.getMainMessageByParentId(conversationId, parentId) + if (!message) { + return null + } + return message + } + + destroy() { + this.searchManager.destroy() + } + + /** + * 创建会话的分支 + * @param targetConversationId 源会话ID + * @param targetMessageId 目标消息ID(截止到该消息的所有消息将被复制) + * @param newTitle 新会话标题 + * @param settings 新会话设置 + * @returns 新创建的会话ID + */ + async forkConversation( + targetConversationId: string, + targetMessageId: string, + newTitle: string, + settings?: Partial + ): Promise { + try { + // 1. 获取源会话信息 + const sourceConversation = await this.sqlitePresenter.getConversation(targetConversationId) + if (!sourceConversation) { + throw new Error('源会话不存在') + } + + // 2. 创建新会话 + const newConversationId = await this.sqlitePresenter.createConversation(newTitle) + + // 更新会话设置 + if (settings || sourceConversation.settings) { + await this.updateConversationSettings( + newConversationId, + settings || sourceConversation.settings + ) + } + + // 更新is_new标志 + await this.sqlitePresenter.updateConversation(newConversationId, { is_new: 0 }) + + // 3. 获取源会话中的消息历史 + const message = await this.messageManager.getMessage(targetMessageId) + if (!message) { + throw new Error('目标消息不存在') + } + + // 获取目标消息之前的所有消息(包括目标消息) + const messageHistory = await this.getMessageHistory(targetMessageId, 100) + + // 4. 直接操作数据库复制消息到新会话 + for (const msg of messageHistory) { + // 只复制已发送成功的消息 + if (msg.status !== 'sent') { + continue + } + + // 获取消息序号 + const orderSeq = (await this.sqlitePresenter.getMaxOrderSeq(newConversationId)) + 1 + + // 解析元数据 + const metadata: MESSAGE_METADATA = { + totalTokens: msg.usage?.total_tokens || 0, + generationTime: 0, + firstTokenTime: 0, + tokensPerSecond: 0, + contextUsage: 0, + inputTokens: msg.usage?.input_tokens || 0, + outputTokens: msg.usage?.output_tokens || 0, + ...(msg.model_id ? { model: msg.model_id } : {}), + ...(msg.model_provider ? { provider: msg.model_provider } : {}) + } + + // 计算token数量 + const tokenCount = msg.usage?.total_tokens || 0 + + // 内容处理(确保是字符串) + const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + + // 直接插入消息记录 + await this.sqlitePresenter.insertMessage( + newConversationId, // 新会话ID + content, // 内容 + msg.role, // 角色 + '', // 无父消息ID + JSON.stringify(metadata), // 元数据 + orderSeq, // 序号 + tokenCount, // token数 + 'sent', // 状态固定为sent + 0, // 不是上下文边界 + 0 // 不是变体 + ) + } + + // 在所有数据库操作完成后,调用广播方法 + await this.broadcastThreadListUpdate() + + // 5. 触发会话创建事件 + return newConversationId + } catch (error) { + console.error('分支会话失败:', error) + throw error + } + } + + // 翻译文本 + async translateText(text: string, tabId: number): Promise { + try { + let conversation = await this.getActiveConversation(tabId) + if (!conversation) { + // 创建一个临时对话用于翻译 + const defaultProvider = this.configPresenter.getDefaultProviders()[0] + const models = await this.llmProviderPresenter.getModelList(defaultProvider.id) + const defaultModel = models[0] + const conversationId = await this.createConversation( + '临时翻译对话', + { + modelId: defaultModel.id, + providerId: defaultProvider.id + }, + tabId + ) + conversation = await this.getConversation(conversationId) + } + + const { providerId, modelId } = conversation.settings + const messages: ChatMessage[] = [ + { + role: 'system', + content: + '你是一个翻译助手。请将用户输入的文本翻译成中文。只返回翻译结果,不要添加任何其他内容。' + }, + { + role: 'user', + content: text + } + ] + + let translatedText = '' + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + messages, + modelId, + 'translate-' + Date.now(), + 0.3, + 1000 + ) + + for await (const event of stream) { + if (event.type === 'response') { + const msg = event.data as LLMAgentEventData + if (msg.content) { + translatedText += msg.content + } + } else if (event.type === 'error') { + const msg = event.data as { eventId: string; error: string } + throw new Error(msg.error || '翻译失败') + } + } + + return translatedText.trim() + } catch (error) { + console.error('翻译失败:', error) + throw error + } + } + + // AI询问 + async askAI(text: string, tabId: number): Promise { + try { + let conversation = await this.getActiveConversation(tabId) + if (!conversation) { + // 创建一个临时对话用于AI询问 + const defaultProvider = this.configPresenter.getDefaultProviders()[0] + const models = await this.llmProviderPresenter.getModelList(defaultProvider.id) + const defaultModel = models[0] + const conversationId = await this.createConversation( + '临时AI对话', + { + modelId: defaultModel.id, + providerId: defaultProvider.id + }, + tabId + ) + conversation = await this.getConversation(conversationId) + } + + const { providerId, modelId } = conversation.settings + const messages: ChatMessage[] = [ + { + role: 'system', + content: '你是一个AI助手。请简洁地回答用户的问题。' + }, + { + role: 'user', + content: text + } + ] + + let aiAnswer = '' + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + messages, + modelId, + 'ask-ai-' + Date.now(), + 0.7, + 1000 + ) + + for await (const event of stream) { + if (event.type === 'response') { + const msg = event.data as LLMAgentEventData + if (msg.content) { + aiAnswer += msg.content + } + } else if (event.type === 'error') { + const msg = event.data as { eventId: string; error: string } + throw new Error(msg.error || 'AI回答失败') + } + } + + return aiAnswer.trim() + } catch (error) { + console.error('AI询问失败:', error) + throw error + } + } + + private async broadcastThreadListUpdate(): Promise { + // 1. 获取所有会话 (假设9999足够大) + const result = await this.sqlitePresenter.getConversationList(1, this.fetchThreadLength) + + // 2. 分离置顶和非置顶会话 + const pinnedConversations: CONVERSATION[] = [] + const normalConversations: CONVERSATION[] = [] + + result.list.forEach((conv) => { + if (conv.is_pinned === 1) { + pinnedConversations.push(conv) + } else { + normalConversations.push(conv) + } + }) + + // 3. 对置顶会话按更新时间排序 + pinnedConversations.sort((a, b) => b.updatedAt - a.updatedAt) + + // 4. 对普通会话按更新时间排序 + normalConversations.sort((a, b) => b.updatedAt - a.updatedAt) + + // 5. 按日期分组 + const groupedThreads: Map = new Map() + + // 先添加置顶分组(如果有置顶会话) + if (pinnedConversations.length > 0) { + groupedThreads.set('Pinned', pinnedConversations) + } + + // 再添加普通会话的日期分组 + normalConversations.forEach((conv) => { + const date = new Date(conv.updatedAt).toISOString().split('T')[0] + if (!groupedThreads.has(date)) { + groupedThreads.set(date, []) + } + groupedThreads.get(date)!.push(conv) + }) + + const finalGroupedList = Array.from(groupedThreads.entries()).map(([dt, dtThreads]) => ({ + dt, + dtThreads + })) + + // 6. 广播这个格式化好的完整列表 + eventBus.sendToRenderer( + CONVERSATION_EVENTS.LIST_UPDATED, + SendTarget.ALL_WINDOWS, + finalGroupedList + ) + } + + /** + * 导出会话内容 + * @param conversationId 会话ID + * @param format 导出格式 ('markdown' | 'html' | 'txt') + * @returns 包含文件名和内容的对象 + */ + async exportConversation( + conversationId: string, + format: 'markdown' | 'html' | 'txt' = 'markdown' + ): Promise<{ + filename: string + content: string + }> { + try { + // 获取会话信息 + const conversation = await this.getConversation(conversationId) + if (!conversation) { + throw new Error('会话不存在') + } + + // 获取所有消息 + const { list: messages } = await this.getMessages(conversationId, 1, 10000) + + // 过滤掉未发送成功的消息 + const validMessages = messages.filter((msg) => msg.status === 'sent') + + // 生成文件名 - 使用简化的时间戳格式 + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, '-') + .replace('T', '_') + .substring(0, 19) + const extension = format === 'markdown' ? 'md' : format + const filename = `export_deepchat_${timestamp}.${extension}` + + // 生成内容(在主进程中直接处理,避免Worker的复杂性) + let content: string + switch (format) { + case 'markdown': + content = this.exportToMarkdown(conversation, validMessages) + break + case 'html': + content = this.exportToHtml(conversation, validMessages) + break + case 'txt': + content = this.exportToText(conversation, validMessages) + break + default: + throw new Error(`不支持的导出格式: ${format}`) + } + + return { filename, content } + } catch (error) { + console.error('Failed to export conversation:', error) + throw error + } + } + + /** + * 导出为 Markdown 格式 + */ + private exportToMarkdown(conversation: CONVERSATION, messages: Message[]): string { + const lines: string[] = [] + + // 标题和元信息 + lines.push(`# ${conversation.title}`) + lines.push('') + lines.push(`**Export Time:** ${new Date().toLocaleString()}`) + lines.push(`**Conversation ID:** ${conversation.id}`) + lines.push(`**Message Count:** ${messages.length}`) + if (conversation.settings.modelId) { + lines.push(`**Model:** ${conversation.settings.modelId}`) + } + if (conversation.settings.providerId) { + lines.push(`**Provider:** ${conversation.settings.providerId}`) + } + lines.push('') + lines.push('---') + lines.push('') + + // 处理每条消息 + for (const message of messages) { + const messageTime = new Date(message.timestamp).toLocaleString() + + if (message.role === 'user') { + lines.push(`## 👤 用户 (${messageTime})`) + lines.push('') + + const userContent = message.content as UserMessageContent + const messageText = userContent.content + ? this.formatUserMessageContent(userContent.content) + : userContent.text + + lines.push(messageText) + + // 处理文件附件 + if (userContent.files && userContent.files.length > 0) { + lines.push('') + lines.push('**附件:**') + for (const file of userContent.files) { + lines.push(`- ${file.name} (${file.mimeType})`) + } + } + + // 处理链接 + if (userContent.links && userContent.links.length > 0) { + lines.push('') + lines.push('**链接:**') + for (const link of userContent.links) { + lines.push(`- ${link}`) + } + } + } else if (message.role === 'assistant') { + lines.push(`## 🤖 助手 (${messageTime})`) + lines.push('') + + const assistantBlocks = message.content as AssistantMessageBlock[] + + for (const block of assistantBlocks) { + switch (block.type) { + case 'content': + if (block.content) { + lines.push(block.content) + lines.push('') + } + break + + case 'reasoning_content': + if (block.content) { + lines.push('### 🤔 思考过程') + lines.push('') + lines.push('```') + lines.push(block.content) + lines.push('```') + lines.push('') + } + break + + case 'tool_call': + if (block.tool_call) { + lines.push(`### 🔧 工具调用: ${block.tool_call.name}`) + lines.push('') + if (block.tool_call.params) { + lines.push('**参数:**') + lines.push('```json') + try { + const params = JSON.parse(block.tool_call.params) + lines.push(JSON.stringify(params, null, 2)) + } catch { + lines.push(block.tool_call.params) + } + lines.push('```') + lines.push('') + } + if (block.tool_call.response) { + lines.push('**响应:**') + lines.push('```') + lines.push(block.tool_call.response) + lines.push('```') + lines.push('') + } + } + break + + case 'search': + lines.push('### 🔍 网络搜索') + if (block.extra?.total) { + lines.push(`找到 ${block.extra.total} 个搜索结果`) + } + lines.push('') + break + + case 'image': + lines.push('### 🖼️ 图片') + lines.push('*[图片内容]*') + lines.push('') + break + + case 'error': + if (block.content) { + lines.push(`### ❌ 错误`) + lines.push('') + lines.push(`\`${block.content}\``) + lines.push('') + } + break + + case 'artifact-thinking': + if (block.content) { + lines.push('### 💭 创作思考') + lines.push('') + lines.push('```') + lines.push(block.content) + lines.push('```') + lines.push('') + } + break + } + } + } + + lines.push('---') + lines.push('') + } + + return lines.join('\n') + } + + /** + * 导出为 HTML 格式 + */ + private exportToHtml(conversation: CONVERSATION, messages: Message[]): string { + const lines: string[] = [] + + // HTML 头部 + lines.push('') + lines.push('') + lines.push('') + lines.push(' ') + lines.push(' ') + lines.push(` ${this.escapeHtml(conversation.title)}`) + lines.push(' ') + lines.push('') + lines.push('') + + // 标题和元信息 + lines.push('
') + lines.push(`

${this.escapeHtml(conversation.title)}

`) + lines.push(`

导出时间: ${new Date().toLocaleString()}

`) + lines.push(`

会话ID: ${conversation.id}

`) + lines.push(`

消息数量: ${messages.length}

`) + if (conversation.settings.modelId) { + lines.push( + `

模型: ${this.escapeHtml(conversation.settings.modelId)}

` + ) + } + if (conversation.settings.providerId) { + lines.push( + `

提供商: ${this.escapeHtml(conversation.settings.providerId)}

` + ) + } + lines.push('
') + + // 处理每条消息 + for (const message of messages) { + const messageTime = new Date(message.timestamp).toLocaleString() + + if (message.role === 'user') { + lines.push(`
`) + lines.push( + `
👤 用户 (${messageTime})
` + ) + + const userContent = message.content as UserMessageContent + const messageText = userContent.content + ? this.formatUserMessageContent(userContent.content) + : userContent.text + + lines.push(`
${this.escapeHtml(messageText).replace(/\n/g, '
')}
`) + + // 处理文件附件 + if (userContent.files && userContent.files.length > 0) { + lines.push('
') + lines.push(' 附件:') + lines.push('
    ') + for (const file of userContent.files) { + lines.push( + `
  • ${this.escapeHtml(file.name)} (${this.escapeHtml(file.mimeType)})
  • ` + ) + } + lines.push('
') + lines.push('
') + } + + // 处理链接 + if (userContent.links && userContent.links.length > 0) { + lines.push('
') + lines.push(' 链接:') + lines.push(' ') + lines.push('
') + } + + lines.push('
') + } else if (message.role === 'assistant') { + lines.push(`
`) + lines.push( + `
🤖 助手 (${messageTime})
` + ) + + const assistantBlocks = message.content as AssistantMessageBlock[] + + for (const block of assistantBlocks) { + switch (block.type) { + case 'content': + if (block.content) { + lines.push( + `
${this.escapeHtml(block.content).replace(/\n/g, '
')}
` + ) + } + break + + case 'reasoning_content': + if (block.content) { + lines.push('
') + lines.push(' 🤔 思考过程:') + lines.push(`
${this.escapeHtml(block.content)}
`) + lines.push('
') + } + break + + case 'tool_call': + if (block.tool_call) { + lines.push('
') + lines.push( + ` 🔧 工具调用: ${this.escapeHtml(block.tool_call.name || '')}` + ) + if (block.tool_call.params) { + lines.push('
参数:
') + lines.push( + `
${this.escapeHtml(block.tool_call.params)}
` + ) + } + if (block.tool_call.response) { + lines.push('
响应:
') + lines.push( + `
${this.escapeHtml(block.tool_call.response)}
` + ) + } + lines.push('
') + } + break + + case 'search': + lines.push('
') + lines.push(' 🔍 网络搜索') + if (block.extra?.total) { + lines.push(`

找到 ${block.extra.total} 个搜索结果

`) + } + lines.push('
') + break + + case 'image': + lines.push('
') + lines.push(' 🖼️ 图片') + lines.push('

[图片内容]

') + lines.push('
') + break + + case 'error': + if (block.content) { + lines.push('
') + lines.push(' ❌ 错误') + lines.push(`

${this.escapeHtml(block.content)}

`) + lines.push('
') + } + break + + case 'artifact-thinking': + if (block.content) { + lines.push('
') + lines.push(' 💭 创作思考:') + lines.push(`
${this.escapeHtml(block.content)}
`) + lines.push('
') + } + break + } + } + + lines.push('
') + } + } + + // HTML 尾部 + lines.push('') + lines.push('') + + return lines.join('\n') + } + + /** + * 导出为纯文本格式 + */ + private exportToText(conversation: CONVERSATION, messages: Message[]): string { + const lines: string[] = [] + + // 标题和元信息 + lines.push(`${conversation.title}`) + lines.push(''.padEnd(conversation.title.length, '=')) + lines.push('') + lines.push(`导出时间: ${new Date().toLocaleString()}`) + lines.push(`会话ID: ${conversation.id}`) + lines.push(`消息数量: ${messages.length}`) + if (conversation.settings.modelId) { + lines.push(`模型: ${conversation.settings.modelId}`) + } + if (conversation.settings.providerId) { + lines.push(`提供商: ${conversation.settings.providerId}`) + } + lines.push('') + lines.push(''.padEnd(80, '-')) + lines.push('') + + // 处理每条消息 + for (const message of messages) { + const messageTime = new Date(message.timestamp).toLocaleString() + + if (message.role === 'user') { + lines.push(`[用户] ${messageTime}`) + lines.push('') + + const userContent = message.content as UserMessageContent + const messageText = userContent.content + ? this.formatUserMessageContent(userContent.content) + : userContent.text + + lines.push(messageText) + + // 处理文件附件 + if (userContent.files && userContent.files.length > 0) { + lines.push('') + lines.push('附件:') + for (const file of userContent.files) { + lines.push(`- ${file.name} (${file.mimeType})`) + } + } + + // 处理链接 + if (userContent.links && userContent.links.length > 0) { + lines.push('') + lines.push('链接:') + for (const link of userContent.links) { + lines.push(`- ${link}`) + } + } + } else if (message.role === 'assistant') { + lines.push(`[助手] ${messageTime}`) + lines.push('') + + const assistantBlocks = message.content as AssistantMessageBlock[] + + for (const block of assistantBlocks) { + switch (block.type) { + case 'content': + if (block.content) { + lines.push(block.content) + lines.push('') + } + break + + case 'reasoning_content': + if (block.content) { + lines.push('[思考过程]') + lines.push(block.content) + lines.push('') + } + break + + case 'tool_call': + if (block.tool_call) { + lines.push(`[工具调用] ${block.tool_call.name}`) + if (block.tool_call.params) { + lines.push('参数:') + lines.push(block.tool_call.params) + } + if (block.tool_call.response) { + lines.push('响应:') + lines.push(block.tool_call.response) + } + lines.push('') + } + break + + case 'search': + lines.push('[网络搜索]') + if (block.extra?.total) { + lines.push(`找到 ${block.extra.total} 个搜索结果`) + } + lines.push('') + break + + case 'image': + lines.push('[图片内容]') + lines.push('') + break + + case 'error': + if (block.content) { + lines.push(`[错误] ${block.content}`) + lines.push('') + } + break + + case 'artifact-thinking': + if (block.content) { + lines.push('[创作思考]') + lines.push(block.content) + lines.push('') + } + break + } + } + } + + lines.push(''.padEnd(80, '-')) + lines.push('') + } + + return lines.join('\n') + } + + /** + * HTML 转义辅助函数 + */ + private escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + + // 权限响应处理方法 - 重新设计为基于消息数据的流程 + async handlePermissionResponse( + messageId: string, + toolCallId: string, + granted: boolean, + permissionType: 'read' | 'write' | 'all', + remember: boolean = true + ): Promise { + console.log(`[ThreadPresenter] Handling permission response:`, { + messageId, + toolCallId, + granted, + permissionType, + remember + }) + + try { + // 1. 获取消息并更新权限块状态 + const message = await this.messageManager.getMessage(messageId) + if (!message || message.role !== 'assistant') { + const errorMsg = `Message not found or not an assistant message (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + const content = message.content as AssistantMessageBlock[] + const permissionBlock = content.find( + (block) => + block.type === 'action' && + block.action_type === 'tool_call_permission' && + block.tool_call?.id === toolCallId + ) + + if (!permissionBlock) { + const errorMsg = `Permission block not found (messageId: ${messageId}, toolCallId: ${toolCallId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + console.error( + `[ThreadPresenter] Available blocks:`, + content.map((block) => ({ + type: block.type, + toolCallId: block.tool_call?.id + })) + ) + throw new Error(errorMsg) + } + + console.log( + `[ThreadPresenter] Found permission block for tool: ${permissionBlock.tool_call?.name}` + ) + + // 2. 更新权限块状态 + permissionBlock.status = granted ? 'granted' : 'denied' + if (permissionBlock.extra) { + permissionBlock.extra.needsUserAction = false + if (granted) { + permissionBlock.extra.grantedPermissions = permissionType + } + } + + // 3. 保存消息更新 + await this.messageManager.editMessage(messageId, JSON.stringify(content)) + console.log(`[ThreadPresenter] Updated permission block status to: ${permissionBlock.status}`) + + if (granted) { + // 4. 权限授予流程 + const serverName = permissionBlock?.extra?.serverName as string + if (!serverName) { + const errorMsg = `Server name not found in permission block (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + console.log( + `[ThreadPresenter] Granting permission: ${permissionType} for server: ${serverName}` + ) + console.log( + `[ThreadPresenter] Waiting for permission configuration to complete before restarting agent loop...` + ) + + try { + // 等待权限配置完成 + await presenter.mcpPresenter.grantPermission(serverName, permissionType, remember) + console.log(`[ThreadPresenter] Permission granted successfully`) + + // 等待MCP服务重启完成 + console.log( + `[ThreadPresenter] Permission configuration completed, waiting for MCP service restart...` + ) + await this.waitForMcpServiceReady(serverName) + + console.log( + `[ThreadPresenter] MCP service ready, now restarting agent loop for message: ${messageId}` + ) + } catch (permissionError) { + console.error(`[ThreadPresenter] Failed to grant permission:`, permissionError) + // 权限授予失败,将状态更新为错误 + permissionBlock.status = 'error' + await this.messageManager.editMessage(messageId, JSON.stringify(content)) + throw permissionError + } + + // 5. 现在重启agent loop + await this.restartAgentLoopAfterPermission(messageId) + } else { + console.log( + `[ThreadPresenter] Permission denied, ending generation for message: ${messageId}` + ) + // 6. 权限被拒绝 - 正常结束消息 + await this.finalizeMessageAfterPermissionDenied(messageId) + } + } catch (error) { + console.error(`[ThreadPresenter] Failed to handle permission response:`, error) + + // 确保消息状态正确更新 + try { + const message = await this.messageManager.getMessage(messageId) + if (message) { + await this.messageManager.handleMessageError(messageId, String(error)) + } + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + + // 重新启动agent loop (权限授予后) + private async restartAgentLoopAfterPermission(messageId: string): Promise { + console.log( + `[ThreadPresenter] Restarting agent loop after permission for message: ${messageId}` + ) + + try { + // 获取消息和会话信息 + const message = await this.messageManager.getMessage(messageId) + if (!message) { + const errorMsg = `Message not found (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + const conversationId = message.conversationId + console.log(`[ThreadPresenter] Found message in conversation: ${conversationId}`) + + // 验证权限是否生效 - 获取最新的服务器配置 + const content = message.content as AssistantMessageBlock[] + const permissionBlock = content.find( + (block) => + block.type === 'action' && + block.action_type === 'tool_call_permission' && + block.status === 'granted' + ) + + if (!permissionBlock) { + const errorMsg = `No granted permission block found (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + console.error( + `[ThreadPresenter] Available blocks:`, + content.map((block) => ({ + type: block.type, + status: block.status, + toolCallId: block.tool_call?.id + })) + ) + throw new Error(errorMsg) + } + + if (permissionBlock?.extra?.serverName) { + console.log( + `[ThreadPresenter] Verifying permission is active for server: ${permissionBlock.extra.serverName}` + ) + try { + const servers = await this.configPresenter.getMcpServers() + const serverConfig = servers[permissionBlock.extra.serverName as string] + console.log( + `[ThreadPresenter] Current server permissions:`, + serverConfig?.autoApprove || [] + ) + } catch (configError) { + console.warn(`[ThreadPresenter] Failed to verify server permissions:`, configError) + } + } + + // 如果消息还在generating状态,直接继续 + const state = this.generatingMessages.get(messageId) + if (state) { + console.log(`[ThreadPresenter] Message still in generating state, resuming from memory`) + await this.resumeStreamCompletion(conversationId, messageId) + return + } + + // 否则重新启动完整的agent loop + console.log(`[ThreadPresenter] Message not in generating state, starting fresh agent loop`) + + // 重新创建生成状态 + const assistantMessage = message as AssistantMessage + + this.generatingMessages.set(messageId, { + message: assistantMessage, + conversationId, + startTime: Date.now(), + firstTokenTime: null, + promptTokens: 0, + reasoningStartTime: null, + reasoningEndTime: null, + lastReasoningTime: null + }) + + console.log(`[ThreadPresenter] Created new generating state for message: ${messageId}`) + + // 启动新的流式完成 + await this.startStreamCompletion(conversationId, messageId) + } catch (error) { + console.error(`[ThreadPresenter] Failed to restart agent loop:`, error) + + // 确保清理生成状态 + this.generatingMessages.delete(messageId) + + try { + await this.messageManager.handleMessageError(messageId, String(error)) + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + + // 权限被拒绝后完成消息 + private async finalizeMessageAfterPermissionDenied(messageId: string): Promise { + console.log(`[ThreadPresenter] Finalizing message after permission denied: ${messageId}`) + + try { + const message = await this.messageManager.getMessage(messageId) + if (!message) return + + const content = message.content as AssistantMessageBlock[] + + // 将所有loading状态的块设为success,但保留权限块的状态 + content.forEach((block) => { + if (block.type === 'action' && block.action_type === 'tool_call_permission') { + // 权限块保持其当前状态(granted/denied/error) + return + } + if (block.status === 'loading') { + block.status = 'success' + } + }) + + // 添加权限被拒绝的提示 + content.push({ + type: 'error', + content: 'Permission denied by user', + status: 'error', + timestamp: Date.now() + }) + + await this.messageManager.editMessage(messageId, JSON.stringify(content)) + await this.messageManager.updateMessageStatus(messageId, 'sent') + + // 清理生成状态 + this.generatingMessages.delete(messageId) + + // 发送结束事件 + eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, { + eventId: messageId, + userStop: false + }) + + console.log(`[ThreadPresenter] Message finalized after permission denial: ${messageId}`) + } catch (error) { + console.error(`[ThreadPresenter] Failed to finalize message after permission denial:`, error) + } + } + + // 恢复流式完成 (用于内存状态存在的情况) + private async resumeStreamCompletion(conversationId: string, messageId: string): Promise { + const state = this.generatingMessages.get(messageId) + if (!state) { + console.log( + `[ThreadPresenter] No generating state found for ${messageId}, starting fresh agent loop` + ) + await this.startStreamCompletion(conversationId) + return + } + + try { + console.log(`[ThreadPresenter] Resuming stream completion for message: ${messageId}`) + + // 关键修复:重新构建上下文,确保包含被中断的工具调用信息 + const conversation = await this.getConversation(conversationId) + if (!conversation) { + const errorMsg = `Conversation not found (conversationId: ${conversationId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + + const { + providerId, + modelId, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + } = conversation.settings + const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + + if (!modelConfig) { + console.warn( + `[ThreadPresenter] Model config not found for ${modelId} (${providerId}), using default` + ) + } + + // 查找被权限中断的工具调用 + const pendingToolCall = this.findPendingToolCallAfterPermission(state.message.content) + + if (!pendingToolCall) { + console.warn( + `[ThreadPresenter] No pending tool call found after permission grant, using normal context` + ) + // 如果没有找到待执行的工具调用,使用正常流程 + await this.startStreamCompletion(conversationId, messageId) + return + } + + console.log( + `[ThreadPresenter] Found pending tool call: ${pendingToolCall.name} with ID: ${pendingToolCall.id}` + ) + + // 获取对话上下文(基于助手消息,它会自动找到相应的用户消息) + const { contextMessages, userMessage } = await this.prepareConversationContext( + conversationId, + messageId // 使用助手消息ID,让prepareConversationContext自动解析 + ) + + console.log( + `[ThreadPresenter] Prepared conversation context with ${contextMessages.length} messages` + ) + + // 构建专门的继续执行上下文 + const finalContent = await this.buildContinueToolCallContext( + conversation, + contextMessages, + userMessage, + pendingToolCall, + modelConfig + ) + + console.log(`[ThreadPresenter] Built continue context for tool: ${pendingToolCall.name}`) + + // Continue the agent loop with the correct context + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + finalContent, + modelId, + messageId, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + ) + + for await (const event of stream) { + const msg = event.data + if (event.type === 'response') { + await this.handleLLMAgentResponse(msg) + } else if (event.type === 'error') { + await this.handleLLMAgentError(msg) + } else if (event.type === 'end') { + await this.handleLLMAgentEnd(msg) + } + } + } catch (error) { + console.error('[ThreadPresenter] Failed to resume stream completion:', error) + + // 确保清理生成状态 + this.generatingMessages.delete(messageId) + + try { + await this.messageManager.handleMessageError(messageId, String(error)) + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + + // 等待MCP服务重启完成并准备就绪 + private async waitForMcpServiceReady( + serverName: string, + maxWaitTime: number = 3000 + ): Promise { + console.log(`[ThreadPresenter] Waiting for MCP service ${serverName} to be ready...`) + + const startTime = Date.now() + const checkInterval = 100 // 100ms + + return new Promise((resolve) => { + const checkReady = async () => { + try { + // 检查服务是否正在运行 + const isRunning = await presenter.mcpPresenter.isServerRunning(serverName) + + if (isRunning) { + // 服务正在运行,再等待一下确保完全初始化 + setTimeout(() => { + console.log(`[ThreadPresenter] MCP service ${serverName} is ready`) + resolve() + }, 200) + return + } + + // 检查是否超时 + if (Date.now() - startTime > maxWaitTime) { + console.warn( + `[ThreadPresenter] Timeout waiting for MCP service ${serverName} to be ready` + ) + resolve() // 超时也继续,避免阻塞 + return + } + + // 继续等待 + setTimeout(checkReady, checkInterval) + } catch (error) { + console.error(`[ThreadPresenter] Error checking MCP service status:`, error) + resolve() // 出错也继续,避免阻塞 + } + } + + checkReady() + }) + } + + // 查找权限授予后待执行的工具调用 + private findPendingToolCallAfterPermission( + content: AssistantMessageBlock[] + ): { id: string; name: string; params: string } | null { + // 查找已授权的权限块 + const grantedPermissionBlock = content.find( + (block) => + block.type === 'action' && + block.action_type === 'tool_call_permission' && + block.status === 'granted' + ) + + if (!grantedPermissionBlock?.tool_call) { + return null + } + + const { id, name, params } = grantedPermissionBlock.tool_call + if (!id || !name || !params) { + console.warn( + `[ThreadPresenter] Incomplete tool call info in permission block:`, + grantedPermissionBlock.tool_call + ) + return null + } + + return { id, name, params } + } + + // 构建继续工具调用执行的上下文 + private async buildContinueToolCallContext( + conversation: any, + contextMessages: any[], + userMessage: any, + pendingToolCall: { id: string; name: string; params: string }, + modelConfig: any + ): Promise { + const { systemPrompt } = conversation.settings + const formattedMessages: ChatMessage[] = [] + + // 1. 添加系统提示(包含当前时间信息) + if (systemPrompt) { + const finalSystemPrompt = this.enhanceSystemPromptWithDateTime(systemPrompt) + formattedMessages.push({ + role: 'system', + content: finalSystemPrompt + }) + } + + // 2. 添加上下文消息 + const contextChatMessages = this.addContextMessages( + contextMessages, + false, + modelConfig.functionCall + ) + formattedMessages.push(...contextChatMessages) + + // 3. 添加当前用户消息 + const userContent = userMessage.content + const msgText = userContent.content + ? this.formatUserMessageContent(userContent.content) + : userContent.text + const finalUserContent = `${msgText}${getFileContext(userContent.files || [])}` + + formattedMessages.push({ + role: 'user', + content: finalUserContent + }) + + // 4. 添加助手消息,说明需要执行工具调用 + if (modelConfig.functionCall) { + // 对于原生支持函数调用的模型,添加tool_calls + formattedMessages.push({ + role: 'assistant', + tool_calls: [ + { + id: pendingToolCall.id, + type: 'function', + function: { + name: pendingToolCall.name, + arguments: pendingToolCall.params + } + } + ] + }) + + // 添加一个虚拟的工具响应,说明权限已经授予 + formattedMessages.push({ + role: 'tool', + tool_call_id: pendingToolCall.id, + content: `Permission granted. Please proceed with executing the ${pendingToolCall.name} function.` + }) + } else { + // 对于非原生支持的模型,使用文本提示 + formattedMessages.push({ + role: 'assistant', + content: `I need to call the ${pendingToolCall.name} function with the following parameters: ${pendingToolCall.params}` + }) + + formattedMessages.push({ + role: 'user', + content: `Permission has been granted for the ${pendingToolCall.name} function. Please proceed with the execution.` + }) + } + + return formattedMessages + } + + /** + * 为系统提示词添加当前时间信息 + * @param systemPrompt 原始系统提示词 + * @param isImageGeneration 是否为图片生成模型 + * @returns 处理后的系统提示词 + */ + private enhanceSystemPromptWithDateTime( + systemPrompt: string, + isImageGeneration: boolean = false + ): string { + // 如果是图片生成模型或者系统提示词为空,则直接返回原值 + if (isImageGeneration || !systemPrompt || !systemPrompt.trim()) { + return systemPrompt + } + + // 生成当前时间字符串,包含完整的时区信息 + const currentDateTime = new Date().toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', + hour12: false + }) + + return `${systemPrompt}\nToday's date and time is ${currentDateTime}` + } + + /** + * 直接处理内容的方法 + */ + private async processContentDirectly( + eventId: string, + content: string, + currentTime: number + ): Promise { + const state = this.generatingMessages.get(eventId) + if (!state) return + + // 检查是否需要分块处理 + if (this.shouldSplitContent(content)) { + await this.processLargeContentInChunks(eventId, content, currentTime) + } else { + await this.processNormalContent(eventId, content, currentTime) + } + } + + /** + * 分块处理大内容 + */ + private async processLargeContentInChunks( + eventId: string, + content: string, + currentTime: number + ): Promise { + const state = this.generatingMessages.get(eventId) + if (!state) return + + console.log(`[ThreadPresenter] Processing large content in chunks: ${content.length} bytes`) + + const lastBlock = state.message.content[state.message.content.length - 1] + let contentBlock: any + + if (lastBlock && lastBlock.type === 'content') { + contentBlock = lastBlock + } else { + this.finalizeLastBlock(state) + contentBlock = { + type: 'content', + content: '', + status: 'loading', + timestamp: currentTime + } + state.message.content.push(contentBlock) + } + + // 直接添加内容,不做复杂分块 + contentBlock.content += content + + // 只更新数据库,不额外发送到渲染器(避免重复发送) + await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) + } +} diff --git a/src/main/presenter/threadPresenter/messageManager.ts b/src/main/presenter/threadPresenter/messageManager.ts new file mode 100644 index 0000000..5913891 --- /dev/null +++ b/src/main/presenter/threadPresenter/messageManager.ts @@ -0,0 +1,309 @@ +import { + IMessageManager, + MESSAGE_METADATA, + MESSAGE_ROLE, + MESSAGE_STATUS, + ISQLitePresenter, + SQLITE_MESSAGE +} from '@shared/presenter' +import { Message, AssistantMessageBlock } from '@shared/chat' +import { eventBus, SendTarget } from '@/eventbus' +import { CONVERSATION_EVENTS } from '@/events' + +export class MessageManager implements IMessageManager { + private sqlitePresenter: ISQLitePresenter + + constructor(sqlitePresenter: ISQLitePresenter) { + this.sqlitePresenter = sqlitePresenter + } + + private convertToMessage(sqliteMessage: SQLITE_MESSAGE): Message { + let metadata: MESSAGE_METADATA | null = null + try { + metadata = JSON.parse(sqliteMessage.metadata) + } catch (e) { + console.error('Failed to parse metadata', e) + } + return { + id: sqliteMessage.id, + conversationId: sqliteMessage.conversation_id, + parentId: sqliteMessage.parent_id, + role: sqliteMessage.role as MESSAGE_ROLE, + content: JSON.parse(sqliteMessage.content), + timestamp: sqliteMessage.created_at, + status: sqliteMessage.status as MESSAGE_STATUS, + usage: { + context_usage: metadata?.contextUsage ?? 0, + tokens_per_second: metadata?.tokensPerSecond ?? 0, + total_tokens: metadata?.totalTokens ?? 0, + generation_time: metadata?.generationTime ?? 0, + first_token_time: metadata?.firstTokenTime ?? 0, + input_tokens: metadata?.inputTokens ?? 0, + output_tokens: metadata?.outputTokens ?? 0, + reasoning_start_time: metadata?.reasoningStartTime ?? 0, + reasoning_end_time: metadata?.reasoningEndTime ?? 0 + }, + avatar: '', + name: '', + model_name: metadata?.model ?? '', + model_id: metadata?.model ?? '', + model_provider: metadata?.provider ?? '', + error: '', + is_variant: sqliteMessage.is_variant, + variants: sqliteMessage.variants?.map((variant) => this.convertToMessage(variant)) || [] + } + } + + async sendMessage( + conversationId: string, + content: string, + role: MESSAGE_ROLE, + parentId: string, + isVariant: boolean, + metadata: MESSAGE_METADATA, + searchResults?: string + ): Promise { + const maxOrderSeq = await this.sqlitePresenter.getMaxOrderSeq(conversationId) + const msgId = await this.sqlitePresenter.insertMessage( + conversationId, + content, + role, + parentId, + JSON.stringify(metadata), + maxOrderSeq + 1, + 0, + 'pending', + 0, + isVariant ? 1 : 0 + ) + + if (searchResults) { + await this.sqlitePresenter.addMessageAttachment(msgId, 'search_results', searchResults) + } + const message = await this.getMessage(msgId) + if (!message) { + throw new Error('Failed to create message') + } + return message + } + + async editMessage(messageId: string, content: string): Promise { + await this.sqlitePresenter.updateMessage(messageId, { content }) + const message = await this.sqlitePresenter.getMessage(messageId) + if (!message) { + throw new Error(`Message ${messageId} not found`) + } + const msg = this.convertToMessage(message) + eventBus.sendToRenderer(CONVERSATION_EVENTS.MESSAGE_EDITED, SendTarget.ALL_WINDOWS, messageId) + if (msg.parentId) { + eventBus.sendToRenderer( + CONVERSATION_EVENTS.MESSAGE_EDITED, + SendTarget.ALL_WINDOWS, + msg.parentId + ) + } + return msg + } + + async deleteMessage(messageId: string): Promise { + await this.sqlitePresenter.deleteMessage(messageId) + } + + async retryMessage(messageId: string, metadata: MESSAGE_METADATA): Promise { + const originalMessage = await this.getMessage(messageId) + if (!originalMessage) { + throw new Error(`Message ${messageId} not found`) + } + + // 创建一个新的变体消息 + const variantMessage = await this.sendMessage( + originalMessage.conversationId, + JSON.stringify([]), + originalMessage.role as MESSAGE_ROLE, + originalMessage.parentId || '', + true, + metadata + ) + + return variantMessage + } + + async getMessage(messageId: string): Promise { + const message = await this.sqlitePresenter.getMessage(messageId) + if (!message) { + throw new Error(`Message ${messageId} not found`) + } + return this.convertToMessage(message) + } + + async getMessageVariants(messageId: string): Promise { + const variants = await this.sqlitePresenter.getMessageVariants(messageId) + return variants.map((variant) => this.convertToMessage(variant)) + } + + async getMainMessageByParentId( + conversationId: string, + parentId: string + ): Promise { + const message = await this.sqlitePresenter.getMainMessageByParentId(conversationId, parentId) + if (!message) { + return null + } + return this.convertToMessage(message) + } + + async getMessageThread( + conversationId: string, + page: number, + pageSize: number + ): Promise<{ total: number; list: Message[] }> { + const sqliteMessages = await this.sqlitePresenter.queryMessages(conversationId) + const start = (page - 1) * pageSize + const end = start + pageSize + + // 处理消息的排序和变体关系 + const messages = sqliteMessages + .sort((a, b) => { + // 首先按创建时间排序 + const timeCompare = a.created_at - b.created_at + if (timeCompare !== 0) return timeCompare + // 如果创建时间相同,按序号排序 + return a.order_seq - b.order_seq + }) + .map((msg) => this.convertToMessage(msg)) + + return { + total: messages.length, + list: messages.slice(start, end) + } + } + + async updateMessageStatus(messageId: string, status: MESSAGE_STATUS): Promise { + await this.sqlitePresenter.updateMessage(messageId, { status }) + } + + async updateMessageMetadata( + messageId: string, + metadata: Partial + ): Promise { + const message = await this.sqlitePresenter.getMessage(messageId) + if (!message) { + return + } + const updatedMetadata = { + ...JSON.parse(message.metadata), + ...metadata + } + await this.sqlitePresenter.updateMessage(messageId, { + metadata: JSON.stringify(updatedMetadata) + }) + } + + async markMessageAsContextEdge(messageId: string, isEdge: boolean): Promise { + await this.sqlitePresenter.updateMessage(messageId, { + isContextEdge: isEdge ? 1 : 0 + }) + } + + async getContextMessages(conversationId: string, messageCount: number): Promise { + const sqliteMessages = await this.sqlitePresenter.queryMessages(conversationId) + + // 按创建时间和序号倒序排序 + const messages = sqliteMessages + .sort((a, b) => { + // 首先按创建时间倒序排序 + const timeCompare = b.created_at - a.created_at + if (timeCompare !== 0) return timeCompare + // 如果创建时间相同,按序号倒序排序 + return b.order_seq - a.order_seq + }) + .slice(0, messageCount) // 只取需要的消息数量 + .sort((a, b) => { + // 再次按正序排序以保持对话顺序 + const timeCompare = a.created_at - b.created_at + if (timeCompare !== 0) return timeCompare + return a.order_seq - b.order_seq + }) + .map((msg) => this.convertToMessage(msg)) + + return messages + } + + async getLastUserMessage(conversationId: string): Promise { + const sqliteMessage = await this.sqlitePresenter.getLastUserMessage(conversationId) + if (!sqliteMessage) { + return null + } + return this.convertToMessage(sqliteMessage) + } + + async clearAllMessages(conversationId: string): Promise { + await this.sqlitePresenter.deleteAllMessagesInConversation(conversationId) + } + /** + * 初始化未完成的消息 + */ + public async initializeUnfinishedMessages(): Promise { + try { + // 获取所有对话 + const { list: conversations } = await this.sqlitePresenter.getConversationList(1, 1000) + + for (const conversation of conversations) { + // 获取每个对话的消息 + const { list: messages } = await this.getMessageThread(conversation.id, 1, 1000) + + // 找出所有pending状态的assistant消息 + const pendingMessages = messages.filter( + (msg) => msg.role === 'assistant' && msg.status === 'pending' + ) + + // 处理每个未完成的消息 + for (const message of pendingMessages) { + await this.handleMessageError(message.id, 'common.error.sessionInterrupted') + } + } + } catch (error) { + console.error('初始化未完成消息失败:', error) + } + } + /** + * 处理消息错误状态的公共函数 + * @param messageId 消息ID + * @param errorMessage 错误信息 + */ + public async handleMessageError( + messageId: string, + errorMessage: string = 'common.error.requestFailed' + ): Promise { + const message = await this.getMessage(messageId) + if (!message) { + return + } + + let content: AssistantMessageBlock[] = [] + try { + content = message.content as AssistantMessageBlock[] + } catch { + content = [] + } + + // 将所有loading状态的block改为error + content.forEach((block: AssistantMessageBlock) => { + if (block.status === 'loading') { + block.status = 'error' + } + }) + + // 添加错误信息block + content.push({ + type: 'error', + content: errorMessage, + status: 'error', + timestamp: Date.now() + }) + + // 更新消息状态和内容 + await this.updateMessageStatus(messageId, 'error') + await this.editMessage(messageId, JSON.stringify(content)) + } +} diff --git a/src/main/presenter/threadPresenter/searchManager.ts b/src/main/presenter/threadPresenter/searchManager.ts new file mode 100644 index 0000000..99afa54 --- /dev/null +++ b/src/main/presenter/threadPresenter/searchManager.ts @@ -0,0 +1,1404 @@ +import { app, BrowserWindow, screen } from 'electron' +import path from 'path' +import { SearchEngineTemplate } from '@shared/chat' +import { ContentEnricher } from './contentEnricher' +import { SearchResult } from '@shared/presenter' +import { is } from '@electron-toolkit/utils' +import { presenter } from '@/presenter' +import { eventBus } from '@/eventbus' +import { CONFIG_EVENTS } from '@/events' +import { jsonrepair } from 'jsonrepair' +import { SEARCH_PROMPT_TEMPLATE } from './const' + +const helperPage = path.join(app.getAppPath(), 'resources', 'blankSearch.html') + +// 抽取的脚本模板,使用占位符替代选择器 +const EXTRACTOR_SCRIPT_TEMPLATE = ` + const results = []; + const items = document.querySelectorAll('{{ITEMS_SELECTOR}}'); + items.forEach((item, index) => { + try { + const titleSelectorValue = '{{TITLE_SELECTOR}}'; + const titleEl = titleSelectorValue ? item.querySelector(titleSelectorValue) : null; + + const linkSelectorValue = '{{LINK_SELECTOR}}'; + const linkEl = linkSelectorValue ? item.querySelector(linkSelectorValue) : null; + + const descSelectorValue = '{{DESC_SELECTOR}}'; + const descEl = descSelectorValue ? item.querySelector(descSelectorValue) : null; + + const faviconSelectorValue = '{{FAVICON_SELECTOR}}'; + const faviconEl = faviconSelectorValue ? item.querySelector(faviconSelectorValue) : null; + + if (titleEl && linkEl) { + results.push({ + title: {{TITLE_EXTRACT}}, + url: {{URL_EXTRACT}}, + rank: index + 1, + description: descEl ? {{DESC_EXTRACT}} : '', + icon: {{ICON_EXTRACT}} + }); + } + } catch (e) { + // 如果修改后仍然出现错误,那么可能是其他未预料到的问题 + console.error('Error processing item (unexpected with conditional selectors):', e); + } + }); + return results; +` + +// 定义选择器配置的接口 +interface SelectorConfig { + itemsSelector: string + titleSelector: string + linkSelector: string + descSelector: string + faviconSelector: string + titleExtract: string + urlExtract: string + descExtract: string + iconExtract: string +} + +const searchEngineSelectors: Record = { + sogou: { + itemsSelector: '.news-list li', + titleSelector: 'h3 a', + linkSelector: 'h3 a', + descSelector: 'p.txt-info', + faviconSelector: 'a[data-z="art"] img', + titleExtract: 'titleEl.textContent', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent', + iconExtract: "faviconEl ? faviconEl.src : ''" + }, + google: { + itemsSelector: '#search .MjjYud', + titleSelector: 'h3', + linkSelector: 'a', + descSelector: '.VwiC3b', + faviconSelector: 'img.XNo5Ab', + titleExtract: 'titleEl.textContent', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent', + iconExtract: "faviconEl ? faviconEl.src : ''" + }, + baidu: { + itemsSelector: '#content_left .result', + titleSelector: '.t', + linkSelector: 'a', + descSelector: '.c-abstract', + faviconSelector: '.c-img', + titleExtract: 'titleEl.textContent', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent', + iconExtract: "faviconEl ? faviconEl.getAttribute('src') : ''" + }, + bing: { + itemsSelector: '#b_results h2', + titleSelector: 'h2 a', + linkSelector: 'h2 a', + descSelector: '.b_caption p', + faviconSelector: '.wr_fav img', + titleExtract: 'titleEl.textContent', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent', + iconExtract: "faviconEl?.src ? faviconEl.src : ''" + }, + 'google-scholar': { + itemsSelector: '.gs_r', + titleSelector: '.gs_rt', + linkSelector: '.gs_rt a', + descSelector: '.gs_rs', + faviconSelector: '.gs_rt img', + titleExtract: 'titleEl.textContent', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent', + iconExtract: "faviconEl ? faviconEl.src : ''" + }, + 'baidu-xueshu': { + itemsSelector: '#bdxs_result_lists .sc_default_result', + titleSelector: '.sc_content .t', + linkSelector: '.sc_content a', + descSelector: '.c_abstract', + faviconSelector: '', + titleExtract: 'titleEl.textContent?.trim()', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent?.trim()', + iconExtract: "''" + }, + duckduckgo: { + itemsSelector: 'article.yQDlj3B5DI5YO8c8Ulio', + titleSelector: '.EKtkFWMYpwzMKOYr0GYm.LQVY1Jpkk8nyJ6HBWKAk', + linkSelector: 'a', + descSelector: '.E2eLOJr8HctVnDOTM8fs', + faviconSelector: '.DpVR46dTZaePK29PDkz8 img', + titleExtract: 'titleEl.textContent', + urlExtract: 'linkEl.href', + descExtract: 'descEl.textContent', + iconExtract: "faviconEl ? faviconEl.src : ''" + } +} + +// 根据选择器配置生成提取脚本 +function generateExtractorScript(selectorConfig: SelectorConfig): string { + return EXTRACTOR_SCRIPT_TEMPLATE.replace('{{ITEMS_SELECTOR}}', selectorConfig.itemsSelector) + .replace('{{TITLE_SELECTOR}}', selectorConfig.titleSelector) + .replace('{{LINK_SELECTOR}}', selectorConfig.linkSelector) + .replace('{{DESC_SELECTOR}}', selectorConfig.descSelector) + .replace('{{FAVICON_SELECTOR}}', selectorConfig.faviconSelector) + .replace('{{TITLE_EXTRACT}}', selectorConfig.titleExtract) + .replace('{{URL_EXTRACT}}', selectorConfig.urlExtract) + .replace('{{DESC_EXTRACT}}', selectorConfig.descExtract) + .replace('{{ICON_EXTRACT}}', selectorConfig.iconExtract) +} + +// 生成完整的搜索引擎配置 +function generateDefaultEngines() { + return [ + { + id: 'sogou', + name: 'sogou', + selector: '.news-list', + searchUrl: + 'https://weixin.sogou.com/weixin?ie=utf8&s_from=input&_sug_=y&_sug_type_=&type=2&query={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['sogou']) + }, + { + id: 'google', + name: 'google', + selector: '#search', + searchUrl: 'https://www.google.com/search?q={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['google']) + }, + { + id: 'baidu', + name: 'baidu', + selector: '#content_left', + searchUrl: 'https://www.baidu.com/s?wd={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['baidu']) + }, + { + id: 'bing', + name: 'bing', + selector: '', + searchUrl: 'https://www.bing.com/search?q={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['bing']) + }, + { + id: 'google-scholar', + name: 'google-scholar', + selector: '#gs_res_ccl', + searchUrl: 'https://scholar.google.com/scholar?q={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['google-scholar']) + }, + { + id: 'baidu-xueshu', + name: 'baidu-xueshu', + selector: '#bdxs_result_lists', + searchUrl: 'https://xueshu.baidu.com/s?wd={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['baidu-xueshu']) + }, + { + id: 'duckduckgo', + name: 'duckduckgo', + selector: 'button.cxQwADb9kt3UnKwcXKat', + searchUrl: 'https://duckduckgo.com/?q={query}', + extractorScript: generateExtractorScript(searchEngineSelectors['duckduckgo']) + } + ] +} + +// 初始化默认搜索引擎 +const defaultEngines = generateDefaultEngines() + +// 格式化搜索结果的函数 +export function formatSearchResults(results: SearchResult[]): string { + const formattedResults = results + .map( + (result, index) => `[webpage ${index + 1} begin] +title: ${result.title} +URL: ${result.url} +content:${result.content || ''} +[webpage ${index + 1} end]` + ) + .join('\n\n') + // 记录格式化后的搜索结果 + // console.log('formattedResults:', formattedResults) + return formattedResults +} +// 生成带搜索结果的提示词 +export function generateSearchPrompt(query: string, results: SearchResult[]): string { + if (results.length > 0) { + const searchPrompt = SEARCH_PROMPT_TEMPLATE.replace( + '{{SEARCH_RESULTS}}', + formatSearchResults(results) + ) + .replace('{{USER_QUERY}}', query) + .replace('{{CUR_DATE}}', new Date().toLocaleDateString()) + + // 记录最终生成的提示词 + console.log('generateSearchPrompt', searchPrompt) + + return searchPrompt + } else { + return query + } +} + +export class SearchManager { + private searchWindows: Map = new Map() + private maxConcurrentSearches = 3 + private engines: SearchEngineTemplate[] = defaultEngines + private activeEngine: SearchEngineTemplate = this.engines[0] + private originalWindowSizes: Map = new Map() + private originalWindowPositions: Map = new Map() + private wasFullScreen: Map = new Map() + private searchWindowWidth = 800 + private abortControllers: Map = new Map() + private lastEnginesUpdateTime = 0 + // 保存当前正在使用的选择器配置 + private currentSelectors = { ...searchEngineSelectors } + + constructor() { + // 初始化搜索管理器 + this.setupEventListeners() + } + + /** + * 设置事件监听器,监听搜索引擎更新事件 + */ + private setupEventListeners(): void { + // 监听搜索引擎更新事件 + eventBus.on(CONFIG_EVENTS.SEARCH_ENGINES_UPDATED, () => { + // 标记需要刷新引擎列表 + this.lastEnginesUpdateTime = 0 + }) + } + + /** + * 获取搜索引擎列表,包括默认引擎和自定义引擎 + */ + async getEngines(): Promise { + await this.ensureEnginesUpdated() + return this.engines + } + + /** + * 获取当前活跃的搜索引擎 + */ + getActiveEngine(): SearchEngineTemplate { + return this.activeEngine + } + + /** + * 设置活跃搜索引擎 + * @param engineId 搜索引擎ID + */ + async setActiveEngine(engineId: string): Promise { + console.log('setActiveEngine', engineId) + const engine = this.engines.find((e) => e.id === engineId) + if (engine) { + this.activeEngine = engine + // 保存搜索引擎选择到配置中 + await presenter.configPresenter.setSetting('searchEngine', engineId) + return true + } + return false + } + + /** + * 更新搜索引擎列表 + * @param newEngines 新的搜索引擎列表 + */ + async updateEngines(newEngines: SearchEngineTemplate[]): Promise { + // 保存当前活跃引擎ID + const activeEngineId = this.activeEngine.id + + // 更新引擎列表 + this.engines = newEngines + + // 尝试保持当前活跃引擎 + const engine = this.engines.find((e) => e.id === activeEngineId) + if (engine) { + this.activeEngine = engine + } else { + // 如果当前活跃引擎不在新列表中,选择第一个引擎 + this.activeEngine = this.engines[0] + } + + // 更新自定义引擎到配置 + await this.updateCustomEnginesToConfig() + + // 更新时间戳 + this.lastEnginesUpdateTime = Date.now() + } + + /** + * 确保引擎列表是最新的,如果需要就更新 + */ + private async ensureEnginesUpdated(): Promise { + console.log('ensureEnginesUpdated', this.lastEnginesUpdateTime) + // 如果上次更新时间是0或者距离现在超过24小时,则更新引擎列表 + const currentTime = Date.now() + if ( + this.lastEnginesUpdateTime === 0 || + currentTime - this.lastEnginesUpdateTime > 24 * 60 * 60 * 1000 + ) { + await this.refreshEngines() + } + } + + /** + * 刷新引擎列表,合并默认引擎和自定义引擎 + */ + private async refreshEngines(): Promise { + try { + const configPresenter = presenter.configPresenter + + // 获取自定义搜索引擎 + const customEngines = await configPresenter.getCustomSearchEngines() + + // 尝试获取云端选择器配置,预留接口,方便二次开发的时候下发配置 + this.refreshSelectorsFromCloud() + + // 重新生成默认引擎 + const updatedDefaultEngines = this.regenerateDefaultEngines() + + if (customEngines && customEngines.length > 0) { + // 记住当前活跃引擎ID + const activeEngineId = this.activeEngine.id + + // 合并更新后的默认引擎和自定义引擎 + this.engines = [...updatedDefaultEngines, ...customEngines] + + // 尝试保持当前活跃引擎 + const engine = this.engines.find((e) => e.id === activeEngineId) + if (engine) { + this.activeEngine = engine + } + } else { + // 没有自定义引擎,使用更新后的默认引擎 + this.engines = updatedDefaultEngines + } + + // 更新时间戳 + this.lastEnginesUpdateTime = Date.now() + } catch (error) { + console.error('刷新搜索引擎列表失败:', error) + } + } + + /** + * 从云端获取最新的选择器配置 + */ + private async refreshSelectorsFromCloud(): Promise { + try { + // 这里添加从云端获取选择器配置的逻辑 + // 例如通过API调用或配置服务获取 + const cloudSelectors = await this.fetchSelectorsFromCloud() + + if (cloudSelectors) { + // 安全地合并云端选择器和本地默认选择器 + this.updateSelectorsConfig(cloudSelectors) + } + } catch (error) { + console.error('从云端获取选择器配置失败:', error) + // 出错时继续使用当前选择器配置 + } + } + + /** + * 模拟从云端获取选择器配置的方法 + * 实际实现时,这里应该是一个真正的API调用 + */ + private async fetchSelectorsFromCloud(): Promise> | null> { + try { + // 这里只是一个示例,方便二次开发的用户需要下发配置的情况 + // 例如: + // const response = await fetch('https://your-api.com/search-selectors') + // if (response.ok) { + // return await response.json() + // } + + // 目前返回null,表示没有云端配置 + return null + } catch (error) { + console.error('获取云端选择器配置失败:', error) + return null + } + } + + /** + * 安全地更新选择器配置 + * 确保云端下发的选择器不会包含恶意代码 + */ + private updateSelectorsConfig(cloudSelectors: Record>): void { + // 创建一个新的选择器配置对象 + const updatedSelectors = { ...this.currentSelectors } + + // 遍历云端选择器 + for (const [engineId, cloudSelector] of Object.entries(cloudSelectors)) { + // 检查是否已有此引擎的本地配置 + if (updatedSelectors[engineId]) { + // 安全地更新字段,只允许特定字段 + const safeFields = [ + 'itemsSelector', + 'titleSelector', + 'linkSelector', + 'descSelector', + 'faviconSelector' + ] as const + + // 只更新安全字段,忽略其他字段 + for (const field of safeFields) { + if (typeof cloudSelector[field] === 'string') { + // 进行必要的安全检查,例如检查是否包含脚本标签或危险属性 + const sanitizedValue = this.sanitizeSelector(cloudSelector[field] as string) + updatedSelectors[engineId][field] = sanitizedValue + } + } + + // 对于提取逻辑的字段,采用更严格的安全措施 + const extractFields = ['titleExtract', 'urlExtract', 'descExtract', 'iconExtract'] as const + + for (const field of extractFields) { + if (typeof cloudSelector[field] === 'string') { + // 验证提取表达式是否安全 + const safeExtract = this.validateExtractExpression(cloudSelector[field] as string) + if (safeExtract) { + updatedSelectors[engineId][field] = safeExtract + } + } + } + } else if (this.isValidSelectorConfig(cloudSelector)) { + // 如果是新的引擎配置,验证完整性和安全性后添加 + updatedSelectors[engineId] = this.sanitizeSelectorConfig(cloudSelector) + } + } + + // 更新当前选择器配置 + this.currentSelectors = updatedSelectors + } + + /** + * 验证一个完整的选择器配置是否有效 + */ + private isValidSelectorConfig(config: Partial): boolean { + // 检查所有必需字段是否存在且类型正确 + const requiredFields = [ + 'itemsSelector', + 'titleSelector', + 'linkSelector', + 'titleExtract', + 'urlExtract' + ] as const + + for (const field of requiredFields) { + if (typeof config[field] !== 'string' || !config[field]) { + return false + } + } + + // 验证提取表达式是否安全 + return ( + this.validateExtractExpression(config.titleExtract as string) !== null && + this.validateExtractExpression(config.urlExtract as string) !== null + ) + } + + /** + * 对一个完整的选择器配置进行安全处理 + */ + private sanitizeSelectorConfig(config: Partial): SelectorConfig { + const safeConfig: SelectorConfig = { + itemsSelector: '', + titleSelector: '', + linkSelector: '', + descSelector: '', + faviconSelector: '', + titleExtract: 'null', + urlExtract: 'null', + descExtract: 'null', + iconExtract: 'null' + } + + // 安全处理选择器字段 + const selectorFields = [ + 'itemsSelector', + 'titleSelector', + 'linkSelector', + 'descSelector', + 'faviconSelector' + ] as const + + for (const field of selectorFields) { + safeConfig[field] = + typeof config[field] === 'string' ? this.sanitizeSelector(config[field] as string) : '' + } + + // 安全处理提取表达式字段 + const extractFields = ['titleExtract', 'urlExtract', 'descExtract', 'iconExtract'] as const + + for (const field of extractFields) { + const safeExtract = + typeof config[field] === 'string' + ? this.validateExtractExpression(config[field] as string) + : null + + safeConfig[field] = safeExtract || 'null' + } + + return safeConfig + } + + /** + * 清理选择器字符串,防止XSS攻击 + */ + private sanitizeSelector(selector: string): string { + // 移除可能的JavaScript代码或事件处理器 + const sanitized = selector + .replace(/javascript:|data:| { + try { + // 提取所有标记为自定义的引擎 + const customEngines = this.engines.filter((engine) => engine.isCustom) + + // 更新到配置 + if (customEngines.length > 0) { + const configPresenter = presenter.configPresenter + await configPresenter.setCustomSearchEngines(customEngines) + } + } catch (error) { + console.error('更新自定义搜索引擎到配置失败:', error) + } + } + + private async initSearchWindow(conversationId: string): Promise { + // 直接从 ConfigPresenter 获取搜索预览设置状态 + const searchPreviewEnabled = await presenter.configPresenter.getSearchPreviewEnabled() + + // 如果搜索预览关闭,创建一个隐藏的窗口 + if (!searchPreviewEnabled) { + const searchWindow = new BrowserWindow({ + width: this.searchWindowWidth, + height: 800, + show: false, // 窗口不显示 + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + devTools: is.dev + } + }) + + searchWindow.webContents.session.webRequest.onBeforeSendHeaders( + { urls: ['*://*/*'] }, + (details, callback) => { + const headers = { + ...details.requestHeaders, + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + callback({ requestHeaders: headers }) + } + ) + + this.searchWindows.set(conversationId, searchWindow) + return searchWindow + } + + // 下面是原始代码,当预览启用时执行 + if (this.searchWindows.size >= this.maxConcurrentSearches) { + // 找到最早创建的窗口并销毁 + const [oldestConversationId] = this.searchWindows.keys() + this.destroySearchWindow(oldestConversationId) + } + const mainWindow = presenter.windowPresenter.mainWindow + + // 确保mainWindow存在 + if (!mainWindow) { + console.error('主窗口不存在,无法创建搜索窗口') + throw new Error('主窗口不存在') + } + + // 检查是否处于全屏状态 + const isFullScreen = mainWindow.isFullScreen() + this.wasFullScreen.set(conversationId, isFullScreen) + + // 如果是全屏,先退出全屏 + if (isFullScreen) { + // 保存全屏前的位置和大小(如果可能的话) + this.originalWindowSizes.set(conversationId, { + width: mainWindow.getBounds().width, + height: mainWindow.getBounds().height + }) + this.originalWindowPositions.set(conversationId, { + x: mainWindow.getBounds().x, + y: mainWindow.getBounds().y + }) + + // 退出全屏并等待完成 + mainWindow.setFullScreen(false) + + // 等待退出全屏完成 + await new Promise((resolve) => { + const checkFullScreenState = () => { + if (!mainWindow.isFullScreen()) { + resolve() + } else { + setTimeout(checkFullScreenState, 100) + } + } + checkFullScreenState() + }) + + // 给界面一些时间来重新布局 + await new Promise((resolve) => setTimeout(resolve, 200)) + } else { + // 不是全屏模式,正常保存当前主窗口位置和大小信息 + this.originalWindowPositions.set(conversationId, { + x: mainWindow.getBounds().x, + y: mainWindow.getBounds().y + }) + this.originalWindowSizes.set(conversationId, { + width: mainWindow.getBounds().width, + height: mainWindow.getBounds().height + }) + } + + // 获取当前屏幕可用空间 + const mainWindowBounds = mainWindow.getBounds() + const displayBounds = screen.getDisplayMatching(mainWindowBounds).workArea + + // 检查是否右侧有足够空间 + const rightSpace = + displayBounds.x + displayBounds.width - (mainWindowBounds.x + mainWindowBounds.width) + const needsAdjustment = rightSpace < this.searchWindowWidth + 20 // 加20px作为间隔 + + // 如果需要调整窗口 + if (needsAdjustment) { + // 在全屏模式下退出全屏后,优先采用两个窗口铺满屏幕的方式 + if (isFullScreen) { + const totalWidth = displayBounds.width + const mainWindowWidth = Math.floor(totalWidth * 0.6) // 主窗口占60% + const searchWindowWidth = Math.floor(totalWidth * 0.4) // 搜索窗口占40% + this.searchWindowWidth = searchWindowWidth + + // 设置主窗口尺寸和位置(使用Electron内置动画) + mainWindow.setBounds( + { + x: displayBounds.x, + y: displayBounds.y, + width: mainWindowWidth, + height: displayBounds.height + }, + true + ) // 添加true启用动画 + } else { + // 非全屏模式下的调整逻辑 + // 计算左移窗口所需的空间 + const neededSpace = this.searchWindowWidth + 20 - rightSpace + + // 检查左侧是否有足够空间移动窗口 + const availableLeftSpace = mainWindowBounds.x - displayBounds.x + + // 优先移动窗口位置 + if (availableLeftSpace >= neededSpace) { + // 有足够空间移动窗口位置 + const newX = Math.max(displayBounds.x, mainWindowBounds.x - neededSpace) + // 使用Electron内置动画 + mainWindow.setPosition(newX, mainWindowBounds.y, true) // 添加true启用动画 + } else { + // 左侧空间不足,结合移动和缩放 + // 先尽可能地移动窗口 + if (availableLeftSpace > 0) { + mainWindow.setPosition(displayBounds.x, mainWindowBounds.y, true) // 添加true启用动画 + } + + // 计算需要缩放的大小 + const remainingNeededSpace = neededSpace - availableLeftSpace + if (remainingNeededSpace > 0) { + // 还需要缩放窗口 + const newWidth = Math.max( + 400, // 最小主窗口宽度 + mainWindowBounds.width - remainingNeededSpace + ) + // 使用Electron内置动画 + mainWindow.setSize(newWidth, mainWindowBounds.height, true) // 添加true启用动画 + } + } + } + + // 给窗口一些时间来完成动画 + await new Promise((resolve) => setTimeout(resolve, 300)) + } + + console.log('creating search window') + // 创建搜索窗口 + const searchWindow = new BrowserWindow({ + width: this.searchWindowWidth, + height: isFullScreen ? displayBounds.height : mainWindowBounds.height, + parent: mainWindow, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + devTools: is.dev + } + }) + + // 获取调整后的主窗口位置 + const updatedMainBounds = mainWindow.getBounds() + + // 设置搜索窗口位置在主窗口右侧 + searchWindow.setPosition(updatedMainBounds.x + updatedMainBounds.width, updatedMainBounds.y) + + searchWindow.webContents.session.webRequest.onBeforeSendHeaders( + { urls: ['*://*/*'] }, + (details, callback) => { + const headers = { + ...details.requestHeaders, + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + callback({ requestHeaders: headers }) + } + ) + if (is.dev) { + searchWindow.webContents.openDevTools({ mode: 'detach' }) + } + this.searchWindows.set(conversationId, searchWindow) + return searchWindow + } + + private async destroySearchWindow(conversationId: string) { + const window = this.searchWindows.get(conversationId) + if (window) { + window.destroy() + this.searchWindows.delete(conversationId) + + // 直接从 ConfigPresenter 获取搜索预览设置状态 + const searchPreviewEnabled = await presenter.configPresenter.getSearchPreviewEnabled() + + // 如果搜索预览未启用,不需要恢复主窗口状态 + if (!searchPreviewEnabled) { + return + } + + // 恢复主窗口原始位置和大小 + const originalSize = this.originalWindowSizes.get(conversationId) + const originalPosition = this.originalWindowPositions.get(conversationId) + const wasFullScreen = this.wasFullScreen.get(conversationId) + + if (originalSize && originalPosition) { + const mainWindow = presenter.windowPresenter.mainWindow + if (mainWindow) { + if (wasFullScreen) { + // 如果原来是全屏,先恢复原始尺寸和位置,再进入全屏 + mainWindow.setBounds( + { + x: originalPosition.x, + y: originalPosition.y, + width: originalSize.width, + height: originalSize.height + }, + true + ) // 添加true启用动画 + + // 给UI一些时间来适应新尺寸 + await new Promise((resolve) => setTimeout(resolve, 300)) + + // 重新进入全屏 + mainWindow.setFullScreen(true) + } else { + // 非全屏模式下平滑恢复 + mainWindow.setBounds( + { + x: originalPosition.x, + y: originalPosition.y, + width: originalSize.width, + height: originalSize.height + }, + true + ) // 添加true启用动画 + } + } + + this.originalWindowSizes.delete(conversationId) + this.originalWindowPositions.delete(conversationId) + this.wasFullScreen.delete(conversationId) + } + } + } + + async search(conversationId: string, query: string): Promise { + // 确保引擎列表是最新的 + // await this.ensureEnginesUpdated() + + // 创建用于可能中断搜索的 AbortController + const abortController = new AbortController() + this.abortControllers.set(conversationId, abortController) + + let searchWindow = this.searchWindows.get(conversationId) + if (!searchWindow) { + searchWindow = await this.initSearchWindow(conversationId) + } + + const searchUrl = this.activeEngine.searchUrl.replace('{query}', encodeURIComponent(query)) + console.log('开始加载搜索URL:', searchUrl) + + const loadTimeout = setTimeout(() => { + searchWindow?.webContents.stop() + }, 8000) + + try { + // 检查是否已经被中止 + if (abortController.signal.aborted) { + throw new Error('搜索已被用户取消') + } + + await searchWindow.loadURL(searchUrl) + console.log('搜索URL加载成功') + } catch (error) { + console.error('加载URL失败:', error) + if (abortController.signal.aborted) { + // 如果是用户取消导致的错误,直接返回空结果 + this.destroySearchWindow(conversationId) + this.abortControllers.delete(conversationId) + return [] + } + } finally { + clearTimeout(loadTimeout) + } + + // 检查是否已经被中止 + if (abortController.signal.aborted) { + this.destroySearchWindow(conversationId) + this.abortControllers.delete(conversationId) + return [] + } + if (this.activeEngine.selector) { + await this.waitForSelector(searchWindow, this.activeEngine.selector) + } else { + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + console.log('搜索结果加载完成') + + // 检查是否已经被中止 + if (abortController.signal.aborted) { + this.destroySearchWindow(conversationId) + this.abortControllers.delete(conversationId) + return [] + } + + const results = await this.extractSearchResults(searchWindow) + console.log('搜索结果提取完成:', results?.length) + + // 检查是否已经被中止 + if (abortController.signal.aborted) { + this.destroySearchWindow(conversationId) + this.abortControllers.delete(conversationId) + return [] + } + + const enrichedResults = await this.enrichResults(results.slice(0, 5)) + console.log('详细内容获取完成') + + // 清理资源 + this.abortControllers.delete(conversationId) + + searchWindow + .loadFile(helperPage) + .then(() => { + this.destroySearchWindow(conversationId) + }) + .catch((error) => { + console.error('加载空白页失败:', error) + this.destroySearchWindow(conversationId) + }) + const remainingResults = results.slice(5) // 获取剩余的结果 + const combinedResults = [...enrichedResults, ...remainingResults] // 合并enrichedResults和剩余的results + return combinedResults + } + + private async waitForSelector(window: BrowserWindow, selector: string): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve() // 12秒后自动返回 + }, 12000) + // 如果selector不为空,就等待selector出现 + if (selector) { + window.webContents + .executeJavaScript( + ` + new Promise((innerResolve) => { + if (document.querySelector('${selector}')) { + innerResolve(); + } else { + const observer = new MutationObserver(() => { + if (document.querySelector('${selector}')) { + observer.disconnect(); + innerResolve(); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + } + }) + ` + ) + .then(() => { + resolve() + }) + .catch(() => { + resolve() + }) + + clearTimeout(timeout) + resolve() + } + }) + } + + private async extractSearchResults(window: BrowserWindow): Promise { + try { + // 0. 模拟页面滚动,模拟真实阅读体验 + this.simulatePageScrolling(window) + console.log('extraing', this.activeEngine.id) + const results = await window.webContents.executeJavaScript(` + (function() { + ${this.activeEngine.extractorScript} + })() + `) + // 如果结果为空或长度为0,尝试使用备用方法 + if (!results || results.length === 0) { + console.log('常规提取方法未返回结果,尝试使用备用方法') + return await this.fallbackExtractSearchResults(window) + } + + return results + } catch (error) { + console.error('提取搜索结果失败:', error) + // 出错时也使用备用方法 + return [] + } + } + + /** + * 备用的搜索结果提取方法,当标准提取方法失败时使用 + * 使用AI模型分析页面内容提取搜索结果 + */ + private async fallbackExtractSearchResults(window: BrowserWindow): Promise { + try { + // 1. 执行JS提取当前页面的body内容并清理不相关元素 + const cleanedHtml = await window.webContents.executeJavaScript(` + (function() { + // 克隆body以避免直接修改页面 + const tempDiv = document.createElement('div') + tempDiv.innerHTML = document.body.innerHTML + + // 移除不需要的元素 + const elementsToRemove = tempDiv.querySelectorAll('script, style, svg, iframe, nav, footer, header') + elementsToRemove.forEach(el => el.parentNode.removeChild(el)) + + // 移除广告相关元素 + const adElements = tempDiv.querySelectorAll('[class*="ad"], [id*="ad"], [class*="banner"], [class*="popup"]') + adElements.forEach(el => el.parentNode.removeChild(el)) + + // 移除隐藏元素 + const hiddenElements = tempDiv.querySelectorAll('[style*="display: none"], [style*="display:none"], [style*="visibility: hidden"]') + hiddenElements.forEach(el => el.parentNode.removeChild(el)) + // 移除footer元素 + const footerElements = tempDiv.querySelectorAll('footer') + footerElements.forEach(el => el.parentNode.removeChild(el)) + // 移除header元素 + const headerElements = tempDiv.querySelectorAll('header') + headerElements.forEach(el => el.parentNode.removeChild(el)) + // 移除footer的class + const footerClassElements = tempDiv.querySelectorAll('.footer') + footerClassElements.forEach(el => el.parentNode.removeChild(el)) + // 移除可能是sidebar的元素 + const sidebarElements = tempDiv.querySelectorAll('.side-bar, .sidebar, [class*="sidebar"]') + sidebarElements.forEach(el => el.parentNode.removeChild(el)) + // 返回清理后的HTML + return tempDiv.innerHTML + })() + `) + + // 获取页面URL(用于转换相对链接为绝对链接) + const pageUrl = await window.webContents.getURL() + const pageTitle = await window.webContents.executeJavaScript(`document.title`) + + console.log('转换前的HTML长度:', cleanedHtml.length) + // 2. 使用ContentEnricher将HTML转换为Markdown + let markdownContent = ContentEnricher.convertHtmlToMarkdown(cleanedHtml, pageUrl) + console.log('转换后的Markdown长度:', markdownContent.length) + + // 限制markdown长度,避免过大 + const maxMarkdownLength = 10000 + if (markdownContent.length > maxMarkdownLength) { + markdownContent = markdownContent.substring(0, maxMarkdownLength) + } + + // 3. 构建提示词,使用AI模型提取搜索结果 + const prompt = ` + 请分析标签中的搜索引擎返回的markdown内容,并提取出所有搜索结果。每个搜索结果应包含以下字段: + - title: 结果标题 + - url: 结果链接URL + - rank: 结果的序号(从1开始) + - description: 结果描述或摘要 + - icon: 结果的图标URL(如果存在) + + 搜索页面URL: ${pageUrl} + 搜索页面标题: ${pageTitle} + + 请使用以下JSON数组格式返回结果: + [ + { + "title": "结果标题", + "url": "结果链接", + "rank": 1, + "description": "结果描述", + "icon": "图标URL" + }, + { + "title": "结果标题2", + "url": "结果链接2", + "rank": 2, + "description": "结果描述2", + "icon": "图标URL2" + }, + ... + ] + + 重要提示: + 1. 仅返回有效的搜索结果,忽略广告、推荐内容等。 + 2. 确保返回的是有效的JSON格式。 + 3. 请尽可能提取出完整的URL,如果链接是相对路径,请基于搜索页面URL构建完整URL。 + 4. 如果无法找到某个字段,请使用空字符串代替。 + 5. 请只返回JSON数组,不要返回其他说明文字。 + 6. 如果提取不到结果,请返回空数组[]。 + + + ${markdownContent} + + ` + + // 4. 使用AI模型进行分析 + const searchAssistantModel = presenter.threadPresenter.searchAssistantModel + const searchAssistantProviderId = presenter.threadPresenter.searchAssistantProviderId + if (!searchAssistantModel || !searchAssistantProviderId) { + throw new Error('搜索助手模型或提供商ID未设置') + } + const modelResponse = await presenter.llmproviderPresenter.generateCompletion( + searchAssistantProviderId, + [ + { + role: 'user', + content: prompt + } + ], + searchAssistantModel.id || '', + 0.4 + ) + console.log('模型返回的内容:', modelResponse?.length) + + // 5. 解析模型返回的内容 + try { + // 尝试解析JSON + const jsonStart = modelResponse.indexOf('[') + const jsonEnd = modelResponse.lastIndexOf(']') + 1 + + if (jsonStart >= 0 && jsonEnd > jsonStart) { + const jsonStr = modelResponse.substring(jsonStart, jsonEnd) + const results = JSON.parse(jsonStr) + + // 验证结果格式 + if (Array.isArray(results) && results.length > 0) { + console.log('AI模型成功提取到搜索结果:', results.length) + return results + } + } else if (jsonStart >= 0) { + // 找到了开始的 '[' 但没有找到匹配的结束 ']' + // 这种情况下尝试逐个解析JSON对象 + + // 从jsonStart开始的子字符串 + const incompleteJsonStr = modelResponse.substring(jsonStart) + + // 结果数组 + let results: SearchResult[] = [] + try { + console.log('try to repair json') + results = JSON.parse(jsonrepair(incompleteJsonStr)) + } catch (e: unknown) { + console.error('Error parsing AI model response:', e) + results = [] + } + + if (results.length > 0) { + console.log('成功从不完整JSON中提取到搜索结果:', results.length) + return results + } + } + + // 如果无法解析为JSON或格式不正确 + console.warn('AI模型返回的内容无法解析为有效的搜索结果') + return [] + } catch (error) { + console.error('解析AI模型返回内容失败:', error) + return [] + } + } catch (error) { + console.error('备用提取方法失败:', error) + return [] + } + } + + /** + * 模拟页面滚动,增强用户体验 + * @param window 浏览器窗口 + */ + private async simulatePageScrolling(window: BrowserWindow): Promise { + try { + // 获取页面高度 + const pageHeight = await window.webContents.executeJavaScript(` + document.body.scrollHeight + `) + + // 获取视窗高度 + const viewportHeight = await window.webContents.executeJavaScript(` + window.innerHeight + `) + + // 页面总高度 + const totalHeight = Math.max(pageHeight, 1000) + + // 计算滚动次数和每次滚动的距离 + const scrollIterations = 3 // 滚动3次 + const scrollDistance = Math.min(totalHeight / scrollIterations, viewportHeight * 0.8) + + // 平滑滚动 + for (let i = 0; i < scrollIterations; i++) { + await window.webContents.executeJavaScript(` + new Promise((resolve) => { + // 获取当前滚动位置 + const currentScroll = window.scrollY || document.documentElement.scrollTop; + // 计算目标位置 + const targetScroll = currentScroll + ${scrollDistance}; + + // 使用平滑滚动 + window.scrollTo({ + top: targetScroll, + behavior: 'smooth' + }); + + // 等待滚动完成 + setTimeout(resolve, 300); + }) + `) + + // 给浏览器一点时间来加载潜在的懒加载内容 + await new Promise((resolve) => setTimeout(resolve, 500)) + } + + // 等待一下,让页面完全加载 + await new Promise((resolve) => setTimeout(resolve, 500)) + + console.log('页面滚动完成') + } catch (error) { + console.error('模拟页面滚动失败:', error) + // 失败也继续处理 + } + } + + private async enrichResults(results: SearchResult[]): Promise { + return await ContentEnricher.enrichResults(results) + } + + /** + * 测试搜索引擎功能 + * 打开一个窗口进行测试搜索,窗口将保持在前台直到用户关闭 + * @param query 搜索关键词,默认为"天气" + * @returns 是否成功打开测试窗口 + */ + async testSearch(query: string = '天气'): Promise { + try { + // 确保引擎列表是最新的 + // await this.ensureEnginesUpdated() + + // 创建一个独立的测试窗口 + const testWindow = new BrowserWindow({ + width: 800, + height: 600, + title: `测试搜索 - ${this.activeEngine.name}`, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + devTools: is.dev + } + }) + + // 配置User-Agent + testWindow.webContents.session.webRequest.onBeforeSendHeaders( + { urls: ['*://*/*'] }, + (details, callback) => { + const headers = { + ...details.requestHeaders, + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + } + callback({ requestHeaders: headers }) + } + ) + + // 生成搜索URL + const searchUrl = this.activeEngine.searchUrl.replace('{query}', encodeURIComponent(query)) + console.log('测试搜索URL:', searchUrl) + + // 加载URL + await testWindow.loadURL(searchUrl) + + // 保持窗口在前台 + testWindow.focus() + + // 在窗口关闭时清理资源 + testWindow.on('closed', () => { + console.log('测试搜索窗口已关闭') + }) + + return true + } catch (error) { + console.error('测试搜索失败:', error) + return false + } + } + + /** + * 停止特定会话的搜索操作 + * @param conversationId 会话ID + */ + async stopSearch(conversationId: string): Promise { + console.log('停止搜索, conversationId:', conversationId) + + // 中止搜索操作 + const abortController = this.abortControllers.get(conversationId) + if (abortController) { + abortController.abort() + this.abortControllers.delete(conversationId) + } + + // 关闭搜索窗口 + await this.destroySearchWindow(conversationId) + } + + destroy() { + // 中止所有搜索操作 + for (const controller of this.abortControllers.values()) { + controller.abort() + } + this.abortControllers.clear() + + for (const [conversationId] of this.searchWindows) { + this.destroySearchWindow(conversationId) + } + this.originalWindowSizes.clear() + this.originalWindowPositions.clear() + this.wasFullScreen.clear() + } +} diff --git a/src/main/presenter/trayPresenter.ts b/src/main/presenter/trayPresenter.ts new file mode 100644 index 0000000..4567e01 --- /dev/null +++ b/src/main/presenter/trayPresenter.ts @@ -0,0 +1,80 @@ +import { Tray, Menu, app, nativeImage, NativeImage } from 'electron' +import * as path from 'path' +import { getContextMenuLabels } from '@shared/i18n' +import { presenter } from '.' +import { eventBus } from '@/eventbus' +import { TRAY_EVENTS } from '@/events' + +export class TrayPresenter { + private tray: Tray | null = null + private iconPath: string + + constructor() { + this.iconPath = path.join(app.getAppPath(), 'resources') + } + + private createTray() { + // 根据平台选择不同的图标 + let image: NativeImage | undefined = undefined + + if (process.platform === 'darwin') { + // macOS 平台 + image = nativeImage.createFromPath(path.join(this.iconPath, 'macTrayTemplate.png')) + image = image.resize({ width: 24, height: 24 }) + image.setTemplateImage(true) + } else if (process.platform === 'win32') { + // Windows 平台 + image = nativeImage.createFromPath(path.join(this.iconPath, 'win_tray.ico')) + } else { + // Linux 和其他平台 + image = nativeImage.createFromPath(path.join(this.iconPath, 'linux_tray.png')) + // Linux 下通常使用较小的图标尺寸 + image = image.resize({ width: 22, height: 22 }) + } + + this.tray = new Tray(image) + this.tray.setToolTip('DeepChat') + + // 获取当前系统语言 + const locale = presenter.configPresenter.getLanguage?.() || 'zh-CN' + const labels = getContextMenuLabels(locale) + const contextMenu = Menu.buildFromTemplate([ + { + label: labels.open || '打开/隐藏', + click: () => { + eventBus.sendToMain(TRAY_EVENTS.SHOW_HIDDEN_WINDOW) + } + }, + { + label: labels.checkForUpdates || '检查更新', + click: () => { + eventBus.sendToMain(TRAY_EVENTS.CHECK_FOR_UPDATES) + } + }, + { + label: labels.quit || '退出', + click: async () => { + app.quit() // Exit trigger: tray menu + } + } + ]) + + this.tray.setContextMenu(contextMenu) + + // 点击托盘图标时显示窗口 + this.tray.on('click', () => { + eventBus.sendToMain(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, true) + }) + } + + public init(): void { + this.createTray() + } + + destroy() { + if (this.tray) { + this.tray.destroy() + this.tray = null + } + } +} diff --git a/src/main/presenter/upgradePresenter/index.ts b/src/main/presenter/upgradePresenter/index.ts new file mode 100644 index 0000000..3f62987 --- /dev/null +++ b/src/main/presenter/upgradePresenter/index.ts @@ -0,0 +1,453 @@ +import { app, shell } from 'electron' +import { + IUpgradePresenter, + UpdateStatus, + UpdateProgress, + IConfigPresenter +} from '@shared/presenter' +import { eventBus, SendTarget } from '@/eventbus' +import { UPDATE_EVENTS, WINDOW_EVENTS } from '@/events' +import electronUpdater from 'electron-updater' +import axios from 'axios' +import { compare } from 'compare-versions' +import fs from 'fs' +import path from 'path' + +const { autoUpdater } = electronUpdater + +// 版本信息接口 +interface VersionInfo { + version: string + releaseDate: string + releaseNotes: string + githubUrl: string + downloadUrl: string +} + +// 获取平台和架构信息 +const getPlatformInfo = () => { + const platform = process.platform + const arch = process.arch + let platformString = '' + + if (platform === 'win32') { + platformString = arch === 'arm64' ? 'winarm' : 'winx64' + } else if (platform === 'darwin') { + platformString = arch === 'arm64' ? 'macarm' : 'macx64' + } else if (platform === 'linux') { + platformString = arch === 'arm64' ? 'linuxarm' : 'linuxx64' + } + + return platformString +} + +// 获取版本检查的基础URL +const getVersionCheckBaseUrl = () => { + return 'https://cdn.deepchatai.cn' +} + +// 获取自动更新状态文件路径 +const getUpdateMarkerFilePath = () => { + return path.join(app.getPath('userData'), 'auto_update_marker.json') +} + +export class UpgradePresenter implements IUpgradePresenter { + private _lock: boolean = false + private _status: UpdateStatus = 'not-available' + private _progress: UpdateProgress | null = null + private _error: string | null = null + private _versionInfo: VersionInfo | null = null + private _baseUrl: string + private _lastCheckTime: number = 0 // 上次检查更新的时间戳 + private _updateMarkerPath: string + private _previousUpdateFailed: boolean = false // 标记上次更新是否失败 + private _configPresenter: IConfigPresenter // 配置presenter + + constructor(configPresenter: IConfigPresenter) { + this._configPresenter = configPresenter + this._baseUrl = getVersionCheckBaseUrl() + this._updateMarkerPath = getUpdateMarkerFilePath() + + // 配置自动更新 + autoUpdater.autoDownload = false // 默认不自动下载,由我们手动控制 + autoUpdater.allowDowngrade = false + autoUpdater.autoInstallOnAppQuit = true + + // 错误处理 + autoUpdater.on('error', (e) => { + console.log('自动更新失败', e.message) + this._lock = false + this._status = 'error' + this._error = e.message + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + error: this._error, + info: this._versionInfo + }) + }) + + // 检查更新状态 + autoUpdater.on('checking-for-update', () => { + console.log('正在检查更新') + }) + + // 无可用更新 + autoUpdater.on('update-not-available', () => { + console.log('无可用更新') + this._lock = false + this._status = 'not-available' + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status + }) + }) + + // 有可用更新 + autoUpdater.on('update-available', (info) => { + console.log('检测到新版本', info) + this._status = 'available' + + // 重要:这里不再使用info中的信息更新this._versionInfo + // 而是确保使用之前从versionUrl获取的原始信息 + console.log('使用已保存的版本信息:', this._versionInfo) + // 检测到更新后自动开始下载 + this.startDownloadUpdate() + }) + + // 下载进度 + autoUpdater.on('download-progress', (progressObj) => { + this._lock = true + this._status = 'downloading' + this._progress = { + bytesPerSecond: progressObj.bytesPerSecond, + percent: progressObj.percent, + transferred: progressObj.transferred, + total: progressObj.total + } + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + info: this._versionInfo // 使用已保存的版本信息 + }) + eventBus.sendToRenderer(UPDATE_EVENTS.PROGRESS, SendTarget.ALL_WINDOWS, this._progress) + }) + + // 下载完成 + autoUpdater.on('update-downloaded', (info) => { + console.log('更新下载完成', info) + this._lock = false + this._status = 'downloaded' + + // 写入更新标记文件 + this.writeUpdateMarker(this._versionInfo?.version || info.version) + + // 确保保存完整的更新信息 + console.log('使用已保存的版本信息:', this._versionInfo) + + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + info: this._versionInfo // 使用已保存的版本信息 + }) + }) + + // 监听应用获得焦点事件 + eventBus.on(WINDOW_EVENTS.APP_FOCUS, this.handleAppFocus.bind(this)) + + // 应用启动时检查是否有未完成的更新 + this.checkPendingUpdate() + } + + // 检查是否有未完成的自动更新 + private checkPendingUpdate(): void { + try { + if (fs.existsSync(this._updateMarkerPath)) { + const content = fs.readFileSync(this._updateMarkerPath, 'utf8') + const updateInfo = JSON.parse(content) + const currentVersion = app.getVersion() + console.log('检查未完成的更新', updateInfo, currentVersion) + + // 如果当前版本与目标版本相同,说明更新已完成 + if (updateInfo.version === currentVersion) { + // 删除标记文件 + fs.unlinkSync(this._updateMarkerPath) + return + } + + // 否则说明上次更新失败,标记为错误状态 + console.log('检测到未完成的更新', updateInfo.version) + this._status = 'error' + this._error = '上次自动更新未完成' + this._versionInfo = updateInfo + this._previousUpdateFailed = true // 标记上次更新失败 + + // 删除标记文件 + fs.unlinkSync(this._updateMarkerPath) + + // 通知渲染进程 + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + error: this._error, + info: { + version: updateInfo.version, + releaseDate: updateInfo.releaseDate, + releaseNotes: updateInfo.releaseNotes, + githubUrl: updateInfo.githubUrl, + downloadUrl: updateInfo.downloadUrl + } + }) + } + } catch (error) { + console.error('检查未完成更新失败', error) + // 出错时尝试删除标记文件 + try { + if (fs.existsSync(this._updateMarkerPath)) { + fs.unlinkSync(this._updateMarkerPath) + } + } catch (e) { + console.error('删除更新标记文件失败', e) + } + } + } + + // 写入更新标记文件 + private writeUpdateMarker(version: string): void { + try { + const updateInfo = { + version, + releaseDate: this._versionInfo?.releaseDate || '', + releaseNotes: this._versionInfo?.releaseNotes || '', + githubUrl: this._versionInfo?.githubUrl || '', + downloadUrl: this._versionInfo?.downloadUrl || '', + timestamp: Date.now() + } + + fs.writeFileSync(this._updateMarkerPath, JSON.stringify(updateInfo, null, 2), 'utf8') + console.log('写入更新标记文件成功', this._updateMarkerPath) + } catch (error) { + console.error('写入更新标记文件失败', error) + } + } + + // 处理应用获得焦点事件 + private handleAppFocus(): void { + const now = Date.now() + const twelveHoursInMs = 12 * 60 * 60 * 1000 // 12小时的毫秒数 + // 如果距离上次检查更新超过12小时,则重新检查 + if (now - this._lastCheckTime > twelveHoursInMs) { + this.checkUpdate('autoCheck') + } + } + + /** + * + * @param type 检查更新的类型,'autoCheck'表示自动检查 + * 如果不传则默认为手动检查 + * @returns + */ + async checkUpdate(type?: string): Promise { + if (this._lock) { + return + } + + try { + this._status = 'checking' + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status + }) + + // 首先获取版本信息文件 + const platformString = getPlatformInfo() + const rawChannel = this._configPresenter.getUpdateChannel() + const updateChannel = rawChannel === 'canary' ? 'canary' : 'upgrade' // Sanitize channel + const randomId = Math.floor(Date.now() / 3600000) // Timestamp truncated to hour + const versionPath = updateChannel + const versionUrl = `${this._baseUrl}/${versionPath}/${platformString}.json?noCache=${randomId}` + console.log('versionUrl', versionUrl) + const response = await axios.get(versionUrl, { timeout: 60000 }) // Add network timeout + const remoteVersion = response.data + const currentVersion = app.getVersion() + + // 保存完整的远程版本信息到内存中,作为唯一的标准信息源 + this._versionInfo = { + version: remoteVersion.version, + releaseDate: remoteVersion.releaseDate, + releaseNotes: remoteVersion.releaseNotes, + githubUrl: remoteVersion.githubUrl, + downloadUrl: remoteVersion.downloadUrl + } + + console.log('cache versionInfo:', this._versionInfo) + + // 更新上次检查时间 + this._lastCheckTime = Date.now() + + // 比较版本号 + if (compare(remoteVersion.version, currentVersion, '>')) { + // 有新版本 + + // 如果上次更新失败,这次不再尝试自动更新,直接进入错误状态让用户手动更新 + if (this._previousUpdateFailed) { + console.log('上次更新失败,本次不进行自动更新,改为手动更新') + this._status = 'error' + this._error = '自动更新可能不稳定,请手动下载更新' + + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + error: this._error, + info: this._versionInfo + }) + return + } + + // 设置自动更新的URL + const autoUpdateUrl = + updateChannel === 'canary' + ? `${this._baseUrl}/canary/${platformString}` + : `${this._baseUrl}/upgrade/v${remoteVersion.version}/${platformString}` + console.log('设置自动更新URL:', autoUpdateUrl) + autoUpdater.setFeedURL(autoUpdateUrl) + + try { + // 使用electron-updater检查更新,但不自动下载 + await autoUpdater.checkForUpdates() + } catch (err) { + console.error('自动更新检查失败,回退到手动更新', err) + // 如果自动更新失败,回退到手动更新 + this._status = 'available' + + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + info: this._versionInfo // 使用已保存的版本信息 + }) + } + } else { + // 没有新版本 + this._status = 'not-available' + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + type + }) + } + } catch (error: Error | unknown) { + this._status = 'error' + this._error = error instanceof Error ? error.message : String(error) + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + error: this._error + }) + } + } + + getUpdateStatus() { + return { + status: this._status, + progress: this._progress, + error: this._error, + updateInfo: this._versionInfo + ? { + version: this._versionInfo.version, + releaseDate: this._versionInfo.releaseDate, + releaseNotes: this._versionInfo.releaseNotes, + githubUrl: this._versionInfo.githubUrl, + downloadUrl: this._versionInfo.downloadUrl + } + : null + } + } + + async goDownloadUpgrade(type: 'github' | 'netdisk'): Promise { + if (type === 'github') { + const url = this._versionInfo?.githubUrl + if (url) { + shell.openExternal(url) + } + } else if (type === 'netdisk') { + const url = this._versionInfo?.downloadUrl + if (url) { + shell.openExternal(url) + } + } + } + + // 开始下载更新(如果手动触发) + startDownloadUpdate(): boolean { + if (this._status !== 'available') { + return false + } + try { + this._status = 'downloading' + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + info: this._versionInfo // 使用已保存的版本信息 + }) + autoUpdater.downloadUpdate() + return true + } catch (error: Error | unknown) { + this._status = 'error' + this._error = error instanceof Error ? error.message : String(error) + eventBus.sendToRenderer(UPDATE_EVENTS.STATUS_CHANGED, SendTarget.ALL_WINDOWS, { + status: this._status, + error: this._error + }) + return false + } + } + + // 执行退出并安装 + private _doQuitAndInstall(): void { + console.log('准备退出并安装更新') + try { + // 发送即将重启的消息 + eventBus.sendToRenderer(UPDATE_EVENTS.WILL_RESTART, SendTarget.ALL_WINDOWS) + // 通知需要完全退出应用 + eventBus.sendToMain(WINDOW_EVENTS.FORCE_QUIT_APP) + autoUpdater.quitAndInstall() + // 如果30秒还没完成,就强制退出重启 + setTimeout(() => { + app.quit() // Exit trigger: upgrade + }, 30000) + } catch (e) { + console.error('退出并安装失败', e) + eventBus.sendToRenderer(UPDATE_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { + error: e instanceof Error ? e.message : String(e) + }) + } + } + + // 重启并更新 + restartToUpdate(): boolean { + console.log('重启并更新') + if (this._status !== 'downloaded') { + eventBus.sendToRenderer(UPDATE_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { + error: '更新尚未下载完成' + }) + return false + } + try { + this._doQuitAndInstall() + return true + } catch (e) { + console.error('重启更新失败', e) + eventBus.sendToRenderer(UPDATE_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { + error: e instanceof Error ? e.message : String(e) + }) + return false + } + } + + // 重启应用 + restartApp(): void { + try { + // 发送即将重启的消息 + eventBus.sendToRenderer(UPDATE_EVENTS.WILL_RESTART, SendTarget.ALL_WINDOWS) + // 给UI层一点时间保存状态 + setTimeout(() => { + app.relaunch() + app.exit() + }, 1000) + } catch (e) { + console.error('重启失败', e) + eventBus.sendToRenderer(UPDATE_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { + error: e instanceof Error ? e.message : String(e) + }) + } + } +} diff --git a/src/main/presenter/windowPresenter/FloatingChatWindow.ts b/src/main/presenter/windowPresenter/FloatingChatWindow.ts new file mode 100644 index 0000000..c188124 --- /dev/null +++ b/src/main/presenter/windowPresenter/FloatingChatWindow.ts @@ -0,0 +1,348 @@ +import { BrowserWindow, screen, nativeImage } from 'electron' +import path from 'path' +import logger from '../../../shared/logger' +import { platform, is } from '@electron-toolkit/utils' +import icon from '../../../../resources/icon.png?asset' +import iconWin from '../../../../resources/icon.ico?asset' +import { eventBus } from '../../eventbus' +import { TAB_EVENTS } from '../../events' +import { presenter } from '../' + +interface FloatingChatConfig { + size: { + width: number + height: number + } + minSize: { + width: number + height: number + } + opacity: number + alwaysOnTop: boolean +} + +interface FloatingButtonPosition { + x: number + y: number + width: number + height: number +} + +const DEFAULT_FLOATING_CHAT_CONFIG: FloatingChatConfig = { + size: { + width: 400, + height: 600 + }, + minSize: { + width: 350, + height: 450 + }, + opacity: 0.95, + alwaysOnTop: true +} + +export class FloatingChatWindow { + private window: BrowserWindow | null = null + private config: FloatingChatConfig + private isVisible: boolean = false + private shouldShowWhenReady: boolean = false + + constructor(config?: Partial) { + this.config = { + ...DEFAULT_FLOATING_CHAT_CONFIG, + ...config + } + } + + public async create(floatingButtonPosition?: FloatingButtonPosition): Promise { + if (this.window) { + return + } + + try { + const position = this.calculatePosition(floatingButtonPosition) + const iconFile = nativeImage.createFromPath(process.platform === 'win32' ? iconWin : icon) + const isDev = is.dev + + this.window = new BrowserWindow({ + width: this.config.size.width, + height: this.config.size.height, + minWidth: this.config.minSize.width, + minHeight: this.config.minSize.height, + x: position.x, + y: position.y, + frame: false, + transparent: true, + alwaysOnTop: this.config.alwaysOnTop, + skipTaskbar: true, + resizable: true, + minimizable: false, + maximizable: false, + closable: true, + show: false, + movable: true, + autoHideMenuBar: true, + icon: iconFile, + vibrancy: platform.isMacOS ? 'under-window' : undefined, + visualEffectState: platform.isMacOS ? 'followWindow' : undefined, + backgroundMaterial: platform.isWindows ? 'mica' : undefined, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, '../preload/index.mjs'), + webSecurity: false, + devTools: isDev, + sandbox: false + } + }) + + this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) + this.window.setAlwaysOnTop(true, 'floating') + this.window.setOpacity(this.config.opacity) + this.setupWindowEvents() + this.registerVirtualTab() + + logger.info('FloatingChatWindow created successfully') + + this.loadPageContent() + .then(() => logger.info('FloatingChatWindow page content loaded')) + .catch((error) => logger.error('Failed to load FloatingChatWindow page content:', error)) + } catch (error) { + logger.error('Failed to create FloatingChatWindow:', error) + throw error + } + } + + public show(floatingButtonPosition?: FloatingButtonPosition): void { + if (!this.window) { + return + } + + if (floatingButtonPosition) { + const position = this.calculatePosition(floatingButtonPosition) + this.window.setPosition(position.x, position.y) + } + if (!this.window.isVisible()) { + if (this.window.webContents.isLoading() === false) { + this.window.show() + this.window.focus() + this.refreshWindowData() + } else { + this.window.show() + this.window.focus() + this.shouldShowWhenReady = true + this.window.webContents.once('did-finish-load', () => { + if (this.shouldShowWhenReady) { + this.refreshWindowData() + this.shouldShowWhenReady = false + } + }) + } + } else { + this.window.show() + this.window.focus() + this.refreshWindowData() + } + this.isVisible = true + logger.debug('FloatingChatWindow shown') + } + + public hide(): void { + if (!this.window) { + return + } + + this.window.hide() + this.isVisible = false + logger.debug('FloatingChatWindow hidden') + } + + public toggle(floatingButtonPosition?: FloatingButtonPosition): void { + if (this.isVisible) { + this.hide() + } else { + this.show(floatingButtonPosition) + } + } + + public destroy(): void { + if (this.window) { + this.unregisterVirtualTab() + try { + if (!this.window.isDestroyed()) { + this.window.destroy() + } + } catch (error) { + logger.error('Error destroying FloatingChatWindow:', error) + } + this.window = null + this.isVisible = false + logger.debug('FloatingChatWindow destroyed') + } + } + + public isShowing(): boolean { + return this.window !== null && !this.window.isDestroyed() && this.isVisible + } + + public getWindow(): BrowserWindow | null { + return this.window + } + + private refreshWindowData(): void { + if (this.window && !this.window.isDestroyed()) { + logger.debug('Refreshing floating window data') + setTimeout(() => { + if (this.window && !this.window.isDestroyed()) { + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, this.window.webContents.id) + } + }, 100) + } + } + + private registerVirtualTab(): void { + if (!this.window || this.window.isDestroyed()) { + return + } + + try { + const tabPresenter = presenter.tabPresenter + if (tabPresenter) { + const webContentsId = this.window.webContents.id + logger.info(`Registering virtual tab for floating window, WebContents ID: ${webContentsId}`) + tabPresenter.registerFloatingWindow(webContentsId, this.window.webContents) + } + } catch (error) { + logger.error('Failed to register virtual tab for floating window:', error) + } + } + + private unregisterVirtualTab(): void { + if (!this.window) { + return + } + + try { + const tabPresenter = presenter.tabPresenter + if (tabPresenter) { + const webContentsId = this.window.webContents.id + logger.info( + `Unregistering virtual tab for floating window, WebContents ID: ${webContentsId}` + ) + tabPresenter.unregisterFloatingWindow(webContentsId) + } + } catch (error) { + logger.error('Failed to unregister virtual tab for floating window:', error) + } + } + + private calculatePosition(floatingButtonPosition?: FloatingButtonPosition): { + x: number + y: number + } { + const primaryDisplay = screen.getPrimaryDisplay() + const { workArea } = primaryDisplay + const windowWidth = this.window?.getBounds().width ?? this.config.size.width + const windowHeight = this.window?.getBounds().height ?? this.config.size.height + + if (!floatingButtonPosition) { + const x = workArea.x + workArea.width - windowWidth - 20 + const y = workArea.y + workArea.height - windowHeight - 20 + return { x, y } + } + + const gap = 15 + const buttonBounds = floatingButtonPosition + + let finalX: number + + // 1. Prioritize placing the window on the right side of the button + const rightPositionX = buttonBounds.x + buttonBounds.width + gap + if (rightPositionX + windowWidth <= workArea.x + workArea.width) { + finalX = rightPositionX + } else { + // 2. If right side has no space, try the left side + const leftPositionX = buttonBounds.x - windowWidth - gap + if (leftPositionX >= workArea.x) { + finalX = leftPositionX + } else { + // 3. Fallback: If both sides lack space, align window's right edge with screen's right edge. + finalX = workArea.x + workArea.width - windowWidth + } + } + + // Calculate vertical position: try to center with the button, but stay within screen bounds. + const idealY = buttonBounds.y + (buttonBounds.height - windowHeight) / 2 + const finalY = Math.max( + workArea.y, + Math.min(idealY, workArea.y + workArea.height - windowHeight) + ) + + return { x: Math.round(finalX), y: Math.round(finalY) } + } + + private async loadPageContent(): Promise { + if (!this.window || this.window.isDestroyed()) { + throw new Error('Window is not available for page loading') + } + + const isDev = is.dev + if (isDev) { + await this.window.loadURL('http://localhost:5173/') + } else { + await this.window.loadFile(path.join(__dirname, '../renderer/index.html')) + } + + this.window.webContents.once('did-finish-load', () => { + logger.info('FloatingChatWindow did-finish-load, requesting fresh data') + setTimeout(async () => { + if (this.window && !this.window.isDestroyed()) { + logger.info(`Broadcasting thread list update for floating window`) + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, this.window.webContents.id) + } + }, 300) + }) + } + + private setupWindowEvents(): void { + if (!this.window) { + return + } + + this.window.on('ready-to-show', () => { + if (this.window && !this.window.isDestroyed()) { + if (this.shouldShowWhenReady) { + this.window.show() + this.window.focus() + this.shouldShowWhenReady = false + this.refreshWindowData() + } + } + }) + + this.window.on('close', (event) => { + const windowPresenter = presenter.windowPresenter + const isAppQuitting = windowPresenter?.isApplicationQuitting() || false + if (isAppQuitting) { + logger.info('App is quitting, allowing FloatingChatWindow to close normally') + return + } + event.preventDefault() + this.hide() + logger.debug('FloatingChatWindow close prevented, window hidden instead') + }) + + this.window.on('closed', () => { + this.window = null + this.isVisible = false + }) + + this.window.on('show', () => { + this.isVisible = true + }) + + this.window.on('hide', () => { + this.isVisible = false + }) + } +} diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts new file mode 100644 index 0000000..4be789c --- /dev/null +++ b/src/main/presenter/windowPresenter/index.ts @@ -0,0 +1,1219 @@ +// src\main\presenter\windowPresenter\index.ts +import { BrowserWindow, shell, nativeImage, ipcMain, screen } from 'electron' +import { join } from 'path' +import icon from '../../../../resources/icon.png?asset' // App icon (macOS/Linux) +import iconWin from '../../../../resources/icon.ico?asset' // App icon (Windows) +import { is } from '@electron-toolkit/utils' // Electron utilities +import { IConfigPresenter, IWindowPresenter } from '@shared/presenter' // Window Presenter interface +import { eventBus } from '@/eventbus' // Event bus +import { CONFIG_EVENTS, SYSTEM_EVENTS, WINDOW_EVENTS } from '@/events' // System/Window/Config event constants +import { presenter } from '../' // Global presenter registry +import windowStateManager from 'electron-window-state' // Window state manager +import { SHORTCUT_EVENTS } from '@/events' // Shortcut event constants +// TrayPresenter is globally managed in main/index.ts, this Presenter is not responsible for its lifecycle +import { TabPresenter } from '../tabPresenter' // TabPresenter type +import { FloatingChatWindow } from './FloatingChatWindow' // Floating chat window + +/** + * Window Presenter, responsible for managing all BrowserWindow instances and their lifecycles. + * Including creation, destruction, minimization, maximization, hiding, showing, focus management, and interaction with tabs. + */ +export class WindowPresenter implements IWindowPresenter { + // Map managing all BrowserWindow instances, key is window ID + windows: Map + private configPresenter: IConfigPresenter + // Exit flag indicating if app is in the process of quitting (set by 'before-quit' hook) + private isQuitting: boolean = false + // Current focused window ID (internal record) + private focusedWindowId: number | null = null + // Main window ID + private mainWindowId: number | null = null + // Window focus state management + private windowFocusStates = new Map< + number, + { + lastFocusTime: number + shouldFocus: boolean + isNewWindow: boolean + hasInitialFocus: boolean + } + >() + private floatingChatWindow: FloatingChatWindow | null = null + + constructor(configPresenter: IConfigPresenter) { + this.windows = new Map() + this.configPresenter = configPresenter + + // Register IPC handlers for Renderer to call to get window and WebContents IDs + ipcMain.on('get-window-id', (event) => { + const window = BrowserWindow.fromWebContents(event.sender) + event.returnValue = window ? window.id : null + }) + + ipcMain.on('get-web-contents-id', (event) => { + event.returnValue = event.sender.id + }) + + ipcMain.on('close-floating-window', (event) => { + // Check if sender is the floating chat window + const webContentsId = event.sender.id + if ( + this.floatingChatWindow && + this.floatingChatWindow.getWindow()?.webContents.id === webContentsId + ) { + this.hideFloatingChatWindow() + } + }) + + // Listen for shortcut event: create new window + eventBus.on(SHORTCUT_EVENTS.CREATE_NEW_WINDOW, () => { + console.log('Creating new shell window via shortcut.') + this.createShellWindow({ initialTab: { url: 'local://chat' } }) + }) + + // Listen for shortcut event: create new tab + eventBus.on(SHORTCUT_EVENTS.CREATE_NEW_TAB, async (windowId: number) => { + console.log(`Creating new tab via shortcut for window ${windowId}.`) + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + await (presenter.tabPresenter as TabPresenter).createTab(windowId, 'local://chat', { + active: true + }) + } else { + console.warn( + `Cannot create new tab for window ${windowId}, window does not exist or is destroyed.` + ) + } + }) + + // 监听快捷键事件:关闭当前标签页 + eventBus.on(SHORTCUT_EVENTS.CLOSE_CURRENT_TAB, async (windowId: number) => { + console.log(`Received CLOSE_CURRENT_TAB for window ${windowId}.`) + const window = this.windows.get(windowId) + if (!window || window.isDestroyed()) { + console.warn( + `Cannot handle close tab request, window ${windowId} does not exist or is destroyed.` + ) + return + } + + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const tabsData = await tabPresenterInstance.getWindowTabsData(windowId) + const activeTab = tabsData.find((tab) => tab.isActive) + + if (activeTab) { + if (tabsData.length === 1) { + // 窗口内只有最后一个标签页 + const allWindows = this.getAllWindows() + if (allWindows.length === 1) { + // 是最后一个窗口的最后一个标签页,隐藏窗口 + console.log(`Window ${windowId} is the last window's last tab, hiding window.`) + this.hide(windowId) // 调用 hide() 会触发 hide 逻辑 + } else { + // 不是最后一个窗口的最后一个标签页,关闭窗口 + console.log(`Window ${windowId} has other windows, closing this window.`) + this.close(windowId) // 调用 close() 会触发 'close' 事件处理器 + } + } else { + // 窗口内不止一个标签页,直接关闭当前标签页 + console.log(`Window ${windowId} has multiple tabs, closing active tab ${activeTab.id}.`) + await tabPresenterInstance.closeTab(activeTab.id) + } + } else { + console.warn(`No active tab found in window ${windowId} to close.`) + } + }) + + // 监听系统主题更新事件,通知所有窗口 Renderer + eventBus.on(SYSTEM_EVENTS.SYSTEM_THEME_UPDATED, (isDark: boolean) => { + console.log('System theme updated, notifying all windows.') + this.windows.forEach((window) => { + if (!window.isDestroyed()) { + window.webContents.send('system-theme-updated', isDark) + } else { + console.warn(`Skipping theme update for destroyed window ${window.id}.`) + } + }) + }) + + // 监听内容保护设置变更事件,更新所有窗口并重启应用 + eventBus.on(CONFIG_EVENTS.CONTENT_PROTECTION_CHANGED, (enabled: boolean) => { + console.log(`Content protection setting changed to ${enabled}, restarting application.`) + this.windows.forEach((window) => { + if (!window.isDestroyed()) { + this.updateContentProtection(window, enabled) + } else { + console.warn(`Skipping content protection update for destroyed window ${window.id}.`) + } + }) + // 内容保护变更通常需要重启应用才能完全生效 + setTimeout(() => { + presenter.devicePresenter.restartApp() + }, 1000) + }) + } + + /** + * 获取当前主窗口 (优先返回焦点窗口,否则返回第一个有效窗口)。 + */ + get mainWindow(): BrowserWindow | undefined { + const focused = this.getFocusedWindow() + if (focused && !focused.isDestroyed()) { + return focused + } + const allWindows = this.getAllWindows() + return allWindows.length > 0 && !allWindows[0].isDestroyed() ? allWindows[0] : undefined + } + + /** + * 预览文件。macOS 使用 Quick Look,其他平台使用系统默认应用打开。 + * @param filePath 文件路径。 + */ + previewFile(filePath: string): void { + let targetWindow = this.getFocusedWindow() + if (!targetWindow && this.floatingChatWindow && this.floatingChatWindow.isShowing()) { + const floatingWindow = this.floatingChatWindow.getWindow() + if (floatingWindow) { + targetWindow = floatingWindow + } + } + if (!targetWindow) { + targetWindow = this.mainWindow + } + + if (targetWindow && !targetWindow.isDestroyed()) { + console.log(`Previewing file: ${filePath}`) + if (process.platform === 'darwin') { + targetWindow.previewFile(filePath) + } else { + shell.openPath(filePath) // 使用系统默认应用打开 + } + } else { + console.warn('Cannot preview file, no valid window found.') + } + } + + /** + * 最小化指定 ID 的窗口。 + * @param windowId 窗口 ID。 + */ + minimize(windowId: number): void { + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + console.log(`Minimizing window ${windowId}.`) + window.minimize() + } else { + console.warn(`Failed to minimize window ${windowId}, window does not exist or is destroyed.`) + } + } + + /** + * 最大化/还原指定 ID 的窗口。 + * @param windowId 窗口 ID。 + */ + maximize(windowId: number): void { + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + console.log(`Maximizing/unmaximizing window ${windowId}.`) + if (window.isMaximized()) { + window.unmaximize() + } else { + window.maximize() + } + // 触发恢复逻辑以确保活动标签页的 bounds 更新 + this.handleWindowRestore(windowId).catch((error) => { + console.error( + `Error handling restore logic after maximizing/unmaximizing window ${windowId}:`, + error + ) + }) + } else { + console.warn( + `Failed to maximize/unmaximize window ${windowId}, window does not exist or is destroyed.` + ) + } + } + + /** + * 请求关闭指定 ID 的窗口。这将触发窗口的 'close' 事件。 + * 实际关闭或隐藏行为由 'close' 事件处理程序决定。 + * @param windowId 窗口 ID。 + */ + close(windowId: number): void { + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + console.log(`Requesting to close window ${windowId}, calling window.close().`) + window.close() // 触发 'close' 事件 + } else { + console.warn( + `Failed to request close for window ${windowId}, window does not exist or is destroyed.` + ) + } + } + + /** + * 根据 IWindowPresenter 接口定义的关闭窗口方法。 + * 实际行为与 close(windowId) 相同,由 'close' 事件处理程序决定。 + * @param windowId 窗口 ID。 + * @param forceClose 是否强制关闭 (当前实现由 isQuitting 标志控制,此参数未直接使用)。 + */ + async closeWindow(windowId: number, forceClose: boolean = false): Promise { + console.log(`closeWindow(${windowId}, ${forceClose}) called.`) + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + window.close() // 触发 'close' 事件 + } else { + console.warn( + `Failed to close window ${windowId} in closeWindow, window does not exist or is destroyed.` + ) + } + return Promise.resolve() + } + + /** + * 隐藏指定 ID 的窗口。在全屏模式下,会先退出全屏再隐藏。 + * @param windowId 窗口 ID。 + */ + hide(windowId: number): void { + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + console.log(`Hiding window ${windowId}.`) + // 处理全屏窗口隐藏时的黑屏问题 + if (window.isFullScreen()) { + console.log(`Window ${windowId} is fullscreen, exiting fullscreen before hiding.`) + // 退出全屏后监听 leave-full-screen 事件再隐藏 + window.once('leave-full-screen', () => { + console.log(`Window ${windowId} left fullscreen, proceeding with hide.`) + if (!window.isDestroyed()) { + window.hide() + } else { + console.warn(`Window ${windowId} was destroyed after leaving fullscreen, cannot hide.`) + } + }) + window.setFullScreen(false) // 请求退出全屏 + } else { + console.log(`Window ${windowId} is not fullscreen, hiding directly.`) + window.hide() // 直接隐藏 + } + } else { + console.warn(`Failed to hide window ${windowId}, window does not exist or is destroyed.`) + } + } + + /** + * 显示指定 ID 的窗口。如果未指定 ID,则显示焦点窗口或第一个窗口。 + * @param windowId 可选。要显示的窗口 ID。 + */ + show(windowId?: number): void { + let targetWindow: BrowserWindow | undefined + if (windowId === undefined) { + // 未指定 ID,查找焦点窗口或第一个窗口 + targetWindow = this.getFocusedWindow() || this.getAllWindows()[0] + if (targetWindow && !targetWindow.isDestroyed()) { + console.log(`Showing default window ${targetWindow.id}.`) + } else { + console.warn('No window found to show.') + return + } + } else { + targetWindow = this.windows.get(windowId) + if (targetWindow && !targetWindow.isDestroyed()) { + console.log(`Showing window ${windowId}.`) + } else { + console.warn(`Failed to show window ${windowId}, window does not exist or is destroyed.`) + return + } + } + + targetWindow.show() + targetWindow.focus() // Bring to foreground + // 触发恢复逻辑以确保活动标签页可见且位置正确 + this.handleWindowRestore(targetWindow.id).catch((error) => { + console.error(`Error handling restore logic after showing window ${targetWindow!.id}:`, error) + }) + } + + /** + * 窗口恢复、显示或尺寸变更后的处理逻辑。 + * 主要确保当前活动标签页的 WebContentsView 可见且位置正确。 + * @param windowId 窗口 ID。 + */ + private async handleWindowRestore(windowId: number): Promise { + console.log(`Handling restore/show logic for window ${windowId}.`) + const window = this.windows.get(windowId) + if (!window || window.isDestroyed()) { + console.warn( + `Cannot handle restore/show logic for window ${windowId}, window does not exist or is destroyed.` + ) + return + } + + try { + // 通过 TabPresenter 获取活动标签页 ID + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const activeTabId = await tabPresenterInstance.getActiveTabId(windowId) + + if (activeTabId) { + console.log(`Window ${windowId} restored/shown: activating active tab ${activeTabId}.`) + // 调用 switchTab 会确保视图被关联、可见并更新 bounds + await tabPresenterInstance.switchTab(activeTabId) + } else { + console.warn( + `Window ${windowId} restored/shown: no active tab found, ensuring all views are hidden.` + ) + // 如果没有活动标签页,确保所有视图都隐藏 + const tabsInWindow = await tabPresenterInstance.getWindowTabsData(windowId) + for (const tabData of tabsInWindow) { + const tabView = await tabPresenterInstance.getTab(tabData.id) + if (tabView && !tabView.webContents.isDestroyed()) { + tabView.setVisible(false) // 显式隐藏所有标签页视图 + } + } + } + } catch (error) { + console.error(`Error handling restore/show logic for window ${windowId}:`, error) + } + } + + /** + * 检查指定 ID 的窗口是否已最大化。 + * @param windowId 窗口 ID。 + * @returns 如果窗口存在、有效且已最大化,则返回 true,否则返回 false。 + */ + isMaximized(windowId: number): boolean { + const window = this.windows.get(windowId) + return window && !window.isDestroyed() ? window.isMaximized() : false + } + + /** + * 检查指定 ID 的窗口是否当前获得了焦点。 + * @param windowId 窗口 ID。 + * @returns 如果是焦点窗口,则返回 true,否则返回 false。 + */ + isMainWindowFocused(windowId: number): boolean { + const focusedWindow = this.getFocusedWindow() + return focusedWindow ? focusedWindow.id === windowId : false + } + + /** + * 检查是否应该聚焦标签页 + * @param windowId 窗口 ID + * @param reason 聚焦原因 + */ + private shouldFocusTab( + windowId: number, + reason: 'focus' | 'restore' | 'show' | 'initial' + ): boolean { + const state = this.windowFocusStates.get(windowId) + if (!state) { + return true + } + const now = Date.now() + if (now - state.lastFocusTime < 100) { + console.log(`Skipping focus for window ${windowId}, too frequent (${reason})`) + return false + } + switch (reason) { + case 'initial': + return !state.hasInitialFocus + case 'focus': + return state.shouldFocus + case 'restore': + case 'show': + return state.isNewWindow || state.shouldFocus + default: + return false + } + } + + /** + * 将焦点传递给指定窗口的活动标签页 + * @param windowId 窗口 ID + * @param reason 聚焦原因 + */ + public focusActiveTab( + windowId: number, + reason: 'focus' | 'restore' | 'show' | 'initial' = 'focus' + ): void { + if (!this.shouldFocusTab(windowId, reason)) { + return + } + try { + setTimeout(async () => { + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const tabsData = await tabPresenterInstance.getWindowTabsData(windowId) + const activeTab = tabsData.find((tab) => tab.isActive) + if (activeTab) { + console.log( + `Focusing active tab ${activeTab.id} in window ${windowId} (reason: ${reason})` + ) + await tabPresenterInstance.switchTab(activeTab.id) + const state = this.windowFocusStates.get(windowId) + if (state) { + state.lastFocusTime = Date.now() + if (reason === 'initial') { + state.hasInitialFocus = true + } + if (reason === 'focus' || reason === 'initial') { + state.isNewWindow = false + } + } + } + }, 50) + } catch (error) { + console.error(`Error focusing active tab in window ${windowId}:`, error) + } + } + + /** + * 向所有有效窗口的主 WebContents 和所有标签页的 WebContents 发送消息。 + * @param channel IPC 通道名。 + * @param args 消息参数。 + */ + async sendToAllWindows(channel: string, ...args: unknown[]): Promise { + // 遍历 Map 的值副本,避免迭代过程中 Map 被修改 + for (const window of Array.from(this.windows.values())) { + if (!window.isDestroyed()) { + // 向窗口主 WebContents 发送 + window.webContents.send(channel, ...args) + + // 向窗口内所有标签页的 WebContents 发送 (异步执行) + try { + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const tabsData = await tabPresenterInstance.getWindowTabsData(window.id) + if (tabsData && tabsData.length > 0) { + for (const tabData of tabsData) { + const tab = await tabPresenterInstance.getTab(tabData.id) + if (tab && !tab.webContents.isDestroyed()) { + tab.webContents.send(channel, ...args) + } + } + } + } catch (error) { + console.error(`Error sending message "${channel}" to tabs of window ${window.id}:`, error) + } + } else { + console.warn(`Skipping sending message "${channel}" to destroyed window ${window.id}.`) + } + } + + if (this.floatingChatWindow && this.floatingChatWindow.isShowing()) { + const floatingWindow = this.floatingChatWindow.getWindow() + if (floatingWindow && !floatingWindow.isDestroyed()) { + try { + floatingWindow.webContents.send(channel, ...args) + } catch (error) { + console.error(`Error sending message "${channel}" to floating chat window:`, error) + } + } + } + } + + /** + * 向指定 ID 的窗口的主 WebContents 和其所有标签页的 WebContents 发送消息。 + * @param windowId 目标窗口 ID。 + * @param channel IPC 通道名。 + * @param args 消息参数。 + * @returns 如果消息已尝试发送,返回 true,否则返回 false。 + */ + sendToWindow(windowId: number, channel: string, ...args: unknown[]): boolean { + console.log(`Sending message "${channel}" to window ${windowId}.`) + const window = this.windows.get(windowId) + if (window && !window.isDestroyed()) { + // 向窗口主 WebContents 发送 + window.webContents.send(channel, ...args) + + // 向窗口内所有标签页的 WebContents 发送 (异步执行) + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + tabPresenterInstance + .getWindowTabsData(windowId) + .then((tabsData) => { + if (tabsData && tabsData.length > 0) { + tabsData.forEach(async (tabData) => { + const tab = await tabPresenterInstance.getTab(tabData.id) + if (tab && !tab.webContents.isDestroyed()) { + tab.webContents.send(channel, ...args) + } + }) + } + }) + .catch((error) => { + console.error(`Error sending message "${channel}" to tabs of window ${windowId}:`, error) + }) + return true + } else { + console.warn( + `Failed to send message "${channel}" to window ${windowId}, window does not exist or is destroyed.` + ) + } + return false + } + + /** + * 创建一个新的外壳窗口。 + * @param options 窗口配置选项,包括初始标签页或激活现有标签页。 + * @returns 创建的窗口 ID,如果创建失败则返回 null。 + */ + async createShellWindow(options?: { + activateTabId?: number // 要关联并激活的现有标签页 ID + initialTab?: { + // 窗口创建时要创建的新标签页选项 + url: string + icon?: string + } + x?: number // 初始 X 坐标 + y?: number // 初始 Y 坐标 + }): Promise { + console.log('Creating new shell window.') + + // 根据平台选择图标 + const iconFile = nativeImage.createFromPath(process.platform === 'win32' ? iconWin : icon) + + // 使用窗口状态管理器恢复位置和尺寸 + const shellWindowState = windowStateManager({ + defaultWidth: 800, + defaultHeight: 620 + }) + + // 计算初始位置,确保窗口完全在屏幕范围内 + const initialX = + options?.x !== undefined + ? options.x + : this.validateWindowPosition( + shellWindowState.x, + shellWindowState.width, + shellWindowState.y, + shellWindowState.height + ).x + let initialY = + options?.y !== undefined + ? options?.y + : this.validateWindowPosition( + shellWindowState.x, + shellWindowState.width, + shellWindowState.y, + shellWindowState.height + ).y + + const shellWindow = new BrowserWindow({ + width: shellWindowState.width, + height: shellWindowState.height, + x: initialX, + y: initialY, + show: false, // 先隐藏窗口,等待 ready-to-show 以避免白屏 + autoHideMenuBar: true, // 隐藏菜单栏 + icon: iconFile, // 设置图标 + titleBarStyle: 'hiddenInset', // macOS 风格标题栏 + transparent: process.platform === 'darwin', // macOS 标题栏透明 + vibrancy: process.platform === 'darwin' ? 'under-window' : undefined, // macOS 磨砂效果 + backgroundColor: '#00000000', // 透明背景色 + maximizable: true, // 允许最大化 + frame: process.platform === 'darwin', // macOS 无边框 + hasShadow: true, // macOS 阴影 + trafficLightPosition: process.platform === 'darwin' ? { x: 12, y: 12 } : undefined, // macOS 红绿灯按钮位置 + webPreferences: { + preload: join(__dirname, '../preload/index.mjs'), // Preload 脚本路径 + sandbox: false, // 禁用沙箱,允许 preload 访问 Node.js API + devTools: is.dev // 开发模式下启用 DevTools + }, + roundedCorners: true // Windows 11 圆角 + }) + + if (!shellWindow) { + console.error('Failed to create shell window.') + return null + } + + const windowId = shellWindow.id + this.windows.set(windowId, shellWindow) // 将窗口实例存入 Map + + this.windowFocusStates.set(windowId, { + lastFocusTime: 0, + shouldFocus: true, + isNewWindow: true, + hasInitialFocus: false + }) + + shellWindowState.manage(shellWindow) // 管理窗口状态 + + // 应用内容保护设置 + const contentProtectionEnabled = this.configPresenter.getContentProtectionEnabled() + this.updateContentProtection(shellWindow, contentProtectionEnabled) + + // --- 窗口事件监听 --- + + // 窗口准备就绪时显示 + shellWindow.on('ready-to-show', () => { + console.log(`Window ${windowId} is ready to show.`) + if (!shellWindow.isDestroyed()) { + shellWindow.show() // 显示窗口避免白屏 + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_CREATED, windowId) + } else { + console.warn(`Window ${windowId} was destroyed before ready-to-show.`) + } + }) + + // 窗口获得焦点 + shellWindow.on('focus', () => { + console.log(`Window ${windowId} gained focus.`) + this.focusedWindowId = windowId + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_FOCUSED, windowId) + if (!shellWindow.isDestroyed()) { + shellWindow.webContents.send('window-focused', windowId) + } + this.focusActiveTab(windowId, 'focus') + }) + + // 窗口失去焦点 + shellWindow.on('blur', () => { + console.log(`Window ${windowId} lost focus.`) + if (this.focusedWindowId === windowId) { + this.focusedWindowId = null // 仅当失去焦点的窗口是当前记录的焦点窗口时才清空 + } + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_BLURRED, windowId) + if (!shellWindow.isDestroyed()) { + shellWindow.webContents.send('window-blurred', windowId) + } + }) + + // 窗口最大化 + shellWindow.on('maximize', () => { + console.log(`Window ${windowId} maximized.`) + if (!shellWindow.isDestroyed()) { + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_MAXIMIZED, windowId) + // 触发恢复逻辑更新标签页 bounds + this.handleWindowRestore(windowId).catch((error) => { + console.error(`Error handling restore logic after maximizing window ${windowId}:`, error) + }) + } + }) + + // 窗口取消最大化 + shellWindow.on('unmaximize', () => { + console.log(`Window ${windowId} unmaximized.`) + if (!shellWindow.isDestroyed()) { + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_UNMAXIMIZED, windowId) + // 触发恢复逻辑更新标签页 bounds + this.handleWindowRestore(windowId).catch((error) => { + console.error( + `Error handling restore logic after unmaximizing window ${windowId}:`, + error + ) + }) + } + }) + + // 窗口从最小化恢复 (或通过 show 显式显示) + const handleRestore = async () => { + console.log(`Window ${windowId} restored.`) + this.handleWindowRestore(windowId).catch((error) => { + console.error(`Error handling restore logic for window ${windowId}:`, error) + }) + this.focusActiveTab(windowId, 'restore') + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_RESTORED, windowId) + } + shellWindow.on('restore', handleRestore) + + // 窗口进入全屏 + shellWindow.on('enter-full-screen', () => { + console.log(`Window ${windowId} entered fullscreen.`) + if (!shellWindow.isDestroyed()) { + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_ENTER_FULL_SCREEN, windowId) + // 触发恢复逻辑更新标签页 bounds + this.handleWindowRestore(windowId).catch((error) => { + console.error( + `Error handling restore logic after entering fullscreen for window ${windowId}:`, + error + ) + }) + } + }) + + // 窗口退出全屏 + shellWindow.on('leave-full-screen', () => { + console.log(`Window ${windowId} left fullscreen.`) + if (!shellWindow.isDestroyed()) { + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_LEAVE_FULL_SCREEN, windowId) + // 触发恢复逻辑更新标签页 bounds + this.handleWindowRestore(windowId).catch((error) => { + console.error( + `Error handling restore logic after leaving fullscreen for window ${windowId}:`, + error + ) + }) + } + }) + + // 窗口尺寸改变,通知 TabPresenter 更新所有视图 bounds + shellWindow.on('resize', () => { + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_RESIZE, windowId) + }) + + // 'close' 事件:用户尝试关闭窗口 (点击关闭按钮等)。 + // 此处理程序决定是隐藏窗口还是允许其关闭/销毁。 + shellWindow.on('close', (event) => { + console.log( + `Window ${windowId} close event. isQuitting: ${this.isQuitting}, Platform: ${process.platform}.` + ) + + // 如果应用不是正在退出过程中... + if (!this.isQuitting) { + // 实现隐藏到托盘逻辑: + // 1. 如果是其他窗口,直接关闭 + // 2. 如果是主窗口,判断配置是否允许关闭 + // shouldPreventDefault: true隐藏, false关闭 + const shouldQuitOnClose = this.configPresenter.getCloseToQuit() + const shouldPreventDefault = windowId === this.mainWindowId && !shouldQuitOnClose + + if (shouldPreventDefault) { + console.log(`Window ${windowId}: Preventing default close behavior, hiding instead.`) + event.preventDefault() // 阻止默认窗口关闭行为 + + // 处理全屏窗口隐藏时的黑屏问题 (同 hide 方法) + if (shellWindow.isFullScreen()) { + console.log( + `Window ${windowId} is fullscreen, exiting fullscreen before hiding (close event).` + ) + shellWindow.once('leave-full-screen', () => { + console.log(`Window ${windowId} left fullscreen, proceeding with hide (close event).`) + if (!shellWindow.isDestroyed()) { + shellWindow.hide() + } else { + console.warn( + `Window ${windowId} was destroyed after leaving fullscreen, cannot hide (close event).` + ) + } + }) + shellWindow.setFullScreen(false) + } else { + console.log(`Window ${windowId} is not fullscreen, hiding directly (close event).`) + shellWindow.hide() + } + } else { + // 允许默认关闭行为。这将触发 'closed' 事件。 + console.log( + `Window ${windowId}: Allowing default close behavior (app is quitting or macOS last window configured to quit).` + ) + presenter.tabPresenter.closeTabs(windowId) + } + } else { + // 如果 isQuitting 为 true,表示应用正在主动退出,允许窗口正常关闭 + console.log(`Window ${windowId}: isQuitting is true, allowing default close behavior.`) + } + }) + + // 'closed' 事件:窗口实际关闭并销毁时触发 (在 'close' 事件之后,如果未阻止默认行为) + shellWindow.on('closed', () => { + console.log( + `Window ${windowId} closed event triggered. isQuitting: ${this.isQuitting}, Map size BEFORE delete: ${this.windows.size}` + ) + const windowIdBeingClosed = windowId // 捕获 ID + + // 移除 restore 事件监听器,防止内存泄漏 (其他事件的清理根据需要添加) + shellWindow.removeListener('restore', handleRestore) + + this.windows.delete(windowIdBeingClosed) // 从 Map 中移除 + this.windowFocusStates.delete(windowIdBeingClosed) + shellWindowState.unmanage() // 停止管理窗口状态 + eventBus.sendToMain(WINDOW_EVENTS.WINDOW_CLOSED, windowIdBeingClosed) + console.log( + `Window ${windowIdBeingClosed} closed event handled. Map size AFTER delete: ${this.windows.size}` + ) + + // 如果在非 macOS 平台,且关闭的是最后一个窗口,如果应用并非正在退出,则发出警告。 + // 在隐藏到托盘逻辑下,'closed' 事件仅应在 isQuitting 为 true 时触发。 + if (this.windows.size === 0 && process.platform !== 'darwin') { + console.log(`Last window closed on non-macOS platform.`) + if (!this.isQuitting) { + console.warn( + `Warning: Last window on non-macOS platform triggered closed event, but app is not marked as quitting. This might indicate window destruction instead of hiding.` + ) + } + } + }) + + // --- 加载 Renderer HTML 文件 --- + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + console.log( + `Loading renderer URL in dev mode: ${process.env['ELECTRON_RENDERER_URL']}/shell/index.html` + ) + shellWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/shell/index.html') + } else { + // 生产模式下加载打包后的 HTML 文件 + console.log( + `Loading packaged renderer file: ${join(__dirname, '../renderer/shell/index.html')}` + ) + shellWindow.loadFile(join(__dirname, '../renderer/shell/index.html')) + } + + // --- 处理初始标签页创建或激活 --- + + // 如果提供了 options?.initialTab,等待窗口加载完成,然后创建新标签页 + if (options?.initialTab) { + shellWindow.webContents.once('did-finish-load', async () => { + console.log(`Window ${windowId} did-finish-load, checking for initial tab creation.`) + if (shellWindow.isDestroyed()) { + console.warn( + `Window ${windowId} was destroyed before did-finish-load callback, cannot create initial tab.` + ) + return + } + shellWindow.focus() // 窗口加载完成后聚焦 + try { + console.log(`Creating initial tab, URL: ${options.initialTab!.url}`) + const tabId = await (presenter.tabPresenter as TabPresenter).createTab( + windowId, + options.initialTab!.url, + { active: true } + ) + if (tabId === null) { + console.error(`Failed to create initial tab in new window ${windowId}.`) + } else { + console.log(`Created initial tab ${tabId} in window ${windowId}.`) + } + } catch (error) { + console.error(`Error creating initial tab:`, error) + } + }) + } + + // 如果提供了 activateTabId,表示一个现有标签页 (WebContentsView) 将被 TabPresenter 关联到此新窗口 + // 激活逻辑 (设置可见性、bounds) 在 tabPresenter.attachTab / switchTab 中处理 + if (options?.activateTabId !== undefined) { + // 等待窗口加载完成,然后尝试激活指定标签页 + shellWindow.webContents.once('did-finish-load', async () => { + console.log( + `Window ${windowId} did-finish-load, attempting to activate tab ${options.activateTabId}.` + ) + if (shellWindow.isDestroyed()) { + console.warn( + `Window ${windowId} was destroyed before did-finish-load callback, cannot activate tab ${options.activateTabId}.` + ) + return + } + try { + // 切换到指定标签页,这将处理视图的关联和显示 + await (presenter.tabPresenter as TabPresenter).switchTab(options.activateTabId as number) + console.log(`Requested to switch to tab ${options.activateTabId}.`) + } catch (error) { + console.error( + `Failed to activate tab ${options.activateTabId} after window ${windowId} load:`, + error + ) + } + }) + } + + // 开发模式下可选开启 DevTools + if (is.dev) { + // shellWindow.webContents.openDevTools({ mode: 'detach' }); + } + + console.log(`Shell window ${windowId} created successfully.`) + + if (this.mainWindowId == null) { + this.mainWindowId = windowId // 如果这是第一个窗口,设置为主窗口 ID + } + return windowId // 返回新创建窗口的 ID + } + + /** + * 更新指定窗口的内容保护设置。 + * @param window BrowserWindow 实例。 + * @param enabled 是否启用内容保护。 + */ + private updateContentProtection(window: BrowserWindow, enabled: boolean): void { + if (window.isDestroyed()) { + console.warn(`Attempted to update content protection settings on a destroyed window.`) + return + } + console.log(`Updating content protection for window ${window.id}: ${enabled}`) + + // setContentProtection 阻止截图/屏幕录制 + window.setContentProtection(enabled) + + // setBackgroundThrottling 限制非活动窗口的帧率。 + // 启用内容保护时禁用节流,确保即使窗口非活动也能保持保护。 + window.webContents.setBackgroundThrottling(!enabled) // 启用保护时禁用节流 + window.webContents.setFrameRate(60) // 设置帧率 + window.setBackgroundColor('#00000000') // 设置背景色为透明 + + // macOS 特定的隐藏功能 (用于内容保护) + if (process.platform === 'darwin') { + window.setHiddenInMissionControl(enabled) // 在 Mission Control 中隐藏 + window.setSkipTaskbar(enabled) // 在 Dock 和 Mission Control 切换器中隐藏 + } + } + + /** + * 获取当前获得焦点的 BrowserWindow 实例 (由 Electron 报告并经内部 Map 验证)。 + * @returns 获得焦点的 BrowserWindow 实例,如果无焦点窗口或窗口无效则返回 undefined。 + */ + getFocusedWindow(): BrowserWindow | undefined { + const electronFocusedWindow = BrowserWindow.getFocusedWindow() + + if (electronFocusedWindow) { + const windowId = electronFocusedWindow.id + const ourWindow = this.windows.get(windowId) + + // 验证 Electron 报告的窗口是否在我们管理范围内且有效 + if (ourWindow && !ourWindow.isDestroyed()) { + this.focusedWindowId = windowId // 更新内部记录 + return ourWindow + } else { + // Electron 报告的窗口不在 Map 中或已销毁 + console.warn( + `Electron reported window ${windowId} focused, but it is not managed or is destroyed.` + ) + this.focusedWindowId = null + return undefined + } + } else { + this.focusedWindowId = null // 清空内部记录 + return undefined + } + } + + /** + * 获取所有有效 (未销毁) 的 BrowserWindow 实例数组。 + * @returns BrowserWindow 实例数组。 + */ + getAllWindows(): BrowserWindow[] { + return Array.from(this.windows.values()).filter((window) => !window.isDestroyed()) + } + + /** + * 获取指定窗口的活动标签页 ID。 + * @param windowId 窗口 ID。 + * @returns 活动标签页 ID,如果窗口无效或无活动标签页则返回 undefined。 + */ + async getActiveTabId(windowId: number): Promise { + const window = this.windows.get(windowId) + if (!window || window.isDestroyed()) { + console.warn( + `Cannot get active tab ID for window ${windowId}, window does not exist or is destroyed.` + ) + return undefined + } + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const tabsData = await tabPresenterInstance.getWindowTabsData(windowId) + const activeTab = tabsData.find((tab) => tab.isActive) + return activeTab?.id + } + + /** + * 向指定窗口的活动标签页发送一个事件。 + * @param windowId 目标窗口 ID。 + * @param channel 事件通道。 + * @param args 事件参数。 + * @returns 如果事件已发送到有效活动标签页,返回 true,否则返回 false。 + */ + async sendToActiveTab(windowId: number, channel: string, ...args: unknown[]): Promise { + console.log(`Sending event "${channel}" to active tab of window ${windowId}.`) + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const activeTabId = await tabPresenterInstance.getActiveTabId(windowId) + if (activeTabId) { + const tab = await tabPresenterInstance.getTab(activeTabId) + if (tab && !tab.webContents.isDestroyed()) { + tab.webContents.send(channel, ...args) + console.log(` - Event sent to tab ${activeTabId}.`) + return true + } else { + console.warn( + ` - Active tab ${activeTabId} does not exist or is destroyed, cannot send event.` + ) + } + } else { + console.warn(`No active tab found in window ${windowId}, cannot send event "${channel}".`) + } + return false + } + + /** + * 向“默认”标签页发送消息。 + * 优先级:焦点窗口的活动标签页 > 第一个窗口的活动标签页 > 第一个窗口的第一个标签页。 + * @param channel 消息通道。 + * @param switchToTarget 发送消息后是否切换到目标窗口和标签页。默认为 false。 + * @param args 消息参数。 + * @returns 如果消息已发送,返回 true,否则返回 false。 + */ + async sendToDefaultTab( + channel: string, + switchToTarget: boolean = false, + ...args: unknown[] + ): Promise { + console.log(`Sending message "${channel}" to default tab. Switch to target: ${switchToTarget}.`) + try { + // 优先使用当前获得焦点的窗口 + let targetWindow = this.getFocusedWindow() + let windowId: number | undefined + + if (targetWindow) { + windowId = targetWindow.id + console.log(` - Using focused window ${windowId}`) + } else { + // 如果没有焦点窗口,使用第一个有效窗口 + const windows = this.getAllWindows() + if (windows.length === 0) { + console.warn('No window found to send message to.') + return false + } + targetWindow = windows[0] + windowId = targetWindow.id + console.log(` - No focused window, using first window ${windowId}`) + } + + // 获取目标窗口的所有标签页 + const tabPresenterInstance = presenter.tabPresenter as TabPresenter + const tabsData = await tabPresenterInstance.getWindowTabsData(windowId) + if (tabsData.length === 0) { + console.warn(`Window ${windowId} has no tabs, cannot send message to default tab.`) + return false + } + + // 获取活动标签页,如果没有则取第一个标签页 + const targetTabData = tabsData.find((tab) => tab.isActive) || tabsData[0] + const targetTab = await tabPresenterInstance.getTab(targetTabData.id) + + if (targetTab && !targetTab.webContents.isDestroyed()) { + // 向目标标签页发送消息 + targetTab.webContents.send(channel, ...args) + console.log(` - Message sent to tab ${targetTabData.id} in window ${windowId}.`) + + // 如果需要,切换到目标窗口和标签页 + if (switchToTarget) { + try { + // 激活目标窗口 + if (targetWindow && !targetWindow.isDestroyed()) { + console.log(` - Switching to window ${windowId}`) + targetWindow.show() // 确保窗口可见 + targetWindow.focus() // 将窗口带到前台 + } + + // 如果目标标签页不是活动标签页,则切换 + if (!targetTabData.isActive) { + console.log(` - Switching to tab ${targetTabData.id}`) + await tabPresenterInstance.switchTab(targetTabData.id) + } + // switchTab 已经会调用 bringViewToFront 来设置焦点,无需额外调用 + } catch (error) { + console.error('Error switching to target window/tab:', error) + // 继续,因为消息发送成功 + } + } + + return true // 消息发送成功 + } else { + console.warn( + `Target tab ${targetTabData.id} in window ${windowId} is unavailable or destroyed.` + ) + return false // 目标标签页无效 + } + } catch (error) { + console.error('Error sending message to default tab:', error) + return false // 过程中发生错误 + } + } + + public async createFloatingChatWindow(): Promise { + if (this.floatingChatWindow) { + console.log('FloatingChatWindow already exists') + return + } + + try { + this.floatingChatWindow = new FloatingChatWindow() + await this.floatingChatWindow.create() + console.log('FloatingChatWindow created successfully') + } catch (error) { + console.error('Failed to create FloatingChatWindow:', error) + this.floatingChatWindow = null + throw error + } + } + + public async showFloatingChatWindow(floatingButtonPosition?: { + x: number + y: number + width: number + height: number + }): Promise { + if (!this.floatingChatWindow) { + await this.createFloatingChatWindow() + } + + if (this.floatingChatWindow) { + this.floatingChatWindow.show(floatingButtonPosition) + console.log('FloatingChatWindow shown') + } + } + + public hideFloatingChatWindow(): void { + if (this.floatingChatWindow) { + this.floatingChatWindow.hide() + console.log('FloatingChatWindow hidden') + } + } + + public async toggleFloatingChatWindow(floatingButtonPosition?: { + x: number + y: number + width: number + height: number + }): Promise { + if (!this.floatingChatWindow) { + await this.createFloatingChatWindow() + } + + if (this.floatingChatWindow) { + this.floatingChatWindow.toggle(floatingButtonPosition) + console.log('FloatingChatWindow toggled') + } + } + + public destroyFloatingChatWindow(): void { + if (this.floatingChatWindow) { + this.floatingChatWindow.destroy() + this.floatingChatWindow = null + console.log('FloatingChatWindow destroyed') + } + } + + public isFloatingChatWindowVisible(): boolean { + return this.floatingChatWindow?.isShowing() || false + } + + public getFloatingChatWindow(): FloatingChatWindow | null { + return this.floatingChatWindow + } + + public isApplicationQuitting(): boolean { + return this.isQuitting + } + + public setApplicationQuitting(isQuitting: boolean): void { + this.isQuitting = isQuitting + } + + private validateWindowPosition( + x: number, + width: number, + y: number, + height: number + ): { x: number; y: number } { + const primaryDisplay = screen.getPrimaryDisplay() + const { workArea } = primaryDisplay + const isXValid = x >= workArea.x && x + width <= workArea.x + workArea.width + const isYValid = y >= workArea.y && y + height <= workArea.y + workArea.height + if (!isXValid || !isYValid) { + console.log( + `Window position out of bounds (x: ${x}, y: ${y}, width: ${width}, height: ${height}), centering window` + ) + return { + x: workArea.x + Math.max(0, (workArea.width - width) / 2), + y: workArea.y + Math.max(0, (workArea.height - height) / 2) + } + } + return { x, y } + } +} diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts new file mode 100644 index 0000000..a0f4519 --- /dev/null +++ b/src/main/utils/index.ts @@ -0,0 +1,33 @@ +import { presenter } from '@/presenter' + +export function handleShowHiddenWindow(mustShow: boolean) { + const allWindows = presenter.windowPresenter.getAllWindows() + if (allWindows.length === 0) { + presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + } else { + // 查找目标窗口 (焦点窗口或第一个窗口) + const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0] + + if (!targetWindow.isDestroyed()) { + // 逻辑: 如果窗口可见且不是从托盘点击触发,则隐藏;否则显示并置顶 + if (targetWindow.isVisible() && !mustShow) { + presenter.windowPresenter.hide(targetWindow.id) + } else { + presenter.windowPresenter.show(targetWindow.id) + targetWindow.focus() // 确保窗口置顶 + } + } else { + console.warn('Target window for SHOW_HIDDEN_WINDOW event is destroyed.') // 保持 warn + // 如果目标窗口已销毁,创建新窗口 + presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + } + } +} diff --git a/src/main/utils/strings.ts b/src/main/utils/strings.ts new file mode 100644 index 0000000..98732b3 --- /dev/null +++ b/src/main/utils/strings.ts @@ -0,0 +1,29 @@ +/** + * Sanitizes text content for processing in knowledge base systems. + * Performs the following transformations: + * - Removes backslashes + * - Replaces hash characters with spaces + * - Converts spaced double periods to single periods + * - Normalizes line breaks to \n + * - Collapses multiple consecutive spaces (but preserves single line breaks) + * - Trims leading and trailing whitespace + * + * @param text - The input text to sanitize + * @returns The sanitized text + * @throws Error if input is not a string + */ +export function sanitizeText(text: string) { + if (typeof text !== 'string') { + throw new Error('Input must be a string') + } + if (text.length === 0) { + return text + } + return text + .replace(/\\/g, '') + .replace(/#/g, ' ') + .replace(/\. \./g, '.') + .replace(/(\r\n|\r)/g, '\n') + .replace(/[ \t]+/g, ' ') + .trim() +} diff --git a/src/main/utils/vector.ts b/src/main/utils/vector.ts new file mode 100644 index 0000000..c15d860 --- /dev/null +++ b/src/main/utils/vector.ts @@ -0,0 +1,102 @@ +import { MetricType } from '@shared/presenter' + +export const EMBEDDING_TEST_KEY = 'sample' + +/** + * 计算向量的 L2 范数(欧几里得范数) + * @param vector 输入向量 + * @returns + */ +function calcNorm(vector: number[]): number { + return Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0)) +} + +/** + * 判断一个向量是否已 normalized(L2 范数 ≈ 1) + * @param vector 输入向量 + * @param tolerance 浮点误差容忍范围,默认 1e-3 + * @returns true 表示已 normalized + */ +export function isNormalized(vector: number[], tolerance = 1e-3): boolean { + if (!vector || !Array.isArray(vector) || vector.length === 0) return false + if (tolerance < 0) throw new Error('Tolerance must be non-negative') + if (vector.some((v) => typeof v !== 'number' || !isFinite(v))) return false + + const norm = calcNorm(vector) + return Math.abs(norm - 1) <= tolerance +} +/** + * 向量 normalized 处理 + * @param vector 输入向量 + * @returns normalized 向量 + */ +export function normalized(vector: number[]): number[] { + if (!vector || !Array.isArray(vector) || vector.length === 0) { + throw new Error('Vector cannot be empty') + } + const norm = calcNorm(vector) + if (norm === 0) { + throw new Error('Cannot normalize zero vector') + } + return vector.map((v) => v / norm) +} +/** + * 必定返回 normalized 向量 + * @param vector 输入向量 + * @param tolerance 浮点误差容忍范围,默认 1e-3 + * @returns normalized 向量 + * @description 由于向量长度在多模态应用(或部分RAG应用)中有含义,因此未强制对embedding结果进行向量化,如有需要请自行调用 + */ +export function ensureNormalized(vector: number[], tolerance = 1e-3): number[] { + if (!vector || !Array.isArray(vector) || vector.length === 0) { + throw new Error('Vector cannot be empty') + } + if (tolerance < 0) throw new Error('Tolerance must be non-negative') + const norm = calcNorm(vector) + if (norm === 0) { + throw new Error('Cannot normalize zero vector') + } + if (Math.abs(norm - 1) <= tolerance) { + return vector + } + return vector.map((v) => v / norm) +} + +/** + * 将 similarityQuery 返回的 distance 归一化为 [0,1] confidence + * @param distance 原始 distance + * @param metric 'cosine' | 'ip' + * @returns 0~1 置信度值 + */ +export function normalizeDistance(distance: number, metric: MetricType): number { + if (metric === 'cosine') { + // cosine distance ∈ [0,1],0 越相似,1 越不相似 + // confidence = 1 - distance + const clipped = Math.min(Math.max(distance, 0), 1) + return 1 - clipped + } else if (metric === 'ip') { + // ip distance = -inner_product,可能为负数 + // distance < 0 → 向量夹角 < 90°,相似度高 + // distance = 0 → 向量正交,无相似性 + // distance > 0 → 向量夹角 > 90°,方向相反 + // + // 使用 sigmoid 将其映射到 (0,1) + // 这里使用 distance * k 来调整 sigmoid 的陡峭程度,需要根据经验和需求微调缩放因子k + // k = 0.1 sigmoid 更平滑 + // k = 0.5 sigmoid 更陡峭 + const k = 0.04 + const sigmoid = 1 / (1 + Math.exp(Math.sign(distance) * Math.pow(distance, 2) * k)) + return sigmoid + } else { + throw new Error(`Unsupported metric: ${metric}`) + } +} + +/** + * 获取相似度度量方式 + * @param normalized 是否已 normalized + * @returns 相似度度量方式 + */ +export function getMetric(normalized: boolean): MetricType { + return normalized ? 'cosine' : 'ip' +} diff --git a/src/preload/floating-preload.ts b/src/preload/floating-preload.ts new file mode 100644 index 0000000..7c81ba6 --- /dev/null +++ b/src/preload/floating-preload.ts @@ -0,0 +1,83 @@ +import { contextBridge, ipcRenderer } from 'electron' + +// Define event constants directly to avoid path resolution issues +const FLOATING_BUTTON_EVENTS = { + CLICKED: 'floating-button:clicked', + RIGHT_CLICKED: 'floating-button:right-clicked', + DRAG_START: 'floating-button:drag-start', + DRAG_MOVE: 'floating-button:drag-move', + DRAG_END: 'floating-button:drag-end' +} as const + +// Define floating button API +const floatingButtonAPI = { + // Notify main process that floating button was clicked + onClick: () => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.CLICKED) + } catch (error) { + console.error('FloatingPreload: Error sending IPC message:', error) + } + }, + + onRightClick: () => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) + } catch (error) { + console.error('FloatingPreload: Error sending right click IPC message:', error) + } + }, + + // Drag-related API + onDragStart: (x: number, y: number) => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.DRAG_START, { x, y }) + } catch (error) { + console.error('FloatingPreload: Error sending drag start IPC message:', error) + } + }, + + onDragMove: (x: number, y: number) => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.DRAG_MOVE, { x, y }) + } catch (error) { + console.error('FloatingPreload: Error sending drag move IPC message:', error) + } + }, + + onDragEnd: (x: number, y: number) => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.DRAG_END, { x, y }) + } catch (error) { + console.error('FloatingPreload: Error sending drag end IPC message:', error) + } + }, + + // Listen to events from main process + onConfigUpdate: (callback: (config: any) => void) => { + ipcRenderer.on('floating-button-config-update', (_event, config) => { + callback(config) + }) + }, + + // Remove event listeners + removeAllListeners: () => { + console.log('FloatingPreload: Removing all listeners') + ipcRenderer.removeAllListeners('floating-button-config-update') + } +} + +// Try different ways to expose API +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('floatingButtonAPI', floatingButtonAPI) + } catch (error) { + console.error('=== FloatingPreload: Error exposing API via contextBridge ===:', error) + } +} else { + try { + ;(window as any).floatingButtonAPI = floatingButtonAPI + } catch (error) { + console.error('=== FloatingPreload: Error attaching API to window ===:', error) + } +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts new file mode 100644 index 0000000..aec8a71 --- /dev/null +++ b/src/preload/index.d.ts @@ -0,0 +1,15 @@ +import { ElectronAPI } from '@electron-toolkit/preload' + +declare global { + interface Window { + electron: ElectronAPI + api: { + copyText(text: string): void + copyImage(image: string): void + getPathForFile(file: File): string + getWindowId(): number | null + getWebContentsId(): number + } + floatingButtonAPI: typeof floatingButtonAPI + } +} diff --git a/src/preload/index.ts b/src/preload/index.ts new file mode 100644 index 0000000..530d873 --- /dev/null +++ b/src/preload/index.ts @@ -0,0 +1,61 @@ +import { clipboard, contextBridge, nativeImage, webUtils, webFrame, ipcRenderer } from 'electron' +import { exposeElectronAPI } from '@electron-toolkit/preload' + +// Cache variables +let cachedWindowId: number | undefined = undefined +let cachedWebContentsId: number | undefined = undefined + +// Custom APIs for renderer +const api = { + copyText: (text: string) => { + clipboard.writeText(text) + }, + copyImage: (image: string) => { + const img = nativeImage.createFromDataURL(image) + clipboard.writeImage(img) + }, + getPathForFile: (file: File) => { + return webUtils.getPathForFile(file) + }, + getWindowId: () => { + if (cachedWindowId !== undefined) { + return cachedWindowId + } + cachedWindowId = ipcRenderer.sendSync('get-window-id') + return cachedWindowId + }, + getWebContentsId: () => { + if (cachedWebContentsId !== undefined) { + return cachedWebContentsId + } + cachedWebContentsId = ipcRenderer.sendSync('get-web-contents-id') + return cachedWebContentsId + } +} +exposeElectronAPI() + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('api', api) + } catch (error) { + console.error('Preload: Failed to expose API via contextBridge:', error) + } +} else { + // @ts-ignore (define in dts) + window.api = api +} +window.addEventListener('DOMContentLoaded', () => { + cachedWebContentsId = ipcRenderer.sendSync('get-web-contents-id') + cachedWindowId = ipcRenderer.sendSync('get-window-id') + console.log( + 'Preload: Initialized with WebContentsId:', + cachedWebContentsId, + 'WindowId:', + cachedWindowId + ) + webFrame.setVisualZoomLevelLimits(1, 1) // Disable trackpad zooming + webFrame.setZoomFactor(1) +}) diff --git a/src/renderer/floating/FloatingButton.vue b/src/renderer/floating/FloatingButton.vue new file mode 100644 index 0000000..58237ae --- /dev/null +++ b/src/renderer/floating/FloatingButton.vue @@ -0,0 +1,348 @@ + + + + + diff --git a/src/renderer/floating/env.d.ts b/src/renderer/floating/env.d.ts new file mode 100644 index 0000000..ca7576d --- /dev/null +++ b/src/renderer/floating/env.d.ts @@ -0,0 +1,24 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + const component: DefineComponent<{}, {}, any> + export default component +} + +declare global { + interface Window { + floatingButtonAPI: { + onClick: () => void + onRightClick: () => void + onDragStart: (x: number, y: number) => void + onDragMove: (x: number, y: number) => void + onDragEnd: (x: number, y: number) => void + onConfigUpdate: (callback: (config: any) => void) => void + removeAllListeners: () => void + } + } +} + +export {} diff --git a/src/renderer/floating/index.html b/src/renderer/floating/index.html new file mode 100644 index 0000000..0df14ab --- /dev/null +++ b/src/renderer/floating/index.html @@ -0,0 +1,29 @@ + + + + + + + + Floating Button + + + +
+ + + diff --git a/src/renderer/floating/main.ts b/src/renderer/floating/main.ts new file mode 100644 index 0000000..c0d4590 --- /dev/null +++ b/src/renderer/floating/main.ts @@ -0,0 +1,6 @@ +import '../src/assets/main.css' +import { createApp } from 'vue' +import FloatingButton from './FloatingButton.vue' + +const app = createApp(FloatingButton) +app.mount('#app') diff --git a/src/renderer/index.html b/src/renderer/index.html new file mode 100644 index 0000000..a5c323d --- /dev/null +++ b/src/renderer/index.html @@ -0,0 +1,19 @@ + + + + + DeepChat + + + + + + + +
+ + + diff --git a/src/renderer/public/sounds/sfx-fc.mp3 b/src/renderer/public/sounds/sfx-fc.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..63fe8002ff673fe39014ff80a481e46025078240 GIT binary patch literal 10989 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 z zbSuJuHGls_bviT#7icZsZeT_wMZGv z?<4%-70MJDA7Dd==hIOFjGpQBVNy(BhJ`ULYr~{*V&*8KfS!{L=*_l*+`jm=i49VcP)(_ZUlYPm>-mELmhCd zOB}j>yuI5y={|hq!*M=$o6U#1zBXcjfRP|<`^Hi2NhydgapZboA+ZP%$855wewKRk zAWYXlaOanjv<~Mc?>n{RO*1b02H(!}_|Po5r6hXx7`Lnp{DzNfD#GrvxFJ}Cm8foy zA!+p=_8-bV)0{W}3NO?j1-d+rY)peyIIW7q2~^tVAM#=v`jbrG${pwaK*xG$>Ryk@ zD2^5YlBme>6H4PGT_TFjl{>sPr*psOuY&{+%BrjL(^~VgRJcMElkk6CvhvV)BgYw8 zcS*5F$6wxPkYZMei*xG17t=`1-(A*;C0VjPmpPOk4dHuLqGjyufoe|p{(>%@I8q83I{Q!23Mw@1QDy>n zTy(!_gKr6^x|0dlojB8XJ9>^DbY3mu7El6lru1ùZncwt>y>HnmvJ~4G2^m$NT zWguod1WSFFhF8Oy_L`8tT5@o*gK11cQ!t-anIUP2y*7a%zFMQwRfEI}W?21=H;?YC z&bAY{p5G^!ZB|AzzTg1CMS+#E!`q?Ua?y3vtsE@X;(z;g%zN~fDWdb{xI@GL7*I@N zG8`L=3rn`eE3%X`sbxLxb}h0dH*&4Q35& z4Z)8D govi>z9byOI=gtw8Njvubcm3i2&BcfRHx^3%KTM&%r2qf` literal 0 HcmV?d00001 diff --git a/src/renderer/public/sounds/sfx-typing.mp3 b/src/renderer/public/sounds/sfx-typing.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..bfe3833a4ff06f122e27d1402021bb45c79611f0 GIT binary patch literal 4461 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 +
+ +
+ +
+
+ + + + + diff --git a/src/renderer/shell/components/AppBar.vue b/src/renderer/shell/components/AppBar.vue new file mode 100644 index 0000000..f412479 --- /dev/null +++ b/src/renderer/shell/components/AppBar.vue @@ -0,0 +1,579 @@ + + + + + diff --git a/src/renderer/shell/components/app-bar/AppBarTabItem.vue b/src/renderer/shell/components/app-bar/AppBarTabItem.vue new file mode 100644 index 0000000..3043603 --- /dev/null +++ b/src/renderer/shell/components/app-bar/AppBarTabItem.vue @@ -0,0 +1,63 @@ + + diff --git a/src/renderer/shell/components/icons/MaximizeIcon.vue b/src/renderer/shell/components/icons/MaximizeIcon.vue new file mode 100644 index 0000000..759dc02 --- /dev/null +++ b/src/renderer/shell/components/icons/MaximizeIcon.vue @@ -0,0 +1,15 @@ + diff --git a/src/renderer/shell/components/icons/RestoreIcon.vue b/src/renderer/shell/components/icons/RestoreIcon.vue new file mode 100644 index 0000000..b0c6ea6 --- /dev/null +++ b/src/renderer/shell/components/icons/RestoreIcon.vue @@ -0,0 +1,16 @@ + diff --git a/src/renderer/shell/index.html b/src/renderer/shell/index.html new file mode 100644 index 0000000..350f3d4 --- /dev/null +++ b/src/renderer/shell/index.html @@ -0,0 +1,19 @@ + + + + + DeepChat - Shell + + + + + + + +
+ + + diff --git a/src/renderer/shell/main.ts b/src/renderer/shell/main.ts new file mode 100644 index 0000000..fd2b5ca --- /dev/null +++ b/src/renderer/shell/main.ts @@ -0,0 +1,27 @@ +import '@/assets/main.css' +import { addCollection } from '@iconify/vue' +import lucideIcons from '@iconify-json/lucide/icons.json' +import vscodeIcons from '@iconify-json/vscode-icons/icons.json' +import { createPinia } from 'pinia' +import { createApp } from 'vue' +import App from './App.vue' + +import { createI18n } from 'vue-i18n' +import locales from '@/i18n' + +const i18n = createI18n({ + locale: 'zh-CN', + fallbackLocale: 'en-US', + legacy: false, + messages: locales +}) +// Add icon collections to local registry +addCollection(lucideIcons) +addCollection(vscodeIcons) +const pinia = createPinia() + +const app = createApp(App) + +app.use(pinia) +app.use(i18n) +app.mount('#app') diff --git a/src/renderer/shell/stores/tab.ts b/src/renderer/shell/stores/tab.ts new file mode 100644 index 0000000..5e96cc3 --- /dev/null +++ b/src/renderer/shell/stores/tab.ts @@ -0,0 +1,143 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { usePresenter } from '@/composables/usePresenter' +import { TabData } from '@shared/presenter' +import { TAB_EVENTS } from '@/events' + +export const useTabStore = defineStore('tab', () => { + const tabPresenter = usePresenter('tabPresenter') + const tabs = ref([]) + + const currentTabId = ref(null) + + const addTab = async (tab: { name: string; icon: string; viewType: string }) => { + // if (tabs.value.find((t) => t.viewType === tab.viewType)) { + // return + // } + const windowId = window.api.getWindowId() + console.log('windowId', windowId) + const viewId = await tabPresenter.createTab(windowId ?? 1, `local://${tab.viewType}`) + console.log('viewId', viewId) + + let position = 0 + for (const tab of tabs.value) { + if (tab.position > position) { + position = tab.position + } + } + + const newTab: TabData = { + id: viewId ?? 0, + title: tab.name, + icon: tab.icon, + isActive: true, + position: position + 1, + closable: true, + url: `local://${tab.viewType}` + } + tabs.value.push(newTab) + setCurrentTabId(newTab.id) + return newTab + } + + const removeTab = async (id: number) => { + await tabPresenter.closeTab(tabs.value.find((tab) => tab.id === id)?.id ?? 0) + tabs.value = tabs.value.filter((tab) => tab.id !== id) + } + + const setCurrentTabId = async (id: number) => { + await tabPresenter.switchTab(tabs.value.find((tab) => tab.id === id)?.id ?? 0) + currentTabId.value = id + } + + const reorderTabs = async (newTabIds: number[]) => { + const windowId = window.api.getWindowId() + if (!windowId) return false + + try { + const success = await tabPresenter.reorderTabs(windowId, newTabIds) + if (success) { + const reorderedTabs = newTabIds + .map((id) => tabs.value.find((tab) => tab.id === id)) + .filter(Boolean) as TabData[] + + // Validate that all tabs were found + if (reorderedTabs.length !== newTabIds.length) { + console.warn('Some tab IDs were not found during reorder operation') + return false + } + + tabs.value.splice(0, tabs.value.length, ...reorderedTabs) + } + return success + } catch (error) { + console.error('Failed to reorder tabs:', error) + return false + } + } + + const updateWindowTabs = (windowId: number, tabsData: TabData[]) => { + console.log('updateWindowTabs', windowId, tabsData) + tabs.value = tabsData + for (const tab of tabsData) { + if (tab.isActive) { + currentTabId.value = tab.id + } + } + } + + const init = async () => { + const windowId = window.api.getWindowId() + const tabsData = await tabPresenter.getWindowTabsData(windowId ?? 1) + window.electron.ipcRenderer.on('update-window-tabs', (_, windowId, tabsData: TabData[]) => { + // console.log('update-window-tabs', windowId, tabsData) + updateWindowTabs(windowId, tabsData) + }) + + // Listen for title update events + window.electron.ipcRenderer.on( + TAB_EVENTS.TITLE_UPDATED, + (_, data: { tabId: number; title: string; windowId: number }) => { + const tab = tabs.value.find((t) => t.id === data.tabId) + if (tab) { + tab.title = data.title + } + } + ) + + // console.log('tabsData', tabsData) + if (tabsData.length <= 0) { + // await addTab({ + // name: 'New Tab', + // icon: 'lucide:plus', + // viewType: 'chat' + // }) + } else { + tabs.value = tabsData + for (const tab of tabsData) { + if (tab.isActive) { + currentTabId.value = tab.id + } + } + } + } + + init() + + // addTab({ + // name: 'New Tab', + // icon: 'lucide:plus', + // viewType: 'chat' + // }) + + // setCurrentTabId(tabs.value[0].id) + + return { + tabs, + currentTabId, + addTab, + removeTab, + setCurrentTabId, + reorderTabs + } +}) diff --git a/src/renderer/splash/env.d.ts b/src/renderer/splash/env.d.ts new file mode 100644 index 0000000..beefc6c --- /dev/null +++ b/src/renderer/splash/env.d.ts @@ -0,0 +1,8 @@ +/// +declare module '*.vue' { + import type { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + const component: DefineComponent<{}, {}, any> + export default component +} +export {} diff --git a/src/renderer/splash/index.html b/src/renderer/splash/index.html new file mode 100644 index 0000000..bb906a0 --- /dev/null +++ b/src/renderer/splash/index.html @@ -0,0 +1,29 @@ + + + + + + + + Splash + + + +
+ + + diff --git a/src/renderer/splash/loading.vue b/src/renderer/splash/loading.vue new file mode 100644 index 0000000..c599f4f --- /dev/null +++ b/src/renderer/splash/loading.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/src/renderer/splash/main.ts b/src/renderer/splash/main.ts new file mode 100644 index 0000000..596aa38 --- /dev/null +++ b/src/renderer/splash/main.ts @@ -0,0 +1,6 @@ +import '../src/assets/main.css' +import { createApp } from 'vue' +import Loading from './loading.vue' + +const app = createApp(Loading) +app.mount('#app') diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue new file mode 100644 index 0000000..6347422 --- /dev/null +++ b/src/renderer/src/App.vue @@ -0,0 +1,345 @@ + + + diff --git a/src/renderer/src/assets/geist.ttf b/src/renderer/src/assets/geist.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ad6f2c5ac5e8f57598bbc35e02c00f80961d6b7d GIT binary patch literal 148832 zcmce<2Ygh;`ae7~XL~|ANeB><5FoU$+3ao#EtMLIp(6qT0t7?A5Q?bCMMcDN5fxF< zi>RpB5D^hkk&B3mh+ITKzz|RnBQ=Id$^O63oO3pt5X=32?)&b1=FIcVJTvo5d!{|- zEaQx^rg#|G;NCg8y+&?(xtKAf5*{mi4<0n+--(0nVr=Z|jFqMK9x^PeXV8V`8FOxC ztVzEU`)j;_`CPyQjj#zn}jq;12~gfagk#WN-r7%q2ij->Bp%rtX)!MvHuP3$1(Q3ot$dSO|?lz0BU9XNPq zT}le37heA@^?t?zs=)86nKNdU{p%~`CC0K0jJ0@aW@+KfsiueS1N|7#n>TaMed`Sm z17+OfVS#X&lEUVpL}QteFrS}+s1W{Z?IG0Vcu(r}9BZ6%+J^!lVpD4BF9D92R9rFz zF2RObD%=UMl2=exl6`xrO3f#~a%kzncg6*F`;94TnkU*ne+qQ$M32`Nds00?$O~=E zOiYk3erhfj;4NmAmChiqc~hs9$q)+jmlWZ60?UArF#(%w%ph|4GL(>srD7y#`>QS= z%N)E1aPRMVCOm`rk5_e%;rZElY#WQYD%5&Vs<u|!@y}Lmdg6Dp=>-W zMyuZoU;Mxs_|o8}#F++15N}L@6}%a7M?7d@&51`1ZYk?jO{pbt}TXu6Q%5}qmXj$S-P;w`;+ zPl-E#3zVI+SlD=W51#k2SMYq5twYFbXkX5@^Df9OiI3Cr6nrwD40tM^hG#MV2cGxv z6?m@X7~lM9{vw`h`CE9t&ELWEUH(3vTlpt={+oY+=Wbq(=Lvoi&!6}yJkRiIJZpFj z<6^VejOSL=7Z)FikMR6NAg=gSe9E{&)y@^05`gg$q_hT#nqv@5u>OF!5}`x^7EDMg z2C8s%3@dKb4pX8Lco7s?u#vKNP<-wg#oqVKVbeU1v4=fd*mBqnp0C+D*pEDC*a|*G zrV7eO^Vj$IB!Byme-GVJ^y!#S3qCvb*_d5pzZm>w+uif_gzXu;XWrMt_qx96^iBRZ z1^>BgfA;>I{dawP$9L_&vmfevDF5)u!|C4-ITCjy<7mvWpdZ8!#vc;OgUb`jhg4Ko z)Ku6i!YbNSj5;ym#K@D5lg=NT{n+WpZa>+6N;wsJs@bVdr(CB8{v7dhv!7f2obv!Q33 zo^5?D{9KE3t_q1%NX7sW->MZb$pE(TwWxY+Ds^u<;eV=i{Qm~t`oV%jCsCBI8e zE;YRraVhdrvrEyJ+Fa^z$$80DC8|tS)~bN2=2b1LT2;kV#aFed>QvR^H_LC)zqR@; z?zeWoB~~A-uBfi8uCBgT&8scdwrc;}S ztH!I5*Unt4x@Nc*aIM|7gljq1n_O>oy~Fj+*K=#n)Sj)asl8Tvy;js3YAv;KwVi4^ zd%pEt4}5wW-E*&1A4tSgI{GmTMM^CCJj=(!ny$cEnb0tFWE1Roc$l z&f6~9s%^j9uGwmBMw`iIw%KeUwlG^WzX1PA|BL?B0k(kPKtrHkU{KKMpo^gaVW+|> z!)#$;VbPJnQ9Noy%ciYHw61DbV~X_C0UHyYs$$7%$+{`R#lKU&G(voB1Ao7-Re*uSLy;i#U-e z(#34?i1=K5D~^j_#qWwy304Lu)0ElDKa_`+r<9kJx0Mf-&y{bL<4UDcV~8}w8|;RD zhT(<@hEl@yHs{^k+pCFZBhFPhhzKQMo0K5eeDFpIw>(h_fRS~4to zmXVf8mU))PEvqfBTQ*yESbnfnS((+}8flHU+O55;`PQ-4Y1Z4V_gNpczHj~1`i=Fd z^`f;FEf#KzvnATnZM|&ywk5X5ZL4js+cw*F*!H5WPur^en4iC2q+h(B-LIQpAHSh~ zL=K0<0x6JRL-$}o7epmdB{z3lD{oDFG{CoKK^?%F1qDgR*mQ50xBsb~Nq+gS} znmo|ti6#fp7s3N#13Cqy1Y`#c3>X!#F5umO)94+21BV8V3!D}>CvZ{V!+{3_uLW6x z!h%`_bqsO`Wd`L1jR=|$G(G5!pv6H?1-%%wCFs+jz35F^g`(Mu+*?&Ve`VC4SOZ*ov?4ij-!WOZYr8KY1*=BLQ`kcjHdmY-qQ5;ruQ{{ zwCS@=Uv0X*>G7t&HmwO)!UMvag?9<>8r~~BKYT*?^zhrm-wppHe0TVF;T7S(hSx+W z5rGlSBicrcjhGfOJ7QtPgApqto{xAvVspffh_55Qk2n=^DWW#g8W|QD6WK8`B{Dm5 zVC1OCNs*jY*gUX#=jLh6hc_SFd~)-W<_nuIX}+xa%I5Di|9A7N zEtD2OAac5gAtpi#&Yn{?MyY=|i^IJdG`kB^mw0^hshpqRtKG6Dj zd{}&o`1JT&;_rxmDgMLwBW*;Rgf`AL1#ODjJl*EYHV4`qZ&TT(rfrwDL)t#lc4OPK z?XugAYqzZ3#&-MLRVLUHIwTB9xH(~B!it3F6ZRz>P53e4*Myq(O8bEJvF&eezqI`u z?XPy|(4j|%+d6FM@Kr}s$Nn8BcHGdhrc*|zl1}e;4(y!Od1>cki9v}o5_cr-O+1o# zI*C)fvP*mydza_BeB0%my}8|G?{3ev53moj-(oMcPq)vuFR~8sc+J~Nz0SgByC9Ap0qEiBI%+-I6@q84u_+w zBgZkoG2AiEG1YOKW4_}a$48D|9G+x*^7Q2Uk~b!wbDEub&ikcVH=TbdserXA5-O>i7O-@^owkqxY zv`^B$O8Y78Qo54vpWZG#IXx$RSo*E$^V45QKbn5IYjD?2U59qv-YuzHez#A%H}9U( zeRU60k5N5d>G54prDssj<~gVp4-EVNeTl(GF@9ut&^?Rk?hyC{S zJKXP7zpDPMf0O>L`X}}8-~YD$tNOp%|NZ`-_y4AUO%8ysPUQWPcPa1c0A+yRfY1TW2ecm0aX|8bi~)THj2Q4J=9^9;oYr^- zE8w+;at-FpW;me1f(00MX|TfVhLsv@U{Qtz8f*kSNrO#*`)RO+EmO8=u$7HhMr*Ji zvnw4m*xv_k!sCQR^ABKdzE6Xj)$xOA)SvHE{ke^W@QoVm$83C^2KxhEt-(zIuh8HC zzz=C~AmBwB9K^zSi3SI=a6VFlLjY%Ka456zbPWyzeOnD~3OGcA!?E7KsKF7~5uMiH zNUWc=vS7#Mtxq0RS6A47`7^y-EwFNaQ;XXYtB2<_IGVL!I?t`Jww|x~$6%K=S%YJ- zs~M`naV#2p8rihc(1~X$OFI z5@=^5hA-?(lZWsr*!`7Zouxy*;dPwWg*WEs8)qoeD?)wgZYIiHhQ3fDIhg>iN>OuE zue0GdNpcwjcLu0*+5*s&!e=fhb-&(TsIM2w_d>;9sDKSb%owCr06vNV6|fP=MK2$f zwhSxESuCAl>HzIbgqEPs%)qk@F{p*fXBM7`@GV6hsim2P)Ou-jF>DB+$%ru*Ih2An z1~G}EehQP|qnET4a2e`{XosLj<-r}KrkF1#AL%qrB%L9pZ!PhVsp1H%Yh*brK`{!+&2x4eNCR(xw`m4omf#2&@b> zL49%t>OJdE!>P=fh&KtP{#n*)79eUv8jEFsh++V+GNe!KJsL2LvMIpk!~N^@)jFkm zE|#hNb$-3Uf068~RBO~C)b6*-eE*6c-!YPpSW}P|NsiJ++t$hKXy3XxF&Htvaj5N8 zny!eS&$7{%y86&}Za`l@c3ryJsI8f@1p4@hVGiW&KHO|wh$*wGm%~zTfkoDp}Zqc;XQaCK7cRa|K!WDEBskB7b#+tcpoP{ zyTnn&pjZ@tB}7S3Qk7?vSCtQxZH6dAYeTvr+c3^B(QvC_w&5|ulZIyuFBsl2yodeY zHpAD3LxvOB1D*@$866vaU-Wa)A4Xq_{yiovCMqU6CN8FJj6EhfCOsx6W@gOoF%QJ7 zi+Md}W6b+8AI6$vBVyxXhsBPKeJb|Z*mbdQ$G#i;W9+Fo5od}Ej0=g2ii?ix7}qCm zd|YAcJ@NkWQ{&IXd)mCy=Dju_w)v#Z?lxbyO>eiruGoX^5%v}@KUCwnDsXPhP}6pd z*46AyoM9&NG@i-(@xijhU-6S7(p%!4;-JD56Hcf?l(ve?TViHtj@@ooLk{-3)3DoJ zhQ037hBbya4Vw&GWQh+NDh#I$XHnvq=!Gcp*654TH8G(vkufb|Vq}ROF=?{IWibou zl{g$F9uj+V>@t-2)!6m1@5G+;mN?j_#FJ2BzxeU8#2eadYO}S?wl-g&#A)s3pu`q? z7)tygN=zr^jPFLN8~kb?;{mEy?VGi;*d35;bw2}Ro}X&R)DEicQJc=#^;=*U*9H4R z*G^u?S)KZ;nn3RH9V%8)q`BQH-mF+yG4Xiy@gHD5t!P#eTH#+|tuU116!!QdjFtZc zH}81%@gB#!A2%KMFF#cNP5I~W-&X!!`D^75l;6tOu}a2{zJ0XqQL~yVJ&$%dGX3zQ z`{=};?ZZi{^;Oe&^zrj3u|I#Dzb@v8)nWts?iXS&(V*XUP@DwOPjM)2B}3_>;8bq?zIDP6Q$A350ljUG_)t?Q+j6af% zfpnQFXPw(IBP!l^Mll~VYL~IRcSNU9BWw@vk0sU z20@w+zzSiY*abOGE52JXSKPsdqqo0}GqAVVyKFt%$TqPLa98ke_9<4&Kd|rF5q6Y) z!hT~{Sq=N0dDs>4IjhCJK~o-q`;B&7@b*|KZ-5Ma19v3v;P&Z#tdO^|Q1&5K)>~Lp z_A%DN+gL01Icv#2gN*rtMX?>M9s7p0VP8WM{|EAYAEfv}tbD)2O-2RQ)a5LN9mjle z7!v6OW}#E82Rp+uSS9A*vzQ-##q4wr^W{a{dsJaAxy1UhYOFY}V=lSIhC-Z=;zo8e zH?z^)#BSl(9dUm)j{C8(*wswtA*_%Gvq?ONmGWpd1G|H1Je=KzyO|Om#b)vrYz~iO zck%>w7yAeA%o56 z{3HGa{|5IU$N5vd0yiNi_%r+@zm;#qJ;}R#75|Yxi?h(@_$mG`ewwf5KlA5t^Rb5i z!e8K({3U)C_a^7~%ltflg`F-;z#iVPF7EdQ{uGvNtELZ`=mG|J`-PyJ>moLz9KO1D2iFm zxJJdQaN$vySS;=r4~d6y|M#F+CLR?_!~-~`{ij$i9%J`%JA?tv@XSQAI*cIsi&RqJ zO)K{LrUSTdg7LE2$85&fCUbuy<}h1~TPI)h0M;dBGBGMM8<>AFGOtNYb9 zV0v$0d`s5_^+NUVsw~+n$nRdHxK+k@)xaV74>e|R{k0AwzRs)@&ET^Ma;7n}2=d6+ ztYt4rlLLv|z=$U7LCyRdvg_Y$DNHl@yjzQ<#@&S6>KRCdb&!1Pur5^1SCHPic>_Cu z(UAD`{IpS%gq@*o(%2{;^BrUvnGxXPB}muC%sS+IqoMwiAZzvz>WXS+y6798^P5or=3_oLXO?4Kyra@57w zxRXIGidI;AcV-LO3K0tbr-co2-i4XsE6i1I%i4I5y(9t=ww*nU*=!S=B!mdTjFZHY zL;z-?myrJt$m0d1hk61`eWnw83^&F7FiXFTox*RJH)y6lhq@wzJGK1JHm=lLX3nmw)ubkyAnBR;dR?bd6SU1f6$s!zXHyPdya&SIY3mY&~lc7~Y z3+!?hU@mTh^*}M^{#{tNoD?lZORRZ@$r|y)ym^Z-3!^aNY+$H}7R|9gnu*oMNbIrZ zY24IMO|cptAX;E0@IB^T56n@FfcC;9M@bp>cuhh59_-gxS?%V6{V3NY5tvg;u)es0 z`F|T$3?F0mKaQ0^1^n`1e#d&DmaSn&L>$%$FJcYwDXkST`~HM{PQg6|_a`&nb{Tv0N%qyV?Ag`qwddJC*RUNgu_dptg&WwDo7gLx*pr*t!VlTKAF?eU zv8O*~i$7rx?qZ8SXY0OX>vyxK_OY)IuzS8^>khI{zh_SzVOx%}EkCdaeq>8(FgV#t z0}n)Il97AbE$;eIzZC$b+ZX8a2`y)fjZ>1+GFQSxTnH)p>!Z~geU)84-Oo$>FE zfA_Xc%{H~#JmLMk59fVk`zY$;n?AmC+u&_?eUkaf%1TaE zo3Sf)*T`LCcTLb=`y%!^ z_6P6pw!g>ze*0&B8~SaRZ`}tX55ym6d!WMs$APW~h8`FR+1lp2z6Y-zj5wHZu*1Rp zgCh@)ITU`V>!Epvorm)ekN7^|`(fXYKGNby){(p;1CA6PH5`pQnsYS&=$K=jei-t@ ztv?i(E9HiA|MK|qPUWfPIpt%p4XZic{CKP5ZI2H+KJ@tT{ zuVU^A{}WLs+MZ~4qVoyYiEbylpXhmF+KG~r)hEMGHix>yfFFZ@4E-_kN5_xOAAA0I z$4`BKntRG|D(zIaQ{7KxoXR|P)2X6UvrczEopCzrbibdk|7`v_FCjLyf zGh=>n|1zYqw$cx(4+)j-%5Ih8evSRr{p*Oc>E|w=YjVzUF86%S`9T+QFATpJb+PLu z+oh;Wsh7H4>VGM(D!i&sRlllnzZrjv`pxxQKdFWgUfu2T`O8yln%A_b=~k0hGpJ_R z@6CTt{k`{&d<#HVhU01?rXW$`FOgpuK5xu-eHQf|41i~VrF z4FA)(#*g#Q3aSnpOC56?Mk)O=U`U%OL9Ak7e`%j(p#oOf4cb@O81XUd+^F4xFx7^^ z4HGv5|2y_`q&E<>TjBnZJ&mx>LGw8aCKtP(G0Gs-+HC$i2kfk918|_BWB{2UR?AvUB z_`CKQ7N)$-+BUReEDIDFo-2*o7}f$S2(QI@rL(LPs)M!lEnmkv|CJRR5Wg`?eT@2y z-WSluXq)S3qmi;sQAXlh@(6zPd?+nLFBWFF8THT(cXEHzDo=uT2pfz!r&FVL2>e$_ znogoS+@!y6`Gc&R$Oru&*_jB-Mc%p0CUQL&us#_GJ6N@ZdoF8!9B>ujFt|tfSvHUx zaJ=PXF(w9!fu0NGVkTE@4=-#&f2DjHw^Xjbu*yfEp?XqnCw>RgR&7_fF04R1HER2V zZV5|)b@6{nm*5o96KVZji@QH1`9>@HvM!<-3vbl6VQIq0V$DW%GC zn-=?wG16v8HxqEE?1$91^nQcBfOhPUc08}TxcfZI`cqrVHb9+FS!J1sf8vw!e+_+U z2J9l(cTtazvIPEuCX*k>_#;`1e97|P;m&4#lz8Uwjw6gGD_ z^r;BcQFnyv7V#*qcmec#;AY9NP9htwE;|x^`r<#3AN9>g5Izd+83j8YFwy6zxVVpX zQ!c?ShaZg{mul7V?zt@c1LT4dfbapZQ<3*t*zHJ{#%h0zhY5g3B8<|&7?E}={8j?T zSf{=y%SPqMKs(Y{(fbq1s`o=Vr+<(7NkSJ)1)q^q0`n<^&Auqc8dOA;`)!=PcD z!1LhtgHFOIz@@OAfRAO-68@WX(56jQV$lDf&!FTZpCfQ_Vyv`;je{KuTY!F#z4HG& z?<S1N+&T0?e!qs&Twx+{17>Q1NnV$ z?_+_S#@Eeo$0LmFG|euC3oFd3<@W>kwFSUoCC|j`Or!^ka|mfgQ^ae=+VTDfBRd-Y znA^Dr{SNYo>V)b>uOq!rp)XjJrRc-OXtSYk+prXbbryH1R)LJ;k0W24MDqc#7T~a= zk7^Bz;ErU2l)<1M$((X60KOncQ^a?$Pr-hHv>|5|BWNcAC%KdeKMT^UhTFo&;yx7X zr36^jjRNeaTIk;=Kt?4fUE$)cMLdAn$F72 z=>KliK|9YCC51I@h@%JNZGaoBqY=Nx=ybn3K~HD637REbhb0ZhbtkMrJ7NDWE!H4) zeQ*ZgAlS!XC&5BqV(#z6>Arq5>>iw$TtRy8u`bkqplvri0reZZi+7&d2MUQP_l$R9KSFkVYS=+ONB zcru3jM?7gWg%bb+GvYMJ59e!i2NlSIFf!?6D2z45$y)>{o8g=c?~pVSi|%~>`&ich zQ!LB>DVF&!W63K%lnrmr)T@!dDp`wyg5olM=fr|pg?!G$iPLBDX_JbJrtnFVN@h&w zV<(puOyoo1QNRb{Ev#bRM?SOUvzvUn<VLVgVUOZFTPCV1t)>*SB%wn5n&7L`ntuI4rIDe#a z(S0yStI{(V?ak=~2-1@vXe8i9iXil5^iYD@0Sc5*TR;I4Y6GZ=gyI4DOQ@S~I`HaO=(fSeV>eUc+9%>EKIjEzWme;RE>~=r7#F^Z5`y6uSMx`3OFekK&{G z&3p{Mg^%Uq`0LQ~e-krP)fwR;rxF>DR+w(3wm1m=L z%lSIo!Qt)=chGbLJps4WlW+q)SxgZnVjk`a{gilx-tDR@e^b;o$IaMj>8JV&w6O_q zFg(Fl;EZ!6&g`kC;&G1K4t=OS`j(!8K>hjS=HMZml@q5pO2oAWlNhIcV8B2{{e>s=)H7T(A5L*JvmK=w|+ z6TnF-WTk<{Ksp9t=dgwe-0Q!9y}%tXFG+W|coB*Sgx?{1bpW%7TR^uPYgD4!jlF;x zz8mQp_(Z(N76{5Pq-^06z`-wAk>EaykH_;XqG4b0K*apYho5MK(HnXcO7EWGoijkM zY7o`hR9L-cz%yF8l@g=G5}hbfnky|8>ftH{=UFI|7h@(*Ia1=H1=cMz3N@8oz?STnt2=o-Gmx}ujU0N{AFf`yQoXm zDd+SF3&Rb9Pi)+CAYXnBcNJ7qYHi`|QuwChhJebh)|(AJ@seYK z+oqH7r5gHPi$iaIsy<2eeDv7ykl=di9P-lalQQ*DoE@SJQ;2c7g{&9d+{^eaugEuQVQd8-urgB&-zL3v{#ziss z9FMtTD9?lB%7A2YLMtwT$Kf5`a2~`3_QRM|K}0i?=p_b=B+(9ce$9d9ib2ATaoYkny8`dNUBYu6e*@1X z?4kv3+(zPgj1PfNcisojZoD_1U3o7&(|InQX*`E1a=se~ECmuxK*DF@>Eao9I(bio zPlD|4fwcaKx)Ye0{)OjC%;kD77O3Cbuu9iwd3~;XN}J2Hc?{#4Mob~*|NC)&`~X`@ zW23=ZKwk;y>wvz{ZR`(?P6y=UC>lo%Rt2x}*YtHkqwUld^_76S?0|a4P8coH<%ab` zx|lDj#BX>9^D@>rziV$|UY9G1NOcrxD~sDuW_@j;&)^-jSy`KrA)`nV^v9}WBpby> zV;;t+Z?FQ<*B|=o;}_g`{)(Bo3KIPaNr(n(kw^K9-qlD8w12c{C1OM@-gIm&;zb+u zgCz8lWb_~xdRGd1UfK<-mSXhMAE2po9J)Fupsn*G^md@VBiAtz>KK;m8JctC&$9|^ zjYfXhGtxWmpL4+#tfD5cH|Sm#`WJ6v#nOhg#eKU6I$I`g#u5)OE6+zqVR;lqf4RUQd*4NW9=5nEVP>aDf1A1mNa6dhhbLbKE zf&SZVSY6M;+1PDT_iQ$wgITp7`yCo;^YC`;e0~SN6X()*@dYdodMIVkC7H|bhOXB_ z=&wOfjSWyW)o>SaKTg61L04@lZahxl{^BO7>-GqA!~V(gaWj4gR&(>A$@VBl?_H3R zLonA3h28;a#XSz~#3!I3a1B~+Phwn;fOgtb*daWF5wHL!5DTHPayPWno`Xi(YW_Tp z3h1M~fIj^aM#mW3_^)MKppo_pGyq?Ph5%N(+InBtnR^G?ZSP9WIns;MwdX#9{@gbH z3Ez%4{&(CviP=Z#Rf{pr!k+)KuIIjsGpsL);2Y-H*h_VjE`ZPsDcdZ?OaG z!9eT`qp?CZU>6#P`RCu5kD5T=!-UmGPv{+ghIa)%hraQb*mrydea5e$*ZU1_G=k*X zAWQs5o&b$yIOAm*&?^2`91!1$gV29GEWU^C?@@6~m9$uKd!lAFJKv&ZUtxYrDk+8~HfplX7ltAdm217eGRBFkFLpL^3X~y1HqA+K)pdFDm zL(={m`(eCk&<=XFq-om`TDF~)M5T*jr}#3V>f;X`f0~lxzbJPuJllPLccxJlsTcaa899lc1e*vDLcoUIlW+F>5LL{=8P#b zN(yiF%br+NI&t>&$;E~9Y}u1$lod>zSXfeK%9&UIDmbMx3d&5m60_xc!_B!`GP!Cp zlzdWdR<@c=YA;i78N;u4UFvF(!(r=Pmx_$#u&Y72PD7sw1*Lv{>f(sL{mgx}ocgLc znfjI$6;CSkqw?6Dnf7FRl3%~N7+O-Ps+;UKo=r9_KqZ(eNt{(u3;!jWE$!d z&Fzp(IFd3=!(_`e8CEaHVOnvAsl_!8D=jLSVj3n3Z5p9wIHE2?>xfB3g{6hFie{Nc zOermxQ|LFUF3>bedQpZ+xxG?Nqh)m4=z0y1nUb0nNVS=ioRvuDggDt%ai>eiP5Fs9 z^K<&C)u6$$P!jf+(OqtdnEI0A^6M)zlW2dHYjXrz@U1m1q`c?dfQjcdQ`p8z`CZC!J+l*Rr-ZBs9e>vLGh)h zC39TgL^@fv4ATHpfv@n$H5X`|vp{VIzlnA2YMbb7L(?SLHB5!lu@!m)%!OL~LN&hH zF|%^i5#dTRO_4GDrqm@XgOVJ{wkdU~$nNY&QiF0`hN&bqrq;!=7Ri2#I|y@;mSK^a zp{YoA8BsJ%%`!Pfl_PFdj!dgdLrXkOb(2#}#j-n_iY14dZ?>sK^)2!7b-I;8$dMWK zM)eGp4-<~+Ng9><(7RGhv!t)ztOnUTlF|%iBtOc0$lb{*>q%Lr*{b}QT`#BET8U?? zB{t5M@?*9tKjx|#&8^GGI#=&drn##8xV-lhl!!oht)^3hEe;9svb9i&{jla7gmkI;0Hn*LtNyYz0(q zErM50B&m!zd>8@jWyImjh@&ARj{1xsqL&eeFC#$Ix~JS7b&Mc_ml21lm_n2N)NZB0 zvY92UGLoFF11$f7g z4E5Irk3`Hx)6|%%6ELHG%-Qu(Z|738xxHR!U|m0w!J+lrxe-4px>GHa$4yS8>n=;# zxH1XI5#dZy*AdPnRWdk})S1?qq>dP;U31ekx->o9sl|7xu3Zba>*=_)@D$Be7Z=VX zRX8~9s{C>$Woq%&Rgg0&OQY9uoi11N@1?rh`p2nBG^a!3%c1kF(K|Fg9U7kwjW37B zmqX*rq4TAuujTL1_;Kib>U27;mtW6EdS(z}`#o~Gf+TG5lWs&;AV zxb$?iA|`89ovallS!*hn)+EVV6}YsfP1c$sS;uucttykXrg3WdIko(p8sAQxZ;jrm z@#)m~bZUG#HNKo0UrwDbJ$)^Ir^b&{=ToQCalQO{J{rF+EgzSbk4wwPrKhL!r{(9; z_}3;yr(2^-(aMve<&&nRm!^ehYT=n$I$2tMWNH3c8hw^VpRLj5XywS&=yG+um)dQT zWrtGdGpCBEbBfdD+q=}2a;h4~C3{yYHY(F6Oe&E5D>*X_>kS=sB&j3M;p`>ZaHOQj zYJw}-ge$8Fu4D(UtY)~9eYmn&;L2)-E347r%vI@}ZrLpyPPeQExUyQ|%3{Km%>-9g zCtNjur(4b6=}xuIFD#uw10>CYU8Mxf(yMH;i9Fzx&7A?qQZ}`;kZ|+l8M8~JTQo<8 z&nlWn;j^$=Eg_(=2wwpuf|4ScfSLuVJjg60KxqGLbo~8C|yC!_?>d0`|RpAX+OHb>HcC9PgwXSGayQ1BmX^)+H z`^>3@CFANlQ%Va9$_h*6^d0rbL7`fZFF6%F*=|n`pIKO1G-DDSS6~M`Nv<`ar{t%4 zOKEMJIHPz*i39^?6;3azLz@&97Z=UMwpj-H70#Q80Vm-=6`nL>t{Sn)ZLl`oU@ehUc>XAi;>G+)Jjt9|_9Qu;Q3tZ|;L66rNr-GbxUw_A zU#FAf9=IIuaHU)To$MTNWn;o$$_2Pm&cI)G7PzW7%uVhUR9rY~me!QA?IL_UeEp+u z^!E*p^bKyD@NoCaG zR154#&ejB+JSLEUdUv@Y(jXR1=rp{;Z5SuS%yY3Yo)vuCP_r=?&G z54@BFlIE3am0r{dG@mrp2R!REZq;YPjIyaJG<)VGomv&dNx3RZNxAB1O3GA6VUosD zlDY(PB&qWgTy<0@WjO<|S12tin1Y^PHoH_yHaEpzqOxXO*}Y7(p(~aR>2PIdo6CrCt#(~l)k$2d z8kf4NbGWpky40x{xLRedY;~$m)>fg(_7tn!hE1F?eS%ePFA1WoaWt4ExA)2R>_lkL zX@2UG0e%`T`#5Mbt#Wtf6I(WpJz3jFBs<)4TMJj;)~YFJ>+fW3{hh2W?vj(Wl(ofN zaIr}Fkr}!eNK8Rg5jTT2;f~Gd9w}A@q?!&3N)uoa{JA-v-0^E_E8ZrT! z%v-Z%8%!F4&yU|)NcWO-|7>F!xYaU{&L?X-@%GYU7KOohD~po)pKWo*j~_G^iB^Xd zz#O-n%B_W^B`k4zK`H2`-#YzP=;}}i=`fIvhpz8w21Z(U&{x6Juj95D|FFryGXoOE zy|{ZGgIn8P(Apq92Z{@ghF0aIMbqjz=sXDcn~>6No}gB{qD=@Dbb^js0LlrP3{Ace zZJ{8z9<(Ohh|kBpaSz;=1|TMB7b>_#dW4snqLId%faaOPn6Pf55Pm^C$86)$K^eXf z+*wyk-`_+CUogIsd?o(1*a@zp5Z?$Yg}n|OdRSBvipAcer;RiQ6=kv5iMvAJeol$s z2K-*&UT%X**l?&lP6f9L?p32@J>i25;FB~BDOJ?kLZlFACtm|;8L7Tp`i_^r_};qa z%is$g61p9M2A4OVkQ&^g^N>7QgE5HXtqt5KPYPL%7;gfa7P15(GJo9AJ;7{2*AQZs zX$rbutxGXF54SHonzV3D&_!H~+natU0oAjtCqZ1QoGOTq^fg?tY~Z39=_D!84_B!K zYbJg7k}vl;PBdd+LT3NyhMHSeW*nW&;xQe0)R*H_k1i#+Ljo8}8m7|@c|0o9Op zYb<-R>^*)DS@BhLTPgJO_E{I+VCGooSc|O_@HOE&)5ofsRO^iE|^W_(t-L#%<~3HF5Zs za-9jVNMki1cY|uoS8g!p&0d3=%9rI&Anw6u22zf+1jsn&%|A72j+pnEclnr)%w#q; zV%C{go0l~(3(aKaG-4*3hifL!T;F7wQ+nN~Af@ zy3jNbU(fDgN;Jir!c1Y-RFlobjOV2(HuX9X#E1#n8bJBg-`0ywDJY zKSDTzQtdRXAzJf7Q;BleumSNOp%Nl(w6|r8@`bWRS#OC{)2c=zxv_;4k8DbS7rs&c5S}RYGAvCl8Lx!|REv7Q6wSjV`Bl-J;40#42 zH2)}kh&QC047uRNTM*9f1!WFVvIhb6lW~f@G%pejFZ1Ff35T}W&S3gtk)nhH!uyq= zDfQCaE#rKNkZv-hgEu5gh9n|{w5t?+hlA#4;`(_RVnGPrEkMX*Z^$MYGT4j9OMJYJ zb2(%2UWnO4#@q`?mWOBPH0&uEXE{RBCCy#lkPl=?A1{7f;@Mt&tHjHw-bX$TKARBE zuvTblV8o~Fog?XcCC^Dpv=u0)0befhFtloik<|W8WC*qYV=|--LhK_m5b`u2_eeJ& z^qb&rBk_%1-U3k8$T;M+Q^uqI-$%wv!1wUfI$I)fPT?bJ5WbCYwnO5lflrptVas-#dE>?h8+VWN70B?Nb571maK;n z4*?I7dCfMbcT-oVuaP_CAgpP}w)RiW?@#MwBa51>{eDENz^_+m5l z>)`bxoYF5Oc+-dufQZ|zBVqu31!(&S>i^*)B(%>6tiSkltj@A1J@ze9-Xl}a19VJ6 zk$|cs6a(mrgt~~GApj8fARtu=Vbp~9OTSToC|KcPfZ`-H8j#u>9p3(Mej(A^BpFiID_@Wy z$Gnt>2#3~T69z598;t>a>GjQMnI6RsqY&sWHfD~YSKMeeA8@ReB%L5|t@1}PHWMvy!0175 zp>}Psc&`U7fF9+=X>SXqHZPt;I48-d(kDy&oEPs&xY8N>cpJ1|1@BKdH0X5tV2MA< zugJZ#U^9RVHk^FZD=>k>0Xu${-iZphc~l zp?D9ZzRAHmZK|1s5GS<7$wWzmk07cBn&vked|yp9+yFVJn=8m~C}iFiq(;yc9tAyf zdLBlttwKw#<+BIGgKMl{xYgv(PXfYM>r8R{Fj}w(rxI!l3bc>{4f!$n-T}^Qcr4*; zF@M7Eb+m>8P5C&jALSuLuiH_w9>cp=sR=t-M;Jirkn?zA<+A-Kob4~irxz$ZTnGD&;4%9 zj!$FGdl_>cX>^~En%n0g0L1n^E*#J6li&$toj0pZDT6i9hVc@0WNpFFsb{`Ch!8 z#1p)@Q{tn%_-Khw@ZxPH-rkFsNcqf0`!yWSLXvp*dc%iG z{6R17CLHe`Xz6j1eK;%8adwe#{+t(IDe*78_&kY|lverqQQ{UI=P?qWrsM1$i7)Wt zw@EzLi-$@aEu*EkLE^o<__q?@>&0UwzTAs{Eb(H`G%db~aJJYJgfHnQ5(Kmg)CyJt zdL9qUI5Y#PWmro%{`C`bU%7%laaM+hdBb=fxk8xK&b)M;=yh$Vm!e^AWOL=~b_Eb7Y*3Udl%# zeq6`7RpPB>nsi>#S`T6Wlp&kE_|1f~mxxkS5abORASv6)kX=Z_%Q2VZg)Q>d)qF`= z*FG|LxerzArk4z_<6)V^n|b4ZL^$4O)a!DR3=cso*>Vr#c}e04-tdP9zhU9wjTM5TI(0bcc$ne{|_$rB)dE2r`;rx3g?rt^_0DGR;$ z2#L?|;^QPC*EAh7Gw@BkVm}js*`~hj5 zfLSBYt6so))fwy~E3s#M2|KH5oCe@uow47c6Rh>}1nX^ig0(@OV7((xur|sQtaq6K z<#%W{4sHlqbSqr7MM4%*3%{n}s56Pz^|UnL&;#)P|FeZDe&cQU`XLh*`u;n!wXizn zm!Ocu|MKUlY&^9;@^6gVgOTr9pK;bV%(K47n}mCI^jzudFa5MU@%@k+e!ls7Dl$nD zFrouYy{_KRQ<>?eyy@qu&a}f)PnN&N($>Rtw|-t3kCVTzpTu`$mg@ArF7c+*`sU@? zGoVby+fzsF*)pKavm)cpI-feovo>Q6(%K^9T*z2d7xLdho`ad!{+k%6D@m`Hp|QB2 zA=y~(=cydrPqv3v?~L^vOa#*JLYAmU{J%z=JvQn|>h-MS4PCuo=>DEn88da5T+a>_ zL%*%(?@RMXu%0%)RCB}6Q_;YS#{d7|=h@q1Rb$C`w)R-j&|l}Tp?^c1`ej4E8*p1s zTC4tl^7GVK6aTFL{Z*!(Q$4rqG@i=F%Yttudw-4fXK&#D0q|631z_Br|D&jXRyJRm z(J;QJdT>A8@6XcEY5ptM<|pN-p6(y@70-oCIo7C+{(kyYA68n+*86#`!oNBzO84}v z>bYOa3(Ny(lMvni@3@`{Ynqn>aRQKHEg)!H8Dl#%) zA16Og1>`&AzNa!HonS8=WWMKghOCc2YHweEz0URcf8}~AdMx`h8d>juG{0A8?ESO2 ze-!39H89*$;nP1aWQKSudv@~DbA11#@$Xke`Fs&#GQI;r-+-WR?v>(Od-LS?9Tv(j zHav~*G|*QX*5NA*pZ@W86Y%e7`|*tge4mKjv=FN&F4v-*R#t+wm0y-J;5!=E5ON(W z+7NtesTTh@?7{b9n+lMyAbfWT-$TT=mw*bo)sgpE_$FHvefx;G#k~N&V1Yft1Tg{d zM10%Th;Q{3G7G-fH5BRp-ILsvBqO&}BuPDEJ;xI{q~e-||46P511{8Ut7H?8vo~%WPSj;CArraj$`!&Jx_% z9f^Bz9v>i%GMk&$=N$cmt^K82lolxkcaw>IWxL~f-fCS!nZ}9!M{jQ>Xq`j zgPz#aQN9Q9#7M{UKDr4LM?oW?v#RuZJtKqG>`AAo1YM`Pi&;9pr-+}Y(mh$qyo>Jf z;AFI?+C5a_+8P+)QZC~wBJu~kkLO^{D$hZ8mW)qpV3L=6+}#k@?WuL!J;&S~0LRPl zN_Vt}(*B!2U%4aWYkc@dCmwupNe7;8@FU*za*%6qM|w`VL+bhYM51+*2Px!q^RpmMDI_neF3wn;aPJ0mmzPzH&P)Bv4zA*X9x&;pnf5cP6 zpXY+B;>P$5sp@c<_72w(&q>z--B-5T1(#RedVY3&!Gvo^!*uKDT_5RiLzmL}tDkRO z=(Ha2q{F%^`%8dpvyL;HYXi#rI_iF{?uRy5O|YlNJs$o~(|Q4I^0+Kh1@+O)ZJtWk zBa#MnPG!=0^i;W)=>Ft3_VcX+J)TZSd?0^sUC6Y-gC0&U@?NB`VL*GQm)~mF9OOS! zqnUsYc zqTPtLVfkgA;GyA9*27P(UbMDFTTJqtab*yVz8`5k9mo=*-Qh}SOlz;bE~j4qp6Z+x zC}$$lNvWB|atxzyQyAK1bz?NTUt?oU4{PXx z_XD0v=SAJ$Q{$}s)A)hTpNJOiQLi8Q?lCCt!p&abh4azi|gha3DG zc;GB%o9C?aupF~mzd@gobx5Qr_kPa>=N`{_S1*aLaPIP~b#6z0tdlicoLi}EGA)#W zWQ(Ll`Ot=&^t3&{I@gmw{e0Whvqhzs{(HbrV=}(NZIa=j^?(P0eSeZCod*1R!)&fA zUf6(gUURPV;tc>PBPubTWV;ZL&KJ?nG;hnfn#j=?q|9U{=W`9B|4&fjp4`FhQ%5*Aj9OF9O-*NwnkH3?~e?!x7Ofp zHeNk6wl8S19pxe!AT7<;>s{%(Od#2UHD)x%-+VcbAH;g{82lhTw6x28d646l(ynwa zZs=bR_mLYYlics?dDf!?U;59>FU@bY&JxH1xkkktFcrK^fb1MYw4N&Ga6OL1D_oEG z%75v15OVUMb1>3~lsJu7$P<#A5~s9%=5F%)qaW3eZ3Hn@2GF%OGYrFy)-a(&ZiNQ?TtSNGG)qvN{ksp_@BD>ttt@6^|Y6z-``#vG9B z-La!>H+m|Q-}KE>(pM(G;!O`_^jt|^lu$Tt}^(ohm5OEUZ(m{d2n`c&>17k zNAgcQ*Fc$&ZwLX|3{+5gH#l-$YA1=(z3@PhMDC+sL!h^5g}w zji}Da(p;sNt%01R^&+LA%MwXXaj31YX!9iIX^iK2GEG`zHsDX^p)t21Z9`nVAH*WbQ;1ji5L0pnLNe(p2uQEvq5;60R$`2D&gAUn{u;)CM^ z!usfP84!SVH^il|+|S8Y@m||rf86=_?|&Ek?WY%voBQm~!(9B_-~T7JSE5BaGV79G zVtbvNZt(9fD-X%);P7AhS#@*-^OVxTxZCt)@zHKsXY_}g{s80oVL6uJx!m7w{O^zY z9Vg|~QN#FO{v%=iasGktU|#$K(f*}DkCvz4c0u93C@jreNbjX_fimk47p%+7efKXd z)KQ2-Mix*1){)2gB)$0+8F>16N9IwYLFQu~5TtwY81S~UjOp+^z(MwfKly1)zjBmz z7c>v>hY#p%bMp}tmh}8U_<0_>bh z#dc+Xn!$EuKaTSdWLFNx2S0x6A!RZ9a&tUXF>ly7268AssUzy`SEWzx((}-WZPi98oJmM_{eA3uE{PzI@g1;Kz=>- z%Ze9lN1?F(e0*8F;P=$8f`5P2XK5QM{UA8}^ed%D*qDOf|FfSd4|8cE53}+#_x)G- zJ^f_q8`F1}`eqAXCC2}IVU9Xx{)@-S|C`*Pe)S*m@Bi`jw|_SeX)gNjPV?WD-hWJ+ zoQEOv=_$d%=F`c7^_VYVUlk8J%n@r`{JU*J5cBHXoIA~py+&Rg7t8^(9zZ!31L%aa$+5+`Ga`^91}&X&3o zr`}zKzl*S^{d>S&i~a21;#-O@#h&(iaOUEDe753maM$pi*tPyAaTo3reiyeA{|&bV z{|2`Oe~NwVpQ}Rr9{W~8#VfR%N4$o;ORL1|v`0z2MLT%J+t|UgO}s-plEnM83rYMP z`*ltdAJ85h@d@_m>=&P^m#LSD!`NMOKupo@9PARpo+IoMdIYS>Eq`N!YjA(+iJ#y^K(5zI?q5=UIbK>D0&t( z{Je;tWgc$iXE#A*J;Mh96;4I)8OW+~5E#MdJ*r9}o>k>Dh946YGPj@ea|pvSh7}B( z2r9p1_-n>2W>~^7j$sZ%BSGbLevV*BX$eI>w=;%M5)fQQ<@=1uBB;E<@M%Dygfe~! zV?N?%I&nf3RScUMPux>Q1H%G_l0RmCdo!R=7Bb$#&jeNFK8Eiw+)q&C0tzLXp^W

rj%R3M7{mBa35p_qzLfDAKi|*z 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

NsIMl5YXN4#T)YYb20 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 literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/images/fastgpt.png b/src/renderer/src/assets/images/fastgpt.png new file mode 100644 index 0000000000000000000000000000000000000000..0d59a77437ce18925357124d8bc415770304fb79 GIT binary patch literal 1828 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 W3+bC{saVti0000 + + + + + + + + + + \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/adobe-color.svg b/src/renderer/src/assets/llm-icons/adobe-color.svg new file mode 100644 index 0000000..279492a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/adobe-color.svg @@ -0,0 +1 @@ +Adobe \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/adobe-text.svg b/src/renderer/src/assets/llm-icons/adobe-text.svg new file mode 100644 index 0000000..b92aa18 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/adobe-text.svg @@ -0,0 +1 @@ +Adobe \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/adobe.svg b/src/renderer/src/assets/llm-icons/adobe.svg new file mode 100644 index 0000000..99e9696 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/adobe.svg @@ -0,0 +1 @@ +Adobe \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/adobefirefly-color.svg b/src/renderer/src/assets/llm-icons/adobefirefly-color.svg new file mode 100644 index 0000000..c6f8a66 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/adobefirefly-color.svg @@ -0,0 +1 @@ +AdobeFirefly \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/adobefirefly-text.svg b/src/renderer/src/assets/llm-icons/adobefirefly-text.svg new file mode 100644 index 0000000..aa16eb8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/adobefirefly-text.svg @@ -0,0 +1 @@ +AdobeFirefly \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/adobefirefly.svg b/src/renderer/src/assets/llm-icons/adobefirefly.svg new file mode 100644 index 0000000..39efe8d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/adobefirefly.svg @@ -0,0 +1 @@ +AdobeFirefly \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai21-brand-color.svg b/src/renderer/src/assets/llm-icons/ai21-brand-color.svg new file mode 100644 index 0000000..531879d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai21-brand-color.svg @@ -0,0 +1 @@ +AI21 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai21-brand.svg b/src/renderer/src/assets/llm-icons/ai21-brand.svg new file mode 100644 index 0000000..966b23b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai21-brand.svg @@ -0,0 +1 @@ +AI21 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai21-text.svg b/src/renderer/src/assets/llm-icons/ai21-text.svg new file mode 100644 index 0000000..966b23b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai21-text.svg @@ -0,0 +1 @@ +AI21 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai21.svg b/src/renderer/src/assets/llm-icons/ai21.svg new file mode 100644 index 0000000..e6b8301 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai21.svg @@ -0,0 +1 @@ +AI21 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai360-color.svg b/src/renderer/src/assets/llm-icons/ai360-color.svg new file mode 100644 index 0000000..8bb2066 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai360-color.svg @@ -0,0 +1 @@ +AI360 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai360-text.svg b/src/renderer/src/assets/llm-icons/ai360-text.svg new file mode 100644 index 0000000..8d1112b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai360-text.svg @@ -0,0 +1 @@ +AI360 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ai360.svg b/src/renderer/src/assets/llm-icons/ai360.svg new file mode 100644 index 0000000..9e615b5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ai360.svg @@ -0,0 +1 @@ +AI360 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aihubmix.png b/src/renderer/src/assets/llm-icons/aihubmix.png new file mode 100644 index 0000000000000000000000000000000000000000..a68895cf5ef0371c71755483c27532d996c08079 GIT binary patch literal 5723 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 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aimass-text.svg b/src/renderer/src/assets/llm-icons/aimass-text.svg new file mode 100644 index 0000000..fd870fe --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aimass-text.svg @@ -0,0 +1 @@ +AiMass \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aimass.svg b/src/renderer/src/assets/llm-icons/aimass.svg new file mode 100644 index 0000000..a1b2618 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aimass.svg @@ -0,0 +1 @@ +AiMass \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibaba-brand-color.svg b/src/renderer/src/assets/llm-icons/alibaba-brand-color.svg new file mode 100644 index 0000000..ed57cb7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibaba-brand-color.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibaba-brand.svg b/src/renderer/src/assets/llm-icons/alibaba-brand.svg new file mode 100644 index 0000000..ed7c4c5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibaba-brand.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibaba-color.svg b/src/renderer/src/assets/llm-icons/alibaba-color.svg new file mode 100644 index 0000000..bb458d7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibaba-color.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibaba-text-cn.svg b/src/renderer/src/assets/llm-icons/alibaba-text-cn.svg new file mode 100644 index 0000000..410b098 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibaba-text-cn.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibaba-text.svg b/src/renderer/src/assets/llm-icons/alibaba-text.svg new file mode 100644 index 0000000..17edc3a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibaba-text.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibaba.svg b/src/renderer/src/assets/llm-icons/alibaba.svg new file mode 100644 index 0000000..544fa10 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibaba.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibabacloud-color.svg b/src/renderer/src/assets/llm-icons/alibabacloud-color.svg new file mode 100644 index 0000000..56c2078 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibabacloud-color.svg @@ -0,0 +1 @@ +AlibabaCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibabacloud-text-cn.svg b/src/renderer/src/assets/llm-icons/alibabacloud-text-cn.svg new file mode 100644 index 0000000..b853939 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibabacloud-text-cn.svg @@ -0,0 +1 @@ +AlibabaCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibabacloud-text.svg b/src/renderer/src/assets/llm-icons/alibabacloud-text.svg new file mode 100644 index 0000000..272c6f6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibabacloud-text.svg @@ -0,0 +1 @@ +AlibabaCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/alibabacloud.svg b/src/renderer/src/assets/llm-icons/alibabacloud.svg new file mode 100644 index 0000000..53232f6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/alibabacloud.svg @@ -0,0 +1 @@ +AlibabaCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/antgroup-brand-color.svg b/src/renderer/src/assets/llm-icons/antgroup-brand-color.svg new file mode 100644 index 0000000..ee70765 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/antgroup-brand-color.svg @@ -0,0 +1 @@ +AntGroup \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/antgroup-brand.svg b/src/renderer/src/assets/llm-icons/antgroup-brand.svg new file mode 100644 index 0000000..c98ce62 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/antgroup-brand.svg @@ -0,0 +1 @@ +AntGroup \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/antgroup-color.svg b/src/renderer/src/assets/llm-icons/antgroup-color.svg new file mode 100644 index 0000000..cad1c5d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/antgroup-color.svg @@ -0,0 +1 @@ +AntGroup \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/antgroup-text-cn.svg b/src/renderer/src/assets/llm-icons/antgroup-text-cn.svg new file mode 100644 index 0000000..77203b2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/antgroup-text-cn.svg @@ -0,0 +1 @@ +AntGroup \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/antgroup-text.svg b/src/renderer/src/assets/llm-icons/antgroup-text.svg new file mode 100644 index 0000000..540e918 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/antgroup-text.svg @@ -0,0 +1 @@ +AntGroup \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/antgroup.svg b/src/renderer/src/assets/llm-icons/antgroup.svg new file mode 100644 index 0000000..5d3b0f7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/antgroup.svg @@ -0,0 +1 @@ +AntGroup \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/anthropic-text.svg b/src/renderer/src/assets/llm-icons/anthropic-text.svg new file mode 100644 index 0000000..566a37d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/anthropic-text.svg @@ -0,0 +1 @@ +Anthropic \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/anthropic.svg b/src/renderer/src/assets/llm-icons/anthropic.svg new file mode 100644 index 0000000..5b81844 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/anthropic.svg @@ -0,0 +1 @@ +Anthropic \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/automatic-color.svg b/src/renderer/src/assets/llm-icons/automatic-color.svg new file mode 100644 index 0000000..6536b13 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/automatic-color.svg @@ -0,0 +1 @@ +Automatic \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/automatic-text.svg b/src/renderer/src/assets/llm-icons/automatic-text.svg new file mode 100644 index 0000000..e571a68 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/automatic-text.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/automatic.svg b/src/renderer/src/assets/llm-icons/automatic.svg new file mode 100644 index 0000000..47be0fd --- /dev/null +++ b/src/renderer/src/assets/llm-icons/automatic.svg @@ -0,0 +1 @@ +Automatic \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aws-bedrock.svg b/src/renderer/src/assets/llm-icons/aws-bedrock.svg new file mode 100644 index 0000000..e05aa0f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aws-bedrock.svg @@ -0,0 +1,5 @@ + + + Icon-Architecture/64/Arch_Amazon-Bedrock_64 + + diff --git a/src/renderer/src/assets/llm-icons/aws-brand-color.svg b/src/renderer/src/assets/llm-icons/aws-brand-color.svg new file mode 100644 index 0000000..46cb50e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aws-brand-color.svg @@ -0,0 +1 @@ +AWS \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aws-brand.svg b/src/renderer/src/assets/llm-icons/aws-brand.svg new file mode 100644 index 0000000..e1d01b6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aws-brand.svg @@ -0,0 +1 @@ +AWS \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aws-color.svg b/src/renderer/src/assets/llm-icons/aws-color.svg new file mode 100644 index 0000000..6d3ac02 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aws-color.svg @@ -0,0 +1 @@ +AWS \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aws-text.svg b/src/renderer/src/assets/llm-icons/aws-text.svg new file mode 100644 index 0000000..24cf159 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aws-text.svg @@ -0,0 +1 @@ +AWS \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aws.svg b/src/renderer/src/assets/llm-icons/aws.svg new file mode 100644 index 0000000..9b3acb7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aws.svg @@ -0,0 +1 @@ +AWS \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aya-color.svg b/src/renderer/src/assets/llm-icons/aya-color.svg new file mode 100644 index 0000000..027e839 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aya-color.svg @@ -0,0 +1 @@ +Aya \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aya-text.svg b/src/renderer/src/assets/llm-icons/aya-text.svg new file mode 100644 index 0000000..3afb2b8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aya-text.svg @@ -0,0 +1 @@ +Aya \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/aya.svg b/src/renderer/src/assets/llm-icons/aya.svg new file mode 100644 index 0000000..0201852 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/aya.svg @@ -0,0 +1 @@ +Aya \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/azure-color.svg b/src/renderer/src/assets/llm-icons/azure-color.svg new file mode 100644 index 0000000..4d95a08 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/azure-color.svg @@ -0,0 +1 @@ +Azure \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/azure-text.svg b/src/renderer/src/assets/llm-icons/azure-text.svg new file mode 100644 index 0000000..19a0e6c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/azure-text.svg @@ -0,0 +1 @@ +Azure \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/azure.svg b/src/renderer/src/assets/llm-icons/azure.svg new file mode 100644 index 0000000..fe090f1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/azure.svg @@ -0,0 +1 @@ +Azure \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/azureai-color.svg b/src/renderer/src/assets/llm-icons/azureai-color.svg new file mode 100644 index 0000000..c4ed5c4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/azureai-color.svg @@ -0,0 +1 @@ +AzureAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/azureai-text.svg b/src/renderer/src/assets/llm-icons/azureai-text.svg new file mode 100644 index 0000000..b03dc26 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/azureai-text.svg @@ -0,0 +1 @@ +AzureAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/azureai.svg b/src/renderer/src/assets/llm-icons/azureai.svg new file mode 100644 index 0000000..8fb4d39 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/azureai.svg @@ -0,0 +1 @@ +AzureAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baichuan-color.svg b/src/renderer/src/assets/llm-icons/baichuan-color.svg new file mode 100644 index 0000000..32dc155 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baichuan-color.svg @@ -0,0 +1 @@ +Baichuan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baichuan-text.svg b/src/renderer/src/assets/llm-icons/baichuan-text.svg new file mode 100644 index 0000000..2949900 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baichuan-text.svg @@ -0,0 +1 @@ +Baichuan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baichuan.svg b/src/renderer/src/assets/llm-icons/baichuan.svg new file mode 100644 index 0000000..527aa03 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baichuan.svg @@ -0,0 +1 @@ +Baichuan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baidu-brand-color.svg b/src/renderer/src/assets/llm-icons/baidu-brand-color.svg new file mode 100644 index 0000000..b588bfc --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baidu-brand-color.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baidu-brand.svg b/src/renderer/src/assets/llm-icons/baidu-brand.svg new file mode 100644 index 0000000..eafb652 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baidu-brand.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baidu-color.svg b/src/renderer/src/assets/llm-icons/baidu-color.svg new file mode 100644 index 0000000..ead7f89 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baidu-color.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baidu-text-cn.svg b/src/renderer/src/assets/llm-icons/baidu-text-cn.svg new file mode 100644 index 0000000..3cf939c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baidu-text-cn.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baidu-text.svg b/src/renderer/src/assets/llm-icons/baidu-text.svg new file mode 100644 index 0000000..66e8ebe --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baidu-text.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baidu.svg b/src/renderer/src/assets/llm-icons/baidu.svg new file mode 100644 index 0000000..79a032e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baidu.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baiducloud-color.svg b/src/renderer/src/assets/llm-icons/baiducloud-color.svg new file mode 100644 index 0000000..40565ef --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baiducloud-color.svg @@ -0,0 +1 @@ +BaiduCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baiducloud-text.svg b/src/renderer/src/assets/llm-icons/baiducloud-text.svg new file mode 100644 index 0000000..e2ed081 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baiducloud-text.svg @@ -0,0 +1 @@ +BaiduCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/baiducloud.svg b/src/renderer/src/assets/llm-icons/baiducloud.svg new file mode 100644 index 0000000..7cf9ee5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/baiducloud.svg @@ -0,0 +1 @@ +BaiduCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bedrock-color.svg b/src/renderer/src/assets/llm-icons/bedrock-color.svg new file mode 100644 index 0000000..e0f929a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bedrock-color.svg @@ -0,0 +1 @@ +Bedrock \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bedrock-text.svg b/src/renderer/src/assets/llm-icons/bedrock-text.svg new file mode 100644 index 0000000..898a1bc --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bedrock-text.svg @@ -0,0 +1 @@ +Bedrock \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bedrock.svg b/src/renderer/src/assets/llm-icons/bedrock.svg new file mode 100644 index 0000000..a564939 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bedrock.svg @@ -0,0 +1 @@ +Bedrock \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bing-color.svg b/src/renderer/src/assets/llm-icons/bing-color.svg new file mode 100644 index 0000000..bd9f49c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bing-color.svg @@ -0,0 +1 @@ +Bing \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bing-text.svg b/src/renderer/src/assets/llm-icons/bing-text.svg new file mode 100644 index 0000000..76e14cc --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bing-text.svg @@ -0,0 +1 @@ +Bing \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bing.svg b/src/renderer/src/assets/llm-icons/bing.svg new file mode 100644 index 0000000..39db83d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bing.svg @@ -0,0 +1 @@ +Bing \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bytedance-brand-color.svg b/src/renderer/src/assets/llm-icons/bytedance-brand-color.svg new file mode 100644 index 0000000..dfd5842 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bytedance-brand-color.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bytedance-brand.svg b/src/renderer/src/assets/llm-icons/bytedance-brand.svg new file mode 100644 index 0000000..9b13d6d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bytedance-brand.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bytedance-color.svg b/src/renderer/src/assets/llm-icons/bytedance-color.svg new file mode 100644 index 0000000..761016b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bytedance-color.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bytedance-text-cn.svg b/src/renderer/src/assets/llm-icons/bytedance-text-cn.svg new file mode 100644 index 0000000..b4a9730 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bytedance-text-cn.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bytedance-text.svg b/src/renderer/src/assets/llm-icons/bytedance-text.svg new file mode 100644 index 0000000..96f1bf2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bytedance-text.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/bytedance.svg b/src/renderer/src/assets/llm-icons/bytedance.svg new file mode 100644 index 0000000..01ca9d8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/bytedance.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/chatglm-color.svg b/src/renderer/src/assets/llm-icons/chatglm-color.svg new file mode 100644 index 0000000..97f48f2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/chatglm-color.svg @@ -0,0 +1 @@ +ChatGLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/chatglm-text.svg b/src/renderer/src/assets/llm-icons/chatglm-text.svg new file mode 100644 index 0000000..234c7b2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/chatglm-text.svg @@ -0,0 +1 @@ +ChatGLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/chatglm.svg b/src/renderer/src/assets/llm-icons/chatglm.svg new file mode 100644 index 0000000..63bafb9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/chatglm.svg @@ -0,0 +1 @@ +ChatGLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/civitai-color.svg b/src/renderer/src/assets/llm-icons/civitai-color.svg new file mode 100644 index 0000000..50f921e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/civitai-color.svg @@ -0,0 +1 @@ +Civitai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/civitai-text-color.svg b/src/renderer/src/assets/llm-icons/civitai-text-color.svg new file mode 100644 index 0000000..bca1d92 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/civitai-text-color.svg @@ -0,0 +1 @@ +Civitai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/civitai-text.svg b/src/renderer/src/assets/llm-icons/civitai-text.svg new file mode 100644 index 0000000..fc4a520 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/civitai-text.svg @@ -0,0 +1 @@ +Civitai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/civitai.svg b/src/renderer/src/assets/llm-icons/civitai.svg new file mode 100644 index 0000000..6add97d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/civitai.svg @@ -0,0 +1 @@ +Civitai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/claude-color.svg b/src/renderer/src/assets/llm-icons/claude-color.svg new file mode 100644 index 0000000..62dc0db --- /dev/null +++ b/src/renderer/src/assets/llm-icons/claude-color.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/claude-text.svg b/src/renderer/src/assets/llm-icons/claude-text.svg new file mode 100644 index 0000000..6febc44 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/claude-text.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/claude.svg b/src/renderer/src/assets/llm-icons/claude.svg new file mode 100644 index 0000000..e29f328 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/clipdrop-text.svg b/src/renderer/src/assets/llm-icons/clipdrop-text.svg new file mode 100644 index 0000000..63d7b11 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/clipdrop-text.svg @@ -0,0 +1 @@ +Clipdrop \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/clipdrop.svg b/src/renderer/src/assets/llm-icons/clipdrop.svg new file mode 100644 index 0000000..943e41c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/clipdrop.svg @@ -0,0 +1 @@ +Clipdrop \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cloudflare-color.svg b/src/renderer/src/assets/llm-icons/cloudflare-color.svg new file mode 100644 index 0000000..2414144 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cloudflare-color.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cloudflare-text.svg b/src/renderer/src/assets/llm-icons/cloudflare-text.svg new file mode 100644 index 0000000..9611c8f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cloudflare-text.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cloudflare.svg b/src/renderer/src/assets/llm-icons/cloudflare.svg new file mode 100644 index 0000000..394375a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cloudflare.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/codegeex-color.svg b/src/renderer/src/assets/llm-icons/codegeex-color.svg new file mode 100644 index 0000000..d1d2df3 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/codegeex-color.svg @@ -0,0 +1 @@ +CodeGeeX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/codegeex-text.svg b/src/renderer/src/assets/llm-icons/codegeex-text.svg new file mode 100644 index 0000000..b8bc287 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/codegeex-text.svg @@ -0,0 +1 @@ +CodeGeeX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/codegeex.svg b/src/renderer/src/assets/llm-icons/codegeex.svg new file mode 100644 index 0000000..57f03b3 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/codegeex.svg @@ -0,0 +1 @@ +CodeGeeX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cogvideo-color.svg b/src/renderer/src/assets/llm-icons/cogvideo-color.svg new file mode 100644 index 0000000..c5da6de --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cogvideo-color.svg @@ -0,0 +1 @@ +CogVideo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cogvideo-text.svg b/src/renderer/src/assets/llm-icons/cogvideo-text.svg new file mode 100644 index 0000000..3086061 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cogvideo-text.svg @@ -0,0 +1 @@ +CogVideo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cogvideo.svg b/src/renderer/src/assets/llm-icons/cogvideo.svg new file mode 100644 index 0000000..ce217b2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cogvideo.svg @@ -0,0 +1 @@ +CogVideo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cogview-color.svg b/src/renderer/src/assets/llm-icons/cogview-color.svg new file mode 100644 index 0000000..c7a3bfe --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cogview-color.svg @@ -0,0 +1 @@ +CogView \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cogview-text.svg b/src/renderer/src/assets/llm-icons/cogview-text.svg new file mode 100644 index 0000000..1f54a5e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cogview-text.svg @@ -0,0 +1 @@ +CogView \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cogview.svg b/src/renderer/src/assets/llm-icons/cogview.svg new file mode 100644 index 0000000..dd6ad04 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cogview.svg @@ -0,0 +1 @@ +CogView \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cohere-color.svg b/src/renderer/src/assets/llm-icons/cohere-color.svg new file mode 100644 index 0000000..94bcb82 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cohere-color.svg @@ -0,0 +1 @@ +Cohere \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cohere-text.svg b/src/renderer/src/assets/llm-icons/cohere-text.svg new file mode 100644 index 0000000..cc06542 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cohere-text.svg @@ -0,0 +1 @@ +Cohere \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cohere.svg b/src/renderer/src/assets/llm-icons/cohere.svg new file mode 100644 index 0000000..f64a434 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cohere.svg @@ -0,0 +1 @@ +Cohere \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/colab-color.svg b/src/renderer/src/assets/llm-icons/colab-color.svg new file mode 100644 index 0000000..7fc0d6b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/colab-color.svg @@ -0,0 +1 @@ +Colab \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/colab-text.svg b/src/renderer/src/assets/llm-icons/colab-text.svg new file mode 100644 index 0000000..055cf85 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/colab-text.svg @@ -0,0 +1 @@ +Colab \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/colab.svg b/src/renderer/src/assets/llm-icons/colab.svg new file mode 100644 index 0000000..ce4067e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/colab.svg @@ -0,0 +1 @@ +Colab \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/comfyui-color.svg b/src/renderer/src/assets/llm-icons/comfyui-color.svg new file mode 100644 index 0000000..5219948 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/comfyui-color.svg @@ -0,0 +1 @@ +ComfyUI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/comfyui-text.svg b/src/renderer/src/assets/llm-icons/comfyui-text.svg new file mode 100644 index 0000000..7cd78b8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/comfyui-text.svg @@ -0,0 +1 @@ +ComfyUI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/comfyui.svg b/src/renderer/src/assets/llm-icons/comfyui.svg new file mode 100644 index 0000000..388f518 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/comfyui.svg @@ -0,0 +1 @@ +ComfyUI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/copilot-color.svg b/src/renderer/src/assets/llm-icons/copilot-color.svg new file mode 100644 index 0000000..04865f8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/copilot-color.svg @@ -0,0 +1 @@ +Copilot \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/copilot-text.svg b/src/renderer/src/assets/llm-icons/copilot-text.svg new file mode 100644 index 0000000..f0d0eb8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/copilot-text.svg @@ -0,0 +1 @@ +Copilot \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/copilot.svg b/src/renderer/src/assets/llm-icons/copilot.svg new file mode 100644 index 0000000..59872d0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/copilot.svg @@ -0,0 +1 @@ +Copilot \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/coze-text.svg b/src/renderer/src/assets/llm-icons/coze-text.svg new file mode 100644 index 0000000..329c44a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/coze-text.svg @@ -0,0 +1 @@ +Coze \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/coze.svg b/src/renderer/src/assets/llm-icons/coze.svg new file mode 100644 index 0000000..743f6d6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/coze.svg @@ -0,0 +1 @@ +Coze \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cursor-text.svg b/src/renderer/src/assets/llm-icons/cursor-text.svg new file mode 100644 index 0000000..5bbb75b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cursor-text.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/cursor.svg b/src/renderer/src/assets/llm-icons/cursor.svg new file mode 100644 index 0000000..abadee5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/cursor.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dalle-color.svg b/src/renderer/src/assets/llm-icons/dalle-color.svg new file mode 100644 index 0000000..79192c0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dalle-color.svg @@ -0,0 +1 @@ +DALL-E \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dalle-text.svg b/src/renderer/src/assets/llm-icons/dalle-text.svg new file mode 100644 index 0000000..589e58a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dalle-text.svg @@ -0,0 +1 @@ +DALL-E \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dalle.svg b/src/renderer/src/assets/llm-icons/dalle.svg new file mode 100644 index 0000000..03cc6d1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dalle.svg @@ -0,0 +1 @@ +DALL-E \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dbrx-brand-color.svg b/src/renderer/src/assets/llm-icons/dbrx-brand-color.svg new file mode 100644 index 0000000..08db74a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dbrx-brand-color.svg @@ -0,0 +1 @@ +DBRX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dbrx-brand.svg b/src/renderer/src/assets/llm-icons/dbrx-brand.svg new file mode 100644 index 0000000..5a808cf --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dbrx-brand.svg @@ -0,0 +1 @@ +DBRX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dbrx-color.svg b/src/renderer/src/assets/llm-icons/dbrx-color.svg new file mode 100644 index 0000000..cd079ce --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dbrx-color.svg @@ -0,0 +1 @@ +DBRX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dbrx-text.svg b/src/renderer/src/assets/llm-icons/dbrx-text.svg new file mode 100644 index 0000000..9b5d7f1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dbrx-text.svg @@ -0,0 +1 @@ +DBRX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dbrx.svg b/src/renderer/src/assets/llm-icons/dbrx.svg new file mode 100644 index 0000000..ac34567 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dbrx.svg @@ -0,0 +1 @@ +DBRX \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepai-text.svg b/src/renderer/src/assets/llm-icons/deepai-text.svg new file mode 100644 index 0000000..c129b7b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepai-text.svg @@ -0,0 +1 @@ +DeepAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepai.svg b/src/renderer/src/assets/llm-icons/deepai.svg new file mode 100644 index 0000000..500edd0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepai.svg @@ -0,0 +1 @@ +DeepAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepmind-color.svg b/src/renderer/src/assets/llm-icons/deepmind-color.svg new file mode 100644 index 0000000..7fbfab2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepmind-color.svg @@ -0,0 +1 @@ +DeepMind \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepmind-text.svg b/src/renderer/src/assets/llm-icons/deepmind-text.svg new file mode 100644 index 0000000..4c2dbf1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepmind-text.svg @@ -0,0 +1 @@ +DeepMind \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepmind.svg b/src/renderer/src/assets/llm-icons/deepmind.svg new file mode 100644 index 0000000..dd508f4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepmind.svg @@ -0,0 +1 @@ +DeepMind \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepseek-color.svg b/src/renderer/src/assets/llm-icons/deepseek-color.svg new file mode 100644 index 0000000..3fc2302 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepseek-color.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepseek-text.svg b/src/renderer/src/assets/llm-icons/deepseek-text.svg new file mode 100644 index 0000000..8e137b5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepseek-text.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/deepseek.svg b/src/renderer/src/assets/llm-icons/deepseek.svg new file mode 100644 index 0000000..dc224e4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dify-color.svg b/src/renderer/src/assets/llm-icons/dify-color.svg new file mode 100644 index 0000000..cd2c6d2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dify-color.svg @@ -0,0 +1 @@ +Dify \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dify-text-color.svg b/src/renderer/src/assets/llm-icons/dify-text-color.svg new file mode 100644 index 0000000..29f70cc --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dify-text-color.svg @@ -0,0 +1 @@ +Dify \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dify-text.svg b/src/renderer/src/assets/llm-icons/dify-text.svg new file mode 100644 index 0000000..888f136 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dify-text.svg @@ -0,0 +1 @@ +Dify \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/dify.svg b/src/renderer/src/assets/llm-icons/dify.svg new file mode 100644 index 0000000..8cc9f20 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/dify.svg @@ -0,0 +1 @@ +Dify \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/doubao-color.svg b/src/renderer/src/assets/llm-icons/doubao-color.svg new file mode 100644 index 0000000..e251145 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/doubao-color.svg @@ -0,0 +1 @@ +Doubao \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/doubao-text.svg b/src/renderer/src/assets/llm-icons/doubao-text.svg new file mode 100644 index 0000000..dff5207 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/doubao-text.svg @@ -0,0 +1 @@ +Doubao \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/doubao.svg b/src/renderer/src/assets/llm-icons/doubao.svg new file mode 100644 index 0000000..5a1169c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/doubao.svg @@ -0,0 +1 @@ +Doubao \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fal-text.svg b/src/renderer/src/assets/llm-icons/fal-text.svg new file mode 100644 index 0000000..3422d69 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fal-text.svg @@ -0,0 +1 @@ +Fal \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fal.svg b/src/renderer/src/assets/llm-icons/fal.svg new file mode 100644 index 0000000..c41076f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fal.svg @@ -0,0 +1 @@ +Fal \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fireworks-color.svg b/src/renderer/src/assets/llm-icons/fireworks-color.svg new file mode 100644 index 0000000..a23445c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fireworks-color.svg @@ -0,0 +1 @@ +Fireworks \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fireworks-text.svg b/src/renderer/src/assets/llm-icons/fireworks-text.svg new file mode 100644 index 0000000..24f378d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fireworks-text.svg @@ -0,0 +1 @@ +Fireworks \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fireworks.svg b/src/renderer/src/assets/llm-icons/fireworks.svg new file mode 100644 index 0000000..cdc014c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fireworks.svg @@ -0,0 +1 @@ +Fireworks \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fishaudio-text.svg b/src/renderer/src/assets/llm-icons/fishaudio-text.svg new file mode 100644 index 0000000..52ae0b7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fishaudio-text.svg @@ -0,0 +1 @@ +FishAudio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/fishaudio.svg b/src/renderer/src/assets/llm-icons/fishaudio.svg new file mode 100644 index 0000000..3b88941 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/fishaudio.svg @@ -0,0 +1 @@ +FishAudio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/flux-text.svg b/src/renderer/src/assets/llm-icons/flux-text.svg new file mode 100644 index 0000000..a5e2313 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/flux-text.svg @@ -0,0 +1 @@ +Flux \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/flux.svg b/src/renderer/src/assets/llm-icons/flux.svg new file mode 100644 index 0000000..12a4ba1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/flux.svg @@ -0,0 +1 @@ +Flux \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemini-brand-color.svg b/src/renderer/src/assets/llm-icons/gemini-brand-color.svg new file mode 100644 index 0000000..33a2934 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemini-brand-color.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemini-brand.svg b/src/renderer/src/assets/llm-icons/gemini-brand.svg new file mode 100644 index 0000000..c0b2370 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemini-brand.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemini-color.svg b/src/renderer/src/assets/llm-icons/gemini-color.svg new file mode 100644 index 0000000..878eb62 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemini-color.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemini-text.svg b/src/renderer/src/assets/llm-icons/gemini-text.svg new file mode 100644 index 0000000..489b7ee --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemini-text.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemini.svg b/src/renderer/src/assets/llm-icons/gemini.svg new file mode 100644 index 0000000..9f9098e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemini.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemma-color.svg b/src/renderer/src/assets/llm-icons/gemma-color.svg new file mode 100644 index 0000000..ed81051 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemma-color.svg @@ -0,0 +1 @@ +Gemma \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemma-text.svg b/src/renderer/src/assets/llm-icons/gemma-text.svg new file mode 100644 index 0000000..982fa4b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemma-text.svg @@ -0,0 +1 @@ +Gemma \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/gemma.svg b/src/renderer/src/assets/llm-icons/gemma.svg new file mode 100644 index 0000000..53f31c9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/gemma.svg @@ -0,0 +1 @@ +Gemma \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/giteeai-text.svg b/src/renderer/src/assets/llm-icons/giteeai-text.svg new file mode 100644 index 0000000..efeee08 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/giteeai-text.svg @@ -0,0 +1 @@ +GiteeAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/giteeai.svg b/src/renderer/src/assets/llm-icons/giteeai.svg new file mode 100644 index 0000000..e84ad63 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/giteeai.svg @@ -0,0 +1 @@ +GiteeAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/github-text.svg b/src/renderer/src/assets/llm-icons/github-text.svg new file mode 100644 index 0000000..393adda --- /dev/null +++ b/src/renderer/src/assets/llm-icons/github-text.svg @@ -0,0 +1 @@ +Github \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/github.svg b/src/renderer/src/assets/llm-icons/github.svg new file mode 100644 index 0000000..7a51b8e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/github.svg @@ -0,0 +1 @@ +Github \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/githubcopilot-text.svg b/src/renderer/src/assets/llm-icons/githubcopilot-text.svg new file mode 100644 index 0000000..44f10e2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/githubcopilot-text.svg @@ -0,0 +1 @@ +GithubCopilot \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/githubcopilot.svg b/src/renderer/src/assets/llm-icons/githubcopilot.svg new file mode 100644 index 0000000..3cbf22a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/githubcopilot.svg @@ -0,0 +1 @@ +GithubCopilot \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/glif-text.svg b/src/renderer/src/assets/llm-icons/glif-text.svg new file mode 100644 index 0000000..295a689 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/glif-text.svg @@ -0,0 +1 @@ +Glif \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/glif.svg b/src/renderer/src/assets/llm-icons/glif.svg new file mode 100644 index 0000000..0e71525 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/glif.svg @@ -0,0 +1 @@ +Glif \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/google-brand-color.svg b/src/renderer/src/assets/llm-icons/google-brand-color.svg new file mode 100644 index 0000000..76ca8b5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/google-brand-color.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/google-brand.svg b/src/renderer/src/assets/llm-icons/google-brand.svg new file mode 100644 index 0000000..c01a680 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/google-brand.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/google-color.svg b/src/renderer/src/assets/llm-icons/google-color.svg new file mode 100644 index 0000000..e8e0f86 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/google-color.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/google.svg b/src/renderer/src/assets/llm-icons/google.svg new file mode 100644 index 0000000..e93a82c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/grok-text.svg b/src/renderer/src/assets/llm-icons/grok-text.svg new file mode 100644 index 0000000..d00e6e1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/grok-text.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/grok.svg b/src/renderer/src/assets/llm-icons/grok.svg new file mode 100644 index 0000000..536e713 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/grok.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/groq-text.svg b/src/renderer/src/assets/llm-icons/groq-text.svg new file mode 100644 index 0000000..8104825 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/groq-text.svg @@ -0,0 +1 @@ +Groq \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/groq.svg b/src/renderer/src/assets/llm-icons/groq.svg new file mode 100644 index 0000000..d3480d0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/groq.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/src/renderer/src/assets/llm-icons/hailuo-color.svg b/src/renderer/src/assets/llm-icons/hailuo-color.svg new file mode 100644 index 0000000..b85c321 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hailuo-color.svg @@ -0,0 +1 @@ +Hailuo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hailuo-text.svg b/src/renderer/src/assets/llm-icons/hailuo-text.svg new file mode 100644 index 0000000..1554062 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hailuo-text.svg @@ -0,0 +1 @@ +Hailuo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hailuo.svg b/src/renderer/src/assets/llm-icons/hailuo.svg new file mode 100644 index 0000000..93db13c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hailuo.svg @@ -0,0 +1 @@ +Hailuo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/haiper-text.svg b/src/renderer/src/assets/llm-icons/haiper-text.svg new file mode 100644 index 0000000..c46bafa --- /dev/null +++ b/src/renderer/src/assets/llm-icons/haiper-text.svg @@ -0,0 +1 @@ +Haiper \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/haiper.svg b/src/renderer/src/assets/llm-icons/haiper.svg new file mode 100644 index 0000000..f806a40 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/haiper.svg @@ -0,0 +1 @@ +Haiper \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hedra-text.svg b/src/renderer/src/assets/llm-icons/hedra-text.svg new file mode 100644 index 0000000..ed3a71e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hedra-text.svg @@ -0,0 +1 @@ +Hedra \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hedra.svg b/src/renderer/src/assets/llm-icons/hedra.svg new file mode 100644 index 0000000..bd41b02 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hedra.svg @@ -0,0 +1 @@ +Hedra \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/higress-color.svg b/src/renderer/src/assets/llm-icons/higress-color.svg new file mode 100644 index 0000000..0155ece --- /dev/null +++ b/src/renderer/src/assets/llm-icons/higress-color.svg @@ -0,0 +1 @@ +Higress \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/higress-text.svg b/src/renderer/src/assets/llm-icons/higress-text.svg new file mode 100644 index 0000000..c734428 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/higress-text.svg @@ -0,0 +1 @@ +Higress \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/higress.svg b/src/renderer/src/assets/llm-icons/higress.svg new file mode 100644 index 0000000..a5e3a22 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/higress.svg @@ -0,0 +1 @@ +Higress \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/huggingface-color.svg b/src/renderer/src/assets/llm-icons/huggingface-color.svg new file mode 100644 index 0000000..fc0c80d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/huggingface-color.svg @@ -0,0 +1 @@ +HuggingFace \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/huggingface-text.svg b/src/renderer/src/assets/llm-icons/huggingface-text.svg new file mode 100644 index 0000000..5952dee --- /dev/null +++ b/src/renderer/src/assets/llm-icons/huggingface-text.svg @@ -0,0 +1 @@ +HuggingFace \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/huggingface.svg b/src/renderer/src/assets/llm-icons/huggingface.svg new file mode 100644 index 0000000..4545933 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/huggingface.svg @@ -0,0 +1 @@ +HuggingFace \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hunyuan-color.svg b/src/renderer/src/assets/llm-icons/hunyuan-color.svg new file mode 100644 index 0000000..4967224 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hunyuan-color.svg @@ -0,0 +1 @@ +Hunyuan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hunyuan-text.svg b/src/renderer/src/assets/llm-icons/hunyuan-text.svg new file mode 100644 index 0000000..4e4707f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hunyuan-text.svg @@ -0,0 +1 @@ +Hunyuan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/hunyuan.svg b/src/renderer/src/assets/llm-icons/hunyuan.svg new file mode 100644 index 0000000..724a815 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/hunyuan.svg @@ -0,0 +1 @@ +Hunyuan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ideogram-text.svg b/src/renderer/src/assets/llm-icons/ideogram-text.svg new file mode 100644 index 0000000..8f0235d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ideogram-text.svg @@ -0,0 +1 @@ +Ideogram \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ideogram.svg b/src/renderer/src/assets/llm-icons/ideogram.svg new file mode 100644 index 0000000..ba6f7f6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ideogram.svg @@ -0,0 +1 @@ +Ideogram \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/internlm-color.svg b/src/renderer/src/assets/llm-icons/internlm-color.svg new file mode 100644 index 0000000..8e5daef --- /dev/null +++ b/src/renderer/src/assets/llm-icons/internlm-color.svg @@ -0,0 +1 @@ +InternLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/internlm-text.svg b/src/renderer/src/assets/llm-icons/internlm-text.svg new file mode 100644 index 0000000..1f82c1c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/internlm-text.svg @@ -0,0 +1 @@ +InternLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/internlm.svg b/src/renderer/src/assets/llm-icons/internlm.svg new file mode 100644 index 0000000..15098c6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/internlm.svg @@ -0,0 +1 @@ +InternLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/jina-color.svg b/src/renderer/src/assets/llm-icons/jina-color.svg new file mode 100644 index 0000000..d2fa4a5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/jina-color.svg @@ -0,0 +1 @@ +Jina \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/jina-text.svg b/src/renderer/src/assets/llm-icons/jina-text.svg new file mode 100644 index 0000000..9cbb6dd --- /dev/null +++ b/src/renderer/src/assets/llm-icons/jina-text.svg @@ -0,0 +1 @@ +Jina \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/jina.svg b/src/renderer/src/assets/llm-icons/jina.svg new file mode 100644 index 0000000..3b3bb7e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/jina.svg @@ -0,0 +1 @@ +Jina \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/kimi-color.svg b/src/renderer/src/assets/llm-icons/kimi-color.svg new file mode 100644 index 0000000..ec5db53 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/kimi-color.svg @@ -0,0 +1 @@ +Kimi \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/kimi-text.svg b/src/renderer/src/assets/llm-icons/kimi-text.svg new file mode 100644 index 0000000..df88e25 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/kimi-text.svg @@ -0,0 +1 @@ +Kimi \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/kimi.svg b/src/renderer/src/assets/llm-icons/kimi.svg new file mode 100644 index 0000000..4355c52 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/kimi.svg @@ -0,0 +1 @@ +Kimi \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/kling-color.svg b/src/renderer/src/assets/llm-icons/kling-color.svg new file mode 100644 index 0000000..8dc9588 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/kling-color.svg @@ -0,0 +1 @@ +Kling \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/kling-text.svg b/src/renderer/src/assets/llm-icons/kling-text.svg new file mode 100644 index 0000000..d287c87 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/kling-text.svg @@ -0,0 +1 @@ +Kling \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/kling.svg b/src/renderer/src/assets/llm-icons/kling.svg new file mode 100644 index 0000000..32fb6be --- /dev/null +++ b/src/renderer/src/assets/llm-icons/kling.svg @@ -0,0 +1 @@ +Kling \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/langchain-color.svg b/src/renderer/src/assets/llm-icons/langchain-color.svg new file mode 100644 index 0000000..c4deef0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/langchain-color.svg @@ -0,0 +1 @@ +LangChain \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/langchain-text.svg b/src/renderer/src/assets/llm-icons/langchain-text.svg new file mode 100644 index 0000000..e6d48d1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/langchain-text.svg @@ -0,0 +1 @@ +LangChain \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/langchain.svg b/src/renderer/src/assets/llm-icons/langchain.svg new file mode 100644 index 0000000..d5b4e3b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/langchain.svg @@ -0,0 +1 @@ +LangChain \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/langfuse-color.svg b/src/renderer/src/assets/llm-icons/langfuse-color.svg new file mode 100644 index 0000000..10eb847 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/langfuse-color.svg @@ -0,0 +1 @@ +Langfuse \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/langfuse-text.svg b/src/renderer/src/assets/llm-icons/langfuse-text.svg new file mode 100644 index 0000000..62d70f0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/langfuse-text.svg @@ -0,0 +1 @@ +Langfuse \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/langfuse.svg b/src/renderer/src/assets/llm-icons/langfuse.svg new file mode 100644 index 0000000..d94196b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/langfuse.svg @@ -0,0 +1 @@ +Langfuse \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lightricks-text.svg b/src/renderer/src/assets/llm-icons/lightricks-text.svg new file mode 100644 index 0000000..9801272 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lightricks-text.svg @@ -0,0 +1 @@ +Lightricks \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lightricks.svg b/src/renderer/src/assets/llm-icons/lightricks.svg new file mode 100644 index 0000000..c2893d0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lightricks.svg @@ -0,0 +1 @@ +Lightricks \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/livekit-color.svg b/src/renderer/src/assets/llm-icons/livekit-color.svg new file mode 100644 index 0000000..6acae42 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/livekit-color.svg @@ -0,0 +1 @@ +LiveKit \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/livekit-text.svg b/src/renderer/src/assets/llm-icons/livekit-text.svg new file mode 100644 index 0000000..a5e9889 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/livekit-text.svg @@ -0,0 +1 @@ +LiveKit \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/livekit.svg b/src/renderer/src/assets/llm-icons/livekit.svg new file mode 100644 index 0000000..c950e8d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/livekit.svg @@ -0,0 +1 @@ +LiveKit \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/llava-color.svg b/src/renderer/src/assets/llm-icons/llava-color.svg new file mode 100644 index 0000000..24311ee --- /dev/null +++ b/src/renderer/src/assets/llm-icons/llava-color.svg @@ -0,0 +1 @@ +LLaVA \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/llava-text.svg b/src/renderer/src/assets/llm-icons/llava-text.svg new file mode 100644 index 0000000..d7ca1a4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/llava-text.svg @@ -0,0 +1 @@ +LLaVA \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/llava.svg b/src/renderer/src/assets/llm-icons/llava.svg new file mode 100644 index 0000000..e82076a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/llava.svg @@ -0,0 +1 @@ +LLaVA \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lmstudio-text.svg b/src/renderer/src/assets/llm-icons/lmstudio-text.svg new file mode 100644 index 0000000..5064807 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lmstudio-text.svg @@ -0,0 +1 @@ +LM Studio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lmstudio.svg b/src/renderer/src/assets/llm-icons/lmstudio.svg new file mode 100644 index 0000000..ea0816b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lmstudio.svg @@ -0,0 +1 @@ +LM Studio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lobehub-color.svg b/src/renderer/src/assets/llm-icons/lobehub-color.svg new file mode 100644 index 0000000..3cb00ac --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lobehub-color.svg @@ -0,0 +1 @@ +LobeHub \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lobehub-text.svg b/src/renderer/src/assets/llm-icons/lobehub-text.svg new file mode 100644 index 0000000..f9fd5e5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lobehub-text.svg @@ -0,0 +1 @@ +LobeHub \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/lobehub.svg b/src/renderer/src/assets/llm-icons/lobehub.svg new file mode 100644 index 0000000..9288946 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/lobehub.svg @@ -0,0 +1 @@ +LobeHub \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/luma-color.svg b/src/renderer/src/assets/llm-icons/luma-color.svg new file mode 100644 index 0000000..02d8cbe --- /dev/null +++ b/src/renderer/src/assets/llm-icons/luma-color.svg @@ -0,0 +1 @@ +Luma \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/luma-text.svg b/src/renderer/src/assets/llm-icons/luma-text.svg new file mode 100644 index 0000000..dedb143 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/luma-text.svg @@ -0,0 +1 @@ +Luma \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/luma.svg b/src/renderer/src/assets/llm-icons/luma.svg new file mode 100644 index 0000000..9fe40cb --- /dev/null +++ b/src/renderer/src/assets/llm-icons/luma.svg @@ -0,0 +1 @@ +Luma \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/magic-text.svg b/src/renderer/src/assets/llm-icons/magic-text.svg new file mode 100644 index 0000000..15e3f64 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/magic-text.svg @@ -0,0 +1 @@ +Magic \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/magic.svg b/src/renderer/src/assets/llm-icons/magic.svg new file mode 100644 index 0000000..19bd821 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/magic.svg @@ -0,0 +1 @@ +Magic \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/meta-brand-color.svg b/src/renderer/src/assets/llm-icons/meta-brand-color.svg new file mode 100644 index 0000000..2dc5cf7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/meta-brand-color.svg @@ -0,0 +1 @@ +Meta \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/meta-brand.svg b/src/renderer/src/assets/llm-icons/meta-brand.svg new file mode 100644 index 0000000..752e01e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/meta-brand.svg @@ -0,0 +1 @@ +Meta \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/meta-color.svg b/src/renderer/src/assets/llm-icons/meta-color.svg new file mode 100644 index 0000000..0e5a40a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/meta-color.svg @@ -0,0 +1 @@ +Meta \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/meta-text.svg b/src/renderer/src/assets/llm-icons/meta-text.svg new file mode 100644 index 0000000..04ee7af --- /dev/null +++ b/src/renderer/src/assets/llm-icons/meta-text.svg @@ -0,0 +1 @@ +Meta \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/meta.svg b/src/renderer/src/assets/llm-icons/meta.svg new file mode 100644 index 0000000..7daf2eb --- /dev/null +++ b/src/renderer/src/assets/llm-icons/meta.svg @@ -0,0 +1 @@ +Meta \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/midjourney-text.svg b/src/renderer/src/assets/llm-icons/midjourney-text.svg new file mode 100644 index 0000000..6ccba53 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/midjourney-text.svg @@ -0,0 +1 @@ +Midjourney \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/midjourney.svg b/src/renderer/src/assets/llm-icons/midjourney.svg new file mode 100644 index 0000000..e59e3b0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/midjourney.svg @@ -0,0 +1 @@ +Midjourney \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/minimax-color.svg b/src/renderer/src/assets/llm-icons/minimax-color.svg new file mode 100644 index 0000000..2a60bd4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/minimax-color.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/minimax-text.svg b/src/renderer/src/assets/llm-icons/minimax-text.svg new file mode 100644 index 0000000..2989641 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/minimax-text.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/minimax.svg b/src/renderer/src/assets/llm-icons/minimax.svg new file mode 100644 index 0000000..1d32449 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/minimax.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/mistral-color.svg b/src/renderer/src/assets/llm-icons/mistral-color.svg new file mode 100644 index 0000000..b411a52 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/mistral-color.svg @@ -0,0 +1 @@ +Mistral \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/mistral-text.svg b/src/renderer/src/assets/llm-icons/mistral-text.svg new file mode 100644 index 0000000..ce3f0a7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/mistral-text.svg @@ -0,0 +1 @@ +Mistral \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/mistral.svg b/src/renderer/src/assets/llm-icons/mistral.svg new file mode 100644 index 0000000..23b8f2e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/mistral.svg @@ -0,0 +1 @@ +Mistral \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/modelscope-color.svg b/src/renderer/src/assets/llm-icons/modelscope-color.svg new file mode 100644 index 0000000..afbaa17 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/modelscope-color.svg @@ -0,0 +1 @@ +ModelScope \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/modelscope-text.svg b/src/renderer/src/assets/llm-icons/modelscope-text.svg new file mode 100644 index 0000000..01c6030 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/modelscope-text.svg @@ -0,0 +1 @@ +ModelScope \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/modelscope.svg b/src/renderer/src/assets/llm-icons/modelscope.svg new file mode 100644 index 0000000..f9bca8b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/modelscope.svg @@ -0,0 +1 @@ +ModelScope \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/moonshot-text.svg b/src/renderer/src/assets/llm-icons/moonshot-text.svg new file mode 100644 index 0000000..bfd871d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/moonshot-text.svg @@ -0,0 +1 @@ +MoonshotAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/moonshot.svg b/src/renderer/src/assets/llm-icons/moonshot.svg new file mode 100644 index 0000000..fb56ac1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/moonshot.svg @@ -0,0 +1 @@ +MoonshotAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/myshell-color.svg b/src/renderer/src/assets/llm-icons/myshell-color.svg new file mode 100644 index 0000000..0f6075c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/myshell-color.svg @@ -0,0 +1 @@ +MyShell \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/myshell-text.svg b/src/renderer/src/assets/llm-icons/myshell-text.svg new file mode 100644 index 0000000..9a5dee8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/myshell-text.svg @@ -0,0 +1 @@ +MyShell \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/myshell.svg b/src/renderer/src/assets/llm-icons/myshell.svg new file mode 100644 index 0000000..c4553e4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/myshell.svg @@ -0,0 +1 @@ +MyShell \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/notion-text.svg b/src/renderer/src/assets/llm-icons/notion-text.svg new file mode 100644 index 0000000..dab414c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/notion-text.svg @@ -0,0 +1 @@ +Notion \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/notion.svg b/src/renderer/src/assets/llm-icons/notion.svg new file mode 100644 index 0000000..e92465e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/notion.svg @@ -0,0 +1 @@ +Notion \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/nova-color.svg b/src/renderer/src/assets/llm-icons/nova-color.svg new file mode 100644 index 0000000..2a8e7dd --- /dev/null +++ b/src/renderer/src/assets/llm-icons/nova-color.svg @@ -0,0 +1 @@ +Nova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/nova-text.svg b/src/renderer/src/assets/llm-icons/nova-text.svg new file mode 100644 index 0000000..5558171 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/nova-text.svg @@ -0,0 +1 @@ +Nova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/nova.svg b/src/renderer/src/assets/llm-icons/nova.svg new file mode 100644 index 0000000..266d07a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/nova.svg @@ -0,0 +1 @@ +Nova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/novita-color.svg b/src/renderer/src/assets/llm-icons/novita-color.svg new file mode 100644 index 0000000..0658ce0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/novita-color.svg @@ -0,0 +1 @@ +Novita AI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/novita-text.svg b/src/renderer/src/assets/llm-icons/novita-text.svg new file mode 100644 index 0000000..f6440f8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/novita-text.svg @@ -0,0 +1 @@ +Novita AI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/novita.svg b/src/renderer/src/assets/llm-icons/novita.svg new file mode 100644 index 0000000..353f007 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/novita.svg @@ -0,0 +1 @@ +Novita AI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/nvidia-color.svg b/src/renderer/src/assets/llm-icons/nvidia-color.svg new file mode 100644 index 0000000..a9683c2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/nvidia-color.svg @@ -0,0 +1 @@ +Nvidia \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/nvidia-text.svg b/src/renderer/src/assets/llm-icons/nvidia-text.svg new file mode 100644 index 0000000..91e413d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/nvidia-text.svg @@ -0,0 +1 @@ +Nvidia \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/nvidia.svg b/src/renderer/src/assets/llm-icons/nvidia.svg new file mode 100644 index 0000000..2ffd7fc --- /dev/null +++ b/src/renderer/src/assets/llm-icons/nvidia.svg @@ -0,0 +1 @@ +Nvidia \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ollama-text.svg b/src/renderer/src/assets/llm-icons/ollama-text.svg new file mode 100644 index 0000000..6d112cd --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ollama-text.svg @@ -0,0 +1 @@ +Ollama \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ollama.svg b/src/renderer/src/assets/llm-icons/ollama.svg new file mode 100644 index 0000000..cc887e3 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ollama.svg @@ -0,0 +1 @@ +Ollama \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openai-text.svg b/src/renderer/src/assets/llm-icons/openai-text.svg new file mode 100644 index 0000000..204161f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openai-text.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openai.svg b/src/renderer/src/assets/llm-icons/openai.svg new file mode 100644 index 0000000..50d94d6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openai.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openchat-color.svg b/src/renderer/src/assets/llm-icons/openchat-color.svg new file mode 100644 index 0000000..1ca8611 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openchat-color.svg @@ -0,0 +1 @@ +OpenChat \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openchat-text.svg b/src/renderer/src/assets/llm-icons/openchat-text.svg new file mode 100644 index 0000000..cde466f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openchat-text.svg @@ -0,0 +1 @@ +OpenChat \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openchat.svg b/src/renderer/src/assets/llm-icons/openchat.svg new file mode 100644 index 0000000..54e70a8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openchat.svg @@ -0,0 +1 @@ +OpenChat \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openrouter-text.svg b/src/renderer/src/assets/llm-icons/openrouter-text.svg new file mode 100644 index 0000000..678031e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openrouter-text.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/openrouter.svg b/src/renderer/src/assets/llm-icons/openrouter.svg new file mode 100644 index 0000000..e6cca2a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/openrouter.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/palm-color.svg b/src/renderer/src/assets/llm-icons/palm-color.svg new file mode 100644 index 0000000..8e6af5b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/palm-color.svg @@ -0,0 +1 @@ +PaLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/palm-text.svg b/src/renderer/src/assets/llm-icons/palm-text.svg new file mode 100644 index 0000000..bdd73bb --- /dev/null +++ b/src/renderer/src/assets/llm-icons/palm-text.svg @@ -0,0 +1 @@ +PaLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/palm.svg b/src/renderer/src/assets/llm-icons/palm.svg new file mode 100644 index 0000000..b2369ba --- /dev/null +++ b/src/renderer/src/assets/llm-icons/palm.svg @@ -0,0 +1 @@ +PaLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/perplexity-color.svg b/src/renderer/src/assets/llm-icons/perplexity-color.svg new file mode 100644 index 0000000..5f5a5ab --- /dev/null +++ b/src/renderer/src/assets/llm-icons/perplexity-color.svg @@ -0,0 +1 @@ +Perplexity \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/perplexity-text.svg b/src/renderer/src/assets/llm-icons/perplexity-text.svg new file mode 100644 index 0000000..2218bbb --- /dev/null +++ b/src/renderer/src/assets/llm-icons/perplexity-text.svg @@ -0,0 +1 @@ +Perplexity \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/perplexity.svg b/src/renderer/src/assets/llm-icons/perplexity.svg new file mode 100644 index 0000000..f7c917c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/perplexity.svg @@ -0,0 +1 @@ +Perplexity \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pika-text.svg b/src/renderer/src/assets/llm-icons/pika-text.svg new file mode 100644 index 0000000..3052686 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pika-text.svg @@ -0,0 +1 @@ +Pika \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pika.svg b/src/renderer/src/assets/llm-icons/pika.svg new file mode 100644 index 0000000..497c964 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pika.svg @@ -0,0 +1 @@ +Pika \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pixverse-color.svg b/src/renderer/src/assets/llm-icons/pixverse-color.svg new file mode 100644 index 0000000..6680e89 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pixverse-color.svg @@ -0,0 +1 @@ +PixVerse \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pixverse-text.svg b/src/renderer/src/assets/llm-icons/pixverse-text.svg new file mode 100644 index 0000000..9b47bbb --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pixverse-text.svg @@ -0,0 +1 @@ +PixVerse \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pixverse.svg b/src/renderer/src/assets/llm-icons/pixverse.svg new file mode 100644 index 0000000..e293ce7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pixverse.svg @@ -0,0 +1 @@ +PixVerse \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/poe-color.svg b/src/renderer/src/assets/llm-icons/poe-color.svg new file mode 100644 index 0000000..1083eff --- /dev/null +++ b/src/renderer/src/assets/llm-icons/poe-color.svg @@ -0,0 +1 @@ +Poe \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/poe-text.svg b/src/renderer/src/assets/llm-icons/poe-text.svg new file mode 100644 index 0000000..1121f04 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/poe-text.svg @@ -0,0 +1 @@ +Poe \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/poe.svg b/src/renderer/src/assets/llm-icons/poe.svg new file mode 100644 index 0000000..0df53f5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/poe.svg @@ -0,0 +1 @@ +Poe \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pollinations-text.svg b/src/renderer/src/assets/llm-icons/pollinations-text.svg new file mode 100644 index 0000000..f883aea --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pollinations-text.svg @@ -0,0 +1 @@ +Pollinations \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/pollinations.svg b/src/renderer/src/assets/llm-icons/pollinations.svg new file mode 100644 index 0000000..65b3b6d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/pollinations.svg @@ -0,0 +1 @@ +Pollinations \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ppio-brand-color.svg b/src/renderer/src/assets/llm-icons/ppio-brand-color.svg new file mode 100644 index 0000000..f296b9a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ppio-brand-color.svg @@ -0,0 +1 @@ +PPIO \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ppio-brand.svg b/src/renderer/src/assets/llm-icons/ppio-brand.svg new file mode 100644 index 0000000..895e1cc --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ppio-brand.svg @@ -0,0 +1 @@ +PPIO \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ppio-color.svg b/src/renderer/src/assets/llm-icons/ppio-color.svg new file mode 100644 index 0000000..988fc6e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ppio-color.svg @@ -0,0 +1 @@ +PPIO \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ppio-text-cn.svg b/src/renderer/src/assets/llm-icons/ppio-text-cn.svg new file mode 100644 index 0000000..ef73c89 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ppio-text-cn.svg @@ -0,0 +1 @@ +PPIO \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ppio-text.svg b/src/renderer/src/assets/llm-icons/ppio-text.svg new file mode 100644 index 0000000..53dabe0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ppio-text.svg @@ -0,0 +1 @@ +PPIO \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/ppio.svg b/src/renderer/src/assets/llm-icons/ppio.svg new file mode 100644 index 0000000..4d845d7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/ppio.svg @@ -0,0 +1 @@ +PPIO \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/qingyan-color.svg b/src/renderer/src/assets/llm-icons/qingyan-color.svg new file mode 100644 index 0000000..f7ea2c1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qingyan-color.svg @@ -0,0 +1 @@ +Qingyan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/qingyan-text.svg b/src/renderer/src/assets/llm-icons/qingyan-text.svg new file mode 100644 index 0000000..ba4040a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qingyan-text.svg @@ -0,0 +1 @@ +Qingyan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/qingyan.svg b/src/renderer/src/assets/llm-icons/qingyan.svg new file mode 100644 index 0000000..161265e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qingyan.svg @@ -0,0 +1 @@ +Qingyan \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/qiniu.svg b/src/renderer/src/assets/llm-icons/qiniu.svg new file mode 100644 index 0000000..2500531 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qiniu.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/src/assets/llm-icons/qwen-color.svg b/src/renderer/src/assets/llm-icons/qwen-color.svg new file mode 100644 index 0000000..e1199f8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qwen-color.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/qwen-text.svg b/src/renderer/src/assets/llm-icons/qwen-text.svg new file mode 100644 index 0000000..8d47837 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qwen-text.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/qwen.svg b/src/renderer/src/assets/llm-icons/qwen.svg new file mode 100644 index 0000000..a4bb382 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/qwen.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/recraft-text.svg b/src/renderer/src/assets/llm-icons/recraft-text.svg new file mode 100644 index 0000000..60a1286 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/recraft-text.svg @@ -0,0 +1 @@ +Recraft \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/recraft.svg b/src/renderer/src/assets/llm-icons/recraft.svg new file mode 100644 index 0000000..b4ea390 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/recraft.svg @@ -0,0 +1 @@ +Recraft \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/replicate-brand.svg b/src/renderer/src/assets/llm-icons/replicate-brand.svg new file mode 100644 index 0000000..ee7d8a1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/replicate-brand.svg @@ -0,0 +1 @@ +Replicate \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/replicate.svg b/src/renderer/src/assets/llm-icons/replicate.svg new file mode 100644 index 0000000..48f148a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/replicate.svg @@ -0,0 +1 @@ +Replicate \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/replit-color.svg b/src/renderer/src/assets/llm-icons/replit-color.svg new file mode 100644 index 0000000..7a80400 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/replit-color.svg @@ -0,0 +1 @@ +Replit \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/replit-text.svg b/src/renderer/src/assets/llm-icons/replit-text.svg new file mode 100644 index 0000000..72fef87 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/replit-text.svg @@ -0,0 +1 @@ +Replit \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/replit.svg b/src/renderer/src/assets/llm-icons/replit.svg new file mode 100644 index 0000000..496bb08 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/replit.svg @@ -0,0 +1 @@ +Replit \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/runway-text.svg b/src/renderer/src/assets/llm-icons/runway-text.svg new file mode 100644 index 0000000..16c0dee --- /dev/null +++ b/src/renderer/src/assets/llm-icons/runway-text.svg @@ -0,0 +1 @@ +Runway \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/runway.svg b/src/renderer/src/assets/llm-icons/runway.svg new file mode 100644 index 0000000..034add5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/runway.svg @@ -0,0 +1 @@ +Runway \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/rwkv-color.svg b/src/renderer/src/assets/llm-icons/rwkv-color.svg new file mode 100644 index 0000000..dc3aa7d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/rwkv-color.svg @@ -0,0 +1 @@ +RWKV \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/rwkv-text.svg b/src/renderer/src/assets/llm-icons/rwkv-text.svg new file mode 100644 index 0000000..7cb30ec --- /dev/null +++ b/src/renderer/src/assets/llm-icons/rwkv-text.svg @@ -0,0 +1 @@ +RWKV \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/rwkv.svg b/src/renderer/src/assets/llm-icons/rwkv.svg new file mode 100644 index 0000000..1712de6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/rwkv.svg @@ -0,0 +1 @@ +RWKV \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sensenova-brand-color.svg b/src/renderer/src/assets/llm-icons/sensenova-brand-color.svg new file mode 100644 index 0000000..006ba38 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sensenova-brand-color.svg @@ -0,0 +1 @@ +SenseNova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sensenova-brand.svg b/src/renderer/src/assets/llm-icons/sensenova-brand.svg new file mode 100644 index 0000000..548ab3d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sensenova-brand.svg @@ -0,0 +1 @@ +SenseNova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sensenova-color.svg b/src/renderer/src/assets/llm-icons/sensenova-color.svg new file mode 100644 index 0000000..17f1d82 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sensenova-color.svg @@ -0,0 +1 @@ +SenseNova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sensenova-text.svg b/src/renderer/src/assets/llm-icons/sensenova-text.svg new file mode 100644 index 0000000..ec67ca8 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sensenova-text.svg @@ -0,0 +1 @@ +SenseNova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sensenova.svg b/src/renderer/src/assets/llm-icons/sensenova.svg new file mode 100644 index 0000000..81bba1a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sensenova.svg @@ -0,0 +1 @@ +SenseNova \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/siliconcloud-color.svg b/src/renderer/src/assets/llm-icons/siliconcloud-color.svg new file mode 100644 index 0000000..c678506 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/siliconcloud-color.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/siliconcloud-text.svg b/src/renderer/src/assets/llm-icons/siliconcloud-text.svg new file mode 100644 index 0000000..580ba8f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/siliconcloud-text.svg @@ -0,0 +1 @@ +SiliconCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/siliconcloud.svg b/src/renderer/src/assets/llm-icons/siliconcloud.svg new file mode 100644 index 0000000..a64429b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/siliconcloud.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/src/renderer/src/assets/llm-icons/spark-color.svg b/src/renderer/src/assets/llm-icons/spark-color.svg new file mode 100644 index 0000000..50c8fae --- /dev/null +++ b/src/renderer/src/assets/llm-icons/spark-color.svg @@ -0,0 +1 @@ +Spark \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/spark-text.svg b/src/renderer/src/assets/llm-icons/spark-text.svg new file mode 100644 index 0000000..7839cbd --- /dev/null +++ b/src/renderer/src/assets/llm-icons/spark-text.svg @@ -0,0 +1 @@ +Spark \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/spark.svg b/src/renderer/src/assets/llm-icons/spark.svg new file mode 100644 index 0000000..d2d43d9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/spark.svg @@ -0,0 +1 @@ +Spark \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stability-brand-color.svg b/src/renderer/src/assets/llm-icons/stability-brand-color.svg new file mode 100644 index 0000000..2726cee --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stability-brand-color.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stability-brand.svg b/src/renderer/src/assets/llm-icons/stability-brand.svg new file mode 100644 index 0000000..566e60d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stability-brand.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stability-color.svg b/src/renderer/src/assets/llm-icons/stability-color.svg new file mode 100644 index 0000000..b418e75 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stability-color.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stability-text.svg b/src/renderer/src/assets/llm-icons/stability-text.svg new file mode 100644 index 0000000..e571a68 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stability-text.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stability.svg b/src/renderer/src/assets/llm-icons/stability.svg new file mode 100644 index 0000000..da67fe9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stability.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stepfun-color.svg b/src/renderer/src/assets/llm-icons/stepfun-color.svg new file mode 100644 index 0000000..7098952 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stepfun-color.svg @@ -0,0 +1 @@ +Stepfun \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stepfun-text.svg b/src/renderer/src/assets/llm-icons/stepfun-text.svg new file mode 100644 index 0000000..d5ce750 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stepfun-text.svg @@ -0,0 +1 @@ +Stepfun \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/stepfun.svg b/src/renderer/src/assets/llm-icons/stepfun.svg new file mode 100644 index 0000000..a34f14e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/stepfun.svg @@ -0,0 +1 @@ +Stepfun \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/suno-text.svg b/src/renderer/src/assets/llm-icons/suno-text.svg new file mode 100644 index 0000000..5b1f1d5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/suno-text.svg @@ -0,0 +1 @@ +Suno \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/suno.svg b/src/renderer/src/assets/llm-icons/suno.svg new file mode 100644 index 0000000..f73f29c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/suno.svg @@ -0,0 +1 @@ +Suno \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sync-text.svg b/src/renderer/src/assets/llm-icons/sync-text.svg new file mode 100644 index 0000000..9464918 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sync-text.svg @@ -0,0 +1 @@ +Sync \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/sync.svg b/src/renderer/src/assets/llm-icons/sync.svg new file mode 100644 index 0000000..2089b0b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/sync.svg @@ -0,0 +1 @@ +Sync \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencent-brand-color.svg b/src/renderer/src/assets/llm-icons/tencent-brand-color.svg new file mode 100644 index 0000000..f52f56b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencent-brand-color.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencent-brand.svg b/src/renderer/src/assets/llm-icons/tencent-brand.svg new file mode 100644 index 0000000..799661b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencent-brand.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencent-color.svg b/src/renderer/src/assets/llm-icons/tencent-color.svg new file mode 100644 index 0000000..98da272 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencent-color.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencent-text-cn.svg b/src/renderer/src/assets/llm-icons/tencent-text-cn.svg new file mode 100644 index 0000000..0b7ea34 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencent-text-cn.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencent-text.svg b/src/renderer/src/assets/llm-icons/tencent-text.svg new file mode 100644 index 0000000..44b4782 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencent-text.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencent.svg b/src/renderer/src/assets/llm-icons/tencent.svg new file mode 100644 index 0000000..135a0c0 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencent.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencentcloud-color.svg b/src/renderer/src/assets/llm-icons/tencentcloud-color.svg new file mode 100644 index 0000000..4e7d033 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencentcloud-color.svg @@ -0,0 +1 @@ +TencentCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencentcloud-text.svg b/src/renderer/src/assets/llm-icons/tencentcloud-text.svg new file mode 100644 index 0000000..43b142c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencentcloud-text.svg @@ -0,0 +1 @@ +TencentCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tencentcloud.svg b/src/renderer/src/assets/llm-icons/tencentcloud.svg new file mode 100644 index 0000000..c14c91b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tencentcloud.svg @@ -0,0 +1 @@ +TencentCloud \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tiangong-color.svg b/src/renderer/src/assets/llm-icons/tiangong-color.svg new file mode 100644 index 0000000..184897a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tiangong-color.svg @@ -0,0 +1 @@ +Tiangong \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tiangong-text.svg b/src/renderer/src/assets/llm-icons/tiangong-text.svg new file mode 100644 index 0000000..def830e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tiangong-text.svg @@ -0,0 +1 @@ +Tiangong \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tiangong.svg b/src/renderer/src/assets/llm-icons/tiangong.svg new file mode 100644 index 0000000..f88d01a --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tiangong.svg @@ -0,0 +1 @@ +Tiangong \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tii-color.svg b/src/renderer/src/assets/llm-icons/tii-color.svg new file mode 100644 index 0000000..6b42df2 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tii-color.svg @@ -0,0 +1 @@ +Technology Innovation Institute \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tii-text.svg b/src/renderer/src/assets/llm-icons/tii-text.svg new file mode 100644 index 0000000..0fefc36 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tii-text.svg @@ -0,0 +1 @@ +Technology Innovation Institute \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tii.svg b/src/renderer/src/assets/llm-icons/tii.svg new file mode 100644 index 0000000..9ef331c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tii.svg @@ -0,0 +1 @@ +Technology Innovation Institute \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/together-brand-color.svg b/src/renderer/src/assets/llm-icons/together-brand-color.svg new file mode 100644 index 0000000..6f9be50 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/together-brand-color.svg @@ -0,0 +1 @@ +together.ai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/together-brand.svg b/src/renderer/src/assets/llm-icons/together-brand.svg new file mode 100644 index 0000000..887ad7c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/together-brand.svg @@ -0,0 +1 @@ +together.ai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/together-color.svg b/src/renderer/src/assets/llm-icons/together-color.svg new file mode 100644 index 0000000..a853e78 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/together-color.svg @@ -0,0 +1 @@ +together.ai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/together-text.svg b/src/renderer/src/assets/llm-icons/together-text.svg new file mode 100644 index 0000000..887ad7c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/together-text.svg @@ -0,0 +1 @@ +together.ai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/together.svg b/src/renderer/src/assets/llm-icons/together.svg new file mode 100644 index 0000000..76e249c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/together.svg @@ -0,0 +1 @@ +together.ai \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tokenflux-color.svg b/src/renderer/src/assets/llm-icons/tokenflux-color.svg new file mode 100644 index 0000000..c012d74 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tokenflux-color.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + diff --git a/src/renderer/src/assets/llm-icons/tripo-color.svg b/src/renderer/src/assets/llm-icons/tripo-color.svg new file mode 100644 index 0000000..748a22b --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tripo-color.svg @@ -0,0 +1 @@ +Tripo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tripo-text.svg b/src/renderer/src/assets/llm-icons/tripo-text.svg new file mode 100644 index 0000000..7ad059c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tripo-text.svg @@ -0,0 +1 @@ +Tripo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/tripo.svg b/src/renderer/src/assets/llm-icons/tripo.svg new file mode 100644 index 0000000..40f4c37 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/tripo.svg @@ -0,0 +1 @@ +Tripo \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/udio-color.svg b/src/renderer/src/assets/llm-icons/udio-color.svg new file mode 100644 index 0000000..53aa6b6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/udio-color.svg @@ -0,0 +1 @@ +Udio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/udio-text.svg b/src/renderer/src/assets/llm-icons/udio-text.svg new file mode 100644 index 0000000..38c632f --- /dev/null +++ b/src/renderer/src/assets/llm-icons/udio-text.svg @@ -0,0 +1 @@ +Udio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/udio.svg b/src/renderer/src/assets/llm-icons/udio.svg new file mode 100644 index 0000000..3d85bb7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/udio.svg @@ -0,0 +1 @@ +Udio \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/upstage-color.svg b/src/renderer/src/assets/llm-icons/upstage-color.svg new file mode 100644 index 0000000..604d53c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/upstage-color.svg @@ -0,0 +1 @@ +Upsate \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/upstage-text.svg b/src/renderer/src/assets/llm-icons/upstage-text.svg new file mode 100644 index 0000000..1079c15 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/upstage-text.svg @@ -0,0 +1 @@ +Upsate \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/upstage.svg b/src/renderer/src/assets/llm-icons/upstage.svg new file mode 100644 index 0000000..fbe5b22 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/upstage.svg @@ -0,0 +1 @@ +Upsate \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/v0.svg b/src/renderer/src/assets/llm-icons/v0.svg new file mode 100644 index 0000000..97b8129 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/v0.svg @@ -0,0 +1 @@ +V0 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vercel-text.svg b/src/renderer/src/assets/llm-icons/vercel-text.svg new file mode 100644 index 0000000..a40f528 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vercel-text.svg @@ -0,0 +1 @@ +Vercel \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vercel.svg b/src/renderer/src/assets/llm-icons/vercel.svg new file mode 100644 index 0000000..486cb95 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vercel.svg @@ -0,0 +1 @@ +Vercel \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vertexai-color.svg b/src/renderer/src/assets/llm-icons/vertexai-color.svg new file mode 100644 index 0000000..e721368 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vertexai-color.svg @@ -0,0 +1 @@ +VertexAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vertexai-text.svg b/src/renderer/src/assets/llm-icons/vertexai-text.svg new file mode 100644 index 0000000..aaa35d9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vertexai-text.svg @@ -0,0 +1 @@ +VertexAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vertexai.svg b/src/renderer/src/assets/llm-icons/vertexai.svg new file mode 100644 index 0000000..25b47d6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vertexai.svg @@ -0,0 +1 @@ +VertexAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vidu-color.svg b/src/renderer/src/assets/llm-icons/vidu-color.svg new file mode 100644 index 0000000..c826ec5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vidu-color.svg @@ -0,0 +1 @@ +Vidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vidu-text.svg b/src/renderer/src/assets/llm-icons/vidu-text.svg new file mode 100644 index 0000000..b20dbfa --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vidu-text.svg @@ -0,0 +1 @@ +Vidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vidu.svg b/src/renderer/src/assets/llm-icons/vidu.svg new file mode 100644 index 0000000..0cb1182 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vidu.svg @@ -0,0 +1 @@ +Vidu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/viggle-text.svg b/src/renderer/src/assets/llm-icons/viggle-text.svg new file mode 100644 index 0000000..e80cd91 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/viggle-text.svg @@ -0,0 +1 @@ +Viggle \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/viggle.svg b/src/renderer/src/assets/llm-icons/viggle.svg new file mode 100644 index 0000000..fa29619 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/viggle.svg @@ -0,0 +1 @@ +Viggle \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vllm-color.svg b/src/renderer/src/assets/llm-icons/vllm-color.svg new file mode 100644 index 0000000..54acc3d --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vllm-color.svg @@ -0,0 +1 @@ +vLLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vllm-text.svg b/src/renderer/src/assets/llm-icons/vllm-text.svg new file mode 100644 index 0000000..8bfc9d6 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vllm-text.svg @@ -0,0 +1 @@ +vLLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/vllm.svg b/src/renderer/src/assets/llm-icons/vllm.svg new file mode 100644 index 0000000..24eb159 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/vllm.svg @@ -0,0 +1 @@ +vLLM \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/volcengine-color.svg b/src/renderer/src/assets/llm-icons/volcengine-color.svg new file mode 100644 index 0000000..ecf6d75 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/volcengine-color.svg @@ -0,0 +1 @@ +Volcengine \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/volcengine-text.svg b/src/renderer/src/assets/llm-icons/volcengine-text.svg new file mode 100644 index 0000000..8fed542 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/volcengine-text.svg @@ -0,0 +1 @@ +Volcengine \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/volcengine.svg b/src/renderer/src/assets/llm-icons/volcengine.svg new file mode 100644 index 0000000..28556c3 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/volcengine.svg @@ -0,0 +1 @@ +Volcengine \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/wenxin-color.svg b/src/renderer/src/assets/llm-icons/wenxin-color.svg new file mode 100644 index 0000000..e3b48c9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/wenxin-color.svg @@ -0,0 +1 @@ +Wenxin \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/wenxin-text.svg b/src/renderer/src/assets/llm-icons/wenxin-text.svg new file mode 100644 index 0000000..49fa6fd --- /dev/null +++ b/src/renderer/src/assets/llm-icons/wenxin-text.svg @@ -0,0 +1 @@ +Wenxin \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/wenxin.svg b/src/renderer/src/assets/llm-icons/wenxin.svg new file mode 100644 index 0000000..a9d78c9 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/wenxin.svg @@ -0,0 +1 @@ +Wenxin \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/workersai-color.svg b/src/renderer/src/assets/llm-icons/workersai-color.svg new file mode 100644 index 0000000..c5e0daa --- /dev/null +++ b/src/renderer/src/assets/llm-icons/workersai-color.svg @@ -0,0 +1 @@ +WorkersAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/workersai-text.svg b/src/renderer/src/assets/llm-icons/workersai-text.svg new file mode 100644 index 0000000..6dec725 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/workersai-text.svg @@ -0,0 +1 @@ +WorkersAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/workersai.svg b/src/renderer/src/assets/llm-icons/workersai.svg new file mode 100644 index 0000000..52080b4 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/workersai.svg @@ -0,0 +1 @@ +WorkersAI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/xai-text.svg b/src/renderer/src/assets/llm-icons/xai-text.svg new file mode 100644 index 0000000..d00e6e1 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/xai-text.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/xai.svg b/src/renderer/src/assets/llm-icons/xai.svg new file mode 100644 index 0000000..536e713 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/xai.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/xuanyuan-color.svg b/src/renderer/src/assets/llm-icons/xuanyuan-color.svg new file mode 100644 index 0000000..06894aa --- /dev/null +++ b/src/renderer/src/assets/llm-icons/xuanyuan-color.svg @@ -0,0 +1 @@ +轩辕 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/xuanyuan-text.svg b/src/renderer/src/assets/llm-icons/xuanyuan-text.svg new file mode 100644 index 0000000..a2f8c7e --- /dev/null +++ b/src/renderer/src/assets/llm-icons/xuanyuan-text.svg @@ -0,0 +1 @@ +轩辕 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/xuanyuan.svg b/src/renderer/src/assets/llm-icons/xuanyuan.svg new file mode 100644 index 0000000..e415946 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/xuanyuan.svg @@ -0,0 +1 @@ +轩辕 \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/yi-color.svg b/src/renderer/src/assets/llm-icons/yi-color.svg new file mode 100644 index 0000000..c73b642 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/yi-color.svg @@ -0,0 +1 @@ +Yi \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/yi-text.svg b/src/renderer/src/assets/llm-icons/yi-text.svg new file mode 100644 index 0000000..183ef13 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/yi-text.svg @@ -0,0 +1 @@ +01.AI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/yi.svg b/src/renderer/src/assets/llm-icons/yi.svg new file mode 100644 index 0000000..07a0367 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/yi.svg @@ -0,0 +1 @@ +Yi \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zeabur-color.svg b/src/renderer/src/assets/llm-icons/zeabur-color.svg new file mode 100644 index 0000000..165ebb7 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zeabur-color.svg @@ -0,0 +1 @@ +Zeabur \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zeabur-text.svg b/src/renderer/src/assets/llm-icons/zeabur-text.svg new file mode 100644 index 0000000..0af8406 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zeabur-text.svg @@ -0,0 +1 @@ +Zeabur \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zeabur.svg b/src/renderer/src/assets/llm-icons/zeabur.svg new file mode 100644 index 0000000..4fc8137 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zeabur.svg @@ -0,0 +1 @@ +Zeabur \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zeroone-text.svg b/src/renderer/src/assets/llm-icons/zeroone-text.svg new file mode 100644 index 0000000..183ef13 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zeroone-text.svg @@ -0,0 +1 @@ +01.AI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zeroone.svg b/src/renderer/src/assets/llm-icons/zeroone.svg new file mode 100644 index 0000000..ab67ec5 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zeroone.svg @@ -0,0 +1 @@ +01.AI \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zhipu-color.svg b/src/renderer/src/assets/llm-icons/zhipu-color.svg new file mode 100644 index 0000000..0c6e61c --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zhipu-color.svg @@ -0,0 +1 @@ +Zhipu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zhipu-text.svg b/src/renderer/src/assets/llm-icons/zhipu-text.svg new file mode 100644 index 0000000..277cbee --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zhipu-text.svg @@ -0,0 +1 @@ +Zhipu \ No newline at end of file diff --git a/src/renderer/src/assets/llm-icons/zhipu.svg b/src/renderer/src/assets/llm-icons/zhipu.svg new file mode 100644 index 0000000..f7b1252 --- /dev/null +++ b/src/renderer/src/assets/llm-icons/zhipu.svg @@ -0,0 +1 @@ +Zhipu \ No newline at end of file diff --git a/src/renderer/src/assets/logo-dark.png b/src/renderer/src/assets/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a652d4ce444167bba1844722233bdc7655800a21 GIT binary patch literal 23218 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 literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/logo.png b/src/renderer/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e12b67382e78b7526f4d10aee5ecd86e0720306f GIT binary patch literal 53089 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℘s- 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 literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css new file mode 100644 index 0000000..3302f2a --- /dev/null +++ b/src/renderer/src/assets/main.css @@ -0,0 +1 @@ +@import './style.css'; diff --git a/src/renderer/src/assets/mcp-icons/higress.avif b/src/renderer/src/assets/mcp-icons/higress.avif new file mode 100644 index 0000000000000000000000000000000000000000..f8cfc8de76c3c164cc876016e5981a2428e4caae GIT binary patch literal 1455 zcmZQzV30{GsVqn=%S>Yc0uY^>nP!-qnF!=F&d5zINd$=lfnr8VP7#F3z)+BxTmoam zXug8Xl3Xx{5lEV3=Hw@XcrFeMOh5<(jC&YBGC;s&31KmRg^4ik0t#DXX6AvN0ah;x zvp18L#p{G6h^AVbV4L~8D#4hrXZ)ue_sgv-V$}^SF_DVJ5AAW!$VW2 zm6>jv>9}p9jL|f$<5s?A&(cg7mu5`$e-P@m#_jdJ9s6ptQ!V!`c;3>JRIuNmsZw-? zL!a3yoyM@K>V7>lB~9&09$rhidTagXqmI(mDdq$GbeIrM|{steyA9@j;FM1A4mJi9@UJy zOmjs}_pMmG-_Cir@29het}a^Um@Ydt`XR?PR_6xiPpWk<`ApBRv|kt4CUR_DZF0Vz z!nxCL`xpLNad<*!$e;eT20t|4^r~`*eW`h*E86-{tl#_l-UrN9CvSdLn7H^*>tu!b z`<-GnS^YC?KD^*{i)qJ{{7IeGOkojnm%aup>ts8Sc~qON<=3_*kp+Jy*8cpw!&Nx# z>>XD%DURfQR!agG)laRCF>lSfv;4@ts%|&edX3M;zYAYGCd}Lzdi=1HSD(PX2*LQ* zG9oqJGM>)y=Mp)4Wt3#+o;eu7`qlCE?GNd{Y!*}*d=O?cyn9sjSf};7o1Dx^$LlYs z{AZfk8Iu4^IGh%sgu}?oHo<5Wvx^CXBMV2sMY-rZ9oIexXJ50AzW3JfyzKg%@Q(ts zud>;FeNr~q3C0_5BxUEzTzSG~&DwG0 z-z3{ZEOFOQCO=%FbawyiZl{<}g$I=St~i;0+}NS%qPY6(f@GB$D{Q0d|1Wa2a>-x+ zfAK%RFJBB*wEu*e?LO%2AAi4u^@l>(PjiVz6Q$0C0!jPaOev!rvy0v4#=B?q>@*hI zv|Mrf%CNwLKRLWVR%+H?KDSBe|Jy_A?~0-on6Eq8ay@CEp?h_8%%jz3BSK^PCKk+@ zIOWe~$yauffBtevDxcV9yTNsPIzy!3$`HpD`7GOubBdHW{ntu%^1PD#otSSZQ1xPE z$oAry)<4n<(&jrKa^#e`yYSy)#)!YqBzWI`GfP?3@WpA?E^bc2>UoOhNB313wud1+>H#RMLmz1_5 F0RU71Fl7J$ literal 0 HcmV?d00001 diff --git a/src/renderer/src/assets/style.css b/src/renderer/src/assets/style.css new file mode 100644 index 0000000..fd8e685 --- /dev/null +++ b/src/renderer/src/assets/style.css @@ -0,0 +1,153 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@plugin @tailwindcss/typography; + +@font-face { + font-family: 'Geist'; + src: url('./geist.ttf') format('truetype'); +} + +@layer base { + :root { + --base-50: 210 28% 100%; + --base-100: 210 28% 98.1%; + --base-200: 214.3 21.44% 93.4%; + --base-300: 212.7 17.44% 85.9%; + --base-400: 215 12.16% 67.1%; + --base-500: 215.4 9.04% 48.9%; + --base-600: 215.3 11.44% 36.5%; + --base-700: 215.3 16% 28.7%; + --base-800: 217.2 22.08% 19.5%; + --base-900: 222.2 33.92% 13.2%; + --base-950: 222.2 33.92% 13.2%; + --base-1000: 0 0% 1.2%; + + --primary-50: 225.9 100% 96.7%; + --primary-100: 226.5 100% 93.9%; + --primary-200: 228 96.5% 88.8%; + --primary-300: 229.7 93.5% 81.8%; + --primary-400: 234.5 89.5% 73.9%; + --primary-500: 238.7 83.5% 66.7%; + --primary-600: 243.4 75.4% 58.6%; + --primary-700: 244.5 57.9% 50.6%; + --primary-800: 243.7 54.5% 41.4%; + --primary-900: 242.2 47.4% 34.3%; + --primary-950: 242.2 47.4% 34.3%; + --primary-1000: 249 55.9% 13.3%; + + --white: 0 0% 100%; + --black: 0 0% 0%; + + --container: var(--white); + --background: var(--white); + --foreground: var(--base-800); + --card: 0 0% 100%; + --card-foreground: var(--base-800); + --popover: 0 0% 100%; + --popover-foreground: var(--base-800); + --primary: var(--primary-600); + --primary-foreground: 0 0% 100%; + --secondary: var(--base-200); + --secondary-foreground: var(--base-700); + --muted: var(--base-100); + --muted-foreground: var(--base-400); + --accent: var(--base-100); + --accent-foreground: var(--base-800); + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: var(--base-200); + --input: var(--base-300); + --ring: var(--primary-600); + --chart-1: var(--primary-600); + --chart-2: var(--primary-200); + --chart-3: var(--primary-400); + --chart-4: var(--primary-300); + --chart-5: var(--primary-100); + --radius: 0.75rem; + --sidebar: 0 0% 100%; + --sidebar-foreground: var(--base-800); + --sidebar-primary: var(--primary-600); + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: var(--base-50); + --sidebar-accent-foreground: var(--base-800); + --sidebar-border: var(--base-200); + --sidebar-ring: var(--primary-600); + + --display-weight: 700; + --text-weight: 400; + + --usage-low: 142 71% 45%; /* 亮绿 */ + --usage-mid: 48 96% 53%; /* 亮黄 */ + --usage-high: 0 72% 51%; /* 亮红 */ + } + + .dark { + --container: var(--black); + --background: var(--base-950); + --foreground: var(--base-200); + --card: var(--base-900); + --card-foreground: var(--base-200); + --popover: var(--base-900); + --popover-foreground: var(--base-200); + --primary: var(--primary-600); + --primary-foreground: 0 0% 100%; + --secondary: var(--base-700); + --secondary-foreground: var(--base-50); + --muted: var(--base-800); + --muted-foreground: var(--base-500); + --accent: var(--base-800); + --accent-foreground: var(--base-200); + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: var(--base-800); + --input: var(--base-700); + --ring: var(--primary-600); + --chart-1: var(--primary-600); + --chart-2: var(--primary-200); + --chart-3: var(--primary-400); + --chart-4: var(--primary-300); + --chart-5: var(--primary-100); + --sidebar: var(--base-900); + --sidebar-foreground: var(--base-200); + --sidebar-primary: var(--primary-600); + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: var(--base-800); + --sidebar-accent-foreground: var(--base-200); + --sidebar-border: var(--base-800); + --sidebar-ring: var(--primary-600); + + --usage-low: 142 40% 60%; /* 暗绿 */ + --usage-mid: 48 80% 60%; /* 暗黄 */ + --usage-high: 0 70% 65%; /* 暗红 */ + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-transparent text-foreground font-text; + } + html { + font-family: + 'Geist', + Noto Sans, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji'; + } +} + +@layer base { + .font-display { + font-weight: var(--display-weight); + } + .font-text { + font-weight: var(--text-weight); + } +} diff --git a/src/renderer/src/components/ChatConfig.vue b/src/renderer/src/components/ChatConfig.vue new file mode 100644 index 0000000..75d6ec6 --- /dev/null +++ b/src/renderer/src/components/ChatConfig.vue @@ -0,0 +1,739 @@ + + + + + diff --git a/src/renderer/src/components/message/MessageList.vue b/src/renderer/src/components/message/MessageList.vue new file mode 100644 index 0000000..9e0d6af --- /dev/null +++ b/src/renderer/src/components/message/MessageList.vue @@ -0,0 +1,486 @@ + + + + + diff --git a/src/renderer/src/components/message/MessageTextContent.vue b/src/renderer/src/components/message/MessageTextContent.vue new file mode 100644 index 0000000..1b0037c --- /dev/null +++ b/src/renderer/src/components/message/MessageTextContent.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/message/MessageToolbar.vue b/src/renderer/src/components/message/MessageToolbar.vue new file mode 100644 index 0000000..5fec7b5 --- /dev/null +++ b/src/renderer/src/components/message/MessageToolbar.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/renderer/src/components/message/ReferencePreview.vue b/src/renderer/src/components/message/ReferencePreview.vue new file mode 100644 index 0000000..639adea --- /dev/null +++ b/src/renderer/src/components/message/ReferencePreview.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/renderer/src/components/message/SelectedTextContextMenu.vue b/src/renderer/src/components/message/SelectedTextContextMenu.vue new file mode 100644 index 0000000..e8e9684 --- /dev/null +++ b/src/renderer/src/components/message/SelectedTextContextMenu.vue @@ -0,0 +1,39 @@ + + + + diff --git a/src/renderer/src/components/popup/TranslatePopup.vue b/src/renderer/src/components/popup/TranslatePopup.vue new file mode 100644 index 0000000..d14eaa9 --- /dev/null +++ b/src/renderer/src/components/popup/TranslatePopup.vue @@ -0,0 +1,159 @@ + + + + + + diff --git a/src/renderer/src/components/settings/AboutUsSettings.vue b/src/renderer/src/components/settings/AboutUsSettings.vue new file mode 100644 index 0000000..0487240 --- /dev/null +++ b/src/renderer/src/components/settings/AboutUsSettings.vue @@ -0,0 +1,243 @@ + + + diff --git a/src/renderer/src/components/settings/AddCustomProviderDialog.vue b/src/renderer/src/components/settings/AddCustomProviderDialog.vue new file mode 100644 index 0000000..426aba7 --- /dev/null +++ b/src/renderer/src/components/settings/AddCustomProviderDialog.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue b/src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue new file mode 100644 index 0000000..304a596 --- /dev/null +++ b/src/renderer/src/components/settings/AnthropicProviderSettingsDetail.vue @@ -0,0 +1,730 @@ + + + diff --git a/src/renderer/src/components/settings/AzureProviderConfig.vue b/src/renderer/src/components/settings/AzureProviderConfig.vue new file mode 100644 index 0000000..0b0fdcd --- /dev/null +++ b/src/renderer/src/components/settings/AzureProviderConfig.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/renderer/src/components/settings/BedrockProviderSettingsDetail.vue b/src/renderer/src/components/settings/BedrockProviderSettingsDetail.vue new file mode 100644 index 0000000..9ee948d --- /dev/null +++ b/src/renderer/src/components/settings/BedrockProviderSettingsDetail.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue b/src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue new file mode 100644 index 0000000..ce2d3f7 --- /dev/null +++ b/src/renderer/src/components/settings/BuiltinKnowledgeSettings.vue @@ -0,0 +1,1071 @@ + + + diff --git a/src/renderer/src/components/settings/CommonSettings.vue b/src/renderer/src/components/settings/CommonSettings.vue new file mode 100644 index 0000000..20d09f5 --- /dev/null +++ b/src/renderer/src/components/settings/CommonSettings.vue @@ -0,0 +1,846 @@ + + + + diff --git a/src/renderer/src/components/settings/DataSettings.vue b/src/renderer/src/components/settings/DataSettings.vue new file mode 100644 index 0000000..5918faf --- /dev/null +++ b/src/renderer/src/components/settings/DataSettings.vue @@ -0,0 +1,342 @@ + + + diff --git a/src/renderer/src/components/settings/DifyKnowledgeSettings.vue b/src/renderer/src/components/settings/DifyKnowledgeSettings.vue new file mode 100644 index 0000000..1f559a0 --- /dev/null +++ b/src/renderer/src/components/settings/DifyKnowledgeSettings.vue @@ -0,0 +1,439 @@ + + + diff --git a/src/renderer/src/components/settings/DisplaySettings.vue b/src/renderer/src/components/settings/DisplaySettings.vue new file mode 100644 index 0000000..75ffc51 --- /dev/null +++ b/src/renderer/src/components/settings/DisplaySettings.vue @@ -0,0 +1,252 @@ + + + diff --git a/src/renderer/src/components/settings/FastGptKnowledgeSettings.vue b/src/renderer/src/components/settings/FastGptKnowledgeSettings.vue new file mode 100644 index 0000000..406e5f7 --- /dev/null +++ b/src/renderer/src/components/settings/FastGptKnowledgeSettings.vue @@ -0,0 +1,438 @@ + + + diff --git a/src/renderer/src/components/settings/GeminiSafetyConfig.vue b/src/renderer/src/components/settings/GeminiSafetyConfig.vue new file mode 100644 index 0000000..79fb4ca --- /dev/null +++ b/src/renderer/src/components/settings/GeminiSafetyConfig.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/renderer/src/components/settings/GitHubCopilotOAuth.vue b/src/renderer/src/components/settings/GitHubCopilotOAuth.vue new file mode 100644 index 0000000..4f148aa --- /dev/null +++ b/src/renderer/src/components/settings/GitHubCopilotOAuth.vue @@ -0,0 +1,270 @@ + + + diff --git a/src/renderer/src/components/settings/KnowledgeBaseSettings.vue b/src/renderer/src/components/settings/KnowledgeBaseSettings.vue new file mode 100644 index 0000000..4230712 --- /dev/null +++ b/src/renderer/src/components/settings/KnowledgeBaseSettings.vue @@ -0,0 +1,204 @@ + + + diff --git a/src/renderer/src/components/settings/KnowledgeFile.vue b/src/renderer/src/components/settings/KnowledgeFile.vue new file mode 100644 index 0000000..89810fc --- /dev/null +++ b/src/renderer/src/components/settings/KnowledgeFile.vue @@ -0,0 +1,442 @@ + + + diff --git a/src/renderer/src/components/settings/KnowledgeFileItem.vue b/src/renderer/src/components/settings/KnowledgeFileItem.vue new file mode 100644 index 0000000..3eca04c --- /dev/null +++ b/src/renderer/src/components/settings/KnowledgeFileItem.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/renderer/src/components/settings/McpBuiltinMarket.vue b/src/renderer/src/components/settings/McpBuiltinMarket.vue new file mode 100644 index 0000000..faa382d --- /dev/null +++ b/src/renderer/src/components/settings/McpBuiltinMarket.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/renderer/src/components/settings/McpSettings.vue b/src/renderer/src/components/settings/McpSettings.vue new file mode 100644 index 0000000..9338fa6 --- /dev/null +++ b/src/renderer/src/components/settings/McpSettings.vue @@ -0,0 +1,458 @@ + + + diff --git a/src/renderer/src/components/settings/ModelCheckDialog.vue b/src/renderer/src/components/settings/ModelCheckDialog.vue new file mode 100644 index 0000000..6a2d4ac --- /dev/null +++ b/src/renderer/src/components/settings/ModelCheckDialog.vue @@ -0,0 +1,206 @@ + + + diff --git a/src/renderer/src/components/settings/ModelConfigDialog.vue b/src/renderer/src/components/settings/ModelConfigDialog.vue new file mode 100644 index 0000000..47871bc --- /dev/null +++ b/src/renderer/src/components/settings/ModelConfigDialog.vue @@ -0,0 +1,913 @@ + + + diff --git a/src/renderer/src/components/settings/ModelConfigItem.vue b/src/renderer/src/components/settings/ModelConfigItem.vue new file mode 100644 index 0000000..9cfbe8b --- /dev/null +++ b/src/renderer/src/components/settings/ModelConfigItem.vue @@ -0,0 +1,125 @@ + + + diff --git a/src/renderer/src/components/settings/ModelProviderSettings.vue b/src/renderer/src/components/settings/ModelProviderSettings.vue new file mode 100644 index 0000000..5174d15 --- /dev/null +++ b/src/renderer/src/components/settings/ModelProviderSettings.vue @@ -0,0 +1,349 @@ + + + + + diff --git a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue new file mode 100644 index 0000000..4d38c27 --- /dev/null +++ b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue @@ -0,0 +1,388 @@ + + + diff --git a/src/renderer/src/components/settings/ModelScopeMcpSync.vue b/src/renderer/src/components/settings/ModelScopeMcpSync.vue new file mode 100644 index 0000000..16b4734 --- /dev/null +++ b/src/renderer/src/components/settings/ModelScopeMcpSync.vue @@ -0,0 +1,167 @@ + + + diff --git a/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue b/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue new file mode 100644 index 0000000..bdcb180 --- /dev/null +++ b/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue @@ -0,0 +1,923 @@ + + + diff --git a/src/renderer/src/components/settings/PromptSetting.vue b/src/renderer/src/components/settings/PromptSetting.vue new file mode 100644 index 0000000..bd42a0a --- /dev/null +++ b/src/renderer/src/components/settings/PromptSetting.vue @@ -0,0 +1,1368 @@ + + + + + diff --git a/src/renderer/src/components/settings/ProviderApiConfig.vue b/src/renderer/src/components/settings/ProviderApiConfig.vue new file mode 100644 index 0000000..67f46dc --- /dev/null +++ b/src/renderer/src/components/settings/ProviderApiConfig.vue @@ -0,0 +1,233 @@ + + + diff --git a/src/renderer/src/components/settings/ProviderDialogContainer.vue b/src/renderer/src/components/settings/ProviderDialogContainer.vue new file mode 100644 index 0000000..0e2c322 --- /dev/null +++ b/src/renderer/src/components/settings/ProviderDialogContainer.vue @@ -0,0 +1,157 @@ + + + diff --git a/src/renderer/src/components/settings/ProviderModelList.vue b/src/renderer/src/components/settings/ProviderModelList.vue new file mode 100644 index 0000000..835614d --- /dev/null +++ b/src/renderer/src/components/settings/ProviderModelList.vue @@ -0,0 +1,272 @@ + + diff --git a/src/renderer/src/components/settings/ProviderModelManager.vue b/src/renderer/src/components/settings/ProviderModelManager.vue new file mode 100644 index 0000000..bf4a204 --- /dev/null +++ b/src/renderer/src/components/settings/ProviderModelManager.vue @@ -0,0 +1,96 @@ + + + diff --git a/src/renderer/src/components/settings/ProviderRateLimitConfig.vue b/src/renderer/src/components/settings/ProviderRateLimitConfig.vue new file mode 100644 index 0000000..e980c8b --- /dev/null +++ b/src/renderer/src/components/settings/ProviderRateLimitConfig.vue @@ -0,0 +1,283 @@ + + + diff --git a/src/renderer/src/components/settings/RagflowKnowledgeSettings.vue b/src/renderer/src/components/settings/RagflowKnowledgeSettings.vue new file mode 100644 index 0000000..6a2afeb --- /dev/null +++ b/src/renderer/src/components/settings/RagflowKnowledgeSettings.vue @@ -0,0 +1,456 @@ + + + diff --git a/src/renderer/src/components/settings/ShortcutSettings.vue b/src/renderer/src/components/settings/ShortcutSettings.vue new file mode 100644 index 0000000..9dc37b0 --- /dev/null +++ b/src/renderer/src/components/settings/ShortcutSettings.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/src/renderer/src/components/ui/MessageDialog.vue b/src/renderer/src/components/ui/MessageDialog.vue new file mode 100644 index 0000000..d7c9a8c --- /dev/null +++ b/src/renderer/src/components/ui/MessageDialog.vue @@ -0,0 +1,99 @@ + + + diff --git a/src/renderer/src/components/ui/UpdateDialog.vue b/src/renderer/src/components/ui/UpdateDialog.vue new file mode 100644 index 0000000..68d8a49 --- /dev/null +++ b/src/renderer/src/components/ui/UpdateDialog.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/renderer/src/components/ui/accordion/Accordion.vue b/src/renderer/src/components/ui/accordion/Accordion.vue new file mode 100644 index 0000000..1cd25bb --- /dev/null +++ b/src/renderer/src/components/ui/accordion/Accordion.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/accordion/AccordionContent.vue b/src/renderer/src/components/ui/accordion/AccordionContent.vue new file mode 100644 index 0000000..448be78 --- /dev/null +++ b/src/renderer/src/components/ui/accordion/AccordionContent.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/renderer/src/components/ui/accordion/AccordionItem.vue b/src/renderer/src/components/ui/accordion/AccordionItem.vue new file mode 100644 index 0000000..d0875a9 --- /dev/null +++ b/src/renderer/src/components/ui/accordion/AccordionItem.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/renderer/src/components/ui/accordion/AccordionTrigger.vue b/src/renderer/src/components/ui/accordion/AccordionTrigger.vue new file mode 100644 index 0000000..45889b9 --- /dev/null +++ b/src/renderer/src/components/ui/accordion/AccordionTrigger.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/renderer/src/components/ui/accordion/index.ts b/src/renderer/src/components/ui/accordion/index.ts new file mode 100644 index 0000000..9340ac0 --- /dev/null +++ b/src/renderer/src/components/ui/accordion/index.ts @@ -0,0 +1,4 @@ +export { default as Accordion } from './Accordion.vue' +export { default as AccordionContent } from './AccordionContent.vue' +export { default as AccordionItem } from './AccordionItem.vue' +export { default as AccordionTrigger } from './AccordionTrigger.vue' diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialog.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialog.vue new file mode 100644 index 0000000..8fb30de --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialog.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogAction.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogAction.vue new file mode 100644 index 0000000..60e5af7 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogAction.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogCancel.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogCancel.vue new file mode 100644 index 0000000..61c5bb8 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogCancel.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogContent.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogContent.vue new file mode 100644 index 0000000..a1f7dd5 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogContent.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogDescription.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogDescription.vue new file mode 100644 index 0000000..9f6db43 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogDescription.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogFooter.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogFooter.vue new file mode 100644 index 0000000..55d0a0e --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogFooter.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogHeader.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogHeader.vue new file mode 100644 index 0000000..c61c449 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogTitle.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogTitle.vue new file mode 100644 index 0000000..6cd13f9 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogTitle.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/AlertDialogTrigger.vue b/src/renderer/src/components/ui/alert-dialog/AlertDialogTrigger.vue new file mode 100644 index 0000000..4f5e2fd --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/AlertDialogTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/alert-dialog/index.ts b/src/renderer/src/components/ui/alert-dialog/index.ts new file mode 100644 index 0000000..448d519 --- /dev/null +++ b/src/renderer/src/components/ui/alert-dialog/index.ts @@ -0,0 +1,9 @@ +export { default as AlertDialog } from './AlertDialog.vue' +export { default as AlertDialogAction } from './AlertDialogAction.vue' +export { default as AlertDialogCancel } from './AlertDialogCancel.vue' +export { default as AlertDialogContent } from './AlertDialogContent.vue' +export { default as AlertDialogDescription } from './AlertDialogDescription.vue' +export { default as AlertDialogFooter } from './AlertDialogFooter.vue' +export { default as AlertDialogHeader } from './AlertDialogHeader.vue' +export { default as AlertDialogTitle } from './AlertDialogTitle.vue' +export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue' diff --git a/src/renderer/src/components/ui/alert/Alert.vue b/src/renderer/src/components/ui/alert/Alert.vue new file mode 100644 index 0000000..964d61d --- /dev/null +++ b/src/renderer/src/components/ui/alert/Alert.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/alert/AlertDescription.vue b/src/renderer/src/components/ui/alert/AlertDescription.vue new file mode 100644 index 0000000..2971a87 --- /dev/null +++ b/src/renderer/src/components/ui/alert/AlertDescription.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/alert/AlertTitle.vue b/src/renderer/src/components/ui/alert/AlertTitle.vue new file mode 100644 index 0000000..e28dd37 --- /dev/null +++ b/src/renderer/src/components/ui/alert/AlertTitle.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/alert/index.ts b/src/renderer/src/components/ui/alert/index.ts new file mode 100644 index 0000000..eab6356 --- /dev/null +++ b/src/renderer/src/components/ui/alert/index.ts @@ -0,0 +1,23 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as Alert } from './Alert.vue' +export { default as AlertDescription } from './AlertDescription.vue' +export { default as AlertTitle } from './AlertTitle.vue' + +export const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', + { + variants: { + variant: { + default: 'bg-background text-foreground', + destructive: + 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +export type AlertVariants = VariantProps diff --git a/src/renderer/src/components/ui/aspect-ratio/AspectRatio.vue b/src/renderer/src/components/ui/aspect-ratio/AspectRatio.vue new file mode 100644 index 0000000..6529181 --- /dev/null +++ b/src/renderer/src/components/ui/aspect-ratio/AspectRatio.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/aspect-ratio/index.ts b/src/renderer/src/components/ui/aspect-ratio/index.ts new file mode 100644 index 0000000..3faf121 --- /dev/null +++ b/src/renderer/src/components/ui/aspect-ratio/index.ts @@ -0,0 +1 @@ +export { default as AspectRatio } from './AspectRatio.vue' diff --git a/src/renderer/src/components/ui/avatar/Avatar.vue b/src/renderer/src/components/ui/avatar/Avatar.vue new file mode 100644 index 0000000..ed6ef22 --- /dev/null +++ b/src/renderer/src/components/ui/avatar/Avatar.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/renderer/src/components/ui/avatar/AvatarFallback.vue b/src/renderer/src/components/ui/avatar/AvatarFallback.vue new file mode 100644 index 0000000..a671a21 --- /dev/null +++ b/src/renderer/src/components/ui/avatar/AvatarFallback.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/avatar/AvatarImage.vue b/src/renderer/src/components/ui/avatar/AvatarImage.vue new file mode 100644 index 0000000..43499fa --- /dev/null +++ b/src/renderer/src/components/ui/avatar/AvatarImage.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/renderer/src/components/ui/avatar/index.ts b/src/renderer/src/components/ui/avatar/index.ts new file mode 100644 index 0000000..5367952 --- /dev/null +++ b/src/renderer/src/components/ui/avatar/index.ts @@ -0,0 +1,24 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as Avatar } from './Avatar.vue' +export { default as AvatarFallback } from './AvatarFallback.vue' +export { default as AvatarImage } from './AvatarImage.vue' + +export const avatarVariant = cva( + 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', + { + variants: { + size: { + sm: 'h-10 w-10 text-xs', + base: 'h-16 w-16 text-2xl', + lg: 'h-32 w-32 text-5xl', + }, + shape: { + circle: 'rounded-full', + square: 'rounded-md', + }, + }, + }, +) + +export type AvatarVariants = VariantProps diff --git a/src/renderer/src/components/ui/badge/Badge.vue b/src/renderer/src/components/ui/badge/Badge.vue new file mode 100644 index 0000000..9ed8039 --- /dev/null +++ b/src/renderer/src/components/ui/badge/Badge.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/badge/index.ts b/src/renderer/src/components/ui/badge/index.ts new file mode 100644 index 0000000..6d3a562 --- /dev/null +++ b/src/renderer/src/components/ui/badge/index.ts @@ -0,0 +1,23 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as Badge } from './Badge.vue' + +export const badgeVariants = cva( + 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + { + variants: { + variant: { + default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', + secondary: 'border-transparent bg-secondary text-muted-foreground hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', + outline: 'text-foreground' + } + }, + defaultVariants: { + variant: 'default' + } + } +) + +export type BadgeVariants = VariantProps diff --git a/src/renderer/src/components/ui/breadcrumb/Breadcrumb.vue b/src/renderer/src/components/ui/breadcrumb/Breadcrumb.vue new file mode 100644 index 0000000..72ca143 --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/Breadcrumb.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue b/src/renderer/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue new file mode 100644 index 0000000..67c01ac --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/BreadcrumbItem.vue b/src/renderer/src/components/ui/breadcrumb/BreadcrumbItem.vue new file mode 100644 index 0000000..42e721c --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/BreadcrumbItem.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/BreadcrumbLink.vue b/src/renderer/src/components/ui/breadcrumb/BreadcrumbLink.vue new file mode 100644 index 0000000..df004a1 --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/BreadcrumbLink.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/BreadcrumbList.vue b/src/renderer/src/components/ui/breadcrumb/BreadcrumbList.vue new file mode 100644 index 0000000..60856cc --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/BreadcrumbList.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/BreadcrumbPage.vue b/src/renderer/src/components/ui/breadcrumb/BreadcrumbPage.vue new file mode 100644 index 0000000..fe43bda --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/BreadcrumbPage.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/BreadcrumbSeparator.vue b/src/renderer/src/components/ui/breadcrumb/BreadcrumbSeparator.vue new file mode 100644 index 0000000..61c61d2 --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/BreadcrumbSeparator.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/renderer/src/components/ui/breadcrumb/index.ts b/src/renderer/src/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..0590983 --- /dev/null +++ b/src/renderer/src/components/ui/breadcrumb/index.ts @@ -0,0 +1,7 @@ +export { default as Breadcrumb } from './Breadcrumb.vue' +export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue' +export { default as BreadcrumbItem } from './BreadcrumbItem.vue' +export { default as BreadcrumbLink } from './BreadcrumbLink.vue' +export { default as BreadcrumbList } from './BreadcrumbList.vue' +export { default as BreadcrumbPage } from './BreadcrumbPage.vue' +export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue' diff --git a/src/renderer/src/components/ui/button/Button.vue b/src/renderer/src/components/ui/button/Button.vue new file mode 100644 index 0000000..889e9d5 --- /dev/null +++ b/src/renderer/src/components/ui/button/Button.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/renderer/src/components/ui/button/index.ts b/src/renderer/src/components/ui/button/index.ts new file mode 100644 index 0000000..2879211 --- /dev/null +++ b/src/renderer/src/components/ui/button/index.ts @@ -0,0 +1,33 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as Button } from './Button.vue' + +export const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-muted-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline' + }, + size: { + default: 'h-9 px-4 py-2', + xs: 'h-7 rounded px-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } +) + +export type ButtonVariants = VariantProps diff --git a/src/renderer/src/components/ui/card/Card.vue b/src/renderer/src/components/ui/card/Card.vue new file mode 100644 index 0000000..94b6903 --- /dev/null +++ b/src/renderer/src/components/ui/card/Card.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/renderer/src/components/ui/card/CardContent.vue b/src/renderer/src/components/ui/card/CardContent.vue new file mode 100644 index 0000000..785913a --- /dev/null +++ b/src/renderer/src/components/ui/card/CardContent.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/card/CardDescription.vue b/src/renderer/src/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..d5faedd --- /dev/null +++ b/src/renderer/src/components/ui/card/CardDescription.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/card/CardFooter.vue b/src/renderer/src/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..1ed2efe --- /dev/null +++ b/src/renderer/src/components/ui/card/CardFooter.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/card/CardHeader.vue b/src/renderer/src/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..951d227 --- /dev/null +++ b/src/renderer/src/components/ui/card/CardHeader.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/card/CardTitle.vue b/src/renderer/src/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..fc302e2 --- /dev/null +++ b/src/renderer/src/components/ui/card/CardTitle.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/renderer/src/components/ui/card/index.ts b/src/renderer/src/components/ui/card/index.ts new file mode 100644 index 0000000..9ff6d5e --- /dev/null +++ b/src/renderer/src/components/ui/card/index.ts @@ -0,0 +1,6 @@ +export { default as Card } from './Card.vue' +export { default as CardContent } from './CardContent.vue' +export { default as CardDescription } from './CardDescription.vue' +export { default as CardFooter } from './CardFooter.vue' +export { default as CardHeader } from './CardHeader.vue' +export { default as CardTitle } from './CardTitle.vue' diff --git a/src/renderer/src/components/ui/checkbox/Checkbox.vue b/src/renderer/src/components/ui/checkbox/Checkbox.vue new file mode 100644 index 0000000..9d3d2e4 --- /dev/null +++ b/src/renderer/src/components/ui/checkbox/Checkbox.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/renderer/src/components/ui/checkbox/index.ts b/src/renderer/src/components/ui/checkbox/index.ts new file mode 100644 index 0000000..8c28c28 --- /dev/null +++ b/src/renderer/src/components/ui/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from './Checkbox.vue' diff --git a/src/renderer/src/components/ui/collapsible/collapsible.ts b/src/renderer/src/components/ui/collapsible/collapsible.ts new file mode 100644 index 0000000..f039b5e --- /dev/null +++ b/src/renderer/src/components/ui/collapsible/collapsible.ts @@ -0,0 +1,119 @@ +import { defineComponent, h, provide, inject, ref, computed, watch } from 'vue' + +const COLLAPSIBLE_SYMBOL = Symbol('collapsible') + +export const Collapsible = defineComponent({ + name: 'Collapsible', + props: { + open: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + } + }, + emits: ['update:open'], + setup(props, { slots, emit }) { + const isOpen = ref(props.open) + + // Watch for prop changes + watch( + () => props.open, + (newValue) => { + isOpen.value = newValue + } + ) + + // Update parent when internal state changes + watch(isOpen, (newValue) => { + if (newValue !== props.open) { + emit('update:open', newValue) + } + }) + + // Provide context to children + provide(COLLAPSIBLE_SYMBOL, { + isOpen, + disabled: computed(() => props.disabled), + toggle: () => { + if (!props.disabled) { + isOpen.value = !isOpen.value + emit('update:open', isOpen.value) + } + } + }) + + return () => + h( + 'div', + { + 'data-state': isOpen.value ? 'open' : 'closed', + class: 'collapsible' + }, + slots.default?.() + ) + } +}) + +export const CollapsibleTrigger = defineComponent({ + name: 'CollapsibleTrigger', + setup(_, { slots, attrs }) { + const collapsible = inject(COLLAPSIBLE_SYMBOL, null) as { + isOpen: { value: boolean } + disabled: { value: boolean } + toggle: () => void + } | null + + if (!collapsible) { + console.error('CollapsibleTrigger must be used within a Collapsible') + return () => null + } + + return () => + h( + 'button', + { + type: 'button', + 'data-state': collapsible.isOpen.value ? 'open' : 'closed', + disabled: collapsible.disabled.value, + 'aria-expanded': collapsible.isOpen.value, + onClick: collapsible.toggle, + ...attrs + }, + slots.default?.() + ) + } +}) + +export const CollapsibleContent = defineComponent({ + name: 'CollapsibleContent', + setup(_, { slots, attrs }) { + const collapsible = inject(COLLAPSIBLE_SYMBOL, null) as { + isOpen: { value: boolean } + disabled: { value: boolean } + toggle: () => void + } | null + + if (!collapsible) { + console.error('CollapsibleContent must be used within a Collapsible') + return () => null + } + + return () => { + if (!collapsible.isOpen.value) { + return null + } + + return h( + 'div', + { + 'data-state': collapsible.isOpen.value ? 'open' : 'closed', + ...attrs + }, + slots.default?.() + ) + } + } +}) diff --git a/src/renderer/src/components/ui/collapsible/index.ts b/src/renderer/src/components/ui/collapsible/index.ts new file mode 100644 index 0000000..dc19bd9 --- /dev/null +++ b/src/renderer/src/components/ui/collapsible/index.ts @@ -0,0 +1,3 @@ +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './collapsible.js' + +export { Collapsible, CollapsibleContent, CollapsibleTrigger } diff --git a/src/renderer/src/components/ui/context-menu/ContextMenu.vue b/src/renderer/src/components/ui/context-menu/ContextMenu.vue new file mode 100644 index 0000000..363e9c6 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenu.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuCheckboxItem.vue b/src/renderer/src/components/ui/context-menu/ContextMenuCheckboxItem.vue new file mode 100644 index 0000000..5b82d05 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuCheckboxItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuContent.vue b/src/renderer/src/components/ui/context-menu/ContextMenuContent.vue new file mode 100644 index 0000000..a7a55cd --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuContent.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuGroup.vue b/src/renderer/src/components/ui/context-menu/ContextMenuGroup.vue new file mode 100644 index 0000000..b7458d7 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuGroup.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuItem.vue b/src/renderer/src/components/ui/context-menu/ContextMenuItem.vue new file mode 100644 index 0000000..ab8e077 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuItem.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuLabel.vue b/src/renderer/src/components/ui/context-menu/ContextMenuLabel.vue new file mode 100644 index 0000000..933cdbf --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuLabel.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuPortal.vue b/src/renderer/src/components/ui/context-menu/ContextMenuPortal.vue new file mode 100644 index 0000000..73dc714 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuPortal.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuRadioGroup.vue b/src/renderer/src/components/ui/context-menu/ContextMenuRadioGroup.vue new file mode 100644 index 0000000..33273a7 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuRadioGroup.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuRadioItem.vue b/src/renderer/src/components/ui/context-menu/ContextMenuRadioItem.vue new file mode 100644 index 0000000..eb2002e --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuRadioItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuSeparator.vue b/src/renderer/src/components/ui/context-menu/ContextMenuSeparator.vue new file mode 100644 index 0000000..b3a8e4f --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuSeparator.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuShortcut.vue b/src/renderer/src/components/ui/context-menu/ContextMenuShortcut.vue new file mode 100644 index 0000000..0d4da92 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuShortcut.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuSub.vue b/src/renderer/src/components/ui/context-menu/ContextMenuSub.vue new file mode 100644 index 0000000..7abc360 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuSub.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuSubContent.vue b/src/renderer/src/components/ui/context-menu/ContextMenuSubContent.vue new file mode 100644 index 0000000..0ba6888 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuSubContent.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuSubTrigger.vue b/src/renderer/src/components/ui/context-menu/ContextMenuSubTrigger.vue new file mode 100644 index 0000000..8c32509 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuSubTrigger.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/ContextMenuTrigger.vue b/src/renderer/src/components/ui/context-menu/ContextMenuTrigger.vue new file mode 100644 index 0000000..22e417b --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/ContextMenuTrigger.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/renderer/src/components/ui/context-menu/index.ts b/src/renderer/src/components/ui/context-menu/index.ts new file mode 100644 index 0000000..3ed59e6 --- /dev/null +++ b/src/renderer/src/components/ui/context-menu/index.ts @@ -0,0 +1,14 @@ +export { default as ContextMenu } from './ContextMenu.vue' +export { default as ContextMenuCheckboxItem } from './ContextMenuCheckboxItem.vue' +export { default as ContextMenuContent } from './ContextMenuContent.vue' +export { default as ContextMenuGroup } from './ContextMenuGroup.vue' +export { default as ContextMenuItem } from './ContextMenuItem.vue' +export { default as ContextMenuLabel } from './ContextMenuLabel.vue' +export { default as ContextMenuRadioGroup } from './ContextMenuRadioGroup.vue' +export { default as ContextMenuRadioItem } from './ContextMenuRadioItem.vue' +export { default as ContextMenuSeparator } from './ContextMenuSeparator.vue' +export { default as ContextMenuShortcut } from './ContextMenuShortcut.vue' +export { default as ContextMenuSub } from './ContextMenuSub.vue' +export { default as ContextMenuSubContent } from './ContextMenuSubContent.vue' +export { default as ContextMenuSubTrigger } from './ContextMenuSubTrigger.vue' +export { default as ContextMenuTrigger } from './ContextMenuTrigger.vue' diff --git a/src/renderer/src/components/ui/dialog/Dialog.vue b/src/renderer/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..a04c026 --- /dev/null +++ b/src/renderer/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogClose.vue b/src/renderer/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..a64703e --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogContent.vue b/src/renderer/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..988acf9 --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogDescription.vue b/src/renderer/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..878ef2b --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogFooter.vue b/src/renderer/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..ac2d0c1 --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogHeader.vue b/src/renderer/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..b2c9085 --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogScrollContent.vue b/src/renderer/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..f059445 --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogTitle.vue b/src/renderer/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..1f049e3 --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/DialogTrigger.vue b/src/renderer/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..ee0c12f --- /dev/null +++ b/src/renderer/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/dialog/index.ts b/src/renderer/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..ca8cfea --- /dev/null +++ b/src/renderer/src/components/ui/dialog/index.ts @@ -0,0 +1,9 @@ +export { default as Dialog } from './Dialog.vue' +export { default as DialogClose } from './DialogClose.vue' +export { default as DialogContent } from './DialogContent.vue' +export { default as DialogDescription } from './DialogDescription.vue' +export { default as DialogFooter } from './DialogFooter.vue' +export { default as DialogHeader } from './DialogHeader.vue' +export { default as DialogScrollContent } from './DialogScrollContent.vue' +export { default as DialogTitle } from './DialogTitle.vue' +export { default as DialogTrigger } from './DialogTrigger.vue' diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenu.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenu.vue new file mode 100644 index 0000000..b83d90b --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenu.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..5f14d02 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuContent.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..6523e9e --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuGroup.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..3f20135 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuItem.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..32a2cbd --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuLabel.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..10494d4 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..4a72790 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..6f54fd0 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..dac6a16 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue new file mode 100644 index 0000000..abaeda6 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSub.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..e0f4bd7 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..92078d9 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..6d7dea6 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..8efd5a9 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/renderer/src/components/ui/dropdown-menu/index.ts b/src/renderer/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..6011f35 --- /dev/null +++ b/src/renderer/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,16 @@ +export { default as DropdownMenu } from './DropdownMenu.vue' + +export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue' +export { default as DropdownMenuContent } from './DropdownMenuContent.vue' +export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue' +export { default as DropdownMenuItem } from './DropdownMenuItem.vue' +export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue' +export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue' +export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue' +export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue' +export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue' +export { default as DropdownMenuSub } from './DropdownMenuSub.vue' +export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue' +export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue' +export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' +export { DropdownMenuPortal } from 'radix-vue' diff --git a/src/renderer/src/components/ui/emoji-picker/EmojiPicker.vue b/src/renderer/src/components/ui/emoji-picker/EmojiPicker.vue new file mode 100644 index 0000000..5a072d8 --- /dev/null +++ b/src/renderer/src/components/ui/emoji-picker/EmojiPicker.vue @@ -0,0 +1,505 @@ + + + diff --git a/src/renderer/src/components/ui/emoji-picker/index.ts b/src/renderer/src/components/ui/emoji-picker/index.ts new file mode 100644 index 0000000..5980e56 --- /dev/null +++ b/src/renderer/src/components/ui/emoji-picker/index.ts @@ -0,0 +1,4 @@ +import EmojiPicker from './EmojiPicker.vue' + +export { EmojiPicker } +export default EmojiPicker diff --git a/src/renderer/src/components/ui/hover-card/HoverCard.vue b/src/renderer/src/components/ui/hover-card/HoverCard.vue new file mode 100644 index 0000000..f17c9d1 --- /dev/null +++ b/src/renderer/src/components/ui/hover-card/HoverCard.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/hover-card/HoverCardContent.vue b/src/renderer/src/components/ui/hover-card/HoverCardContent.vue new file mode 100644 index 0000000..89c9213 --- /dev/null +++ b/src/renderer/src/components/ui/hover-card/HoverCardContent.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/renderer/src/components/ui/hover-card/HoverCardTrigger.vue b/src/renderer/src/components/ui/hover-card/HoverCardTrigger.vue new file mode 100644 index 0000000..3e300b9 --- /dev/null +++ b/src/renderer/src/components/ui/hover-card/HoverCardTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/hover-card/index.ts b/src/renderer/src/components/ui/hover-card/index.ts new file mode 100644 index 0000000..9e4ccc2 --- /dev/null +++ b/src/renderer/src/components/ui/hover-card/index.ts @@ -0,0 +1,3 @@ +export { default as HoverCard } from './HoverCard.vue' +export { default as HoverCardContent } from './HoverCardContent.vue' +export { default as HoverCardTrigger } from './HoverCardTrigger.vue' diff --git a/src/renderer/src/components/ui/input/Input.vue b/src/renderer/src/components/ui/input/Input.vue new file mode 100644 index 0000000..c569ee5 --- /dev/null +++ b/src/renderer/src/components/ui/input/Input.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/renderer/src/components/ui/input/index.ts b/src/renderer/src/components/ui/input/index.ts new file mode 100644 index 0000000..a691dd6 --- /dev/null +++ b/src/renderer/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input.vue' diff --git a/src/renderer/src/components/ui/label/Label.vue b/src/renderer/src/components/ui/label/Label.vue new file mode 100644 index 0000000..5ad1568 --- /dev/null +++ b/src/renderer/src/components/ui/label/Label.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/renderer/src/components/ui/label/index.ts b/src/renderer/src/components/ui/label/index.ts new file mode 100644 index 0000000..572c2f0 --- /dev/null +++ b/src/renderer/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from './Label.vue' diff --git a/src/renderer/src/components/ui/menubar/Menubar.vue b/src/renderer/src/components/ui/menubar/Menubar.vue new file mode 100644 index 0000000..1b8188f --- /dev/null +++ b/src/renderer/src/components/ui/menubar/Menubar.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarCheckboxItem.vue b/src/renderer/src/components/ui/menubar/MenubarCheckboxItem.vue new file mode 100644 index 0000000..d5e56dd --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarCheckboxItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarContent.vue b/src/renderer/src/components/ui/menubar/MenubarContent.vue new file mode 100644 index 0000000..ef09e78 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarContent.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarGroup.vue b/src/renderer/src/components/ui/menubar/MenubarGroup.vue new file mode 100644 index 0000000..853976b --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarGroup.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarItem.vue b/src/renderer/src/components/ui/menubar/MenubarItem.vue new file mode 100644 index 0000000..685b4be --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarItem.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarLabel.vue b/src/renderer/src/components/ui/menubar/MenubarLabel.vue new file mode 100644 index 0000000..574ce49 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarLabel.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarMenu.vue b/src/renderer/src/components/ui/menubar/MenubarMenu.vue new file mode 100644 index 0000000..fec5ee5 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarMenu.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarRadioGroup.vue b/src/renderer/src/components/ui/menubar/MenubarRadioGroup.vue new file mode 100644 index 0000000..60a8cd1 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarRadioGroup.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarRadioItem.vue b/src/renderer/src/components/ui/menubar/MenubarRadioItem.vue new file mode 100644 index 0000000..270e4d1 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarRadioItem.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarSeparator.vue b/src/renderer/src/components/ui/menubar/MenubarSeparator.vue new file mode 100644 index 0000000..234334e --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarShortcut.vue b/src/renderer/src/components/ui/menubar/MenubarShortcut.vue new file mode 100644 index 0000000..0d4da92 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarShortcut.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarSub.vue b/src/renderer/src/components/ui/menubar/MenubarSub.vue new file mode 100644 index 0000000..6b76cd3 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarSub.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarSubContent.vue b/src/renderer/src/components/ui/menubar/MenubarSubContent.vue new file mode 100644 index 0000000..8886096 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarSubContent.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarSubTrigger.vue b/src/renderer/src/components/ui/menubar/MenubarSubTrigger.vue new file mode 100644 index 0000000..a629505 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarSubTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/MenubarTrigger.vue b/src/renderer/src/components/ui/menubar/MenubarTrigger.vue new file mode 100644 index 0000000..ac1f825 --- /dev/null +++ b/src/renderer/src/components/ui/menubar/MenubarTrigger.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/renderer/src/components/ui/menubar/index.ts b/src/renderer/src/components/ui/menubar/index.ts new file mode 100644 index 0000000..600c23e --- /dev/null +++ b/src/renderer/src/components/ui/menubar/index.ts @@ -0,0 +1,15 @@ +export { default as Menubar } from './Menubar.vue' +export { default as MenubarCheckboxItem } from './MenubarCheckboxItem.vue' +export { default as MenubarContent } from './MenubarContent.vue' +export { default as MenubarGroup } from './MenubarGroup.vue' +export { default as MenubarItem } from './MenubarItem.vue' +export { default as MenubarLabel } from './MenubarLabel.vue' +export { default as MenubarMenu } from './MenubarMenu.vue' +export { default as MenubarRadioGroup } from './MenubarRadioGroup.vue' +export { default as MenubarRadioItem } from './MenubarRadioItem.vue' +export { default as MenubarSeparator } from './MenubarSeparator.vue' +export { default as MenubarShortcut } from './MenubarShortcut.vue' +export { default as MenubarSub } from './MenubarSub.vue' +export { default as MenubarSubContent } from './MenubarSubContent.vue' +export { default as MenubarSubTrigger } from './MenubarSubTrigger.vue' +export { default as MenubarTrigger } from './MenubarTrigger.vue' diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenu.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenu.vue new file mode 100644 index 0000000..a0b5bec --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenu.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuContent.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuContent.vue new file mode 100644 index 0000000..101f450 --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuContent.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuIndicator.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuIndicator.vue new file mode 100644 index 0000000..bbf7eb8 --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuIndicator.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuItem.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuItem.vue new file mode 100644 index 0000000..50e1565 --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuItem.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuLink.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuLink.vue new file mode 100644 index 0000000..30c91c6 --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuLink.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuList.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuList.vue new file mode 100644 index 0000000..4cf0d10 --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuList.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuTrigger.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuTrigger.vue new file mode 100644 index 0000000..b266d0a --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuTrigger.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/NavigationMenuViewport.vue b/src/renderer/src/components/ui/navigation-menu/NavigationMenuViewport.vue new file mode 100644 index 0000000..70fae0e --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/NavigationMenuViewport.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/renderer/src/components/ui/navigation-menu/index.ts b/src/renderer/src/components/ui/navigation-menu/index.ts new file mode 100644 index 0000000..9b45738 --- /dev/null +++ b/src/renderer/src/components/ui/navigation-menu/index.ts @@ -0,0 +1,14 @@ +import { cva } from 'class-variance-authority' + +export { default as NavigationMenu } from './NavigationMenu.vue' +export { default as NavigationMenuContent } from './NavigationMenuContent.vue' +export { default as NavigationMenuIndicator } from './NavigationMenuIndicator.vue' +export { default as NavigationMenuItem } from './NavigationMenuItem.vue' +export { default as NavigationMenuLink } from './NavigationMenuLink.vue' +export { default as NavigationMenuList } from './NavigationMenuList.vue' +export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue' +export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue' + +export const navigationMenuTriggerStyle = cva( + 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50', +) diff --git a/src/renderer/src/components/ui/number-field/NumberField.vue b/src/renderer/src/components/ui/number-field/NumberField.vue new file mode 100644 index 0000000..3c7db46 --- /dev/null +++ b/src/renderer/src/components/ui/number-field/NumberField.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/renderer/src/components/ui/number-field/NumberFieldContent.vue b/src/renderer/src/components/ui/number-field/NumberFieldContent.vue new file mode 100644 index 0000000..2cde64c --- /dev/null +++ b/src/renderer/src/components/ui/number-field/NumberFieldContent.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/number-field/NumberFieldDecrement.vue b/src/renderer/src/components/ui/number-field/NumberFieldDecrement.vue new file mode 100644 index 0000000..bbfc5bd --- /dev/null +++ b/src/renderer/src/components/ui/number-field/NumberFieldDecrement.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/renderer/src/components/ui/number-field/NumberFieldIncrement.vue b/src/renderer/src/components/ui/number-field/NumberFieldIncrement.vue new file mode 100644 index 0000000..be1417e --- /dev/null +++ b/src/renderer/src/components/ui/number-field/NumberFieldIncrement.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/renderer/src/components/ui/number-field/NumberFieldInput.vue b/src/renderer/src/components/ui/number-field/NumberFieldInput.vue new file mode 100644 index 0000000..a84182f --- /dev/null +++ b/src/renderer/src/components/ui/number-field/NumberFieldInput.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/number-field/index.ts b/src/renderer/src/components/ui/number-field/index.ts new file mode 100644 index 0000000..5489697 --- /dev/null +++ b/src/renderer/src/components/ui/number-field/index.ts @@ -0,0 +1,5 @@ +export { default as NumberField } from './NumberField.vue' +export { default as NumberFieldContent } from './NumberFieldContent.vue' +export { default as NumberFieldDecrement } from './NumberFieldDecrement.vue' +export { default as NumberFieldIncrement } from './NumberFieldIncrement.vue' +export { default as NumberFieldInput } from './NumberFieldInput.vue' diff --git a/src/renderer/src/components/ui/popover/Popover.vue b/src/renderer/src/components/ui/popover/Popover.vue new file mode 100644 index 0000000..da5f709 --- /dev/null +++ b/src/renderer/src/components/ui/popover/Popover.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/renderer/src/components/ui/popover/PopoverContent.vue b/src/renderer/src/components/ui/popover/PopoverContent.vue new file mode 100644 index 0000000..6744b58 --- /dev/null +++ b/src/renderer/src/components/ui/popover/PopoverContent.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/renderer/src/components/ui/popover/PopoverTrigger.vue b/src/renderer/src/components/ui/popover/PopoverTrigger.vue new file mode 100644 index 0000000..6c94449 --- /dev/null +++ b/src/renderer/src/components/ui/popover/PopoverTrigger.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/renderer/src/components/ui/popover/index.ts b/src/renderer/src/components/ui/popover/index.ts new file mode 100644 index 0000000..0384c99 --- /dev/null +++ b/src/renderer/src/components/ui/popover/index.ts @@ -0,0 +1,4 @@ +export { default as Popover } from './Popover.vue' +export { default as PopoverContent } from './PopoverContent.vue' +export { default as PopoverTrigger } from './PopoverTrigger.vue' +export { PopoverAnchor } from 'radix-vue' diff --git a/src/renderer/src/components/ui/progress/Progress.vue b/src/renderer/src/components/ui/progress/Progress.vue new file mode 100644 index 0000000..0a1e90a --- /dev/null +++ b/src/renderer/src/components/ui/progress/Progress.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/renderer/src/components/ui/progress/index.ts b/src/renderer/src/components/ui/progress/index.ts new file mode 100644 index 0000000..eace989 --- /dev/null +++ b/src/renderer/src/components/ui/progress/index.ts @@ -0,0 +1 @@ +export { default as Progress } from './Progress.vue' diff --git a/src/renderer/src/components/ui/radio-group/RadioGroup.vue b/src/renderer/src/components/ui/radio-group/RadioGroup.vue new file mode 100644 index 0000000..7fbcf9b --- /dev/null +++ b/src/renderer/src/components/ui/radio-group/RadioGroup.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/renderer/src/components/ui/radio-group/RadioGroupItem.vue b/src/renderer/src/components/ui/radio-group/RadioGroupItem.vue new file mode 100644 index 0000000..0319aa3 --- /dev/null +++ b/src/renderer/src/components/ui/radio-group/RadioGroupItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/renderer/src/components/ui/radio-group/index.ts b/src/renderer/src/components/ui/radio-group/index.ts new file mode 100644 index 0000000..fa1da9c --- /dev/null +++ b/src/renderer/src/components/ui/radio-group/index.ts @@ -0,0 +1,2 @@ +export { default as RadioGroup } from './RadioGroup.vue' +export { default as RadioGroupItem } from './RadioGroupItem.vue' diff --git a/src/renderer/src/components/ui/scroll-area/ScrollArea.vue b/src/renderer/src/components/ui/scroll-area/ScrollArea.vue new file mode 100644 index 0000000..0feeedf --- /dev/null +++ b/src/renderer/src/components/ui/scroll-area/ScrollArea.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/renderer/src/components/ui/scroll-area/ScrollBar.vue b/src/renderer/src/components/ui/scroll-area/ScrollBar.vue new file mode 100644 index 0000000..8cd4a18 --- /dev/null +++ b/src/renderer/src/components/ui/scroll-area/ScrollBar.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/renderer/src/components/ui/scroll-area/index.ts b/src/renderer/src/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..2bd4fae --- /dev/null +++ b/src/renderer/src/components/ui/scroll-area/index.ts @@ -0,0 +1,2 @@ +export { default as ScrollArea } from './ScrollArea.vue' +export { default as ScrollBar } from './ScrollBar.vue' diff --git a/src/renderer/src/components/ui/select/Select.vue b/src/renderer/src/components/ui/select/Select.vue new file mode 100644 index 0000000..adc42fd --- /dev/null +++ b/src/renderer/src/components/ui/select/Select.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectContent.vue b/src/renderer/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..04387f7 --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectContent.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectGroup.vue b/src/renderer/src/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..b0803e1 --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectItem.vue b/src/renderer/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..b3fa768 --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectItem.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectItemText.vue b/src/renderer/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..a0bb5c2 --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectLabel.vue b/src/renderer/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..486150e --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectScrollDownButton.vue b/src/renderer/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..d7550e5 --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectScrollUpButton.vue b/src/renderer/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..772c44e --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectSeparator.vue b/src/renderer/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..cb06b8d --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectTrigger.vue b/src/renderer/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..892dc35 --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/renderer/src/components/ui/select/SelectValue.vue b/src/renderer/src/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..4bc37dd --- /dev/null +++ b/src/renderer/src/components/ui/select/SelectValue.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/select/index.ts b/src/renderer/src/components/ui/select/index.ts new file mode 100644 index 0000000..31b9294 --- /dev/null +++ b/src/renderer/src/components/ui/select/index.ts @@ -0,0 +1,11 @@ +export { default as Select } from './Select.vue' +export { default as SelectContent } from './SelectContent.vue' +export { default as SelectGroup } from './SelectGroup.vue' +export { default as SelectItem } from './SelectItem.vue' +export { default as SelectItemText } from './SelectItemText.vue' +export { default as SelectLabel } from './SelectLabel.vue' +export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' +export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' +export { default as SelectSeparator } from './SelectSeparator.vue' +export { default as SelectTrigger } from './SelectTrigger.vue' +export { default as SelectValue } from './SelectValue.vue' diff --git a/src/renderer/src/components/ui/separator/Separator.vue b/src/renderer/src/components/ui/separator/Separator.vue new file mode 100644 index 0000000..ac09825 --- /dev/null +++ b/src/renderer/src/components/ui/separator/Separator.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/renderer/src/components/ui/separator/index.ts b/src/renderer/src/components/ui/separator/index.ts new file mode 100644 index 0000000..2287bcb --- /dev/null +++ b/src/renderer/src/components/ui/separator/index.ts @@ -0,0 +1 @@ +export { default as Separator } from './Separator.vue' diff --git a/src/renderer/src/components/ui/sheet/Sheet.vue b/src/renderer/src/components/ui/sheet/Sheet.vue new file mode 100644 index 0000000..a04c026 --- /dev/null +++ b/src/renderer/src/components/ui/sheet/Sheet.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetClose.vue b/src/renderer/src/components/ui/sheet/SheetClose.vue new file mode 100644 index 0000000..a64703e --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetClose.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetContent.vue b/src/renderer/src/components/ui/sheet/SheetContent.vue new file mode 100644 index 0000000..367e69b --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetContent.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetDescription.vue b/src/renderer/src/components/ui/sheet/SheetDescription.vue new file mode 100644 index 0000000..b515e42 --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetDescription.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetFooter.vue b/src/renderer/src/components/ui/sheet/SheetFooter.vue new file mode 100644 index 0000000..ac2d0c1 --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetHeader.vue b/src/renderer/src/components/ui/sheet/SheetHeader.vue new file mode 100644 index 0000000..541f48f --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetTitle.vue b/src/renderer/src/components/ui/sheet/SheetTitle.vue new file mode 100644 index 0000000..b102959 --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetTitle.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/SheetTrigger.vue b/src/renderer/src/components/ui/sheet/SheetTrigger.vue new file mode 100644 index 0000000..ee0c12f --- /dev/null +++ b/src/renderer/src/components/ui/sheet/SheetTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/renderer/src/components/ui/sheet/index.ts b/src/renderer/src/components/ui/sheet/index.ts new file mode 100644 index 0000000..4c4e77a --- /dev/null +++ b/src/renderer/src/components/ui/sheet/index.ts @@ -0,0 +1,31 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export { default as Sheet } from './Sheet.vue' +export { default as SheetClose } from './SheetClose.vue' +export { default as SheetContent } from './SheetContent.vue' +export { default as SheetDescription } from './SheetDescription.vue' +export { default as SheetFooter } from './SheetFooter.vue' +export { default as SheetHeader } from './SheetHeader.vue' +export { default as SheetTitle } from './SheetTitle.vue' +export { default as SheetTrigger } from './SheetTrigger.vue' + +export const sheetVariants = cva( + 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + { + variants: { + side: { + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + bottom: + 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + right: + 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + }, + }, + defaultVariants: { + side: 'right', + }, + }, +) + +export type SheetVariants = VariantProps diff --git a/src/renderer/src/components/ui/sidebar/Sidebar.vue b/src/renderer/src/components/ui/sidebar/Sidebar.vue new file mode 100644 index 0000000..565d172 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/Sidebar.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarContent.vue b/src/renderer/src/components/ui/sidebar/SidebarContent.vue new file mode 100644 index 0000000..4b6244a --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarFooter.vue b/src/renderer/src/components/ui/sidebar/SidebarFooter.vue new file mode 100644 index 0000000..9d145c0 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarGroup.vue b/src/renderer/src/components/ui/sidebar/SidebarGroup.vue new file mode 100644 index 0000000..adc6843 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarGroup.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarGroupAction.vue b/src/renderer/src/components/ui/sidebar/SidebarGroupAction.vue new file mode 100644 index 0000000..74cac4e --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarGroupAction.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarGroupContent.vue b/src/renderer/src/components/ui/sidebar/SidebarGroupContent.vue new file mode 100644 index 0000000..37390c9 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarGroupContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarGroupLabel.vue b/src/renderer/src/components/ui/sidebar/SidebarGroupLabel.vue new file mode 100644 index 0000000..83826c3 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarGroupLabel.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarHeader.vue b/src/renderer/src/components/ui/sidebar/SidebarHeader.vue new file mode 100644 index 0000000..eecaddb --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarInput.vue b/src/renderer/src/components/ui/sidebar/SidebarInput.vue new file mode 100644 index 0000000..adf11f9 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarInput.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarInset.vue b/src/renderer/src/components/ui/sidebar/SidebarInset.vue new file mode 100644 index 0000000..27d1db5 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarInset.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenu.vue b/src/renderer/src/components/ui/sidebar/SidebarMenu.vue new file mode 100644 index 0000000..3bfd73e --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenu.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuAction.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuAction.vue new file mode 100644 index 0000000..c91295e --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuAction.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuBadge.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuBadge.vue new file mode 100644 index 0000000..f878968 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuBadge.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuButton.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuButton.vue new file mode 100644 index 0000000..ac6926b --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuButton.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuButtonChild.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuButtonChild.vue new file mode 100644 index 0000000..1d43866 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuButtonChild.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuItem.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuItem.vue new file mode 100644 index 0000000..b600073 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuItem.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuSkeleton.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuSkeleton.vue new file mode 100644 index 0000000..76053a1 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuSkeleton.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuSub.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuSub.vue new file mode 100644 index 0000000..0bb5af7 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuSub.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuSubButton.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuSubButton.vue new file mode 100644 index 0000000..0c63da1 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuSubButton.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarMenuSubItem.vue b/src/renderer/src/components/ui/sidebar/SidebarMenuSubItem.vue new file mode 100644 index 0000000..b04030b --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarMenuSubItem.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarProvider.vue b/src/renderer/src/components/ui/sidebar/SidebarProvider.vue new file mode 100644 index 0000000..5775811 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarProvider.vue @@ -0,0 +1,80 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarRail.vue b/src/renderer/src/components/ui/sidebar/SidebarRail.vue new file mode 100644 index 0000000..9b644cd --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarRail.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarSeparator.vue b/src/renderer/src/components/ui/sidebar/SidebarSeparator.vue new file mode 100644 index 0000000..dc7e93c --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarSeparator.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/SidebarTrigger.vue b/src/renderer/src/components/ui/sidebar/SidebarTrigger.vue new file mode 100644 index 0000000..4bcc61a --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/SidebarTrigger.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/renderer/src/components/ui/sidebar/index.ts b/src/renderer/src/components/ui/sidebar/index.ts new file mode 100644 index 0000000..0b275a9 --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/index.ts @@ -0,0 +1,59 @@ +import type { HTMLAttributes } from 'vue' +import { cva, type VariantProps } from 'class-variance-authority' + +export interface SidebarProps { + side?: 'left' | 'right' + variant?: 'sidebar' | 'floating' | 'inset' + collapsible?: 'offcanvas' | 'icon' | 'none' + class?: HTMLAttributes['class'] +} + +export { default as Sidebar } from './Sidebar.vue' +export { default as SidebarContent } from './SidebarContent.vue' +export { default as SidebarFooter } from './SidebarFooter.vue' +export { default as SidebarGroup } from './SidebarGroup.vue' +export { default as SidebarGroupAction } from './SidebarGroupAction.vue' +export { default as SidebarGroupContent } from './SidebarGroupContent.vue' +export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue' +export { default as SidebarHeader } from './SidebarHeader.vue' +export { default as SidebarInput } from './SidebarInput.vue' +export { default as SidebarInset } from './SidebarInset.vue' +export { default as SidebarMenu } from './SidebarMenu.vue' +export { default as SidebarMenuAction } from './SidebarMenuAction.vue' +export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue' +export { default as SidebarMenuButton } from './SidebarMenuButton.vue' +export { default as SidebarMenuItem } from './SidebarMenuItem.vue' +export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue' +export { default as SidebarMenuSub } from './SidebarMenuSub.vue' +export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue' +export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue' +export { default as SidebarProvider } from './SidebarProvider.vue' +export { default as SidebarRail } from './SidebarRail.vue' +export { default as SidebarSeparator } from './SidebarSeparator.vue' +export { default as SidebarTrigger } from './SidebarTrigger.vue' + +export { useSidebar } from './utils' + +export const sidebarMenuButtonVariants = cva( + 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', + { + variants: { + variant: { + default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', + outline: + 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', + }, + size: { + default: 'h-8 text-sm', + sm: 'h-7 text-xs', + lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) + +export type SidebarMenuButtonVariants = VariantProps diff --git a/src/renderer/src/components/ui/sidebar/utils.ts b/src/renderer/src/components/ui/sidebar/utils.ts new file mode 100644 index 0000000..3ded66c --- /dev/null +++ b/src/renderer/src/components/ui/sidebar/utils.ts @@ -0,0 +1,19 @@ +import type { ComputedRef, Ref } from 'vue' +import { createContext } from 'radix-vue' + +export const SIDEBAR_COOKIE_NAME = 'sidebar:state' +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +export const SIDEBAR_WIDTH = '16rem' +export const SIDEBAR_WIDTH_MOBILE = '18rem' +export const SIDEBAR_WIDTH_ICON = '3rem' +export const SIDEBAR_KEYBOARD_SHORTCUT = 'b' + +export const [useSidebar, provideSidebarContext] = createContext<{ + state: ComputedRef<'expanded' | 'collapsed'> + open: Ref + setOpen: (value: boolean) => void + isMobile: Ref + openMobile: Ref + setOpenMobile: (value: boolean) => void + toggleSidebar: () => void +}>('Sidebar') diff --git a/src/renderer/src/components/ui/skeleton/Skeleton.vue b/src/renderer/src/components/ui/skeleton/Skeleton.vue new file mode 100644 index 0000000..94bc183 --- /dev/null +++ b/src/renderer/src/components/ui/skeleton/Skeleton.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/renderer/src/components/ui/skeleton/index.ts b/src/renderer/src/components/ui/skeleton/index.ts new file mode 100644 index 0000000..be21fad --- /dev/null +++ b/src/renderer/src/components/ui/skeleton/index.ts @@ -0,0 +1 @@ +export { default as Skeleton } from './Skeleton.vue' diff --git a/src/renderer/src/components/ui/slider/Slider.vue b/src/renderer/src/components/ui/slider/Slider.vue new file mode 100644 index 0000000..c9c1a61 --- /dev/null +++ b/src/renderer/src/components/ui/slider/Slider.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/renderer/src/components/ui/slider/index.ts b/src/renderer/src/components/ui/slider/index.ts new file mode 100644 index 0000000..1c945de --- /dev/null +++ b/src/renderer/src/components/ui/slider/index.ts @@ -0,0 +1 @@ +export { default as Slider } from './Slider.vue' diff --git a/src/renderer/src/components/ui/switch/Switch.vue b/src/renderer/src/components/ui/switch/Switch.vue new file mode 100644 index 0000000..922177c --- /dev/null +++ b/src/renderer/src/components/ui/switch/Switch.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/renderer/src/components/ui/switch/index.ts b/src/renderer/src/components/ui/switch/index.ts new file mode 100644 index 0000000..87b4b17 --- /dev/null +++ b/src/renderer/src/components/ui/switch/index.ts @@ -0,0 +1 @@ +export { default as Switch } from './Switch.vue' diff --git a/src/renderer/src/components/ui/tabs/Tab.vue b/src/renderer/src/components/ui/tabs/Tab.vue new file mode 100644 index 0000000..17bba9e --- /dev/null +++ b/src/renderer/src/components/ui/tabs/Tab.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/renderer/src/components/ui/tabs/TabGroup.vue b/src/renderer/src/components/ui/tabs/TabGroup.vue new file mode 100644 index 0000000..cd9d3b0 --- /dev/null +++ b/src/renderer/src/components/ui/tabs/TabGroup.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/renderer/src/components/ui/tabs/TabList.vue b/src/renderer/src/components/ui/tabs/TabList.vue new file mode 100644 index 0000000..47c839c --- /dev/null +++ b/src/renderer/src/components/ui/tabs/TabList.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/renderer/src/components/ui/tabs/TabPanel.vue b/src/renderer/src/components/ui/tabs/TabPanel.vue new file mode 100644 index 0000000..24bac21 --- /dev/null +++ b/src/renderer/src/components/ui/tabs/TabPanel.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/renderer/src/components/ui/tabs/TabPanels.vue b/src/renderer/src/components/ui/tabs/TabPanels.vue new file mode 100644 index 0000000..bd56974 --- /dev/null +++ b/src/renderer/src/components/ui/tabs/TabPanels.vue @@ -0,0 +1,5 @@ + diff --git a/src/renderer/src/components/ui/tabs/index.ts b/src/renderer/src/components/ui/tabs/index.ts new file mode 100644 index 0000000..cccb849 --- /dev/null +++ b/src/renderer/src/components/ui/tabs/index.ts @@ -0,0 +1,7 @@ +import TabGroup from './TabGroup.vue' +import TabList from './TabList.vue' +import Tab from './Tab.vue' +import TabPanels from './TabPanels.vue' +import TabPanel from './TabPanel.vue' + +export { TabGroup, TabList, Tab, TabPanels, TabPanel } diff --git a/src/renderer/src/components/ui/textarea/Textarea.vue b/src/renderer/src/components/ui/textarea/Textarea.vue new file mode 100644 index 0000000..ccbc4d9 --- /dev/null +++ b/src/renderer/src/components/ui/textarea/Textarea.vue @@ -0,0 +1,44 @@ + + +