Compare commits

...

106 Commits

Author SHA1 Message Date
kangfenmao
a046cf32ba fix: artifacts cannot preview 2025-01-14 23:27:54 +08:00
kangfenmao
66bc9cb3f9 refactor: improved type safety and consistency for file handling 2025-01-14 21:02:55 +08:00
kangfenmao
247d1a1846 chore(version): 0.9.9 2025-01-14 20:57:16 +08:00
kangfenmao
0e7fb2b19c refactor: update model group names and sync interval 2025-01-14 20:53:52 +08:00
kangfenmao
8a94bb05ea fix: fix model type logic based on provider properties 2025-01-14 20:32:04 +08:00
Nanami
bc454d4dec feat: add support for qwenlm and image upload (#726)
* feat: add support for qwenlm and image upload

* fix: qwenlm return

* feat: add provider config
2025-01-14 18:59:19 +08:00
Teo
d388aeecfb feat: 添加模型提及功能,支持多个模型一起回答 2025-01-14 17:46:55 +08:00
kangfenmao
3e33ee6cc5 feat: add release workflow behavior control option 2025-01-14 14:55:32 +08:00
kangfenmao
1991df18d2 chore(version): 0.9.8 2025-01-14 14:36:05 +08:00
kangfenmao
de3206b052 chore: update store version and migration 2025-01-14 14:34:36 +08:00
kangfenmao
cb3ed42846 style: update markdown link text color 2025-01-14 13:54:10 +08:00
kangfenmao
edbc8560cc chore(version): 0.9.7 2025-01-14 13:24:54 +08:00
kangfenmao
56761d6f69 fix: improved input validation and debouncing for assistant settings updates 2025-01-14 13:18:34 +08:00
kangfenmao
2b4cfe7cb1 feat: add grounding source info to gemini message 2025-01-14 12:32:50 +08:00
kangfenmao
6a5faa6610 feat: auto focus search input box #705
close #705
2025-01-13 18:09:59 +08:00
kangfenmao
84979a975c feat: add native app regions support 2025-01-13 18:06:22 +08:00
kangfenmao
74740d7fcc style: update pinned apps style and refactor config model 2025-01-13 17:56:16 +08:00
kangfenmao
dff04187be feat: add refresh icon to knowledge base items #567
close #567
2025-01-13 17:42:59 +08:00
kangfenmao
a0a13a4015 feat: added openai model configuration and search parameter logic 2025-01-13 16:42:53 +08:00
kangfenmao
2ad6a1f24c feat: check api use selected model 2025-01-13 16:11:09 +08:00
kangfenmao
cf7c0fc1fc fix: enforce max tokens above 0 in assistantservice #530 2025-01-13 15:03:37 +08:00
kangfenmao
4ecbf3edab feat: csv download #710
close #710
2025-01-13 14:44:30 +08:00
kangfenmao
83cc4ccec7 refactor: update terminology to 'backup' throughout the application 2025-01-13 14:00:35 +08:00
kangfenmao
3998ad08de feat: add qwenlm minapp 2025-01-13 13:52:45 +08:00
kangfenmao
49a5bc7900 refactor: sidebar minapps 2025-01-13 13:04:01 +08:00
hxp0618
7633d70435 feat: MinApp added to the sidebar does not support direct hiding. 2025-01-13 10:13:47 +08:00
hxp0618
ad9fb9aa6d feat: Adjust the order of settings 2025-01-13 10:13:34 +08:00
hxp0618
fc3d15fae8 feat: minApp supports show/hide, add to the sidebar 2025-01-13 10:13:34 +08:00
王瑞
c45fc2bbad feat: add Grok app logo and configuration 2025-01-12 22:33:46 +08:00
kangfenmao
270216f461 chore(version): 0.9.6 2025-01-09 16:23:29 +08:00
kangfenmao
112e90c15c fix: create agent popup error 2025-01-09 09:15:16 +08:00
kangfenmao
c579eff86e chore(version): 0.9.5 2025-01-08 16:52:03 +08:00
kangfenmao
f9f5befc59 fix: window navbar layout 2025-01-08 14:35:48 +08:00
kangfenmao
7271a86677 style: update container component styling and navbar responsiveness 2025-01-08 13:25:34 +08:00
kangfenmao
42ede42f62 feat: narrow layout 2025-01-08 12:44:01 +08:00
kangfenmao
ea7a42f736 style: adjusted padding and container gap styles 2025-01-08 11:06:51 +08:00
kangfenmao
d2836826e7 fix: removed unnecessary conditional logic for attachment button #667 2025-01-08 10:56:22 +08:00
kangfenmao
7d61af7170 Revert "fix:修复单行CodeBlock中显示sub"
This reverts commit 09e6756efe.
2025-01-08 10:46:35 +08:00
kangfenmao
3f4fa9b0ec refactor: refactor upload component layout and styling for responsiveness #674
fix: 当插入文件过多的时候,无法看到输入框了。 close #674
2025-01-08 10:21:17 +08:00
kangfenmao
1bdf6c7955 fix: update model filtering logic to exclude empty ids #493
close #493
2025-01-08 10:00:23 +08:00
kangfenmao
5d005cf5a7 chore: standardize artifact names across platforms 2025-01-08 09:42:38 +08:00
kangfenmao
1fbd727a7b fix: @google/generative-ai local compilation issue #682
close #682
2025-01-07 23:18:18 +08:00
亢奋猫
c9813bb1e2 feature: customizable sidebar module #644 (#680)
* feat:对话的时候支持侧边栏拖拽调整宽度

* feat:对话的时候支持侧边栏拖拽调整宽度

* feat: 隐藏app sidebar 用户体验度提升,不支持隐藏对话

* fix:对话勾选知识库 国际化错误

* refactor: split the SidebarIconsManager module out of DisplaySettings

* style: update SidebarIconsManager style

* ci: fix typecheck

* Revert "feat:对话的时候支持侧边栏拖拽调整宽度"

This reverts commit 58072128f0.

* refactor: merge migrate versions

* refactor: simplify sidebarIcons data structure

* chore: move react-beautiful-dnd to dev dependencies

* chore: use @hello-pangea/dnd replace react-beautiful-dnd

* docs: update translation and formatting of input messages

---------

Co-authored-by: hxp0618 <1169924772@qq.com>
Co-authored-by: huang <hxp0618@gmail.com>
2025-01-07 19:11:12 +08:00
kangfenmao
edac2004a0 feat: add gemini files support 2025-01-07 16:49:11 +08:00
kangfenmao
a051f9fa44 feat: add optional free model tag display 2025-01-07 11:23:32 +08:00
kangfenmao
a70e69caf9 feat: enable web search for zhipu ai provider #657 2025-01-07 10:53:34 +08:00
kangfenmao
4896db93fd fix: improved error message formatting in api service 2025-01-07 10:19:21 +08:00
kangfenmao
2e7ecbc753 feat: add ModelTags component 2025-01-07 09:54:22 +08:00
kangfenmao
f68bd4d8d8 feat: add support for 'aihubmix' models and aihubmix llm provider 2025-01-07 09:46:05 +08:00
kangfenmao
d0948e6f8a feature: customizable sidebar module #644
close #644
2025-01-06 16:59:10 +08:00
kangfenmao
ac9017c031 feat: add search message shortcut #366 2025-01-06 16:29:39 +08:00
kangfenmao
de1d79abb8 fix: the minimum width limit of the window is too large #544
close #544
2025-01-06 16:25:00 +08:00
kangfenmao
ad577818dd fix: generating topic name after exporting prompt file name is invalid #641
close #641
2025-01-06 15:50:57 +08:00
kangfenmao
bb50447a98 fix: Ollama is unable to create a knowledge base using a local embedding model #630 2025-01-06 15:43:20 +08:00
kangfenmao
158f9bf1ad fix: turn off spell check #648
The next version will be released. close #648
2025-01-06 15:10:03 +08:00
kangfenmao
6a9bc103d7 feat: added optional chaining for code variable 2025-01-06 14:54:04 +08:00
xx-moos
529ec3612e fix: 修复 message 显示时间过长的问题 2025-01-06 14:43:31 +08:00
kangfenmao
d241c38c61 style: border radius use var 2025-01-04 22:50:44 +08:00
kangfenmao
ee5ed8c565 style: logo v3
# Conflicts:
#	src/renderer/src/assets/images/logo.png
2025-01-04 21:52:05 +08:00
huang
dc73661678 feat: 支持 mermaid 点击按钮放大缩小以及鼠标滑轮放大缩小 2025-01-04 19:17:39 +08:00
huang
ce973ce3a0 feat: 支持 mermaid 点击按钮放大缩小以及鼠标滑轮放大缩小 2025-01-04 19:17:39 +08:00
huang
a0413158c8 fix: 修复在macOS m1 中点击全屏幕后,点击关闭后黑屏的问题 2025-01-04 19:17:39 +08:00
kangfenmao
6cb3b16451 fix: Qwen2.5和Qwen的划分不合理 #633 2025-01-03 18:05:01 +08:00
huang
08b0990cf9 fix: 中文国际化错误 2025-01-03 17:35:17 +08:00
kangfenmao
10b9940edd chore(version): 0.9.4 2025-01-02 21:34:30 +08:00
kangfenmao
4cbdd563e8 feat: add translations and file management features 2025-01-02 18:29:36 +08:00
kangfenmao
dba1f76db7 feat: update assistantmodelsettings to persist custom parameters 2025-01-02 17:21:33 +08:00
kangfenmao
15fb605eb4 feat: improved form validation and model addition functionality 2025-01-02 16:58:58 +08:00
kangfenmao
1bf147fa6a refactor: improve model generation and handling functionality 2025-01-02 16:39:30 +08:00
kangfenmao
a782b2b4aa fix: 腾讯混元的联网开关 #575 2025-01-02 16:26:24 +08:00
kangfenmao
7f92cb59a6 feat: add more classname 2025-01-02 16:25:50 +08:00
kangfenmao
6009ae84fb fix: 重新发送按钮无反应 #587 2025-01-02 15:42:47 +08:00
kangfenmao
038aa2d5cc feat: paintings add prompt enhancement params 2025-01-02 14:51:52 +08:00
kangfenmao
6384525e20 feat: added error handling and knowledge base provider support 2025-01-02 14:16:37 +08:00
kangfenmao
3fc7911c97 feat: add new branch option to message menubar 2025-01-02 13:41:51 +08:00
kangfenmao
5f55d8c22c style: adjusted padding and border styles in settingsgroup component 2025-01-02 13:37:04 +08:00
kangfenmao
d9f7bcfc21 feat: custom parameters add json type 2025-01-02 13:34:21 +08:00
kangfenmao
aa72794967 feat: improved translation features and settings 2025-01-02 12:21:22 +08:00
zhouxl
09e6756efe fix:修复单行CodeBlock中显示sub 2025-01-02 11:47:34 +08:00
kangfenmao
dde0400f0d chore: update hika app assets and styles 2025-01-02 11:18:15 +08:00
kangfenmao
1d3a01dd49 feat: add sync status show 2025-01-02 11:07:20 +08:00
YongHao Hu
63cdc15bc2 feat: add hika minapp 2025-01-02 11:06:57 +08:00
kangfenmao
b2818f8619 fix: reduce batch size for knowledge service and openai embeddings 2024-12-31 14:41:08 +08:00
kangfenmao
8ef9fb0216 feat: add github auto assignment workflow 2024-12-31 00:43:16 +08:00
kangfenmao
63488e6fab chore(version): 0.9.3 2024-12-31 00:38:20 +08:00
kangfenmao
6d9013f0a1 fix: 知识库无法向量化 MD 文件 #569 2024-12-31 00:11:51 +08:00
kangfenmao
1a68587684 fix: Microsoft Visual C++ Redistributable #577 2024-12-30 15:07:31 +08:00
kangfenmao
47c455b125 feat: 增加保持并发送的功能 #527 2024-12-30 14:09:59 +08:00
kangfenmao
96124cf58e feat: 增加genspark小程序 #578 2024-12-30 13:10:27 +08:00
juzeon
ef975add01 fix: 修复zh-tw语言文件中的乱码 (#579) 2024-12-30 11:49:40 +08:00
n2yt584v2t4nh7y
ed49066bab feat: 添加自定义API参数功能 (#564)
* add custom api parameters

* allow more data types for custom api parameters

* pass parameter to api payload

* add custom parameter settings to sidebar

* remove unnecessary object and array types

* extract API custom parameter method to BaseProvider

* add i18n for custom parameter settings

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2024-12-29 20:19:07 +08:00
kangfenmao
e7545c5a94 feat: 用户自定义话题总结Prompt #562
close #562
2024-12-29 10:20:45 +08:00
kangfenmao
fc35df65b8 feat: add release notes pages 2024-12-29 09:49:22 +08:00
littel_penguin66
56ca81d245 Fix incorrect synchronization behavior of webdav auto sync (#568) 2024-12-29 08:44:21 +08:00
kangfenmao
6bc1f4b640 chore(version): 0.9.2 2024-12-27 23:03:17 +08:00
kangfenmao
ccb216e76a fix: 模型名后面标注一下服务商 #557 2024-12-27 18:09:22 +08:00
kangfenmao
60931b85ff fix: model settings params step size 2024-12-27 16:47:44 +08:00
kangfenmao
dc1dbc7bb6 feat: add jina provider 2024-12-27 16:29:17 +08:00
kangfenmao
5d2efbd62b fix: 需要只发送图片功能 #538 2024-12-27 14:40:44 +08:00
sommermorgentraum
5337017648 feat: Add capabilities for user to load custom CSS #548 2024-12-27 14:11:12 +08:00
kangfenmao
c409256ae9 fix: azure openai embedding 2024-12-27 14:02:53 +08:00
kangfenmao
4ac608052c chore: update dependencies and improve project structure 2024-12-27 12:42:17 +08:00
kangfenmao
5e6aaabb23 fix: 小程序中增加 github copilot #547 2024-12-27 12:10:41 +08:00
kangfenmao
8812daeeee fix: 某些输出包含 sub 无法正常显示 #545 2024-12-27 11:54:11 +08:00
kangfenmao
13e3a8478c feat: added topic message update and search state management 2024-12-27 11:48:12 +08:00
kangfenmao
8687985ccb feat: add windows platform support for node file detection and npm package download 2024-12-26 12:38:51 +08:00
153 changed files with 7013 additions and 3086 deletions

View File

@@ -78,19 +78,10 @@ jobs:
run: node scripts/replace-spaces.js run: node scripts/replace-spaces.js
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: ncipollo/release-action@v1
with: with:
draft: true draft: true
files: | allowUpdates: true
dist/*.exe makeLatest: false
dist/*.zip artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
dist/*.dmg token: ${{ secrets.GH_TOKEN }}
dist/*.AppImage
dist/*.snap
dist/*.deb
dist/*.rpm
dist/*.tar.gz
dist/latest*.yml
dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

View File

@@ -0,0 +1,19 @@
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644
--- a/src/markdown-loader.js
+++ b/src/markdown-loader.js
@@ -1,5 +1,4 @@
import { micromark } from 'micromark';
-import { mdxJsx } from 'micromark-extension-mdx-jsx';
import { gfmHtml, gfm } from 'micromark-extension-gfm';
import createDebugMessages from 'debug';
import fs from 'node:fs';
@@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader {
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
: await stream2buffer(fs.createReadStream(this.filePathOrUrl));
this.debug('MarkdownLoader stream created');
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
this.debug('Markdown parsed...');
const webLoader = new WebLoader({
urlOrContent: result,

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 84 KiB

47
build/nsis-installer.nsh Normal file
View File

@@ -0,0 +1,47 @@
;Inspired by:
; https://gist.github.com/bogdibota/062919938e1ed388b3db5ea31f52955c
; https://stackoverflow.com/questions/34177547/detect-if-visual-c-redistributable-for-visual-studio-2013-is-installed
; https://stackoverflow.com/a/54391388
; https://github.com/GitCommons/cpp-redist-nsis/blob/main/installer.nsh
;Find latests downloads here:
; https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
!include LogicLib.nsh
; https://github.com/electron-userland/electron-builder/issues/1122
!ifndef BUILD_UNINSTALLER
Function checkVCRedist
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
FunctionEnd
!endif
!macro customInit
Push $0
Call checkVCRedist
${If} $0 != "1"
MessageBox MB_YESNO "\
NOTE: ${PRODUCT_NAME} requires $\r$\n\
'Microsoft Visual C++ Redistributable'$\r$\n\
to function properly.$\r$\n$\r$\n\
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
InstallVCRedist:
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
Call checkVCRedist
${If} $0 == "1"
Goto ContinueInstall
${EndIf}
;InstallError:
MessageBox MB_ICONSTOP "\
There was an unexpected error installing$\r$\n\
Microsoft Visual C++ Redistributable.$\r$\n\
The installation of ${PRODUCT_NAME} cannot continue."
DontInstall:
Abort
${EndIf}
ContinueInstall:
Pop $0
!macroend

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -32,6 +32,10 @@ asarUnpack:
- '**/*.{node,dll,metal,exp,lib}' - '**/*.{node,dll,metal,exp,lib}'
win: win:
executableName: Cherry Studio executableName: Cherry Studio
artifactName: ${productName}-${version}-portable.${ext}
target:
- target: nsis
- target: portable
nsis: nsis:
artifactName: ${productName}-${version}-setup.${ext} artifactName: ${productName}-${version}-setup.${ext}
shortcutName: ${productName} shortcutName: ${productName}
@@ -39,9 +43,11 @@ nsis:
createDesktopShortcut: always createDesktopShortcut: always
allowToChangeInstallationDirectory: true allowToChangeInstallationDirectory: true
oneClick: false oneClick: false
include: build/nsis-installer.nsh
mac: mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
notarize: false notarize: false
artifactName: ${productName}-${version}-${arch}.${ext}
extendInfo: extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera. - NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -56,9 +62,8 @@ mac:
arch: arch:
- arm64 - arm64
- x64 - x64
dmg:
artifactName: ${productName}-${version}-${arch}.${ext}
linux: linux:
artifactName: ${productName}-${version}-${arch}.${ext}
target: target:
- target: AppImage - target: AppImage
arch: arch:
@@ -66,8 +71,6 @@ linux:
- x64 - x64
maintainer: electronjs.org maintainer: electronjs.org
category: Utility category: Utility
appImage:
artifactName: ${productName}-${version}-${arch}.${ext}
publish: publish:
provider: generic provider: generic
url: https://cherrystudio.ocool.online url: https://cherrystudio.ocool.online
@@ -77,4 +80,6 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
增加知识库功能 添加模型提及功能,支持多个模型一起回答 @teojs
增加 QweLM 服务提供商 @Nana7mi1
修复删除服务商导致的数据错误白屏问题

View File

@@ -50,7 +50,7 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
exclude: [] exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js', 'chunk-AFE5XGNG.js', 'chunk-QIJABHCK.js']
} }
} }
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.9.1", "version": "0.9.9",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -25,6 +25,7 @@
"typecheck": "npm run typecheck:node && npm run typecheck:web", "typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"build:check": "yarn typecheck",
"build": "npm run typecheck && electron-vite build", "build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"build:unpack": "dotenv npm run build && electron-builder --dir", "build:unpack": "dotenv npm run build && electron-builder --dir",
@@ -49,10 +50,11 @@
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@google/generative-ai": "^0.21.0",
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch", "@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch", "@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
"@llm-tools/embedjs-loader-csv": "^0.1.25", "@llm-tools/embedjs-loader-csv": "^0.1.25",
"@llm-tools/embedjs-loader-markdown": "^0.1.25", "@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch",
"@llm-tools/embedjs-loader-msoffice": "^0.1.25", "@llm-tools/embedjs-loader-msoffice": "^0.1.25",
"@llm-tools/embedjs-loader-pdf": "^0.1.25", "@llm-tools/embedjs-loader-pdf": "^0.1.25",
"@llm-tools/embedjs-loader-sitemap": "^0.1.25", "@llm-tools/embedjs-loader-sitemap": "^0.1.25",
@@ -79,7 +81,6 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@google/generative-ai": "^0.21.0",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
@@ -93,7 +94,7 @@
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"antd": "^5.18.3", "antd": "^5.22.5",
"axios": "^1.7.3", "axios": "^1.7.3",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",

View File

@@ -0,0 +1,202 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Github Releases Timeline</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
</head>
<body id="app">
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
<div class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<!-- Loading状态 -->
<div v-if="loading" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
</div>
<!-- Error 状态 -->
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
<!-- Release 列表 -->
<div v-else class="space-y-8">
<div v-for="release in releases" :key="release.id" class="relative pl-8"
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
<div class="rounded-lg shadow-sm p-6 transition-shadow"
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
{{ release.name || release.tag_name }}
</h2>
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
{{ formatDate(release.published_at) }}
</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
{{ release.tag_name }}
</span>
</div>
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
v-html="renderMarkdown(release.body)"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const md = window.markdownit({
breaks: true,
linkify: true
})
const { createApp } = Vue
createApp({
data() {
return {
releases: [],
loading: true,
error: null,
isDark: false
}
},
methods: {
async fetchReleases() {
try {
this.loading = true
this.error = null
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
if (!response.ok) {
throw new Error('Failed to fetch releases')
}
this.releases = await response.json()
} catch (err) {
this.error = 'Error loading releases: ' + err.message
} finally {
this.loading = false
}
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
},
renderMarkdown(content) {
if (!content) return ''
return md.render(content)
},
initTheme() {
// 从 URL 参数获取主题设置
const url = new URL(window.location.href)
const theme = url.searchParams.get('theme')
this.isDark = theme === 'dark'
}
},
mounted() {
this.initTheme()
this.fetchReleases()
}
}).mount('#app')
</script>
<style>
/* 基础的 Markdown 样式 */
.prose {
line-height: 1.6;
}
.prose h1 {
font-size: 1.5em;
margin: 1em 0;
}
.prose h2 {
font-size: 1.3em;
margin: 0.8em 0;
}
.prose h3 {
font-size: 1.1em;
margin: 0.6em 0;
}
.prose ul {
list-style-type: disc;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose ol {
list-style-type: decimal;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose code {
padding: 0.2em 0.4em;
border-radius: 0.2em;
font-size: 0.9em;
}
.dark .prose code {
background-color: #1f2937;
}
.prose code {
background-color: #f3f4f6;
}
.prose pre code {
display: block;
padding: 1em;
overflow-x: auto;
}
.prose a {
color: #3b82f6;
text-decoration: underline;
}
.dark .prose a {
color: #60a5fa;
}
.prose blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1em;
margin: 1em 0;
}
.dark .prose blockquote {
border-left-color: #374151;
color: #9ca3af;
}
.dark .prose {
color: #e5e7eb;
}
.dark-bg {
background-color: #151515;
}
.bg {
background-color: #f2f2f2;
}
</style>
</body>
</html>

View File

@@ -26,6 +26,11 @@ exports.default = async function (context) {
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl'] const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch) removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
} }
if (platform === 'windows') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
}
} }
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) { function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {

View File

@@ -27,6 +27,13 @@ async function downloadNpm(platform) {
'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz' 'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz'
) )
} }
if (!platform || platform === 'windows') {
downloadNpmPackage(
'@libsql/win32-x64-msvc',
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
)
}
} }
const platformArg = process.argv[2] const platformArg = process.argv[2]

View File

@@ -11,6 +11,7 @@ import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import { ExportService } from './services/ExportService' import { ExportService } from './services/ExportService'
import FileStorage from './services/FileStorage' import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
@@ -116,6 +117,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('file:base64Image', fileManager.base64Image) ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('file:download', fileManager.downloadFile) ipcMain.handle('file:download', fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile) ipcMain.handle('file:copy', fileManager.copyFile)
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
// minapp // minapp
ipcMain.handle('minapp', (_, args) => { ipcMain.handle('minapp', (_, args) => {
@@ -154,4 +156,24 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('knowledge-base:add', KnowledgeService.add) ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove) ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search) ipcMain.handle('knowledge-base:search', KnowledgeService.search)
// window
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
mainWindow?.setMinimumSize(width, height)
})
ipcMain.handle('window:reset-minimum-size', () => {
mainWindow?.setMinimumSize(1080, 600)
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
if (width < 1080) {
mainWindow?.setSize(1080, height)
}
})
// gemini
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
} }

View File

@@ -0,0 +1,74 @@
interface CacheItem<T> {
data: T
timestamp: number
duration: number
}
export class CacheService {
private static cache: Map<string, CacheItem<any>> = new Map()
/**
* Set cache
* @param key Cache key
* @param data Cache data
* @param duration Cache duration (in milliseconds)
*/
static set<T>(key: string, data: T, duration: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
duration
})
}
/**
* Get cache
* @param key Cache key
* @returns Returns data if cache exists and not expired, otherwise returns null
*/
static get<T>(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return null
}
return item.data
}
/**
* Remove specific cache
* @param key Cache key
*/
static remove(key: string): void {
this.cache.delete(key)
}
/**
* Clear all cache
*/
static clear(): void {
this.cache.clear()
}
/**
* Check if cache exists and is valid
* @param key Cache key
* @returns boolean
*/
static has(key: string): boolean {
const item = this.cache.get(key)
if (!item) return false
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return false
}
return true
}
}

View File

@@ -263,6 +263,13 @@ class FileStorage {
} }
} }
public binaryFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const data = await fs.promises.readFile(filePath)
const mime = `image/${path.extname(filePath).slice(1)}`
return { data, mime }
}
public clear = async (): Promise<void> => { public clear = async (): Promise<void> => {
await fs.promises.rmdir(this.storageDir, { recursive: true }) await fs.promises.rmdir(this.storageDir, { recursive: true })
await this.initStorageDir() await this.initStorageDir()

View File

@@ -0,0 +1,63 @@
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
const fileManager = new GoogleAIFileManager(apiKey)
const uploadResult = await fileManager.uploadFile(file.path, {
mimeType: 'application/pdf',
displayName: file.origin_name
})
return uploadResult
}
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
return {
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
mimeType: 'application/pdf'
}
}
static async retrieveFile(
_: Electron.IpcMainInvokeEvent,
file: FileType,
apiKey: string
): Promise<FileMetadataResponse | undefined> {
const fileManager = new GoogleAIFileManager(apiKey)
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await fileManager.listFiles()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static processResponse(response: any, file: FileType) {
if (response.files) {
return response.files
.filter((file) => file.state === FileState.ACTIVE)
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size)
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
const fileManager = new GoogleAIFileManager(apiKey)
return await fileManager.listFiles()
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
const fileManager = new GoogleAIFileManager(apiKey)
await fileManager.deleteFile(fileId)
}
}

View File

@@ -9,7 +9,8 @@ import { DocxLoader, ExcelLoader, PptLoader } from '@llm-tools/embedjs-loader-ms
import { PdfLoader } from '@llm-tools/embedjs-loader-pdf' import { PdfLoader } from '@llm-tools/embedjs-loader-pdf'
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap' import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
import { WebLoader } from '@llm-tools/embedjs-loader-web' import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai' import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { getInstanceName } from '@main/utils'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron' import { app } from 'electron'
@@ -30,19 +31,29 @@ class KnowledgeService {
id, id,
model, model,
apiKey, apiKey,
apiVersion,
baseURL, baseURL,
dimensions dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => { }: KnowledgeBaseParams): Promise<RAGApplication> => {
return new RAGApplicationBuilder() return new RAGApplicationBuilder()
.setModel('NO_MODEL') .setModel('NO_MODEL')
.setEmbeddingModel( .setEmbeddingModel(
new OpenAiEmbeddings({ apiVersion
model, ? new AzureOpenAiEmbeddings({
apiKey, azureOpenAIApiKey: apiKey,
configuration: { baseURL }, azureOpenAIApiVersion: apiVersion,
dimensions, azureOpenAIApiDeploymentName: model,
batchSize: 20 azureOpenAIApiInstanceName: getInstanceName(baseURL),
}) dimensions,
batchSize: 10
})
: new OpenAiEmbeddings({
model,
apiKey,
configuration: { baseURL },
dimensions,
batchSize: 10
})
) )
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) })) .setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build() .build()
@@ -78,12 +89,14 @@ class KnowledgeService {
if (item.type === 'url') { if (item.type === 'url') {
const content = item.content as string const content = item.content as string
if (content.startsWith('http')) { if (content.startsWith('http')) {
// @ts-ignore loader type
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload) return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload)
} }
} }
if (item.type === 'sitemap') { if (item.type === 'sitemap') {
const content = item.content as string const content = item.content as string
// @ts-ignore loader type
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload) return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload)
} }
@@ -111,7 +124,7 @@ class KnowledgeService {
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
} }
if (['.md', '.mdx'].includes(file.ext)) { if (['.md'].includes(file.ext)) {
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload)
} }

View File

@@ -13,6 +13,8 @@ import { configManager } from './ConfigManager'
export class WindowService { export class WindowService {
private static instance: WindowService | null = null private static instance: WindowService | null = null
private mainWindow: BrowserWindow | null = null private mainWindow: BrowserWindow | null = null
private isQuitting: boolean = false
private wasFullScreen: boolean = false
public static getInstance(): WindowService { public static getInstance(): WindowService {
if (!WindowService.instance) { if (!WindowService.instance) {
@@ -42,7 +44,7 @@ export class WindowService {
height: mainWindowState.height, height: mainWindowState.height,
minWidth: 1080, minWidth: 1080,
minHeight: 600, minHeight: 600,
show: true, show: false, // 初始不显示
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: isMac,
vibrancy: 'under-window', vibrancy: 'under-window',
@@ -118,9 +120,20 @@ export class WindowService {
} }
private setupWindowEvents(mainWindow: BrowserWindow) { private setupWindowEvents(mainWindow: BrowserWindow) {
mainWindow.on('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
mainWindow.show() mainWindow.show()
}) })
// 处理全屏相关事件
mainWindow.on('enter-full-screen', () => {
this.wasFullScreen = true
mainWindow.webContents.send('fullscreen-status-changed', true)
})
mainWindow.on('leave-full-screen', () => {
this.wasFullScreen = false
mainWindow.webContents.send('fullscreen-status-changed', false)
})
} }
private setupWebContentsHandlers(mainWindow: BrowserWindow) { private setupWebContentsHandlers(mainWindow: BrowserWindow) {
@@ -182,6 +195,11 @@ export class WindowService {
} }
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) { private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
// 监听应用退出事件
app.on('before-quit', () => {
this.isQuitting = true
})
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
const notInTray = !configManager.isTray() const notInTray = !configManager.isTray()
@@ -191,9 +209,15 @@ export class WindowService {
} }
// Mac // Mac
if (!app.isQuitting) { if (!this.isQuitting) {
event.preventDefault() if (this.wasFullScreen) {
mainWindow.hide() // 如果是全屏状态,直接退出
this.isQuitting = true
app.quit()
} else {
event.preventDefault()
mainWindow.hide()
}
} }
}) })
} }

View File

@@ -14,3 +14,11 @@ export function getDataPath() {
} }
return dataPath return dataPath
} }
export function getInstanceName(baseURL: string) {
try {
return new URL(baseURL).host.split('.')[0]
} catch (error) {
return ''
}
}

View File

@@ -1,4 +1,5 @@
import { ElectronAPI } from '@electron-toolkit/preload' import { ElectronAPI } from '@electron-toolkit/preload'
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces' import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { FileType } from '@renderer/types' import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types' import { WebDavConfig } from '@renderer/types'
@@ -52,6 +53,7 @@ declare global {
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }> base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
download: (url: string) => Promise<FileType | null> download: (url: string) => Promise<FileType | null>
copy: (fileId: string, destPath: string) => Promise<void> copy: (fileId: string, destPath: string) => Promise<void>
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
} }
export: { export: {
toWord: (markdown: string, fileName: string) => Promise<void> toWord: (markdown: string, fileName: string) => Promise<void>
@@ -76,6 +78,17 @@ declare global {
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void> remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]> search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
} }
window: {
setMinimumSize: (width: number, height: number) => Promise<void>
resetMinimumSize: () => Promise<void>
}
gemini: {
uploadFile: (file: FileType, apiKey: string) => Promise<UploadFileResponse>
retrieveFile: (file: FileType, apiKey: string) => Promise<FileMetadataResponse | undefined>
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
listFiles: (apiKey: string) => Promise<ListFilesResponse>
deleteFile: (apiKey: string, fileId: string) => Promise<void>
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer // Custom APIs for renderer
@@ -43,7 +43,8 @@ const api = {
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data), saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId), base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
download: (url: string) => ipcRenderer.invoke('file:download', url), download: (url: string) => ipcRenderer.invoke('file:download', url),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath) copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath),
binaryFile: (fileId: string) => ipcRenderer.invoke('file:binaryFile', fileId)
}, },
export: { export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName) toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
@@ -70,6 +71,17 @@ const api = {
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }), ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base }) ipcRenderer.invoke('knowledge-base:search', { search, base })
},
window: {
setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height),
resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size')
},
gemini: {
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey),
base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
} }
} }

View File

@@ -1,88 +1,91 @@
@font-face { @font-face {
font-family: 'iconfont'; /* Project id 4753420 */ font-family: "iconfont"; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2'); src: url('iconfont.woff2?t=1736309723926') format('woff2'),
url('iconfont.woff?t=1736309723926') format('woff'),
url('iconfont.ttf?t=1736309723926') format('truetype');
} }
.iconfont { .iconfont {
font-family: 'iconfont' !important; font-family: "iconfont" !important;
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-at1:before { .icon-at:before {
content: '\e7df'; content: "\e623";
} }
.icon-at:before { .icon-icon-adaptive-width:before {
content: '\e630'; content: "\e87a";
} }
.icon-a-darkmode:before { .icon-a-darkmode:before {
content: '\e6cd'; content: "\e6cd";
} }
.icon-ai-model:before { .icon-ai-model:before {
content: '\e827'; content: "\e827";
} }
.icon-ai-model1:before { .icon-ai-model1:before {
content: '\ec09'; content: "\ec09";
} }
.icon-gridlines:before { .icon-gridlines:before {
content: '\e942'; content: "\e942";
} }
.icon-inbox:before { .icon-inbox:before {
content: '\e869'; content: "\e869";
} }
.icon-business-smart-assistant:before { .icon-business-smart-assistant:before {
content: '\e601'; content: "\e601";
} }
.icon-copy:before { .icon-copy:before {
content: '\e6ae'; content: "\e6ae";
} }
.icon-ic_send:before { .icon-ic_send:before {
content: '\e795'; content: "\e795";
} }
.icon-dark1:before { .icon-dark1:before {
content: '\e72f'; content: "\e72f";
} }
.icon-theme-light:before { .icon-theme-light:before {
content: '\e6b7'; content: "\e6b7";
} }
.icon-translate_line:before { .icon-translate_line:before {
content: '\e7de'; content: "\e7de";
} }
.icon-history:before { .icon-history:before {
content: '\e758'; content: "\e758";
} }
.icon-hide-sidebar:before { .icon-hide-sidebar:before {
content: '\e8eb'; content: "\e8eb";
} }
.icon-show-sidebar:before { .icon-show-sidebar:before {
content: '\e944'; content: "\e944";
} }
.icon-appstore:before { .icon-appstore:before {
content: '\e792'; content: "\e792";
} }
.icon-chat:before { .icon-chat:before {
content: '\e615'; content: "\e615";
} }
.icon-setting:before { .icon-setting:before {
content: '\e78e'; content: "\e78e";
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -42,6 +42,9 @@
--color-active: rgba(55, 55, 55, 1); --color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333; --color-frame-border: #333;
--color-group-background: var(--color-background-soft); --color-group-background: var(--color-background-soft);
--color-reference: #404040;
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12; --color-reference-background: #0b0e12;
--navbar-background-mac: rgba(30, 30, 30, 0.6); --navbar-background-mac: rgba(30, 30, 30, 0.6);
@@ -60,6 +63,8 @@
--chat-background-user: #28b561; --chat-background-user: #28b561;
--chat-background-assistant: #2c2c2c; --chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black); --chat-text-user: var(--color-black);
--list-item-border-radius: 16px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -100,6 +105,9 @@ body[theme-mode='light'] {
--color-active: var(--color-white-soft); --color-active: var(--color-white-soft);
--color-frame-border: #ddd; --color-frame-border: #ddd;
--color-group-background: var(--color-white); --color-group-background: var(--color-white);
--color-reference: #cfe1ff;
--color-reference-text: #000000;
--color-reference-background: #f1f7ff; --color-reference-background: #f1f7ff;
--navbar-background-mac: rgba(255, 255, 255, 0.6); --navbar-background-mac: rgba(255, 255, 255, 0.6);
@@ -169,12 +177,9 @@ body,
#content-container { #content-container {
background-color: var(--color-background); background-color: var(--color-background);
border-top: 0.5px solid var(--color-border); border-top: 0.5px solid var(--color-border);
} border-top-left-radius: 10px;
#content-container {
border-top-left-radius: 12px;
border-left: 0.5px solid var(--color-border); border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08); box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
} }
.loader { .loader {
@@ -216,10 +221,7 @@ body,
background-color: var(--chat-background); background-color: var(--chat-background);
} }
#inputbar { #inputbar {
border-radius: 0; margin: -5px 15px 15px 15px;
margin: 0;
border: none;
border-top: 1px solid var(--color-border-mute);
background: var(--color-background); background: var(--color-background);
} }
.system-prompt { .system-prompt {

View File

@@ -208,6 +208,14 @@
sup { sup {
top: -0.5em; top: -0.5em;
border-radius: 50%;
background-color: var(--color-reference);
color: var(--color-reference-text);
padding: 2px 5px;
zoom: 0.8;
& > span.link {
color: var(--color-reference-text);
}
} }
sub { sub {
@@ -226,51 +234,55 @@
text-decoration: underline; text-decoration: underline;
} }
} }
}
.footnotes { .footnotes {
margin-top: 1em; margin-top: 1em;
margin-bottom: 1em; margin-bottom: 1em;
padding-top: 1em; padding-top: 1em;
background-color: var(--color-reference-background); background-color: var(--color-reference-background);
border-radius: 8px; border-radius: 8px;
padding: 8px 12px; padding: 8px 12px;
h4 { h4 {
margin-bottom: 5px; margin-bottom: 5px;
font-size: 12px; font-size: 12px;
}
a {
color: var(--color-link);
}
ol {
padding-left: 1em;
margin: 0;
li:last-child {
margin-bottom: 0;
} }
}
ol { li {
padding-left: 1em; font-size: 0.9em;
margin-bottom: 0.5em;
color: var(--color-text-light);
p {
display: inline;
margin: 0; margin: 0;
li:last-child {
margin-bottom: 0;
}
} }
}
li { .footnote-backref {
font-size: 0.9em; font-size: 0.8em;
margin-bottom: 0.5em; vertical-align: super;
color: var(--color-text-light); line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
p { &:hover {
display: inline; text-decoration: underline;
margin: 0;
}
}
.footnote-backref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
} }
} }
} }

View File

@@ -47,19 +47,22 @@ const DragableList: FC<Props<any>> = ({
<Droppable droppableId="droppable" {...droppableProps}> <Droppable droppableId="droppable" {...droppableProps}>
{(provided) => ( {(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}> <div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
{list.map((item, index) => ( {list.map((item, index) => {
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}> const id = item.id || item
{(provided) => ( return (
<div <Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
ref={provided.innerRef} {(provided) => (
{...provided.draggableProps} <div
{...provided.dragHandleProps} ref={provided.innerRef}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}> {...provided.draggableProps}
{children(item, index)} {...provided.dragHandleProps}
</div> style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
)} {children(item, index)}
</Draggable> </div>
))} )}
</Draggable>
)
})}
</div> </div>
)} )}
</Droppable> </Droppable>

View File

@@ -0,0 +1,39 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { MinAppType } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
app: MinAppType
size?: number
style?: React.CSSProperties
}
const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
if (!_app) {
return null
}
return (
<Container
src={_app.logo}
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
...style
}}
/>
)
}
const Container = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
export default MinAppIcon

View File

@@ -10,9 +10,8 @@ interface ListItemProps {
} }
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => { const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
const borderRadius = subtitle ? '10px' : '16px'
return ( return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}> <ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
<ListItemContent> <ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer> <TextContainer>
@@ -26,7 +25,7 @@ const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) =>
const ListItemContainer = styled.div` const ListItemContainer = styled.div`
padding: 7px 12px; padding: 7px 12px;
border-radius: 16px; border-radius: var(--list-item-border-radius);
font-size: 13px; font-size: 13px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -5,6 +5,7 @@ import { useBridge } from '@renderer/hooks/useBridge'
import store from '@renderer/store' import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime' import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer } from 'antd' import { Avatar, Drawer } from 'antd'
import { WebviewTag } from 'electron' import { WebviewTag } from 'electron'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
@@ -28,9 +29,10 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://') const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const onClose = () => { const onClose = async (_delay = 0.3) => {
setOpen(false) setOpen(false)
setTimeout(() => resolve({}), 300) await delay(_delay)
resolve({})
} }
MinApp.onClose = onClose MinApp.onClose = onClose
@@ -58,7 +60,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<ExportOutlined /> <ExportOutlined />
</Button> </Button>
)} )}
<Button onClick={onClose}> <Button onClick={() => onClose()}>
<CloseOutlined /> <CloseOutlined />
</Button> </Button>
</ButtonsGroup> </ButtonsGroup>
@@ -99,7 +101,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<Drawer <Drawer
title={<Title />} title={<Title />}
placement="bottom" placement="bottom"
onClose={onClose} onClose={() => onClose()}
open={open} open={open}
mask={true} mask={true}
rootClassName="minapp-drawer" rootClassName="minapp-drawer"
@@ -202,12 +204,22 @@ const EmptyView = styled.div`
export default class MinApp { export default class MinApp {
static topviewId = 0 static topviewId = 0
static onClose = () => {} static onClose = () => {}
static close() { static app: MinAppType | null = null
TopView.hide('MinApp')
store.dispatch(setMinappShow(false)) static async start(app: MinAppType) {
} if (app?.id && MinApp.app?.id === app?.id) {
static start(app: MinAppType) { return
}
if (MinApp.app) {
// @ts-ignore delay params
await MinApp.onClose(0)
await delay(0)
}
MinApp.app = app
store.dispatch(setMinappShow(true)) store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
TopView.show( TopView.show(
<PopupContainer <PopupContainer
@@ -221,4 +233,10 @@ export default class MinApp {
) )
}) })
} }
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
MinApp.app = null
}
} }

View File

@@ -0,0 +1,36 @@
import { isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import { Model } from '@renderer/types'
import { isFreeModel } from '@renderer/utils'
import { Tag } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import VisionIcon from './Icons/VisionIcon'
import WebSearchIcon from './Icons/WebSearchIcon'
interface ModelTagsProps {
model: Model
showFree?: boolean
}
const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true }) => {
const { t } = useTranslation()
return (
<>
{isVisionModel(model) && <VisionIcon />}
{isWebSearchModel(model) && <WebSearchIcon />}
{showFree && isFreeModel(model) && (
<Tag style={{ marginLeft: 10 }} color="green">
{t('models.free')}
</Tag>
)}
{isEmbeddingModel(model) && (
<Tag style={{ marginLeft: 10 }} color="orange">
{t('models.embedding')}
</Tag>
)}
</>
)
}
export default ModelTags

View File

@@ -1,5 +1,5 @@
import { Center } from '@renderer/components/Layout' import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import App from '@renderer/pages/apps/App' import App from '@renderer/pages/apps/App'
import { Popover } from 'antd' import { Popover } from 'antd'
import { Empty } from 'antd' import { Empty } from 'antd'
@@ -14,9 +14,9 @@ interface Props {
children: React.ReactNode children: React.ReactNode
} }
const AppStorePopover: FC<Props> = ({ children }) => { const MinAppsPopover: FC<Props> = ({ children }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const apps = getAllMinApps() const { minapps } = useMinapps()
useHotkeys('esc', () => { useHotkeys('esc', () => {
setOpen(false) setOpen(false)
@@ -29,10 +29,10 @@ const AppStorePopover: FC<Props> = ({ children }) => {
const content = ( const content = (
<PopoverContent> <PopoverContent>
<AppsContainer> <AppsContainer>
{apps.map((app) => ( {minapps.map((app) => (
<App key={app.id} app={app} onClick={handleClose} size={50} /> <App key={app.id} app={app} onClick={handleClose} size={50} />
))} ))}
{isEmpty(apps) && ( {isEmpty(minapps) && (
<Center> <Center>
<Empty /> <Empty />
</Center> </Center>
@@ -48,7 +48,7 @@ const AppStorePopover: FC<Props> = ({ children }) => {
content={content} content={content}
trigger="click" trigger="click"
placement="bottomRight" placement="bottomRight"
overlayInnerStyle={{ padding: 25 }}> styles={{ body: { padding: 25 } }}>
{children} {children}
</Popover> </Popover>
) )
@@ -58,8 +58,8 @@ const PopoverContent = styled(Scrollbar)``
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(6, minmax(90px, 1fr));
gap: 25px 35px; gap: 18px;
` `
export default AppStorePopover export default MinAppsPopover

View File

@@ -1,7 +1,7 @@
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons' import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon' import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import { getModelLogo, isEmbeddingModel, isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
@@ -12,8 +12,8 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import WebSearchIcon from '../Icons/WebSearchIcon'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import ModelTags from '../ModelTags'
import Scrollbar from '../Scrollbar' import Scrollbar from '../Scrollbar'
type MenuItem = Required<MenuProps>['items'][number] type MenuItem = Required<MenuProps>['items'][number]
@@ -75,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
label: ( label: (
<ModelItem> <ModelItem>
<span> <span>
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />} {m?.name} <ModelTags model={m} />
</span> </span>
<PinIcon <PinIcon
onClick={(e) => { onClick={(e) => {
@@ -115,7 +115,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
.flatMap((p) => p.models || []) .flatMap((p) => p.models || [])
.filter((m) => pinnedModels.includes(getModelUniqId(m))) .filter((m) => pinnedModels.includes(getModelUniqId(m)))
.map((m) => ({ .map((m) => ({
key: getModelUniqId(m), key: getModelUniqId(m) + '_pinned',
label: ( label: (
<ModelItem> <ModelItem>
{m?.name} {isVisionModel(m) && <VisionIcon />} {m?.name} {isVisionModel(m) && <VisionIcon />}

View File

@@ -4,6 +4,7 @@ import { TextAreaProps } from 'antd/lib/input'
import { TextAreaRef } from 'antd/lib/input/TextArea' import { TextAreaRef } from 'antd/lib/input/TextArea'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../TopView' import { TopView } from '../TopView'
@@ -11,13 +12,14 @@ interface ShowParams {
text: string text: string
textareaProps?: TextAreaProps textareaProps?: TextAreaProps
modalProps?: ModalProps modalProps?: ModalProps
children?: (props: { onOk?: () => void; onCancel?: () => void }) => React.ReactNode
} }
interface Props extends ShowParams { interface Props extends ShowParams {
resolve: (data: any) => void resolve: (data: any) => void
} }
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve }) => { const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve, children }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const [textValue, setTextValue] = useState(text) const [textValue, setTextValue] = useState(text)
@@ -68,17 +70,23 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
ref={textareaRef} ref={textareaRef}
rows={2} rows={2}
autoFocus autoFocus
spellCheck={false}
{...textareaProps} {...textareaProps}
value={textValue} value={textValue}
onInput={resizeTextArea} onInput={resizeTextArea}
onChange={(e) => setTextValue(e.target.value)} onChange={(e) => setTextValue(e.target.value)}
/> />
<ChildrenContainer>{children && children({ onOk, onCancel })}</ChildrenContainer>
</Modal> </Modal>
) )
} }
const TopViewKey = 'TextEditPopup' const TopViewKey = 'TextEditPopup'
const ChildrenContainer = styled.div`
position: relative;
`
export default class TextEditPopup { export default class TextEditPopup {
static topviewId = 0 static topviewId = 0
static hide() { static hide() {

View File

@@ -55,7 +55,7 @@ const NavbarCenterContainer = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 ${isMac ? '20px' : '15px'}; padding: 0 ${isMac ? '20px' : 0};
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
` `

View File

@@ -3,15 +3,20 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env' import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useMinapps } from '@renderer/hooks/useMinapps'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import type { MenuProps } from 'antd'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import MinApp from '../MinApp' import MinApp from '../MinApp'
import UserPopup from '../Popups/UserPopup' import UserPopup from '../Popups/UserPopup'
@@ -19,25 +24,21 @@ const Sidebar: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const avatar = useAvatar() const avatar = useAvatar()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { generating } = useRuntime()
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings() const { windowStyle, sidebarIcons } = useSettings()
const { theme, toggleTheme } = useTheme() const { theme, toggleTheme } = useTheme()
const { pinned } = useMinapps()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const onEditUser = () => UserPopup.show() const onEditUser = () => UserPopup.show()
const macTransparentWindow = isMac && windowStyle === 'transparent' const macTransparentWindow = isMac && windowStyle === 'transparent'
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)' const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
const to = (path: string) => { const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' }) const to = async (path: string) => {
return await modelGenerating()
}
navigate(path) navigate(path)
} }
@@ -49,63 +50,19 @@ const Sidebar: FC = () => {
zIndex: minappShow ? 10000 : 'initial' zIndex: minappShow ? 10000 : 'initial'
}}> }}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right"> <MainMenus />
<StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}>
<i className="iconfont icon-chat" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/agents')}>
<Icon className={isRoutes('/agents')}>
<i className="iconfont icon-business-smart-assistant" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/paintings')}>
<Icon className={isRoute('/paintings')}>
<PictureOutlined style={{ fontSize: 16 }} />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/translate')}>
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Tooltip>
{showMinappIcon && (
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" />
</Icon>
</StyledLink>
</Tooltip>
)}
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/knowledge')}>
<Icon className={isRoute('/knowledge')}>
<FileSearchOutlined />
</Icon>
</StyledLink>
</Tooltip>
{showFilesIcon && (
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}>
<FolderOutlined />
</Icon>
</StyledLink>
</Tooltip>
)}
</Menus> </Menus>
</MainMenus> {showPinnedApps && (
<AppsContainer>
<Divider />
<Menus>
<PinnedApps />
</Menus>
</AppsContainer>
)}
</MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}> <Icon onClick={() => toggleTheme()}>
@@ -128,6 +85,82 @@ const Sidebar: FC = () => {
) )
} }
const MainMenus: FC = () => {
const { t } = useTranslation()
const { pathname } = useLocation()
const { sidebarIcons } = useSettings()
const navigate = useNavigate()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const iconMap = {
assistants: <i className="iconfont icon-chat" />,
agents: <i className="iconfont icon-business-smart-assistant" />,
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
translate: <TranslationOutlined />,
minapp: <i className="iconfont icon-appstore" />,
knowledge: <FileSearchOutlined />,
files: <FolderOutlined />
}
const pathMap = {
assistants: '/',
agents: '/agents',
paintings: '/paintings',
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
}
return sidebarIcons.visible.map((icon) => {
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => navigate(path)}>
<Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink>
</Tooltip>
)
})
}
const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
{(app) => {
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: t('minapp.sidebar.remove.title'),
onClick: () => {
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
}
}
]
return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Icon onClick={() => MinApp.start(app)}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
}}
</DragableList>
)
}
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -149,15 +182,18 @@ const AvatarImg = styled(Avatar)`
border: none; border: none;
cursor: pointer; cursor: pointer;
` `
const MainMenus = styled.div` const MainMenusContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column;
overflow: hidden;
` `
const Menus = styled.div` const Menus = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 5px;
` `
const Icon = styled.div` const Icon = styled.div`
@@ -167,7 +203,6 @@ const Icon = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 50%; border-radius: 50%;
margin-bottom: 5px;
-webkit-app-region: none; -webkit-app-region: none;
border: 0.5px solid transparent; border: 0.5px solid transparent;
.iconfont, .iconfont,
@@ -205,4 +240,24 @@ const StyledLink = styled.div`
} }
` `
const AppsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
overflow-y: auto;
overflow-x: hidden;
margin-bottom: 10px;
-webkit-app-region: none;
&::-webkit-scrollbar {
display: none;
}
`
const Divider = styled.div`
width: 50%;
margin: 8px 0;
border-bottom: 0.5px solid var(--color-border);
`
export default Sidebar export default Sidebar

View File

@@ -6,6 +6,10 @@ import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp' import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png' import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png' import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
import GrokAppLogo from '@renderer/assets/images/apps/grok.png'
import HikaLogo from '@renderer/assets/images/apps/hika.webp'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg' import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg' import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp' import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
@@ -13,6 +17,7 @@ import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp' import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp' import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png' import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png' import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png' import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp' import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp'
@@ -31,7 +36,7 @@ import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.p
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
const _apps: MinAppType[] = [ export const DEFAULT_MIN_APPS: MinAppType[] = [
{ {
id: 'openai', id: 'openai',
name: 'ChatGPT', name: 'ChatGPT',
@@ -223,14 +228,42 @@ const _apps: MinAppType[] = [
logo: ThinkAnyLogo, logo: ThinkAnyLogo,
url: 'https://thinkany.ai/', url: 'https://thinkany.ai/',
bodered: true bodered: true
},
{
id: 'hika',
name: 'Hika',
logo: HikaLogo,
url: 'https://hika.fyi/',
bodered: true
},
{
id: 'github-copilot',
name: 'GitHub Copilot',
logo: GithubCopilotLogo,
url: 'https://github.com/copilot'
},
{
id: 'genspark',
name: 'Genspark',
logo: GensparkLogo,
url: 'https://www.genspark.ai/'
},
{
id: 'grok',
name: 'Grok',
logo: GrokAppLogo,
url: 'https://x.com/i/grok',
bodered: true
},
{
id: 'qwenlm',
name: 'QwenLM',
logo: QwenlmAppLogo,
url: 'https://qwenlm.ai/'
} }
] ]
export function getAllMinApps() {
return _apps as MinAppType[]
}
export function startMinAppById(id: string) { export function startMinAppById(id: string) {
const app = getAllMinApps().find((app) => app?.id === id) const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
app && MinApp.start(app) app && MinApp.start(app)
} }

View File

@@ -125,6 +125,8 @@ import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import OpenAI from 'openai' import OpenAI from 'openai'
import { getWebSearchTools } from './tools'
const visionAllowedModels = [ const visionAllowedModels = [
'llava', 'llava',
'moondream', 'moondream',
@@ -152,7 +154,7 @@ export const VISION_REGEX = new RegExp(
) )
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|base|retrieval|uae-|gte-)/i export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina)/i
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
export function getModelLogo(modelId: string) { export function getModelLogo(modelId: string) {
@@ -262,6 +264,94 @@ export function getModelLogo(modelId: string) {
} }
export const SYSTEM_MODELS: Record<string, Model[]> = { export const SYSTEM_MODELS: Record<string, Model[]> = {
qwenlm: [
{
id: 'qwen-plus-latest',
provider: 'qwenlm',
name: 'Qwen2.5-Plus',
group: 'Qwen 2.5'
},
{
id: 'qvq-72b-preview',
provider: 'qwenlm',
name: 'QVQ-72B-Preview',
group: 'QVQ'
},
{
id: 'qwq-32b-preview',
provider: 'qwenlm',
name: 'QwQ-32B-Preview',
group: 'QVQ'
},
{
id: 'qwen2.5-coder-32b-instruct',
provider: 'qwenlm',
name: 'Qwen2.5-Coder-32B-Instruct',
group: 'Qwen 2.5'
},
{
id: 'qwen-vl-max-latest',
provider: 'qwenlm',
name: 'Qwen2-VL-Max',
group: 'Qwen 2'
},
{
id: 'qwen-turbo-latest',
provider: 'qwenlm',
name: 'Qwen2.5-Turbo',
group: 'Qwen 2.5'
},
{
id: 'qwen2.5-72b-instruct',
provider: 'qwenlm',
name: 'Qwen2.5-72B-Instruct',
group: 'Qwen 2.5'
},
{
id: 'qwen2.5-32b-instruct',
provider: 'qwenlm',
name: 'Qwen2.5-32B-Instruct',
group: 'Qwen 2.5'
}
],
aihubmix: [
{
id: 'gpt-4o',
provider: 'aihubmix',
name: 'GPT-4o',
group: 'GPT-4o'
},
{
id: 'claude-3-5-sonnet-latest',
provider: 'aihubmix',
name: 'Claude 3.5 Sonnet',
group: 'Claude 3.5'
},
{
id: 'gemini-2.0-flash-exp-search',
provider: 'aihubmix',
name: 'Gemini 2.0 Flash Exp Search',
group: 'Gemini 2.0'
},
{
id: 'deepseek-chat',
provider: 'aihubmix',
name: 'DeepSeek Chat',
group: 'DeepSeek Chat'
},
{
id: 'aihubmix-Llama-3-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama-3.3-70b',
group: 'Llama 3.3'
},
{
id: 'Qwen/QVQ-72B-Preview',
provider: 'aihubmix',
name: 'Qwen/QVQ-72B',
group: 'Qwen'
}
],
ollama: [], ollama: [],
silicon: [ silicon: [
{ {
@@ -274,7 +364,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
id: 'Qwen/Qwen2.5-7B-Instruct', id: 'Qwen/Qwen2.5-7B-Instruct',
provider: 'silicon', provider: 'silicon',
name: 'Qwen2.5-7B-Instruct', name: 'Qwen2.5-7B-Instruct',
group: 'Qwen2.5' group: 'Qwen'
}, },
{ {
id: 'meta-llama/Llama-3.3-70B-Instruct', id: 'meta-llama/Llama-3.3-70B-Instruct',
@@ -521,9 +611,21 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
], ],
zhipu: [ zhipu: [
{ {
id: 'glm-4', id: 'glm-zero-preview',
provider: 'zhipu', provider: 'zhipu',
name: 'GLM-4', name: 'GLM-Zero-Preview',
group: 'GLM-Zero'
},
{
id: 'glm-4-0520',
provider: 'zhipu',
name: 'GLM-4-0520',
group: 'GLM-4'
},
{
id: 'glm-4-long',
provider: 'zhipu',
name: 'GLM-4-Long',
group: 'GLM-4' group: 'GLM-4'
}, },
{ {
@@ -550,6 +652,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'GLM-4-Flash', name: 'GLM-4-Flash',
group: 'GLM-4' group: 'GLM-4'
}, },
{
id: 'glm-4-flashx',
provider: 'zhipu',
name: 'GLM-4-FlashX',
group: 'GLM-4'
},
{ {
id: 'glm-4v', id: 'glm-4v',
provider: 'zhipu', provider: 'zhipu',
@@ -567,6 +675,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'zhipu', provider: 'zhipu',
name: 'GLM-4-AllTools', name: 'GLM-4-AllTools',
group: 'GLM-4-AllTools' group: 'GLM-4-AllTools'
},
{
id: 'embedding-3',
provider: 'zhipu',
name: 'Embedding-3',
group: 'Embedding'
} }
], ],
moonshot: [ moonshot: [
@@ -700,19 +814,54 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Mistral' group: 'Mistral'
} }
], ],
jina: [], jina: [
aihubmix: [
{ {
id: 'gpt-4o-mini', id: 'jina-clip-v1',
provider: 'aihubmix', provider: 'jina',
name: 'GPT-4o Mini', name: 'jina-clip-v1',
group: 'GPT-4o' group: 'Jina Clip'
}, },
{ {
id: 'aihubmix-Llama-3-70B-Instruct', id: 'jina-clip-v2',
provider: 'aihubmix', provider: 'jina',
name: 'Llama 3 70B Instruct', name: 'jina-clip-v2',
group: 'Llama3' group: 'Jina Clip'
},
{
id: 'jina-embeddings-v2-base-en',
provider: 'jina',
name: 'jina-embeddings-v2-base-en',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-es',
provider: 'jina',
name: 'jina-embeddings-v2-base-es',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-de',
provider: 'jina',
name: 'jina-embeddings-v2-base-de',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-zh',
provider: 'jina',
name: 'jina-embeddings-v2-base-zh',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-code',
provider: 'jina',
name: 'jina-embeddings-v2-base-code',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v3',
provider: 'jina',
name: 'jina-embeddings-v3',
group: 'Jina Embeddings V3'
} }
], ],
fireworks: [ fireworks: [
@@ -912,6 +1061,11 @@ export const TEXT_TO_IMAGES_MODELS = [
} }
] ]
export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
'stabilityai/stable-diffusion-2-1',
'stabilityai/stable-diffusion-xl-base-1.0'
]
export function isTextToImageModel(model: Model): boolean { export function isTextToImageModel(model: Model): boolean {
return TEXT_TO_IMAGE_REGEX.test(model.id) return TEXT_TO_IMAGE_REGEX.test(model.id)
} }
@@ -955,5 +1109,43 @@ export function isWebSearchModel(model: Model): boolean {
return false return false
} }
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp' if (provider?.type === 'openai') {
if (model?.id?.includes('gemini-2.0-flash-exp')) {
return true
}
}
if (provider.id === 'gemini' || provider?.type === 'gemini') {
return model?.id === 'gemini-2.0-flash-exp'
}
if (provider.id === 'hunyuan') {
return model?.id !== 'hunyuan-lite'
}
if (provider.id === 'aihubmix') {
return model?.id === 'gemini-2.0-flash-exp-search'
}
if (provider.id === 'zhipu') {
return model?.id?.startsWith('glm-4-')
}
return false
}
export function getOpenAIWebSearchParams(model: Model): Record<string, any> {
if (isWebSearchModel(model)) {
const webSearchTools = getWebSearchTools(model)
if (model.provider === 'hunyuan') {
return { enable_enhancement: true }
}
return {
tools: webSearchTools
}
}
return {}
} }

View File

@@ -45,7 +45,7 @@ export const AGENT_PROMPT = `
` `
export const SUMMARIZE_PROMPT = export const SUMMARIZE_PROMPT =
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号' '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号'
export const TRANSLATE_PROMPT = export const TRANSLATE_PROMPT =
'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.' 'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.'

View File

@@ -28,6 +28,7 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png' import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png' import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png' import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import QwenLMProviderLogo from '@renderer/assets/images/providers/qwenlm.png'
export function getProviderLogo(providerId: string) { export function getProviderLogo(providerId: string) {
switch (providerId) { switch (providerId) {
@@ -91,6 +92,8 @@ export function getProviderLogo(providerId: string) {
return MistralProviderLogo return MistralProviderLogo
case 'jina': case 'jina':
return JinaProviderLogo return JinaProviderLogo
case 'qwenlm':
return QwenLMProviderLogo
default: default:
return undefined return undefined
} }
@@ -418,5 +421,16 @@ export const PROVIDER_CONFIG = {
docs: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/', docs: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/',
models: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models' models: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models'
} }
},
qwenlm: {
api: {
url: 'https://chat.qwenlm.ai/api/'
},
websites: {
official: 'https://chat.qwenlm.ai',
apiKey: 'https://chat.qwenlm.ai',
docs: 'https://chat.qwenlm.ai',
models: 'https://chat.qwenlm.ai'
}
} }
} }

View File

@@ -0,0 +1,32 @@
import { Model } from '@renderer/types'
import { ChatCompletionTool } from 'openai/resources'
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
if (model?.provider === 'zhipu') {
if (model.id === 'glm-4-alltools') {
return [
{
type: 'web_browser'
} as unknown as ChatCompletionTool
]
}
return [
{
type: 'web_search',
web_search: {
enable: true,
search_result: true
}
} as unknown as ChatCompletionTool
]
}
return [
{
type: 'function',
function: {
name: 'googleSearch'
}
}
]
}

View File

@@ -54,13 +54,12 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
const codeToHtml = async (code: string, language: string) => { const codeToHtml = async (code: string, language: string) => {
if (!highlighter) return '' if (!highlighter) return ''
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!) const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
try { try {
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) { if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
if (language in bundledLanguages || language === 'text') { if (language in bundledLanguages || language === 'text') {
await highlighter.loadLanguage(language as BundledLanguage) await highlighter.loadLanguage(language as BundledLanguage)
console.log(`Loaded language: ${language}`)
} else { } else {
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>` return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
} }

View File

@@ -2,7 +2,6 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime' import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils' import { delay, runAsyncFunction } from '@renderer/utils'
@@ -16,8 +15,7 @@ import useUpdateHandler from './useUpdateHandler'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, webdavAutoSync, webdavSyncInterval } = const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, customCss } = useSettings()
useSettings()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar')) const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@@ -76,11 +74,21 @@ export function useAppInit() {
}) })
}, [dispatch]) }, [dispatch])
useEffect(() => {
webdavAutoSync ? startAutoSync() : stopAutoSync()
}, [webdavAutoSync, webdavSyncInterval])
useEffect(() => { useEffect(() => {
import('@renderer/queue/KnowledgeQueue') import('@renderer/queue/KnowledgeQueue')
}, []) }, [])
useEffect(() => {
const oldCustomCss = document.getElementById('user-defined-custom-css')
if (oldCustomCss) {
oldCustomCss.remove()
}
if (customCss) {
const style = document.createElement('style')
style.id = 'user-defined-custom-css'
style.textContent = customCss
document.head.appendChild(style)
}
}, [customCss])
} }

View File

@@ -1,3 +1,4 @@
import { db } from '@renderer/databases'
import { getDefaultTopic } from '@renderer/services/AssistantService' import { getDefaultTopic } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
@@ -50,8 +51,20 @@ export function useAssistant(id: string) {
dispatch(removeTopic({ assistantId: assistant.id, topic })) dispatch(removeTopic({ assistantId: assistant.id, topic }))
}, },
moveTopic: (topic: Topic, toAssistant: Assistant) => { moveTopic: (topic: Topic, toAssistant: Assistant) => {
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic } })) dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } }))
dispatch(removeTopic({ assistantId: assistant.id, topic })) dispatch(removeTopic({ assistantId: assistant.id, topic }))
// update topic messages in database
db.topics
.where('id')
.equals(topic.id)
.modify((dbTopic) => {
if (dbTopic.messages) {
dbTopic.messages = dbTopic.messages.map((message) => ({
...message,
assistantId: toAssistant.id
}))
}
})
}, },
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })), updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })), updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),

View File

@@ -15,6 +15,7 @@ import {
renameBase, renameBase,
updateBase, updateBase,
updateBases, updateBases,
updateItem as updateItemAction,
updateItemProcessingStatus, updateItemProcessingStatus,
updateNotes updateNotes
} from '@renderer/store/knowledge' } from '@renderer/store/knowledge'
@@ -117,7 +118,8 @@ export const useKnowledge = (baseId: string) => {
await db.knowledge_notes.put(updatedNote) await db.knowledge_notes.put(updatedNote)
dispatch(updateNotes({ baseId, item: updatedNote })) dispatch(updateNotes({ baseId, item: updatedNote }))
} }
setTimeout(() => KnowledgeQueue.checkAllBases(), 0) const noteItem = base?.items.find((item) => item.id === noteId)
noteItem && refreshItem(noteItem)
} }
// 获取笔记内容 // 获取笔记内容
@@ -125,6 +127,10 @@ export const useKnowledge = (baseId: string) => {
return await db.knowledge_notes.get(noteId) return await db.knowledge_notes.get(noteId)
} }
const updateItem = (item: KnowledgeItem) => {
dispatch(updateItemAction({ baseId, item }))
}
// 移除项目 // 移除项目
const removeItem = async (item: KnowledgeItem) => { const removeItem = async (item: KnowledgeItem) => {
dispatch(removeItemAction({ baseId, item })) dispatch(removeItemAction({ baseId, item }))
@@ -138,6 +144,27 @@ export const useKnowledge = (baseId: string) => {
} }
} }
// 刷新项目
const refreshItem = async (item: KnowledgeItem) => {
const status = getProcessingStatus(item.id)
if (status === 'pending' || status === 'processing') {
return
}
if (base && item.uniqueId) {
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) })
updateItem({
...item,
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
uniqueId: undefined
})
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
}
// 更新处理状态 // 更新处理状态
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => { const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
dispatch( dispatch(
@@ -238,7 +265,9 @@ export const useKnowledge = (baseId: string) => {
addNote, addNote,
updateNoteContent, updateNoteContent,
getNoteContent, getNoteContent,
updateItem,
updateItemStatus, updateItemStatus,
refreshItem,
getProcessingStatus, getProcessingStatus,
getProcessingItemsByType, getProcessingItemsByType,
clearCompleted, clearCompleted,

View File

@@ -37,4 +37,32 @@ export const useMermaid = () => {
setTimeout(renderMermaid, 100) setTimeout(renderMermaid, 100)
}, [generating]) }, [generating])
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault()
const mermaidElement = (e.target as HTMLElement).closest('.mermaid')
if (!mermaidElement) return
const svg = mermaidElement.querySelector('svg')
if (!svg) return
const currentScale = parseFloat(svg.style.transform?.match(/scale\((.*?)\)/)?.[1] || '1')
const delta = e.deltaY < 0 ? 0.1 : -0.1
const newScale = Math.max(0.1, Math.min(3, currentScale + delta))
const container = svg.parentElement
if (container) {
container.style.overflow = 'auto'
container.style.position = 'relative'
svg.style.transformOrigin = 'top left'
svg.style.transform = `scale(${newScale})`
}
}
}
document.addEventListener('wheel', handleWheel, { passive: false })
return () => document.removeEventListener('wheel', handleWheel)
}, [])
} }

View File

@@ -0,0 +1,23 @@
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
import { MinAppType } from '@renderer/types'
export const useMinapps = () => {
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
const dispatch = useAppDispatch()
return {
minapps: enabled,
disabled,
pinned,
updateMinapps: (minapps: MinAppType[]) => {
dispatch(setMinApps(minapps))
},
updateDisabledMinapps: (minapps: MinAppType[]) => {
dispatch(setDisabledMinApps(minapps))
},
updatePinnedMinapps: (minapps: MinAppType[]) => {
dispatch(setPinnedMinApps(minapps))
}
}
}

View File

@@ -14,6 +14,7 @@ export function usePaintings() {
paintings, paintings,
addPainting: () => { addPainting: () => {
const newPainting: Painting = { const newPainting: Painting = {
model: TEXT_TO_IMAGES_MODELS[0].id,
id: uuid(), id: uuid(),
urls: [], urls: [],
files: [], files: [],
@@ -24,7 +25,7 @@ export function usePaintings() {
seed: generateRandomSeed(), seed: generateRandomSeed(),
steps: 25, steps: 25,
guidanceScale: 4.5, guidanceScale: 4.5,
model: TEXT_TO_IMAGES_MODELS[0].id promptEnhancement: true
} }
dispatch(addPainting(newPainting)) dispatch(addPainting(newPainting))
return newPainting return newPainting

View File

@@ -1,5 +1,17 @@
import { useAppSelector } from '@renderer/store' import i18n from '@renderer/i18n'
import store, { useAppSelector } from '@renderer/store'
export function useRuntime() { export function useRuntime() {
return useAppSelector((state) => state.runtime) return useAppSelector((state) => state.runtime)
} }
export function modelGenerating() {
const generating = store.getState().runtime.generating
if (generating) {
window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' })
return Promise.reject()
}
return Promise.resolve()
}

View File

@@ -1,13 +1,15 @@
import { useAppDispatch, useAppSelector } from '@renderer/store' import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
SendMessageShortcut, SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut, setSendMessageShortcut as _setSendMessageShortcut,
setSidebarIcons,
setTheme, setTheme,
SettingsState,
setTopicPosition, setTopicPosition,
setTray, setTray,
setWindowStyle setWindowStyle
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types' import { SidebarIcon, ThemeMode } from '@renderer/types'
export function useSettings() { export function useSettings() {
const settings = useAppSelector((state) => state.settings) const settings = useAppSelector((state) => state.settings)
@@ -29,6 +31,15 @@ export function useSettings() {
}, },
setTopicPosition(topicPosition: 'left' | 'right') { setTopicPosition(topicPosition: 'left' | 'right') {
dispatch(setTopicPosition(topicPosition)) dispatch(setTopicPosition(topicPosition))
},
updateSidebarIcons(icons: { visible: SidebarIcon[]; disabled: SidebarIcon[] }) {
dispatch(setSidebarIcons(icons))
},
updateSidebarVisibleIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ visible: icons }))
},
updateSidebarDisabledIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ disabled: icons }))
} }
} }
} }
@@ -41,3 +52,7 @@ export function useMessageStyle() {
isBubbleStyle isBubbleStyle
} }
} }
export const getStoreSetting = (key: keyof SettingsState) => {
return store.getState().settings[key]
}

View File

@@ -112,7 +112,8 @@
"topics.list": "Topic List", "topics.list": "Topic List",
"topics.move_to": "Move to", "topics.move_to": "Move to",
"topics.title": "Topics", "topics.title": "Topics",
"translate": "Translate" "translate": "Translate",
"resend": "Resend"
}, },
"common": { "common": {
"and": "and", "and": "and",
@@ -182,8 +183,14 @@
"name": "Name", "name": "Name",
"open": "Open", "open": "Open",
"size": "Size", "size": "Size",
"type": "Type",
"text": "Text", "text": "Text",
"title": "Files" "title": "Files",
"edit": "Edit",
"delete": "Delete",
"delete.title": "Delete File",
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
"delete.paintings.warning": "Image contains this file, deletion is not possible"
}, },
"history": { "history": {
"continue_chat": "Continue Chatting", "continue_chat": "Continue Chatting",
@@ -211,6 +218,10 @@
"png": "Download PNG", "png": "Download PNG",
"svg": "Download SVG" "svg": "Download SVG"
}, },
"resize": {
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out"
},
"tabs": { "tabs": {
"preview": "Preview", "preview": "Preview",
"source": "Source" "source": "Source"
@@ -220,6 +231,7 @@
"message": { "message": {
"api.connection.failed": "Connection failed", "api.connection.failed": "Connection failed",
"api.connection.success": "Connection successful", "api.connection.success": "Connection successful",
"api.check.model.title": "Select the model to use for detection",
"assistant.added.content": "Assistant added successfully", "assistant.added.content": "Assistant added successfully",
"backup.failed": "Backup failed", "backup.failed": "Backup failed",
"backup.success": "Backup successful", "backup.success": "Backup successful",
@@ -243,7 +255,7 @@
"reset.double.confirm.title": "DATA LOST !!!", "reset.double.confirm.title": "DATA LOST !!!",
"restore.success": "Restored successfully", "restore.success": "Restored successfully",
"save.success.title": "Saved successfully", "save.success.title": "Saved successfully",
"switch.disabled": "Switching is disabled while the assistant is generating", "switch.disabled": "Please wait for the current reply to complete",
"topic.added": "New topic added", "topic.added": "New topic added",
"upgrade.success.button": "Restart", "upgrade.success.button": "Restart",
"upgrade.success.content": "Please restart the application to complete the upgrade", "upgrade.success.content": "Please restart the application to complete the upgrade",
@@ -253,7 +265,9 @@
"error.get_embedding_dimensions": "Failed to get embedding dimensions" "error.get_embedding_dimensions": "Failed to get embedding dimensions"
}, },
"minapp": { "minapp": {
"title": "MinApp" "title": "MinApp",
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.", "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
@@ -278,7 +292,9 @@
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?", "regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
"seed": "Seed", "seed": "Seed",
"seed_tip": "The same seed and prompt can produce similar images", "seed_tip": "The same seed and prompt can produce similar images",
"title": "Images" "title": "Images",
"prompt_enhancement": "Prompt Enhancement",
"prompt_enhancement_tip": "Rewrite prompts into detailed, model-friendly versions when switched on"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -310,7 +326,8 @@
"together": "Together", "together": "Together",
"yi": "Yi", "yi": "Yi",
"zhinao": "360AI", "zhinao": "360AI",
"zhipu": "ZHIPU AI" "zhipu": "ZHIPU AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "About & Feedback", "about": "About & Feedback",
@@ -357,11 +374,16 @@
"webdav.password": "WebDAV Password", "webdav.password": "WebDAV Password",
"webdav.path": "WebDAV Path", "webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "Auto Sync", "webdav.autoSync": "Auto Backup",
"webdav.minutes": "Minutes", "webdav.minutes": "Minutes",
"webdav.restore.button": "Restore from WebDAV", "webdav.restore.button": "Restore from WebDAV",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAV User" "webdav.user": "WebDAV User",
"webdav.syncStatus": "Backup Status",
"webdav.autoSync.off": "Off",
"webdav.noSync": "Waiting for next backup",
"webdav.syncError": "Backup Error",
"webdav.lastSync": "Last Backup"
}, },
"display.title": "Display Settings", "display.title": "Display Settings",
"font_size.title": "Message font size", "font_size.title": "Message font size",
@@ -377,10 +399,24 @@
"general.user_name.placeholder": "Enter your name", "general.user_name.placeholder": "Enter your name",
"general.view_webdav_settings": "View WebDAV settings", "general.view_webdav_settings": "View WebDAV settings",
"general.display.title": "Display Settings", "general.display.title": "Display Settings",
"display.sidebar.translate.icon": "Show Translate icon",
"display.sidebar.painting.icon": "Show Painting icon",
"display.sidebar.minapp.icon": "Show MinApp icon", "display.sidebar.minapp.icon": "Show MinApp icon",
"display.sidebar.knowledge.icon": "Show Knowledge icon",
"display.sidebar.files.icon": "Show Files icon", "display.sidebar.files.icon": "Show Files icon",
"display.sidebar.title": "Sidebar Settings", "display.sidebar.title": "Sidebar Settings",
"display.sidebar.visible": "Show icons",
"display.sidebar.disabled": "Hide icons",
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
"display.sidebar.empty": "Drag the hidden feature from the left side here",
"display.minApp.title": "MinApp Settings",
"display.minApp.visible": "Visible MinApp",
"display.minApp.disabled": "Hidden MinApp",
"display.minApp.empty": "Drag minApp from the left to hide them here",
"": "MinApp that have been added to the sidebar do not support hiding. If you want to hide them, please remove them from the sidebar first.",
"display.topic.title": "Topic Settings", "display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */",
"input.auto_translate_with_space": "Quickly translate with 3 spaces", "input.auto_translate_with_space": "Quickly translate with 3 spaces",
"messages.divider": "Show divider between messages", "messages.divider": "Show divider between messages",
"messages.input.paste_long_text_as_file": "Paste long text as file", "messages.input.paste_long_text_as_file": "Paste long text as file",
@@ -388,7 +424,7 @@
"messages.input.show_estimated_tokens": "Show estimated tokens", "messages.input.show_estimated_tokens": "Show estimated tokens",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.input.title": "Input Settings", "messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input msg", "messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math render engine", "messages.math_engine": "Math render engine",
"messages.model.title": "Model Settings", "messages.model.title": "Model Settings",
"messages.title": "Message Settings", "messages.title": "Message Settings",
@@ -415,6 +451,7 @@
"models.translate_model_prompt_title": "Translate Model Prompt", "models.translate_model_prompt_title": "Translate Model Prompt",
"models.topic_naming_model_setting_title": "Topic Naming Model Settings", "models.topic_naming_model_setting_title": "Topic Naming Model Settings",
"models.enable_topic_naming": "Topic Auto Naming", "models.enable_topic_naming": "Topic Auto Naming",
"models.topic_naming_prompt": "Topic Naming Prompt",
"provider": { "provider": {
"add.name": "Provider Name", "add.name": "Provider Name",
"add.name.placeholder": "Example: OpenAI", "add.name.placeholder": "Example: OpenAI",
@@ -481,7 +518,8 @@
"clear_shortcut": "Clear Shortcut", "clear_shortcut": "Clear Shortcut",
"toggle_show_assistants": "Toggle Assistants", "toggle_show_assistants": "Toggle Assistants",
"toggle_show_topics": "Toggle Topics", "toggle_show_topics": "Toggle Topics",
"copy_last_message": "Copy Last Message" "copy_last_message": "Copy Last Message",
"search_message": "Search Message"
}, },
"theme.auto": "Auto", "theme.auto": "Auto",
"theme.dark": "Dark", "theme.dark": "Dark",
@@ -522,7 +560,7 @@
"show_window": "Show Window", "show_window": "Show Window",
"quit": "Quit" "quit": "Quit"
}, },
"knowledge_base": { "knowledge": {
"title": "Knowledge Base", "title": "Knowledge Base",
"search": "Search knowledge base", "search": "Search knowledge base",
"empty": "No knowledge base found", "empty": "No knowledge base found",
@@ -564,6 +602,7 @@
"directory_placeholder": "Enter Directory Path", "directory_placeholder": "Enter Directory Path",
"model_info": "Model Info", "model_info": "Model Info",
"not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base", "not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base",
"no_provider": "Knowledge base model provider is not set, the knowledge base will no longer be supported, please create a new knowledge base",
"source": "Source" "source": "Source"
}, },
"models": { "models": {
@@ -583,7 +622,19 @@
"embedding": "Embedding", "embedding": "Embedding",
"embedding_model": "Embedding Model", "embedding_model": "Embedding Model",
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage", "embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
"dimensions": "Dimensions {{dimensions}}" "dimensions": "Dimensions {{dimensions}}",
"custom_parameters": "Custom Parameters",
"add_parameter": "Add Parameter",
"parameter_name": "Parameter Name",
"parameter_type": {
"string": "Text",
"number": "Number",
"boolean": "Boolean",
"json": "JSON"
}
},
"prompts": {
"summarize": "You are an assistant who is good at conversation. You need to summarize the user's conversation into a title of 10 characters or less, ensuring it matches the user's primary language without using punctuation or other special symbols."
} }
} }
} }

View File

@@ -112,7 +112,8 @@
"topics.list": "トピックリスト", "topics.list": "トピックリスト",
"topics.move_to": "移動先", "topics.move_to": "移動先",
"topics.title": "トピック", "topics.title": "トピック",
"translate": "翻訳" "translate": "翻訳",
"resend": "再送信"
}, },
"common": { "common": {
"and": "と", "and": "と",
@@ -182,8 +183,14 @@
"name": "名前", "name": "名前",
"open": "開く", "open": "開く",
"size": "サイズ", "size": "サイズ",
"type": "タイプ",
"text": "テキスト", "text": "テキスト",
"title": "ファイル" "title": "ファイル",
"edit": "編集",
"delete": "削除",
"delete.title": "ファイルを削除",
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
"delete.paintings.warning": "画像に含まれているため、削除できません"
}, },
"history": { "history": {
"continue_chat": "チャットを続ける", "continue_chat": "チャットを続ける",
@@ -211,6 +218,10 @@
"png": "PNGをダウンロード", "png": "PNGをダウンロード",
"svg": "SVGをダウンロード" "svg": "SVGをダウンロード"
}, },
"resize": {
"zoom-in": "拡大する",
"zoom-out": "ズームアウト"
},
"tabs": { "tabs": {
"preview": "プレビュー", "preview": "プレビュー",
"source": "ソース" "source": "ソース"
@@ -220,6 +231,7 @@
"message": { "message": {
"api.connection.failed": "接続に失敗しました", "api.connection.failed": "接続に失敗しました",
"api.connection.success": "接続に成功しました", "api.connection.success": "接続に成功しました",
"api.check.model.title": "検出に使用するモデルを選択してください",
"assistant.added.content": "アシスタントが追加されました", "assistant.added.content": "アシスタントが追加されました",
"backup.failed": "バックアップに失敗しました", "backup.failed": "バックアップに失敗しました",
"backup.success": "バックアップに成功しました", "backup.success": "バックアップに成功しました",
@@ -242,7 +254,7 @@
"reset.double.confirm.title": "データが失われます!!!", "reset.double.confirm.title": "データが失われます!!!",
"restore.success": "復元に成功しました", "restore.success": "復元に成功しました",
"save.success.title": "保存に成功しました", "save.success.title": "保存に成功しました",
"switch.disabled": "アシスタントが生成中は切り替え無効す", "switch.disabled": "現在の応答が完了するまで切り替え無効にします",
"topic.added": "新しいトピックが追加されました", "topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動", "upgrade.success.button": "再起動",
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください", "upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
@@ -251,7 +263,9 @@
"copy.success": "コピーしました!" "copy.success": "コピーしました!"
}, },
"minapp": { "minapp": {
"title": "ミニアプリ" "title": "ミニアプリ",
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分", "keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",
@@ -276,7 +290,9 @@
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?", "regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード", "seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます", "seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"title": "画像" "title": "画像",
"prompt_enhancement": "プロンプト強化",
"prompt_enhancement_tip": "オンにすると、プロンプトを詳細でモデルに適したバージョンに書き直します"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -308,7 +324,8 @@
"together": "Together", "together": "Together",
"yi": "零一万物", "yi": "零一万物",
"zhinao": "360智脳", "zhinao": "360智脳",
"zhipu": "智譜AI" "zhipu": "智譜AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "について", "about": "について",
@@ -355,11 +372,16 @@
"webdav.password": "WebDAVパスワード", "webdav.password": "WebDAVパスワード",
"webdav.path": "WebDAVパス", "webdav.path": "WebDAVパス",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同期", "webdav.autoSync": "自動バックアップ",
"webdav.minutes": "分", "webdav.minutes": "分",
"webdav.restore.button": "WebDAVから復元", "webdav.restore.button": "WebDAVから復元",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAVユーザー" "webdav.user": "WebDAVユーザー",
"webdav.syncStatus": "バックアップ状態",
"webdav.autoSync.off": "オフ",
"webdav.noSync": "次回のバックアップを待っています",
"webdav.syncError": "バックアップエラー",
"webdav.lastSync": "最終同期"
}, },
"display.title": "表示設定", "display.title": "表示設定",
"font_size.title": "メッセージのフォントサイズ", "font_size.title": "メッセージのフォントサイズ",
@@ -375,10 +397,23 @@
"general.user_name.placeholder": "ユーザー名を入力", "general.user_name.placeholder": "ユーザー名を入力",
"general.view_webdav_settings": "WebDAV設定を表示", "general.view_webdav_settings": "WebDAV設定を表示",
"general.display.title": "表示設定", "general.display.title": "表示設定",
"display.sidebar.translate.icon": "翻訳のアイコンを表示",
"display.sidebar.painting.icon": "絵画のアイコンを表示",
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示", "display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
"display.sidebar.knowledge.icon": "ナレッジのアイコンを表示",
"display.sidebar.files.icon": "ファイルのアイコンを表示", "display.sidebar.files.icon": "ファイルのアイコンを表示",
"display.sidebar.title": "サイドバー設定", "display.sidebar.title": "サイドバー設定",
"display.sidebar.visible": "アイコンを表示",
"display.sidebar.disabled": "アイコンを非表示",
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
"display.topic.title": "トピック設定", "display.topic.title": "トピック設定",
"display.custom.css": "カスタムCSS",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
"display.minApp.title": "ミニプログラム表示設定",
"display.minApp.visible": "表示中ミニプログラム",
"display.minApp.disabled": "非表示ミニプログラム",
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
"input.auto_translate_with_space": "スペースを3回押して翻訳", "input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け", "messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
@@ -413,6 +448,7 @@
"models.translate_model_prompt_title": "翻訳モデルのプロンプト", "models.translate_model_prompt_title": "翻訳モデルのプロンプト",
"models.topic_naming_model_setting_title": "トピック命名モデルの設定", "models.topic_naming_model_setting_title": "トピック命名モデルの設定",
"models.enable_topic_naming": "トピックの自動命名", "models.enable_topic_naming": "トピックの自動命名",
"models.topic_naming_prompt": "トピック命名プロンプト",
"provider": { "provider": {
"add.name": "プロバイダー名", "add.name": "プロバイダー名",
"add.name.placeholder": "例OpenAI", "add.name.placeholder": "例OpenAI",
@@ -467,7 +503,8 @@
"clear_shortcut": "ショートカットをクリア", "clear_shortcut": "ショートカットをクリア",
"toggle_show_assistants": "アシスタントの表示を切り替え", "toggle_show_assistants": "アシスタントの表示を切り替え",
"toggle_show_topics": "トピックの表示を切り替え", "toggle_show_topics": "トピックの表示を切り替え",
"copy_last_message": "最後のメッセージをコピー" "copy_last_message": "最後のメッセージをコピー",
"search_message": "メッセージを検索"
}, },
"theme.auto": "自動", "theme.auto": "自動",
"theme.dark": "ダークテーマ", "theme.dark": "ダークテーマ",
@@ -508,7 +545,7 @@
"show_window": "ウィンドウを表示", "show_window": "ウィンドウを表示",
"quit": "終了" "quit": "終了"
}, },
"knowledge_base": { "knowledge": {
"title": "ナレッジベース", "title": "ナレッジベース",
"search": "ナレッジベースを検索", "search": "ナレッジベースを検索",
"empty": "ナレッジベースが見つかりません", "empty": "ナレッジベースが見つかりません",
@@ -547,7 +584,11 @@
"sitemap_placeholder": "サイトマップURLを入力", "sitemap_placeholder": "サイトマップURLを入力",
"directories": "ディレクトリ", "directories": "ディレクトリ",
"add_directory": "ディレクトリを追加", "add_directory": "ディレクトリを追加",
"directory_placeholder": "ディレクトリパスを入力" "directory_placeholder": "ディレクトリパスを入力",
"model_info": "モデル情報",
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"source": "ソース"
}, },
"models": { "models": {
"pinned": "固定済み", "pinned": "固定済み",
@@ -565,7 +606,20 @@
"free": "無料モデル", "free": "無料モデル",
"embedding": "埋め込みモデル", "embedding": "埋め込みモデル",
"embedding_model": "埋め込みモデル", "embedding_model": "埋め込みモデル",
"embedding_model_tooltip": "設定->モデルサービス->管理で追加" "embedding_model_tooltip": "設定->モデルサービス->管理で追加",
"dimensions": "{{dimensions}} 次元",
"custom_parameters": "カスタムパラメータ",
"add_parameter": "パラメータを追加",
"parameter_name": "パラメータ名",
"parameter_type": {
"string": "テキスト",
"number": "数値",
"boolean": "真偽値",
"json": "JSON"
}
},
"prompts": {
"summarize": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
} }
} }
} }

View File

@@ -112,7 +112,8 @@
"topics.list": "Список топиков", "topics.list": "Список топиков",
"topics.move_to": "Переместить в", "topics.move_to": "Переместить в",
"topics.title": "Топики", "topics.title": "Топики",
"translate": "Перевести" "translate": "Перевести",
"resend": "Переотправить"
}, },
"common": { "common": {
"and": "и", "and": "и",
@@ -182,8 +183,14 @@
"name": "Имя", "name": "Имя",
"open": "Открыть", "open": "Открыть",
"size": "Размер", "size": "Размер",
"type": "Тип",
"text": "Текст", "text": "Текст",
"title": "Файлы" "title": "Файлы",
"edit": "Редактировать",
"delete": "Удалить",
"delete.title": "Удалить файл",
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно"
}, },
"history": { "history": {
"continue_chat": "Продолжить чат", "continue_chat": "Продолжить чат",
@@ -211,6 +218,10 @@
"png": "Скачать PNG", "png": "Скачать PNG",
"svg": "Скачать SVG" "svg": "Скачать SVG"
}, },
"resize": {
"zoom-in": "Yвеличить",
"zoom-out": "Yменьшить масштаб"
},
"tabs": { "tabs": {
"preview": "Предпросмотр", "preview": "Предпросмотр",
"source": "Исходный код" "source": "Исходный код"
@@ -220,6 +231,7 @@
"message": { "message": {
"api.connection.failed": "Соединение не удалось", "api.connection.failed": "Соединение не удалось",
"api.connection.success": "Соединение успешно", "api.connection.success": "Соединение успешно",
"api.check.model.title": "Выберите модель для проверки",
"assistant.added.content": "Ассистент успешно добавлен", "assistant.added.content": "Ассистент успешно добавлен",
"backup.failed": "Создание резервной копии не удалось", "backup.failed": "Создание резервной копии не удалось",
"backup.success": "Резервная копия успешно создана", "backup.success": "Резервная копия успешно создана",
@@ -243,7 +255,7 @@
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!", "reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
"restore.success": "Успешно восстановлено", "restore.success": "Успешно восстановлено",
"save.success.title": "Успешно сохранено", "save.success.title": "Успешно сохранено",
"switch.disabled": ереключение отключено, пока ассистент генерирует", "switch.disabled": ожалуйста, дождитесь завершения текущего ответа",
"topic.added": "Новый топик добавлен", "topic.added": "Новый топик добавлен",
"upgrade.success.button": "Перезапустить", "upgrade.success.button": "Перезапустить",
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления", "upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
@@ -253,7 +265,9 @@
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания" "error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
}, },
"minapp": { "minapp": {
"title": "Встроенные приложения" "title": "Встроенные приложения",
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", "keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
@@ -278,7 +292,9 @@
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?", "regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
"seed": "Ключ генерации", "seed": "Ключ генерации",
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения", "seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
"title": "Изображения" "title": "Изображения",
"prompt_enhancement": "Улучшение промпта",
"prompt_enhancement_tip": "При включении переписывает промпт в более детальную, модель-ориентированную версию"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -310,7 +326,8 @@
"together": "Together", "together": "Together",
"yi": "Yi", "yi": "Yi",
"zhinao": "360AI", "zhinao": "360AI",
"zhipu": "ZHIPU AI" "zhipu": "ZHIPU AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "О программе и обратная связь", "about": "О программе и обратная связь",
@@ -357,11 +374,16 @@
"webdav.password": "Пароль WebDAV", "webdav.password": "Пароль WebDAV",
"webdav.path": "Путь WebDAV", "webdav.path": "Путь WebDAV",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "Автоматическая синхронизация", "webdav.autoSync": "Автоматическое резервное копирование",
"webdav.minutes": "минут", "webdav.minutes": "минут",
"webdav.restore.button": "Восстановление с WebDAV", "webdav.restore.button": "Восстановление с WebDAV",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "Пользователь WebDAV" "webdav.user": "Пользователь WebDAV",
"webdav.syncStatus": "Статус резервного копирования",
"webdav.autoSync.off": "Выключено",
"webdav.noSync": "Ожидание следующего резервного копирования",
"webdav.syncError": "Ошибка резервного копирования",
"webdav.lastSync": "Последняя синхронизация"
}, },
"display.title": "Настройки отображения", "display.title": "Настройки отображения",
"font_size.title": "Размер шрифта сообщений", "font_size.title": "Размер шрифта сообщений",
@@ -377,10 +399,23 @@
"general.user_name.placeholder": "Введите ваше имя", "general.user_name.placeholder": "Введите ваше имя",
"general.view_webdav_settings": "Просмотр настроек WebDAV", "general.view_webdav_settings": "Просмотр настроек WebDAV",
"general.display.title": "Настройки отображения", "general.display.title": "Настройки отображения",
"display.sidebar.translate.icon": "Показывать иконку перевода",
"display.sidebar.painting.icon": "Показывать иконку рисования",
"display.sidebar.minapp.icon": "Показывать иконку мини-приложения", "display.sidebar.minapp.icon": "Показывать иконку мини-приложения",
"display.sidebar.knowledge.icon": "Показывать иконку знаний",
"display.sidebar.files.icon": "Показывать иконку файлов", "display.sidebar.files.icon": "Показывать иконку файлов",
"display.sidebar.title": "Настройки боковой панели", "display.sidebar.title": "Настройки боковой панели",
"display.sidebar.visible": "Показывать иконки",
"display.sidebar.disabled": "Скрыть иконки",
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
"display.minApp.title": "Настройки отображения мини программы",
"display.minApp.visible": "Отображаемый апплет",
"display.minApp.disabled": "скрытый апплет",
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
"display.topic.title": "Настройки топиков", "display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов", "input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
"messages.divider": "Показывать разделитель между сообщениями", "messages.divider": "Показывать разделитель между сообщениями",
"messages.input.paste_long_text_as_file": "Вставлять длинный текст как файл", "messages.input.paste_long_text_as_file": "Вставлять длинный текст как файл",
@@ -415,6 +450,7 @@
"models.translate_model_prompt_title": "Модель перевода", "models.translate_model_prompt_title": "Модель перевода",
"models.topic_naming_model_setting_title": "Настройки модели именования топика", "models.topic_naming_model_setting_title": "Настройки модели именования топика",
"models.enable_topic_naming": "Автоматическое переименование топика", "models.enable_topic_naming": "Автоматическое переименование топика",
"models.topic_naming_prompt": "Подсказка для именования топика",
"provider": { "provider": {
"add.name": "Имя провайдера", "add.name": "Имя провайдера",
"add.name.placeholder": "Пример: OpenAI", "add.name.placeholder": "Пример: OpenAI",
@@ -481,7 +517,8 @@
"clear_shortcut": "Очистить сочетание клавиш", "clear_shortcut": "Очистить сочетание клавиш",
"toggle_show_assistants": "Переключить отображение ассистентов", "toggle_show_assistants": "Переключить отображение ассистентов",
"toggle_show_topics": "Переключить отображение топиков", "toggle_show_topics": "Переключить отображение топиков",
"copy_last_message": "Копировать последнее сообщение" "copy_last_message": "Копировать последнее сообщение",
"search_message": "Поиск сообщения"
}, },
"theme.auto": "Автоматически", "theme.auto": "Автоматически",
"theme.dark": "Темная", "theme.dark": "Темная",
@@ -522,7 +559,7 @@
"show_window": "Показать окно", "show_window": "Показать окно",
"quit": "Выйти" "quit": "Выйти"
}, },
"knowledge_base": { "knowledge": {
"title": "База знаний", "title": "База знаний",
"search": "Поиск в базе знаний", "search": "Поиск в базе знаний",
"empty": "База знаний не найдена", "empty": "База знаний не найдена",
@@ -564,6 +601,7 @@
"directory_placeholder": "Введите путь к директории", "directory_placeholder": "Введите путь к директории",
"model_info": "Модель информации", "model_info": "Модель информации",
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний", "not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"source": "Источник" "source": "Источник"
}, },
"models": { "models": {
@@ -583,7 +621,19 @@
"embedding": "Встраиваемые модели", "embedding": "Встраиваемые модели",
"embedding_model": "Встраиваемые модели", "embedding_model": "Встраиваемые модели",
"embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление", "embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление",
"dimensions": "{{dimensions}} мер" "dimensions": "{{dimensions}} мер",
"custom_parameters": "Пользовательские параметры",
"add_parameter": "Добавить параметр",
"parameter_name": "Имя параметра",
"parameter_type": {
"string": "Текст",
"number": "Число",
"boolean": "Логическое",
"json": "JSON"
}
},
"prompts": {
"summarize": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов"
} }
} }
} }

View File

@@ -82,7 +82,7 @@
"input.upload": "上传图片或文档", "input.upload": "上传图片或文档",
"input.web_search": "开启网络搜索", "input.web_search": "开启网络搜索",
"input.knowledge_base": "知识库", "input.knowledge_base": "知识库",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已创建", "message.new.branch.created": "新分支已创建",
"message.regenerate.model": "切换模型", "message.regenerate.model": "切换模型",
"message.new.context": "清除上下文", "message.new.context": "清除上下文",
@@ -112,7 +112,8 @@
"topics.list": "话题列表", "topics.list": "话题列表",
"topics.move_to": "移动到", "topics.move_to": "移动到",
"topics.title": "话题", "topics.title": "话题",
"translate": "翻译" "translate": "翻译",
"resend": "重新发送"
}, },
"common": { "common": {
"and": "和", "and": "和",
@@ -183,8 +184,14 @@
"name": "文件名", "name": "文件名",
"open": "打开", "open": "打开",
"size": "大小", "size": "大小",
"type": "类型",
"text": "文本", "text": "文本",
"title": "文件" "title": "文件",
"edit": "编辑",
"delete": "删除",
"delete.title": "删除文件",
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除"
}, },
"history": { "history": {
"continue_chat": "继续聊天", "continue_chat": "继续聊天",
@@ -212,6 +219,10 @@
"png": "下载 PNG", "png": "下载 PNG",
"svg": "下载 SVG" "svg": "下载 SVG"
}, },
"resize": {
"zoom-in": "放大",
"zoom-out": "缩小"
},
"tabs": { "tabs": {
"preview": "预览", "preview": "预览",
"source": "源码" "source": "源码"
@@ -221,6 +232,7 @@
"message": { "message": {
"api.connection.failed": "连接失败", "api.connection.failed": "连接失败",
"api.connection.success": "连接成功", "api.connection.success": "连接成功",
"api.check.model.title": "请选择要检测的模型",
"assistant.added.content": "智能体添加成功", "assistant.added.content": "智能体添加成功",
"backup.failed": "备份失败", "backup.failed": "备份失败",
"backup.success": "备份成功", "backup.success": "备份成功",
@@ -244,7 +256,7 @@
"reset.double.confirm.title": "数据丢失!!!", "reset.double.confirm.title": "数据丢失!!!",
"restore.success": "恢复成功", "restore.success": "恢复成功",
"save.success.title": "保存成功", "save.success.title": "保存成功",
"switch.disabled": "模型回复完成后才能切换", "switch.disabled": "请等待当前回复完成后操作",
"topic.added": "话题添加成功", "topic.added": "话题添加成功",
"upgrade.success.button": "重启", "upgrade.success.button": "重启",
"upgrade.success.content": "重启用以完成升级", "upgrade.success.content": "重启用以完成升级",
@@ -254,7 +266,9 @@
"error.get_embedding_dimensions": "获取嵌入维度失败" "error.get_embedding_dimensions": "获取嵌入维度失败"
}, },
"minapp": { "minapp": {
"title": "小程序" "title": "小程序",
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟", "keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟",
@@ -279,7 +293,9 @@
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?", "regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
"seed": "随机种子", "seed": "随机种子",
"seed_tip": "相同的种子和提示词可以生成相似的图片", "seed_tip": "相同的种子和提示词可以生成相似的图片",
"title": "图片" "title": "图片",
"prompt_enhancement": "提示词增强",
"prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -311,7 +327,8 @@
"together": "Together", "together": "Together",
"yi": "零一万物", "yi": "零一万物",
"zhinao": "360智脑", "zhinao": "360智脑",
"zhipu": "智谱AI" "zhipu": "智谱AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "关于我们", "about": "关于我们",
@@ -358,11 +375,16 @@
"webdav.password": "WebDAV 密码", "webdav.password": "WebDAV 密码",
"webdav.path": "WebDAV 路径", "webdav.path": "WebDAV 路径",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "自动同步", "webdav.autoSync": "自动备份",
"webdav.minutes": "分钟", "webdav.minutes": "分钟",
"webdav.restore.button": "从 WebDAV 恢复", "webdav.restore.button": "从 WebDAV 恢复",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAV 用户名" "webdav.user": "WebDAV 用户名",
"webdav.syncStatus": "备份状态",
"webdav.autoSync.off": "关闭",
"webdav.noSync": "等待下次备份",
"webdav.syncError": "备份错误",
"webdav.lastSync": "上次备份时间"
}, },
"display.title": "显示设置", "display.title": "显示设置",
"font_size.title": "消息字体大小", "font_size.title": "消息字体大小",
@@ -378,10 +400,23 @@
"general.user_name.placeholder": "请输入用户名", "general.user_name.placeholder": "请输入用户名",
"general.view_webdav_settings": "查看 WebDAV 设置", "general.view_webdav_settings": "查看 WebDAV 设置",
"general.display.title": "显示设置", "general.display.title": "显示设置",
"display.sidebar.translate.icon": "显示翻译图标",
"display.sidebar.painting.icon": "显示绘画图标",
"display.sidebar.minapp.icon": "显示小程序图标", "display.sidebar.minapp.icon": "显示小程序图标",
"display.sidebar.knowledge.icon": "显示知识图标",
"display.sidebar.files.icon": "显示文件图标", "display.sidebar.files.icon": "显示文件图标",
"display.sidebar.title": "侧边栏设置", "display.sidebar.title": "侧边栏设置",
"display.sidebar.visible": "显示的图标",
"display.sidebar.disabled": "隐藏的图标",
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
"display.minApp.title": "小程序显示设置",
"display.minApp.visible": "显示的小程序",
"display.minApp.disabled": "隐藏的小程序",
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
"display.topic.title": "话题设置", "display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
"input.auto_translate_with_space": "快速敲击3次空格翻译", "input.auto_translate_with_space": "快速敲击3次空格翻译",
"messages.divider": "消息分割线", "messages.divider": "消息分割线",
"messages.input.paste_long_text_as_file": "长文本粘贴为文件", "messages.input.paste_long_text_as_file": "长文本粘贴为文件",
@@ -416,6 +451,7 @@
"models.translate_model_prompt_title": "翻译模型提示词", "models.translate_model_prompt_title": "翻译模型提示词",
"models.topic_naming_model_setting_title": "话题命名模型设置", "models.topic_naming_model_setting_title": "话题命名模型设置",
"models.enable_topic_naming": "话题自动重命名", "models.enable_topic_naming": "话题自动重命名",
"models.topic_naming_prompt": "话题命名提示词",
"provider": { "provider": {
"add.name": "提供商名称", "add.name": "提供商名称",
"add.name.placeholder": "例如 OpenAI", "add.name.placeholder": "例如 OpenAI",
@@ -470,7 +506,8 @@
"clear_shortcut": "清除快捷键", "clear_shortcut": "清除快捷键",
"toggle_show_assistants": "切换助手显示", "toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示", "toggle_show_topics": "切换话题显示",
"copy_last_message": "复制上一条消息" "copy_last_message": "复制上一条消息",
"search_message": "搜索消息"
}, },
"theme.auto": "跟随系统", "theme.auto": "跟随系统",
"theme.dark": "深色主题", "theme.dark": "深色主题",
@@ -511,7 +548,7 @@
"show_window": "显示窗口", "show_window": "显示窗口",
"quit": "退出" "quit": "退出"
}, },
"knowledge_base": { "knowledge": {
"title": "知识库", "title": "知识库",
"search": "搜索知识库", "search": "搜索知识库",
"empty": "暂无知识库", "empty": "暂无知识库",
@@ -553,6 +590,7 @@
"directory_placeholder": "请输入目录路径", "directory_placeholder": "请输入目录路径",
"model_info": "模型信息", "model_info": "模型信息",
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库", "not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
"no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
"source": "来源" "source": "来源"
}, },
"models": { "models": {
@@ -572,7 +610,19 @@
"embedding": "嵌入模型", "embedding": "嵌入模型",
"embedding_model": "嵌入模型", "embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"dimensions": "{{dimensions}} 维" "dimensions": "{{dimensions}} 维",
"custom_parameters": "自定义参数",
"add_parameter": "添加参数",
"parameter_name": "参数名称",
"parameter_type": {
"string": "文本",
"number": "数字",
"boolean": "布尔值",
"json": "JSON"
}
},
"prompts": {
"summarize": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号"
} }
} }
} }

View File

@@ -82,7 +82,7 @@
"input.upload": "上傳圖片或文檔", "input.upload": "上傳圖片或文檔",
"input.web_search": "開啟網路搜索", "input.web_search": "開啟網路搜索",
"input.knowledge_base": "知識庫", "input.knowledge_base": "知識庫",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已建立", "message.new.branch.created": "新分支已建立",
"message.regenerate.model": "切換模型", "message.regenerate.model": "切換模型",
"message.new.context": "新上下文", "message.new.context": "新上下文",
@@ -112,7 +112,8 @@
"topics.list": "話題列表", "topics.list": "話題列表",
"topics.move_to": "移動到", "topics.move_to": "移動到",
"topics.title": "話題", "topics.title": "話題",
"translate": "翻譯" "translate": "翻譯",
"resend": "重新發送"
}, },
"common": { "common": {
"and": "與", "and": "與",
@@ -167,7 +168,7 @@
"conversation_details": "會話詳情", "conversation_details": "會話詳情",
"conversation_history": "會話歷史", "conversation_history": "會話歷史",
"created": "創建時間", "created": "創建時間",
"last_updated": "最後<EFBFBD><EFBFBD>新", "last_updated": "最後新",
"messages": "訊息數", "messages": "訊息數",
"user": "用戶" "user": "用戶"
}, },
@@ -182,8 +183,14 @@
"name": "名稱", "name": "名稱",
"open": "打開", "open": "打開",
"size": "大小", "size": "大小",
"type": "類型",
"text": "文本", "text": "文本",
"title": "檔案" "title": "檔案",
"edit": "編輯",
"delete": "刪除",
"delete.title": "刪除檔案",
"delete.content": "刪除檔案會刪除檔案在所有消息中的引用,確定要刪除此檔案嗎?",
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除"
}, },
"history": { "history": {
"continue_chat": "繼續聊天", "continue_chat": "繼續聊天",
@@ -211,6 +218,10 @@
"png": "下載 PNG", "png": "下載 PNG",
"svg": "下載 SVG" "svg": "下載 SVG"
}, },
"resize": {
"zoom-in": "放大",
"zoom-out": "縮小"
},
"tabs": { "tabs": {
"preview": "預覽", "preview": "預覽",
"source": "原始碼" "source": "原始碼"
@@ -220,6 +231,7 @@
"message": { "message": {
"api.connection.failed": "連接失敗", "api.connection.failed": "連接失敗",
"api.connection.success": "連接成功", "api.connection.success": "連接成功",
"api.check.model.title": "請選擇要檢測的模型",
"assistant.added.content": "智能體添加成功", "assistant.added.content": "智能體添加成功",
"backup.failed": "備份失敗", "backup.failed": "備份失敗",
"backup.success": "備份成功", "backup.success": "備份成功",
@@ -243,7 +255,7 @@
"reset.double.confirm.title": "資料將會丟失!!!", "reset.double.confirm.title": "資料將會丟失!!!",
"restore.success": "恢復成功", "restore.success": "恢復成功",
"save.success.title": "保存成功", "save.success.title": "保存成功",
"switch.disabled": "助手生成回覆時無法切換", "switch.disabled": "請等待當前回覆完成",
"topic.added": "新話題已添加", "topic.added": "新話題已添加",
"upgrade.success.button": "重新啟動", "upgrade.success.button": "重新啟動",
"upgrade.success.content": "請重新啟動應用以完成升級", "upgrade.success.content": "請重新啟動應用以完成升級",
@@ -253,7 +265,9 @@
"error.get_embedding_dimensions": "獲取嵌入維度失敗" "error.get_embedding_dimensions": "獲取嵌入維度失敗"
}, },
"minapp": { "minapp": {
"title": "小程序" "title": "小程序",
"sidebar.add.title": "添加到側邊欄",
"sidebar.remove.title": "從側邊欄移除"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。", "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
@@ -278,7 +292,9 @@
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?", "regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
"seed": "隨機種子", "seed": "隨機種子",
"seed_tip": "相同的種子和提示詞可以生成相似的圖片", "seed_tip": "相同的種子和提示詞可以生成相似的圖片",
"title": "繪圖" "title": "繪圖",
"prompt_enhancement": "提示詞增強",
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -310,7 +326,8 @@
"together": "Together", "together": "Together",
"yi": "零一萬物", "yi": "零一萬物",
"zhinao": "360智腦", "zhinao": "360智腦",
"zhipu": "智譜AI" "zhipu": "智譜AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "關於與回饋", "about": "關於與回饋",
@@ -357,11 +374,16 @@
"webdav.password": "WebDAV 密碼", "webdav.password": "WebDAV 密碼",
"webdav.path": "WebDAV Path", "webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同步", "webdav.autoSync": "自動備份",
"webdav.minutes": "分鐘", "webdav.minutes": "分鐘",
"webdav.restore.button": "從 WebDAV 恢復", "webdav.restore.button": "從 WebDAV 恢復",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAV 使用者名稱" "webdav.user": "WebDAV 使用者名稱",
"webdav.syncStatus": "備份狀態",
"webdav.autoSync.off": "關閉",
"webdav.noSync": "等待下次備份",
"webdav.syncError": "備份錯誤",
"webdav.lastSync": "上次同步時間"
}, },
"display.title": "顯示設定", "display.title": "顯示設定",
"font_size.title": "訊息字體大小", "font_size.title": "訊息字體大小",
@@ -377,10 +399,23 @@
"general.user_name.placeholder": "輸入您的名稱", "general.user_name.placeholder": "輸入您的名稱",
"general.view_webdav_settings": "查看 WebDAV 設定", "general.view_webdav_settings": "查看 WebDAV 設定",
"general.display.title": "顯示設定", "general.display.title": "顯示設定",
"display.sidebar.translate.icon": "顯示翻譯圖示",
"display.sidebar.painting.icon": "顯示繪圖圖示",
"display.sidebar.minapp.icon": "顯示小程序圖示", "display.sidebar.minapp.icon": "顯示小程序圖示",
"display.sidebar.knowledge.icon": "顯示知識圖示",
"display.sidebar.files.icon": "顯示文件圖示", "display.sidebar.files.icon": "顯示文件圖示",
"display.sidebar.title": "側邊欄設定", "display.sidebar.title": "側邊欄設定",
"display.topic.title": "話題設定", "display.topic.title": "話題設定",
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
"display.sidebar.visible": "顯示的圖標",
"display.sidebar.disabled": "隱藏的圖標",
"display.minApp.title": "小程序顯示設定",
"display.minApp.visible": "顯示的小程序",
"display.minApp.disabled": "隱藏的小程序",
"display.minApp.empty": "把要隱藏的小程序從左側拖拽到這裡",
"display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯", "input.auto_translate_with_space": "快速敲擊3次空格翻譯",
"messages.divider": "訊息間顯示分隔線", "messages.divider": "訊息間顯示分隔線",
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案", "messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
@@ -415,6 +450,7 @@
"models.translate_model_prompt_title": "翻譯模型提示詞", "models.translate_model_prompt_title": "翻譯模型提示詞",
"models.topic_naming_model_setting_title": "話題命名模型設定", "models.topic_naming_model_setting_title": "話題命名模型設定",
"models.enable_topic_naming": "話題自動重命名", "models.enable_topic_naming": "話題自動重命名",
"models.topic_naming_prompt": "話題命名提示詞",
"provider": { "provider": {
"add.name": "提供者名稱", "add.name": "提供者名稱",
"add.name.placeholder": "例如OpenAI", "add.name.placeholder": "例如OpenAI",
@@ -469,7 +505,8 @@
"clear_shortcut": "清除快捷鍵", "clear_shortcut": "清除快捷鍵",
"toggle_show_assistants": "切換助手顯示", "toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示", "toggle_show_topics": "切換話題顯示",
"copy_last_message": "複製上一条消息" "copy_last_message": "複製上一条消息",
"search_message": "搜索消息"
}, },
"theme.auto": "自動", "theme.auto": "自動",
"theme.dark": "深色主題", "theme.dark": "深色主題",
@@ -510,7 +547,7 @@
"show_window": "顯示視窗", "show_window": "顯示視窗",
"quit": "退出" "quit": "退出"
}, },
"knowledge_base": { "knowledge": {
"title": "知識庫", "title": "知識庫",
"search": "搜尋知識庫", "search": "搜尋知識庫",
"empty": "暫無知識庫", "empty": "暫無知識庫",
@@ -552,6 +589,7 @@
"directory_placeholder": "請輸入目錄路徑", "directory_placeholder": "請輸入目錄路徑",
"model_info": "模型信息", "model_info": "模型信息",
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫", "not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
"no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
"source": "來源" "source": "來源"
}, },
"models": { "models": {
@@ -571,7 +609,19 @@
"embedding": "嵌入模型", "embedding": "嵌入模型",
"embedding_model": "嵌入模型", "embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"dimensions": "{{dimensions}} 維" "dimensions": "{{dimensions}} 維",
"custom_parameters": "自定義參數",
"add_parameter": "添加參數",
"parameter_name": "參數名稱",
"parameter_type": {
"string": "文字",
"number": "數字",
"boolean": "布林值",
"json": "JSON"
}
},
"prompts": {
"summarize": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號"
} }
} }
} }

View File

@@ -1,8 +1,19 @@
import KeyvStorage from '@kangfenmao/keyv-storage' import KeyvStorage from '@kangfenmao/keyv-storage'
function init() { import { startAutoSync } from './services/BackupService'
import store from './store'
function initKeyv() {
window.keyv = new KeyvStorage() window.keyv = new KeyvStorage()
window.keyv.init() window.keyv.init()
} }
init() function initAutoSync() {
const { webdavAutoSync } = store.getState().settings
if (webdavAutoSync) {
startAutoSync()
}
}
initKeyv()
initAutoSync()

View File

@@ -132,7 +132,7 @@ const AgentsPage: FC = () => {
key: id, key: id,
children: ( children: (
<TabContent key={group}> <TabContent key={group}>
<Title level={5} key={group} style={{ marginBottom: 16 }}> <Title level={5} key={group} style={{ marginBottom: 10 }}>
{localizedGroupName} {localizedGroupName}
</Title> </Title>
<Row gutter={[20, 20]}> <Row gutter={[20, 20]}>
@@ -272,8 +272,8 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
padding-right: 0 !important; padding-right: 0 !important;
} }
.ant-tabs-nav { .ant-tabs-nav {
min-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')}; min-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
max-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')}; max-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
} }
.ant-tabs-nav-list { .ant-tabs-nav-list {
padding: 10px 8px; padding: 10px 8px;
@@ -283,7 +283,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
} }
.ant-tabs-tab { .ant-tabs-tab {
margin: 0 !important; margin: 0 !important;
border-radius: 16px; border-radius: var(--list-item-border-radius);
margin-bottom: 5px !important; margin-bottom: 5px !important;
font-size: 13px; font-size: 13px;
justify-content: left; justify-content: left;

View File

@@ -81,7 +81,7 @@ const Container = styled.div`
text-align: center; text-align: center;
gap: 10px; gap: 10px;
background-color: var(--color-background); background-color: var(--color-background);
border-radius: 16px; border-radius: 10px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;

View File

@@ -1,6 +1,6 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { Col } from 'antd' import { Col } from 'antd'

View File

@@ -1,6 +1,11 @@
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
@@ -10,23 +15,36 @@ interface Props {
} }
const App: FC<Props> = ({ app, onClick, size = 60 }) => { const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const { t } = useTranslation()
const { minapps, pinned, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
const handleClick = () => { const handleClick = () => {
MinApp.start(app) MinApp.start(app)
onClick?.() onClick?.()
} }
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
}
}
]
if (!isVisible) return null
return ( return (
<Container onClick={handleClick}> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<AppIcon <Container onClick={handleClick}>
src={app.logo} <MinAppIcon size={size} app={app} />
style={{ <AppTitle>{app.name}</AppTitle>
border: app.bodered ? '0.5px solid var(--color-border)' : 'none', </Container>
width: `${size}px`, </Dropdown>
height: `${size}px`
}}
/>
<AppTitle>{app.name}</AppTitle>
</Container>
) )
} }
@@ -36,17 +54,9 @@ const Container = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
max-width: 80px;
width: 72px;
overflow: hidden; overflow: hidden;
` `
const AppIcon = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
const AppTitle = styled.div` const AppTitle = styled.div`
font-size: 12px; font-size: 12px;
margin-top: 5px; margin-top: 5px;

View File

@@ -1,10 +1,10 @@
import { SearchOutlined } from '@ant-design/icons' import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout' import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { Empty, Input } from 'antd' import { Empty, Input } from 'antd'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useMemo, useState } from 'react' import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -13,16 +13,29 @@ import App from './App'
const AppsPage: FC = () => { const AppsPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const apps = useMemo(() => getAllMinApps(), []) const { minapps } = useMinapps()
console.debug('minapps', minapps)
const filteredApps = search const filteredApps = search
? apps.filter( ? minapps.filter(
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase()) (app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
) )
: apps : minapps
// Calculate the required number of lines
const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing)
const rowCount = Math.ceil(filteredApps.length / itemsPerRow)
// Each line height is 85px (60px icon + 5px margin + 12px text + spacing)
const containerHeight = rowCount * 85 + (rowCount - 1) * 25 // 25px is the line spacing.
// Disable right-click menu in blank area
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
}
return ( return (
<Container> <Container onContextMenu={handleContextMenu}>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('minapp.title')} {t('minapp.title')}
@@ -40,7 +53,7 @@ const AppsPage: FC = () => {
</NavbarCenter> </NavbarCenter>
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<AppsContainer> <AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => ( {filteredApps.map((app) => (
<App key={app.id} app={app} /> <App key={app.id} app={app} />
))} ))}
@@ -68,18 +81,18 @@ const ContentContainer = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: auto;
padding: 50px; padding: 50px;
` `
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: flex; display: grid;
min-width: 950px; min-width: 0;
max-width: 950px; max-width: 930px;
flex-direction: row; width: 100%;
flex-wrap: wrap; grid-template-columns: repeat(auto-fill, 90px);
align-content: flex-start; gap: 25px;
gap: 50px; justify-content: center;
` `
export default AppsPage export default AppsPage

View File

@@ -0,0 +1,129 @@
import FileManager from '@renderer/services/FileManager'
import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Col, Image, Row, Spin, Table } from 'antd'
import React, { memo } from 'react'
import styled from 'styled-components'
import GeminiFiles from './GeminiFiles'
interface ContentViewProps {
id: FileTypes | 'all' | string
files?: FileType[]
dataSource?: any[]
columns: any[]
}
const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, columns }) => {
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
return (
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file)}</div>
</ImageInfo>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
)
}
if (id.startsWith('gemini_')) {
return <GeminiFiles id={id.replace('gemini_', '') as string} />
}
return (
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
/>
)
}
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
export default memo(ContentView)

View File

@@ -1,20 +1,36 @@
import { FileImageOutlined, FilePdfOutlined, FileTextOutlined } from '@ant-design/icons' import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
FileImageOutlined,
FilePdfOutlined,
FileTextOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import store from '@renderer/store'
import { FileType, FileTypes } from '@renderer/types' import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils' import { formatFileSize } from '@renderer/utils'
import { Col, Image, Menu, Row, Spin, Table } from 'antd' import type { MenuProps } from 'antd'
import { Button, Dropdown, Menu } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { FC, useState } from 'react' import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import ContentView from './ContentView'
const FilesPage: FC = () => { const FilesPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [fileType, setFileType] = useState<FileTypes | 'all'>('all') const [fileType, setFileType] = useState<FileTypes | 'all' | 'gemini'>('all')
const { providers } = useProviders()
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
const files = useLiveQuery<FileType[]>(() => { const files = useLiveQuery<FileType[]>(() => {
if (fileType === 'all') { if (fileType === 'all') {
@@ -23,56 +39,146 @@ const FilesPage: FC = () => {
return db.files.where('type').equals(fileType).sortBy('count') return db.files.where('type').equals(fileType).sortBy('count')
}, [fileType]) }, [fileType])
const handleDelete = async (fileId: string) => {
const file = await FileManager.getFile(fileId)
const paintings = await store.getState().paintings.paintings
const paintingsFiles = paintings.flatMap((p) => p.files)
if (paintingsFiles.some((p) => p.id === fileId)) {
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
return
}
if (file) {
await FileManager.deleteFile(fileId, true)
}
const topics = await db.topics
.filter((topic) => topic.messages.some((message) => message.files?.some((f) => f.id === fileId)))
.toArray()
if (topics.length > 0) {
for (const topic of topics) {
const updatedMessages = topic.messages.map((message) => ({
...message,
files: message.files?.filter((f) => f.id !== fileId)
}))
await db.topics.update(topic.id, { messages: updatedMessages })
}
}
}
const handleRename = async (fileId: string) => {
const file = await FileManager.getFile(fileId)
if (file) {
const newName = await TextEditPopup.show({ text: file.origin_name })
if (newName) {
FileManager.updateFile({ ...file, origin_name: newName })
}
}
}
const getActionMenu = (fileId: string): MenuProps['items'] => [
{
key: 'rename',
icon: <EditOutlined />,
label: t('files.edit'),
onClick: () => handleRename(fileId)
},
{
key: 'delete',
icon: <DeleteOutlined />,
label: t('files.delete'),
danger: true,
onClick: () => {
window.modal.confirm({
title: t('files.delete.title'),
content: t('files.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => handleDelete(fileId)
})
}
}
]
const dataSource = files?.map((file) => { const dataSource = files?.map((file) => {
return { return {
key: file.id, key: file.id,
file: <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>, file: (
<FileNameText className="text-nowrap" onClick={() => window.api.file.openPath(file.path)}>
{file.origin_name}
</FileNameText>
),
size: formatFileSize(file), size: formatFileSize(file),
size_bytes: file.size,
count: file.count, count: file.count,
created_at: dayjs(file.created_at).format('MM-DD HH:mm'), created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
actions: <a href={'file://' + FileManager.getSafePath(file)}>{t('files.open')}</a> created_at_unix: dayjs(file.created_at).unix(),
actions: (
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']} placement="bottom" arrow>
<Button type="text" size="small" icon={<EllipsisOutlined />} />
</Dropdown>
)
} }
}) })
const columns = [ const columns = useMemo(
{ () => [
title: t('files.name'), {
dataIndex: 'file', title: t('files.name'),
key: 'file', dataIndex: 'file',
width: '300px' key: 'file',
}, width: '300px'
{ },
title: t('files.size'), {
dataIndex: 'size', title: t('files.size'),
key: 'size', dataIndex: 'size',
width: '80px' key: 'size',
}, width: '80px',
{ sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
title: t('files.count'), align: 'center'
dataIndex: 'count', },
key: 'count', {
width: '60px' title: t('files.count'),
}, dataIndex: 'count',
{ key: 'count',
title: t('files.created_at'), width: '60px',
dataIndex: 'created_at', sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
key: 'created_at', align: 'center'
width: '120px' },
}, {
{ title: t('files.created_at'),
title: t('files.actions'), dataIndex: 'created_at',
dataIndex: 'actions', key: 'created_at',
key: 'actions', width: '120px',
width: '50px' align: 'center',
} sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) =>
] b.created_at_unix - a.created_at_unix
},
{
title: t('files.actions'),
dataIndex: 'actions',
key: 'actions',
width: '80px',
align: 'center'
}
],
[t]
)
const menuItems = [ const menuItems = [
{ key: 'all', label: t('files.all'), icon: <FileTextOutlined /> }, { key: 'all', label: t('files.all'), icon: <FileTextOutlined /> },
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> }, { key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> }, { key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> },
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> } { key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> },
] ...geminiProviders.map((provider) => ({
key: 'gemini_' + provider.id,
label: provider.name,
icon: <FilePdfOutlined />
}))
].filter(Boolean) as MenuProps['items']
return ( return (
<Container> <Container>
@@ -84,41 +190,7 @@ const FilesPage: FC = () => {
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} /> <Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
</SideNav> </SideNav>
<TableContainer right> <TableContainer right>
{fileType === FileTypes.IMAGE && files?.length && files?.length > 0 ? ( <ContentView id={fileType} files={files} dataSource={dataSource} columns={columns} />
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file)}</div>
</ImageInfo>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
) : (
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
/>
)}
</TableContainer> </TableContainer>
</ContentContainer> </ContentContainer>
</Container> </Container>
@@ -149,72 +221,7 @@ const TableContainer = styled(Scrollbar)`
const FileNameText = styled.div` const FileNameText = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-text); color: var(--color-text);
` cursor: pointer;
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
` `
const SideNav = styled.div` const SideNav = styled.div`
@@ -233,7 +240,7 @@ const SideNav = styled.div`
line-height: 36px; line-height: 36px;
margin: 4px 0; margin: 4px 0;
width: 100%; width: 100%;
border-radius: 16px; border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
&:hover { &:hover {

View File

@@ -0,0 +1,98 @@
import { DeleteOutlined } from '@ant-design/icons'
import type { FileMetadataResponse } from '@google/generative-ai/server'
import { useProvider } from '@renderer/hooks/useProvider'
import { runAsyncFunction } from '@renderer/utils'
import { Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface GeminiFilesProps {
id: string
}
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
const { provider } = useProvider(id)
const [files, setFiles] = useState<FileMetadataResponse[]>([])
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const fetchFiles = useCallback(async () => {
const { files } = await window.api.gemini.listFiles(provider.apiKey)
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
}, [provider])
const columns: ColumnsType<FileMetadataResponse> = [
{
title: t('files.name'),
dataIndex: 'displayName',
key: 'displayName'
},
{
title: t('files.type'),
dataIndex: 'mimeType',
key: 'mimeType'
},
{
title: t('files.size'),
dataIndex: 'sizeBytes',
key: 'sizeBytes',
render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB`
},
{
title: t('files.created_at'),
dataIndex: 'createTime',
key: 'createTime',
render: (time: string) => new Date(time).toLocaleString()
},
{
title: t('files.actions'),
dataIndex: 'actions',
key: 'actions',
align: 'center',
render: (_, record) => {
return (
<DeleteOutlined
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
onClick={() => {
setFiles(files.filter((file) => file.name !== record.name))
window.api.gemini.deleteFile(provider.apiKey, record.name).catch((error) => {
console.error('Failed to delete file:', error)
setFiles((prev) => [...prev, record])
})
}}
/>
)
}
}
]
useEffect(() => {
runAsyncFunction(async () => {
try {
setLoading(true)
await fetchFiles()
setLoading(false)
} catch (error: any) {
console.error('Failed to fetch files:', error)
window.message.error(error.message)
setLoading(false)
}
})
}, [fetchFiles])
useEffect(() => {
setFiles([])
}, [id])
return (
<Container>
<Table columns={columns} dataSource={files} rowKey="name" loading={loading} />
</Container>
)
}
const Container = styled.div``
export default GeminiFiles

View File

@@ -1,8 +1,8 @@
import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons' import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons'
import { Message, Topic } from '@renderer/types' import { Message, Topic } from '@renderer/types'
import { Input } from 'antd' import { Input, InputRef } from 'antd'
import { last } from 'lodash' import { last } from 'lodash'
import { FC, useState } from 'react' import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -21,9 +21,11 @@ let _message: Message | undefined
const TopicsPage: FC = () => { const TopicsPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [search, setSearch] = useState(_search) const [search, setSearch] = useState(_search)
const [searchKeywords, setSearchKeywords] = useState(_search)
const [stack, setStack] = useState<Route[]>(_stack) const [stack, setStack] = useState<Route[]>(_stack)
const [topic, setTopic] = useState<Topic | undefined>(_topic) const [topic, setTopic] = useState<Topic | undefined>(_topic)
const [message, setMessage] = useState<Message | undefined>(_message) const [message, setMessage] = useState<Message | undefined>(_message)
const inputRef = useRef<InputRef>(null)
_search = search _search = search
_stack = stack _stack = stack
@@ -40,6 +42,7 @@ const TopicsPage: FC = () => {
} }
const onSearch = () => { const onSearch = () => {
setSearchKeywords(search)
setStack(['topics', 'search']) setStack(['topics', 'search'])
setTopic(undefined) setTopic(undefined)
} }
@@ -56,6 +59,12 @@ const TopicsPage: FC = () => {
const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none') const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none')
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
return ( return (
<Container> <Container>
<Header> <Header>
@@ -70,7 +79,9 @@ const TopicsPage: FC = () => {
placeholder={t('history.search.placeholder')} placeholder={t('history.search.placeholder')}
type="search" type="search"
value={search} value={search}
autoFocus
allowClear allowClear
ref={inputRef}
onChange={(e) => setSearch(e.target.value.trimStart())} onChange={(e) => setSearch(e.target.value.trimStart())}
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />} suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
onPressEnter={onSearch} onPressEnter={onSearch}
@@ -84,7 +95,7 @@ const TopicsPage: FC = () => {
/> />
<TopicMessages topic={topic} style={{ display: isShow('topic') }} /> <TopicMessages topic={topic} style={{ display: isShow('topic') }} />
<SearchResults <SearchResults
keywords={isShow('search') ? search : ''} keywords={isShow('search') ? searchKeywords : ''}
onMessageClick={onMessageClick} onMessageClick={onMessageClick}
onTopicClick={onTopicClick} onTopicClick={onTopicClick}
style={{ display: isShow('search') }} style={{ display: isShow('search') }}

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined } from '@ant-design/icons' import { ArrowRightOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useSettings } from '@renderer/hooks/useSettings'
import { default as MessageItem } from '@renderer/pages/home/Messages/Message' import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
import { locateToMessage } from '@renderer/services/MessagesService' import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService' import NavigationService from '@renderer/services/NavigationService'
@@ -15,6 +16,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
const SearchMessage: FC<Props> = ({ message, ...props }) => { const SearchMessage: FC<Props> = ({ message, ...props }) => {
const navigate = NavigationService.navigate! const navigate = NavigationService.navigate!
const { messageStyle } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
if (!message) { if (!message) {
@@ -22,7 +24,7 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
} }
return ( return (
<MessagesContainer {...props}> <MessagesContainer {...props} className={messageStyle}>
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}> <ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
<MessageItem message={message} /> <MessageItem message={message} />
<Button <Button
@@ -45,6 +47,7 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
const MessagesContainer = styled.div` const MessagesContainer = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
overflow-y: scroll; overflow-y: scroll;

View File

@@ -1,5 +1,5 @@
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useShowAssistants } from '@renderer/hooks/useStore' import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { useActiveTopic } from '@renderer/hooks/useTopic'
import NavigationService from '@renderer/services/NavigationService' import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
@@ -22,7 +22,7 @@ const HomePage: FC = () => {
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic) const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants } = useShowAssistants() const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant _activeAssistant = activeAssistant
@@ -35,8 +35,17 @@ const HomePage: FC = () => {
state?.topic && setActiveTopic(state?.topic) state?.topic && setActiveTopic(state?.topic)
}, [state]) }, [state])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics, topicPosition])
return ( return (
<Container> <Container id="home-page">
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} /> <Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{showAssistants && ( {showAssistants && (

View File

@@ -16,9 +16,9 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
} }
return ( return (
<Container> <ContentContainer>
<Upload <Upload
listType="picture-card" listType={files.length > 20 ? 'text' : 'picture-card'}
fileList={files.map((file) => ({ fileList={files.map((file) => ({
uid: file.id, uid: file.id,
url: 'file://' + FileManager.getSafePath(file), url: 'file://' + FileManager.getSafePath(file),
@@ -27,17 +27,15 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
}))} }))}
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))} onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
/> />
</Container> </ContentContainer>
) )
} }
const Container = styled.div` const ContentContainer = styled.div`
display: flex; max-height: 40vh;
flex-direction: row; overflow-y: auto;
gap: 10px; width: 100%;
padding: 10px 20px; padding: 10px 15px 0;
background: var(--color-background);
border-top: 1px solid var(--color-border-mute);
` `
export default AttachmentPreview export default AttachmentPreview

View File

@@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
import { isVisionModel, isWebSearchModel } from '@renderer/config/models' import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useRuntime } from '@renderer/hooks/useRuntime' import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore' import { useShowTopics } from '@renderer/hooks/useStore'
@@ -24,8 +24,8 @@ import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/Toke
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import store, { useAppDispatch, useAppSelector } from '@renderer/store' import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime' import { setGenerating, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, KnowledgeBase, Message, Topic } from '@renderer/types' import { Assistant, FileType, KnowledgeBase, Message, Model, Topic } from '@renderer/types'
import { delay, getFileExtension, uuid } from '@renderer/utils' import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
import { documentExts, imageExts, textExts } from '@shared/config/constant' import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Button, Popconfirm, Tooltip } from 'antd' import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
@@ -35,9 +35,12 @@ import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState }
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import NarrowLayout from '../Messages/NarrowLayout'
import AttachmentButton from './AttachmentButton' import AttachmentButton from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview' import AttachmentPreview from './AttachmentPreview'
import KnowledgeBaseButton from './KnowledgeBaseButton' import KnowledgeBaseButton from './KnowledgeBaseButton'
import MentionModelsButton from './MentionModelsButton'
import MentionModelsInput from './MentionModelsInput'
import SendMessageButton from './SendMessageButton' import SendMessageButton from './SendMessageButton'
import TokenCount from './TokenCount' import TokenCount from './TokenCount'
@@ -62,7 +65,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
showInputEstimatedTokens, showInputEstimatedTokens,
clickAssistantToShowTopic, clickAssistantToShowTopic,
language, language,
autoTranslateWithSpace autoTranslateWithSpace,
sidebarIcons
} = useSettings() } = useSettings()
const [expended, setExpend] = useState(false) const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [estimateTokenCount, setEstimateTokenCount] = useState(0)
@@ -80,27 +84,29 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>()
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base) const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
const [mentionModels, setMentionModels] = useState<Model[]>([])
const isVision = useMemo(() => isVisionModel(model), [model]) const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
const showKnowledgeIcon = sidebarIcons.visible.includes('knowledge')
const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), []) const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), [])
const inputTokenCount = useMemo( const inputTokenCount = useMemo(
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0), () => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
[estimateTextTokens, showInputEstimatedTokens, text] [estimateTextTokens, showInputEstimatedTokens, text]
) )
const newTopicShortcut = useShortcutDisplay('new_topic') const newTopicShortcut = useShortcutDisplay('new_topic')
const inputEmpty = isEmpty(text.trim()) && files.length === 0
_text = text _text = text
_files = files _files = files
_base = selectedKnowledgeBase _base = selectedKnowledgeBase
const sendMessage = useCallback(async () => { const sendMessage = useCallback(async () => {
if (generating) { await modelGenerating()
return
}
if (isEmpty(text.trim())) { if (inputEmpty) {
return return
} }
@@ -123,15 +129,20 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
message.files = await FileManager.uploadFiles(files) message.files = await FileManager.uploadFiles(files)
} }
if (mentionModels.length > 0) {
message.mentions = mentionModels
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message) EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('') setText('')
setFiles([]) setFiles([])
setMentionModels([])
setTimeout(() => setText(''), 500) setTimeout(() => setText(''), 500)
setTimeout(() => resizeTextArea(), 0) setTimeout(() => resizeTextArea(), 0)
setExpend(false) setExpend(false)
}, [assistant.id, assistant.topics, generating, files, text, selectedKnowledgeBase]) }, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files, mentionModels])
const translate = async () => { const translate = async () => {
if (isTranslating) { if (isTranslating) {
@@ -206,10 +217,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
} }
const addNewTopic = useCallback(async () => { const addNewTopic = useCallback(async () => {
if (generating) { await modelGenerating()
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const topic = getDefaultTopic(assistant.id) const topic = getDefaultTopic(assistant.id)
@@ -225,7 +233,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setActiveTopic(topic) setActiveTopic(topic)
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant, clickAssistantToShowTopic, generating, setActiveTopic, setModel, t]) }, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
const clearTopic = async () => { const clearTopic = async () => {
if (generating) { if (generating) {
@@ -386,116 +394,140 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setSelectedKnowledgeBase(base) setSelectedKnowledgeBase(base)
} }
const onMentionModel = useCallback(
(model: Model) => {
const isSelected = mentionModels.some((m) => m.id === model.id)
if (isSelected) {
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
} else {
setMentionModels([...mentionModels, model])
}
},
[mentionModels]
)
const handleRemoveModel = (model: Model) => {
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
}
return ( return (
<Container onDragOver={handleDragOver} onDrop={handleDrop}> <Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<AttachmentPreview files={files} setFiles={setFiles} /> <NarrowLayout style={{ width: '100%' }}>
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}> <InputBarContainer
<Textarea id="inputbar"
value={text} className={classNames('inputbar-container', inputFocus && 'focus')}
onChange={(e) => setText(e.target.value)} ref={containerRef}>
onKeyDown={handleKeyDown} <AttachmentPreview files={files} setFiles={setFiles} />
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')} <MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
autoFocus <Textarea
contextMenu="true" value={text}
variant="borderless" onChange={(e) => setText(e.target.value)}
rows={textareaRows} onKeyDown={handleKeyDown}
ref={textareaRef} placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
style={{ fontSize }} autoFocus
styles={{ textarea: TextareaStyle }} contextMenu="true"
onFocus={() => setInputFocus(true)} variant="borderless"
onBlur={() => setInputFocus(false)} spellCheck={false}
onInput={onInput} rows={textareaRows}
disabled={searching} ref={textareaRef}
onPaste={(e) => onPaste(e.nativeEvent)} style={{ fontSize }}
onClick={() => searching && dispatch(setSearching(false))} styles={{ textarea: TextareaStyle }}
/> onFocus={() => setInputFocus(true)}
<Toolbar> onBlur={() => setInputFocus(false)}
<ToolbarMenu> onInput={onInput}
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow> disabled={searching}
<ToolbarButton type="text" onClick={addNewTopic}> onPaste={(e) => onPaste(e.nativeEvent)}
<FormOutlined /> onClick={() => searching && dispatch(setSearching(false))}
</ToolbarButton> />
</Tooltip> <Toolbar>
{isWebSearchModel(model) && ( <ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.web_search')} arrow> <Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<FormOutlined />
</ToolbarButton>
</Tooltip>
<MentionModelsButton
mentionModels={mentionModels}
onMentionModel={onMentionModel}
ToolbarButton={ToolbarButton}
/>
{isWebSearchModel(model) && (
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<ToolbarButton
type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
<GlobalOutlined
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
)}
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('chat.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton <ToolbarButton
type="text" type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}> onClick={() => {
<GlobalOutlined !showTopics && toggleShowTopics()
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }} setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
/> }}>
<ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} {showKnowledgeIcon && (
<Tooltip placement="top" title={t('chat.input.clear')} arrow> <KnowledgeBaseButton
<Popconfirm selectedBase={selectedKnowledgeBase}
title={t('chat.input.clear.content')} onSelect={handleKnowledgeBaseSelect}
placement="top" ToolbarButton={ToolbarButton}
onConfirm={clearTopic} disabled={files.length > 0}
okButtonProps={{ danger: true }} />
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} )}
okText={t('chat.input.clear')}> <AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<ToolbarButton type="text"> <ToolbarButton type="text" onClick={onNewContext}>
<ClearOutlined /> <Tooltip placement="top" title={t('chat.input.new.context')}>
</ToolbarButton> <PicCenterOutlined />
</Popconfirm> </Tooltip>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton
type="text"
onClick={() => {
!showTopics && toggleShowTopics()
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
}}>
<ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> <Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<KnowledgeBaseButton <ToolbarButton type="text" onClick={onToggleExpended}>
selectedBase={selectedKnowledgeBase} {expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
<AttachmentButton
model={model}
files={files}
setFiles={setFiles}
ToolbarButton={ToolbarButton}
disabled={!!selectedKnowledgeBase}
/>
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />
</Tooltip>
</ToolbarButton>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={onToggleExpended}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} <TokenCount
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />} estimateTokenCount={estimateTokenCount}
</ToolbarMenu> inputTokenCount={inputTokenCount}
</Toolbar> contextCount={contextCount}
</InputBarContainer> ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton>
</Tooltip>
)}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
</NarrowLayout>
</Container> </Container>
) )
} }

View File

@@ -62,6 +62,7 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, Tool
<Popover <Popover
placement="top" placement="top"
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />} content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
overlayStyle={{ maxWidth: 400 }}
trigger="click"> trigger="click">
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}> <ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} /> <FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />

View File

@@ -0,0 +1,155 @@
import { PushpinOutlined } from '@ant-design/icons'
import ModelTags from '@renderer/components/ModelTags'
import { getModelLogo, isEmbeddingModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import { Avatar, Dropdown, Tooltip } from 'antd'
import { first, sortBy } from 'lodash'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { createGlobalStyle } from 'styled-components'
interface Props {
mentionModels: Model[]
onMentionModel: (model: Model) => void
ToolbarButton: any
}
const MentionModelsButton: FC<Props> = ({ onMentionModel: onSelect, ToolbarButton }) => {
const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([])
const { t } = useTranslation()
useEffect(() => {
const loadPinnedModels = async () => {
const setting = await db.settings.get('pinned:models')
setPinnedModels(setting?.value || [])
}
loadPinnedModels()
}, [])
const togglePin = async (modelId: string) => {
const newPinnedModels = pinnedModels.includes(modelId)
? pinnedModels.filter((id) => id !== modelId)
: [...pinnedModels, modelId]
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
setPinnedModels(newPinnedModels)
}
const modelMenuItems = providers
.filter((p) => p.models && p.models.length > 0)
.map((p) => {
const filteredModels = sortBy(p.models, ['group', 'name'])
.filter((m) => !isEmbeddingModel(m))
.map((m) => ({
key: getModelUniqId(m),
label: (
<ModelItem>
<span>
{m?.name} <ModelTags model={m} />
</span>
{/* <Checkbox checked={selectedModels.some((sm) => sm.id === m.id)} /> */}
<PinIcon
onClick={(e) => {
e.stopPropagation()
togglePin(getModelUniqId(m))
}}
$isPinned={pinnedModels.includes(getModelUniqId(m))}>
<PushpinOutlined />
</PinIcon>
</ModelItem>
),
icon: (
<Avatar src={getModelLogo(m.id)} size={24}>
{first(m.name)}
</Avatar>
),
onClick: () => {
onSelect(m)
}
}))
return filteredModels.length > 0
? {
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group' as const,
children: filteredModels
}
: null
})
.filter(Boolean)
if (pinnedModels.length > 0) {
const pinnedItems = modelMenuItems
.flatMap((p) => p?.children || [])
.filter((m) => pinnedModels.includes(m.key))
.map((m) => ({ ...m, key: m.key + 'pinned' }))
if (pinnedItems.length > 0) {
modelMenuItems.unshift({
key: 'pinned',
label: t('models.pinned'),
type: 'group' as const,
children: pinnedItems
})
}
}
return (
<>
<DropdownMenuStyle />
<Dropdown menu={{ items: modelMenuItems }} trigger={['click']} overlayClassName="mention-models-dropdown">
<Tooltip placement="top" title={t('agents.edit.model.select.title')} arrow>
<ToolbarButton type="text">
<i className="iconfont icon-at" style={{ fontSize: 18 }}></i>
</ToolbarButton>
</Tooltip>
</Dropdown>
</>
)
}
const DropdownMenuStyle = createGlobalStyle`
.mention-models-dropdown {
.ant-dropdown-menu {
max-height: 400px;
}
}
`
const ModelItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
width: 100%;
gap: 16px;
&:hover {
.pin-icon {
opacity: 0.3;
}
}
`
const PinIcon = styled.span.attrs({ className: 'pin-icon' })<{ $isPinned: boolean }>`
margin-left: auto;
padding: 0 8px;
opacity: ${(props) => (props.$isPinned ? 1 : 'inherit')};
transition: opacity 0.2s;
right: 0;
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'inherit')};
transform: ${(props) => (props.$isPinned ? 'rotate(-45deg)' : 'none')};
opacity: 0;
&:hover {
opacity: 1 !important;
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'inherit')};
}
`
export default MentionModelsButton

View File

@@ -0,0 +1,26 @@
import { Model } from '@renderer/types'
import { Flex, Tag } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
const MentionModelsInput: FC<{
selectedModels: Model[]
onRemoveModel: (model: Model) => void
}> = ({ selectedModels, onRemoveModel }) => {
return (
<Container gap="4px 0" wrap>
{selectedModels.map((model) => (
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
@{model.name}
</Tag>
))}
</Container>
)
}
const Container = styled(Flex)`
width: 100%;
padding: 10px 15px 0;
`
export default MentionModelsInput

View File

@@ -66,6 +66,9 @@ const Container = styled.div`
font-size: 10px; font-size: 10px;
margin-right: 3px; margin-right: 3px;
} }
@media (max-width: 600px) {
display: none;
}
` `
const Text = styled.div` const Text = styled.div`

View File

@@ -1,7 +1,7 @@
import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons' import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons'
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { AppLogo } from '@renderer/config/env' import { AppLogo } from '@renderer/config/env'
import { extractTitle } from '@renderer/utils/formula' import { extractTitle } from '@renderer/utils/formats'
import { Button } from 'antd' import { Button } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'

View File

@@ -1,7 +1,9 @@
import { CheckOutlined, DownOutlined, RightOutlined } from '@ant-design/icons' import { CheckOutlined, DownloadOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'
import CopyIcon from '@renderer/components/Icons/CopyIcon' import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { HStack } from '@renderer/components/Layout'
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider' import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
import React, { memo, useEffect, useRef, useState } from 'react' import React, { memo, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -16,28 +18,6 @@ interface CodeBlockProps {
[key: string]: any [key: string]: any
} }
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
return (
<CollapseIconWrapper onClick={onClick}>
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</CollapseIconWrapper>
)
}
const ExpandButton: React.FC<{
isExpanded: boolean
onClick: () => void
showButton: boolean
}> = ({ isExpanded, onClick, showButton }) => {
if (!showButton) return null
return (
<ExpandButtonWrapper onClick={onClick}>
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
</ExpandButtonWrapper>
)
}
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => { const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
const { codeShowLineNumbers, fontSize, codeCollapsible } = useSettings() const { codeShowLineNumbers, fontSize, codeCollapsible } = useSettings()
@@ -50,6 +30,8 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const showFooterCopyButton = children && children.length > 500 && !codeCollapsible const showFooterCopyButton = children && children.length > 500 && !codeCollapsible
const showDownloadButton = ['csv', 'json', 'txt', 'md'].includes(language)
useEffect(() => { useEffect(() => {
const loadHighlightedCode = async () => { const loadHighlightedCode = async () => {
const highlightedHtml = await codeToHtml(children, language) const highlightedHtml = await codeToHtml(children, language)
@@ -101,7 +83,10 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
)} )}
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage> <CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
</div> </div>
<CopyButton text={children} /> <HStack gap={12} alignItems="center">
{showDownloadButton && <DownloadButton language={language} data={children} />}
<CopyButton text={children} />
</HStack>
</CodeHeader> </CodeHeader>
<CodeContent <CodeContent
ref={codeContentRef} ref={codeContentRef}
@@ -137,6 +122,28 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
) )
} }
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
return (
<CollapseIconWrapper onClick={onClick}>
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</CollapseIconWrapper>
)
}
const ExpandButton: React.FC<{
isExpanded: boolean
onClick: () => void
showButton: boolean
}> = ({ isExpanded, onClick, showButton }) => {
if (!showButton) return null
return (
<ExpandButtonWrapper onClick={onClick}>
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
</ExpandButtonWrapper>
)
}
const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => { const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
@@ -155,6 +162,19 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
) )
} }
const DownloadButton = ({ language, data }: { language: string; data: string }) => {
const onDownload = () => {
const fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}`
window.api.file.save(fileName, data)
}
return (
<DownloadWrapper onClick={onDownload}>
<DownloadOutlined />
</DownloadWrapper>
)
}
const CodeBlockWrapper = styled.div`` const CodeBlockWrapper = styled.div``
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>` const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
@@ -264,4 +284,18 @@ const CollapseIconWrapper = styled.div`
} }
` `
const DownloadWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--color-text-3);
transition: color 0.3s;
font-size: 16px;
&:hover {
color: var(--color-text-1);
}
`
export default memo(CodeBlock) export default memo(CodeBlock)

View File

@@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types' import { Message } from '@renderer/types'
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula' import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react' import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -19,7 +19,7 @@ import ImagePreview from './ImagePreview'
import Link from './Link' import Link from './Link'
const ALLOWED_ELEMENTS = const ALLOWED_ELEMENTS =
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan)/i /<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan|sub|sup)/i
interface Props { interface Props {
message: Message message: Message
@@ -34,9 +34,9 @@ const Markdown: FC<Props> = ({ message }) => {
const messageContent = useMemo(() => { const messageContent = useMemo(() => {
const empty = isEmpty(message.content) const empty = isEmpty(message.content)
const paused = message.status === 'paused' const paused = message.status === 'paused'
const content = empty && paused ? t('message.chat.completion.paused') : message.content const content = empty && paused ? t('message.chat.completion.paused') : withGeminiGrounding(message)
return removeSvgEmptyLines(escapeBrackets(content)) return removeSvgEmptyLines(escapeBrackets(content))
}, [message.content, message.status, t]) }, [message, t])
const rehypePlugins = useMemo(() => { const rehypePlugins = useMemo(() => {
const hasElements = ALLOWED_ELEMENTS.test(messageContent) const hasElements = ALLOWED_ELEMENTS.test(messageContent)

View File

@@ -18,6 +18,7 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
const { t } = useTranslation() const { t } = useTranslation()
const mermaidId = `mermaid-popup-${Date.now()}` const mermaidId = `mermaid-popup-${Date.now()}`
const [activeTab, setActiveTab] = useState('preview') const [activeTab, setActiveTab] = useState('preview')
const [scale, setScale] = useState(1)
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
@@ -31,6 +32,25 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
resolve({}) resolve({})
} }
const handleZoom = (delta: number) => {
const newScale = Math.max(0.1, Math.min(3, scale + delta))
setScale(newScale)
const element = document.getElementById(mermaidId)
if (!element) return
const svg = element.querySelector('svg')
if (!svg) return
const container = svg.parentElement
if (container) {
container.style.overflow = 'auto'
container.style.position = 'relative'
svg.style.transformOrigin = 'top left'
svg.style.transform = `scale(${newScale})`
}
}
const handleDownload = async (format: 'svg' | 'png') => { const handleDownload = async (format: 'svg' | 'png') => {
try { try {
const element = document.getElementById(mermaidId) const element = document.getElementById(mermaidId)
@@ -110,6 +130,8 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
{activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>} {activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>}
{activeTab === 'preview' && ( {activeTab === 'preview' && (
<> <>
<Button onClick={() => handleZoom(0.1)}>{t('mermaid.resize.zoom-in')}</Button>
<Button onClick={() => handleZoom(-0.1)}>{t('mermaid.resize.zoom-out')}</Button>
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button> <Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button> <Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
</> </>

View File

@@ -86,9 +86,12 @@ const MessageItem: FC<Props> = ({
} }
useEffect(() => { useEffect(() => {
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)] const unsubscribes = [
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler),
EventEmitter.on(EVENT_NAMES.RESEND_MESSAGE + ':' + message.id, onEditMessage)
]
return () => unsubscribes.forEach((unsub) => unsub()) return () => unsubscribes.forEach((unsub) => unsub())
}, [message]) }, [message, onEditMessage])
useEffect(() => { useEffect(() => {
if (message.role === 'user' && !message.usage) { if (message.role === 'user' && !message.usage) {
@@ -106,6 +109,8 @@ const MessageItem: FC<Props> = ({
if (topic && onGetMessages && onSetMessages) { if (topic && onGetMessages && onSetMessages) {
if (message.status === 'sending') { if (message.status === 'sending') {
const messages = onGetMessages() const messages = onGetMessages()
const assistantWithModel = message.model ? { ...assistant, model: message.model } : assistant
fetchChatCompletion({ fetchChatCompletion({
message, message,
messages: messages messages: messages
@@ -114,7 +119,7 @@ const MessageItem: FC<Props> = ({
0, 0,
messages.findIndex((m) => m.id === message.id) messages.findIndex((m) => m.id === message.id)
), ),
assistant, assistant: assistantWithModel,
topic, topic,
onResponse: (msg) => { onResponse: (msg) => {
setMessage(msg) setMessage(msg)
@@ -178,6 +183,7 @@ const MessageItem: FC<Props> = ({
setModel={setModel} setModel={setModel}
onEditMessage={onEditMessage} onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage} onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/> />
</MessageFooter> </MessageFooter>
)} )}

View File

@@ -1,7 +1,7 @@
import { SyncOutlined, TranslationOutlined } from '@ant-design/icons' import { SyncOutlined, TranslationOutlined } from '@ant-design/icons'
import { Message, Model } from '@renderer/types' import { Message, Model } from '@renderer/types'
import { getBriefInfo } from '@renderer/utils' import { getBriefInfo } from '@renderer/utils'
import { Divider } from 'antd' import { Divider, Flex } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BeatLoader from 'react-spinners/BeatLoader' import BeatLoader from 'react-spinners/BeatLoader'
@@ -10,6 +10,7 @@ import styled from 'styled-components'
import Markdown from '../Markdown/Markdown' import Markdown from '../Markdown/Markdown'
import MessageAttachments from './MessageAttachments' import MessageAttachments from './MessageAttachments'
import MessageError from './MessageError' import MessageError from './MessageError'
import MessageSearchResults from './MessageSearchResults'
const MessageContent: React.FC<{ const MessageContent: React.FC<{
message: Message message: Message
@@ -36,6 +37,9 @@ const MessageContent: React.FC<{
return ( return (
<> <>
<Flex gap="8px" wrap>
{message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)}
</Flex>
<Markdown message={message} /> <Markdown message={message} />
{message.translatedContent && ( {message.translatedContent && (
<> <>
@@ -50,6 +54,7 @@ const MessageContent: React.FC<{
</> </>
)} )}
<MessageAttachments message={message} /> <MessageAttachments message={message} />
<MessageSearchResults message={message} />
</> </>
) )
} }
@@ -63,4 +68,8 @@ const MessageContentLoading = styled.div`
margin-bottom: 5px; margin-bottom: 5px;
` `
const MentionTag = styled.span`
color: var(--color-link);
`
export default React.memo(MessageContent) export default React.memo(MessageContent)

View File

@@ -27,7 +27,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => { const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { theme } = useTheme() const { theme } = useTheme()
const { userName } = useSettings() const { userName, sidebarIcons } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const { isBubbleStyle } = useMessageStyle() const { isBubbleStyle } = useMessageStyle()
@@ -40,11 +40,14 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
}, [message.modelId, message.role, model?.id, model?.name, t, userName]) }, [message.modelId, message.role, model?.id, model?.name, t, userName])
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp')
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = useCallback(() => model?.provider && startMinAppById(model.provider), [model?.provider]) const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && startMinAppById(model.provider)
}, [model?.provider, showMinappIcon])
const avatarStyle: CSSProperties | undefined = isBubbleStyle const avatarStyle: CSSProperties | undefined = isBubbleStyle
? { ? {
@@ -54,7 +57,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
: undefined : undefined
return ( return (
<Container> <Container className="message-header">
<AvatarWrapper style={avatarStyle}> <AvatarWrapper style={avatarStyle}>
{isAssistantMessage ? ( {isAssistantMessage ? (
<Avatar <Avatar
@@ -62,7 +65,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
size={35} size={35}
style={{ style={{
borderRadius: '20%', borderRadius: '20%',
cursor: 'pointer', cursor: showMinappIcon ? 'pointer' : 'default',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none', border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined filter: theme === 'dark' ? 'invert(0.05)' : undefined
}} }}

View File

@@ -11,11 +11,12 @@ import {
} from '@ant-design/icons' } from '@ant-design/icons'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import { Message, Model } from '@renderer/types' import { Message, Model } from '@renderer/types'
import { removeTrailingDoubleSpaces } from '@renderer/utils' import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils'
import { Dropdown, Popconfirm, Tooltip } from 'antd' import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { FC, useCallback, useMemo, useState } from 'react' import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -31,6 +32,7 @@ interface Props {
setModel: (model: Model) => void setModel: (model: Model) => void
onEditMessage?: (message: Message) => void onEditMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => void
onGetMessages?: () => Message[]
} }
const MessageMenubar: FC<Props> = (props) => { const MessageMenubar: FC<Props> = (props) => {
@@ -43,7 +45,8 @@ const MessageMenubar: FC<Props> = (props) => {
assistantModel, assistantModel,
setModel, setModel,
onEditMessage, onEditMessage,
onDeleteMessage onDeleteMessage,
onGetMessages
} = props } = props
const { t } = useTranslation() const { t } = useTranslation()
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -67,7 +70,8 @@ const MessageMenubar: FC<Props> = (props) => {
[setModel] [setModel]
) )
const onNewBranch = useCallback(() => { const onNewBranch = useCallback(async () => {
await modelGenerating()
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index) EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
window.message.success({ window.message.success({
content: t('chat.message.new.branch.created'), content: t('chat.message.new.branch.created'),
@@ -75,10 +79,49 @@ const MessageMenubar: FC<Props> = (props) => {
}) })
}, [index, t]) }, [index, t])
const onResend = useCallback(async () => {
await modelGenerating()
const _messages = onGetMessages?.() || []
const index = _messages.findIndex((m) => m.id === message.id)
const nextIndex = index + 1
const nextMessage = _messages[nextIndex]
if (nextMessage && nextMessage.role === 'assistant') {
EventEmitter.emit(EVENT_NAMES.RESEND_MESSAGE + ':' + nextMessage.id, {
...nextMessage,
content: '',
status: 'sending',
modelId: assistantModel?.id || model?.id,
translatedContent: undefined
})
}
if (!nextMessage) {
onDeleteMessage?.(message)
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { ...message, id: uuid() })
}
}, [assistantModel?.id, message, model?.id, onDeleteMessage, onGetMessages])
const onEdit = useCallback(async () => { const onEdit = useCallback(async () => {
const editedText = await TextEditPopup.show({ text: message.content }) let resendMessage = false
const editedText = await TextEditPopup.show({
text: message.content,
children: (props) => (
<ReSendButton
icon={<i className="iconfont icon-ic_send" style={{ color: 'var(--color-primary)' }} />}
onClick={() => {
props.onOk?.()
resendMessage = true
}}>
{t('chat.resend')}
</ReSendButton>
)
})
editedText && onEditMessage?.({ ...message, content: editedText }) editedText && onEditMessage?.({ ...message, content: editedText })
}, [message, onEditMessage]) resendMessage && onResend()
}, [message, onEditMessage, onResend, t])
const handleTranslate = useCallback( const handleTranslate = useCallback(
async (language: string) => { async (language: string) => {
@@ -123,57 +166,23 @@ const MessageMenubar: FC<Props> = (props) => {
onClick: onEdit onClick: onEdit
}, },
{ {
label: t('chat.translate'), label: t('chat.message.new.branch'),
key: 'translate', key: 'new-branch',
icon: isTranslating ? <SyncOutlined spin /> : <TranslationOutlined />, icon: <ForkOutlined />,
children: [ onClick: onNewBranch
{
label: '🇨🇳 ' + t('languages.chinese'),
key: 'translate-chinese',
onClick: () => handleTranslate('chinese')
},
{
label: '🇭🇰 ' + t('languages.chinese-traditional'),
key: 'translate-chinese-traditional',
onClick: () => handleTranslate('chinese-traditional')
},
{
label: '🇬🇧 ' + t('languages.english'),
key: 'translate-english',
onClick: () => handleTranslate('english')
},
{
label: '🇯🇵 ' + t('languages.japanese'),
key: 'translate-japanese',
onClick: () => handleTranslate('japanese')
},
{
label: '🇰🇷 ' + t('languages.korean'),
key: 'translate-korean',
onClick: () => handleTranslate('korean')
},
{
label: '🇷🇺 ' + t('languages.russian'),
key: 'translate-russian',
onClick: () => handleTranslate('russian')
},
{
label: '✖ ' + t('translate.close'),
key: 'translate-close',
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
}
]
} }
], ],
[handleTranslate, isTranslating, message, onEdit, onEditMessage, t] [message, onEdit, onNewBranch, t]
) )
const onAtModelRegenerate = async () => { const onAtModelRegenerate = async () => {
await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model }) const selectedModel = await SelectModelPopup.show({ model })
selectedModel && onRegenerate(selectedModel) selectedModel && onRegenerate(selectedModel)
} }
const onDeleteAndRegenerate = () => { const onDeleteAndRegenerate = async () => {
await modelGenerating()
onEditMessage?.({ onEditMessage?.({
...message, ...message,
content: '', content: '',
@@ -186,7 +195,7 @@ const MessageMenubar: FC<Props> = (props) => {
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}> <Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit}> <ActionButton className="message-action-button" onClick={onEdit}>
<EditOutlined /> <EditOutlined />
</ActionButton> </ActionButton>
@@ -215,16 +224,60 @@ const MessageMenubar: FC<Props> = (props) => {
{canRegenerate && ( {canRegenerate && (
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}> <Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}> <ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
<i className="iconfont icon-at1"></i> <i className="iconfont icon-at"></i>
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
{isAssistantMessage && ( {!isUserMessage && (
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}> <Dropdown
<ActionButton className="message-action-button" onClick={onNewBranch}> menu={{
<ForkOutlined /> items: [
</ActionButton> {
</Tooltip> label: '🇨🇳 ' + t('languages.chinese'),
key: 'translate-chinese',
onClick: () => handleTranslate('chinese')
},
{
label: '🇭🇰 ' + t('languages.chinese-traditional'),
key: 'translate-chinese-traditional',
onClick: () => handleTranslate('chinese-traditional')
},
{
label: '🇬🇧 ' + t('languages.english'),
key: 'translate-english',
onClick: () => handleTranslate('english')
},
{
label: '🇯🇵 ' + t('languages.japanese'),
key: 'translate-japanese',
onClick: () => handleTranslate('japanese')
},
{
label: '🇰🇷 ' + t('languages.korean'),
key: 'translate-korean',
onClick: () => handleTranslate('korean')
},
{
label: '🇷🇺 ' + t('languages.russian'),
key: 'translate-russian',
onClick: () => handleTranslate('russian')
},
{
label: '✖ ' + t('translate.close'),
key: 'translate-close',
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
}
]
}}
trigger={['click']}
placement="topRight"
arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton className="message-action-button">
<TranslationOutlined />
</ActionButton>
</Tooltip>
</Dropdown>
)} )}
<Popconfirm <Popconfirm
title={t('message.message.delete.content')} title={t('message.message.delete.content')}
@@ -282,9 +335,15 @@ const ActionButton = styled.div`
&:hover { &:hover {
color: var(--color-text-1); color: var(--color-text-1);
} }
.icon-at1 { .icon-at {
font-size: 16px; font-size: 16px;
} }
` `
const ReSendButton = styled(Button)`
position: absolute;
top: 10px;
left: 0;
`
export default MessageMenubar export default MessageMenubar

View File

@@ -0,0 +1,96 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { Message } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
message: Message
}
const MessageSearchResults: FC<Props> = ({ message }) => {
const { t } = useTranslation()
if (!message.metadata?.groundingMetadata) {
return null
}
const { groundingChunks, searchEntryPoint } = message.metadata.groundingMetadata
if (!groundingChunks) {
return null
}
let searchEntryContent = searchEntryPoint?.renderedContent
searchEntryContent = searchEntryContent?.replace(
/@media \(prefers-color-scheme: light\)/g,
'body[theme-mode="light"]'
)
searchEntryContent = searchEntryContent?.replace(/@media \(prefers-color-scheme: dark\)/g, 'body[theme-mode="dark"]')
return (
<>
<Container className="footnotes">
<TitleRow>
<Title>{t('common.footnotes')}</Title>
<InfoCircleOutlined />
</TitleRow>
<Sources>
{groundingChunks.map((chunk, index) => (
<SourceItem key={index}>
<Link href={chunk.web?.uri} target="_blank" rel="noopener noreferrer">
{chunk.web?.title}
</Link>
</SourceItem>
))}
</Sources>
</Container>
<SearchEntryPoint dangerouslySetInnerHTML={{ __html: searchEntryContent || '' }} />
</>
)
}
const Container = styled.div`
padding: 16px;
border-radius: 8px;
margin-bottom: 0;
`
const TitleRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
margin-bottom: 10px;
`
const Title = styled.h4`
margin: 0 !important;
`
const Sources = styled.ol`
margin-top: 10px;
`
const SourceItem = styled.li`
margin-bottom: 5px;
`
const Link = styled.a`
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
`
const SearchEntryPoint = styled.div`
margin-top: 10px;
margin-bottom: 10px;
`
export default MessageSearchResults

View File

@@ -26,6 +26,7 @@ import styled from 'styled-components'
import Suggestions from '../components/Suggestions' import Suggestions from '../components/Suggestions'
import MessageItem from './Message' import MessageItem from './Message'
import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt' import Prompt from './Prompt'
interface Props { interface Props {
@@ -96,10 +97,19 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
const onSendMessage = useCallback( const onSendMessage = useCallback(
async (message: Message) => { async (message: Message) => {
const assistantMessage = getAssistantMessage({ assistant, topic }) const assistantMessages: Message[] = []
if (message.mentions?.length) {
message.mentions.forEach((m) => {
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
assistantMessage.model = m
assistantMessages.push(assistantMessage)
})
} else {
assistantMessages.push(getAssistantMessage({ assistant, topic }))
}
setMessages((prev) => { setMessages((prev) => {
const messages = prev.concat([message, assistantMessage]) const messages = prev.concat([message, ...assistantMessages])
db.topics.put({ id: topic.id, messages }) db.topics.put({ id: topic.id, messages })
return messages return messages
}) })
@@ -136,6 +146,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
(message: Message) => { (message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id) const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages) setMessages(_messages)
setDisplayMessages(_messages)
db.topics.update(topic.id, { messages: _messages }) db.topics.update(topic.id, { messages: _messages })
deleteMessageFiles(message) deleteMessageFiles(message)
}, },
@@ -154,7 +165,8 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
}), }),
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => { EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
const lastUserMessage = last(filterMessages(messages).filter((m) => m.role === 'user')) const lastUserMessage = last(filterMessages(messages).filter((m) => m.role === 'user'))
lastUserMessage && onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', modelId: model.id }) lastUserMessage &&
onSendMessage({ ...lastUserMessage, id: uuid(), modelId: model.id, model: model, mentions: [model] })
}), }),
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic), EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => { EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
@@ -282,33 +294,35 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
key={assistant.id} key={assistant.id}
ref={containerRef} ref={containerRef}
right={topicPosition === 'left'}> right={topicPosition === 'left'}>
<Suggestions assistant={assistant} messages={messages} /> <NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<InfiniteScroll <Suggestions assistant={assistant} messages={messages} />
dataLength={displayMessages.length} <InfiniteScroll
next={loadMoreMessages} dataLength={displayMessages.length}
hasMore={hasMore} next={loadMoreMessages}
loader={null} hasMore={hasMore}
inverse={true} loader={null}
scrollableTarget="messages"> inverse={true}
<ScrollContainer> scrollableTarget="messages">
<LoaderContainer $loading={isLoadingMore}> <ScrollContainer>
<BeatLoader size={8} color="var(--color-text-2)" /> <LoaderContainer $loading={isLoadingMore}>
</LoaderContainer> <BeatLoader size={8} color="var(--color-text-2)" />
{displayMessages.map((message, index) => ( </LoaderContainer>
<MessageItem {displayMessages.map((message, index) => (
key={message.id} <MessageItem
message={message} key={message.id}
topic={topic} message={message}
index={index} topic={topic}
hidePresetMessages={assistant.settings?.hideMessages} index={index}
onSetMessages={setMessages} hidePresetMessages={assistant.settings?.hideMessages}
onDeleteMessage={onDeleteMessage} onSetMessages={setMessages}
onGetMessages={onGetMessages} onDeleteMessage={onDeleteMessage}
/> onGetMessages={onGetMessages}
))} />
</ScrollContainer> ))}
</InfiniteScroll> </ScrollContainer>
<Prompt assistant={assistant} key={assistant.prompt} /> </InfiniteScroll>
<Prompt assistant={assistant} key={assistant.prompt} />
</NarrowLayout>
</Container> </Container>
) )
} }

View File

@@ -0,0 +1,25 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { FC, HTMLAttributes } from 'react'
import styled from 'styled-components'
interface Props extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
}
const NarrowLayout: FC<Props> = ({ children, ...props }) => {
const { narrowMode } = useSettings()
if (narrowMode) {
return <Container {...props}>{children}</Container>
}
return children
}
const Container = styled.div`
max-width: 800px;
width: 100%;
margin: 0 auto;
`
export default NarrowLayout

View File

@@ -1,4 +1,4 @@
import AssistantSettingsPopup from '@renderer/components/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -28,7 +28,7 @@ const Container = styled.div`
padding: 10px 20px; padding: 10px 20px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin-bottom: 20px; margin-bottom: 20px;
margin: 0 20px 0 20px; margin: 4px 20px 0 20px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);

View File

@@ -1,15 +1,17 @@
import { FormOutlined, SearchOutlined } from '@ant-design/icons' import { FormOutlined, SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import AppStorePopover from '@renderer/components/Popups/AppStorePopover' import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@@ -25,8 +27,9 @@ interface Props {
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => { const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id) const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition } = useSettings() const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', () => { useShortcut('toggle_show_assistants', () => {
toggleShowAssistants() toggleShowAssistants()
@@ -40,11 +43,15 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
} }
}) })
useShortcut('search_message', () => {
SearchPopup.show()
})
return ( return (
<Navbar> <Navbar className="home-navbar">
{showAssistants && ( {showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}> <NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}> <NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 16 : 0 }}>
<i className="iconfont icon-hide-sidebar" /> <i className="iconfont icon-hide-sidebar" />
</NavbarIcon> </NavbarIcon>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> <NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
@@ -52,12 +59,12 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</NavbarIcon> </NavbarIcon>
</NavbarLeft> </NavbarLeft>
)} )}
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}> <NavbarRight
style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}
className="home-navbar-right">
<HStack alignItems="center"> <HStack alignItems="center">
{!showAssistants && ( {!showAssistants && (
<NavbarIcon <NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8, marginLeft: isMac ? 4 : -12 }}>
onClick={() => toggleShowAssistants()}
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
<i className="iconfont icon-show-sidebar" /> <i className="iconfont icon-show-sidebar" />
</NavbarIcon> </NavbarIcon>
)} )}
@@ -69,19 +76,24 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</TitleText> </TitleText>
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</HStack> </HStack>
<HStack alignItems="center"> <HStack alignItems="center" gap={8}>
<NavbarIcon onClick={() => SearchPopup.show()}> <NarrowIcon onClick={() => SearchPopup.show()}>
<SearchOutlined /> <SearchOutlined />
</NavbarIcon> </NarrowIcon>
<AppStorePopover> <NarrowIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}> <i className="iconfont icon-icon-adaptive-width"></i>
<i className="iconfont icon-appstore" /> </NarrowIcon>
</NavbarIcon> {sidebarIcons.visible.includes('minapp') && (
</AppStorePopover> <MinAppsPopover>
<NarrowIcon>
<i className="iconfont icon-appstore" />
</NarrowIcon>
</MinAppsPopover>
)}
{topicPosition === 'right' && ( {topicPosition === 'right' && (
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}> <NarrowIcon onClick={toggleShowTopics}>
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} /> <i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
</NavbarIcon> </NarrowIcon>
)} )}
</HStack> </HStack>
</NavbarRight> </NavbarRight>
@@ -126,8 +138,17 @@ export const NavbarIcon = styled.div`
const TitleText = styled.span` const TitleText = styled.span`
margin-left: 5px; margin-left: 5px;
font-family: Ubuntu; font-family: Ubuntu;
font-size: 13px; font-size: 12px;
user-select: none; user-select: none;
@media (max-width: 1080px) {
display: none;
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
` `
export default HeaderNavbar export default HeaderNavbar

View File

@@ -1,14 +1,14 @@
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon' import CopyIcon from '@renderer/components/Icons/CopyIcon'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultTopic } from '@renderer/services/AssistantService' import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppSelector } from '@renderer/store'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { Dropdown } from 'antd' import { Dropdown } from 'antd'
@@ -32,7 +32,6 @@ const Assistants: FC<Props> = ({
onCreateDefaultAssistant onCreateDefaultAssistant
}) => { }) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants() const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const generating = useAppSelector((state) => state.runtime.generating)
const [dragging, setDragging] = useState(false) const [dragging, setDragging] = useState(false)
const { removeAllTopics } = useAssistant(activeAssistant.id) const { removeAllTopics } = useAssistant(activeAssistant.id)
const { clickAssistantToShowTopic, topicPosition } = useSettings() const { clickAssistantToShowTopic, topicPosition } = useSettings()
@@ -41,7 +40,7 @@ const Assistants: FC<Props> = ({
const onDelete = useCallback( const onDelete = useCallback(
(assistant: Assistant) => { (assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id)) const _assistant: Assistant | undefined = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateDefaultAssistant() _assistant ? setActiveAssistant(_assistant) : onCreateDefaultAssistant()
removeAssistant(assistant.id) removeAssistant(assistant.id)
}, },
@@ -117,13 +116,8 @@ const Assistants: FC<Props> = ({
) )
const onSwitchAssistant = useCallback( const onSwitchAssistant = useCallback(
(assistant: Assistant): any => { async (assistant: Assistant) => {
if (generating) { await modelGenerating()
return window.message.warning({
content: t('message.switch.disabled'),
key: 'switch-assistant'
})
}
if (topicPosition === 'left' && clickAssistantToShowTopic) { if (topicPosition === 'left' && clickAssistantToShowTopic) {
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR) EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
@@ -131,11 +125,11 @@ const Assistants: FC<Props> = ({
setActiveAssistant(assistant) setActiveAssistant(assistant)
}, },
[clickAssistantToShowTopic, generating, setActiveAssistant, t, topicPosition] [clickAssistantToShowTopic, setActiveAssistant, topicPosition]
) )
return ( return (
<Container> <Container className="assistants-tab">
<DragableList <DragableList
list={assistants} list={assistants}
onUpdate={updateAssistants} onUpdate={updateAssistants}
@@ -187,7 +181,7 @@ const AssistantItem = styled.div`
margin: 0 10px; margin: 0 10px;
padding-right: 35px; padding-right: 35px;
font-family: Ubuntu; font-family: Ubuntu;
border-radius: 16px; border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
cursor: pointer; cursor: pointer;
.iconfont { .iconfont {

View File

@@ -102,7 +102,8 @@ const SettingsTab: FC<Props> = (props) => {
maxTokens: DEFAULT_MAX_TOKENS, maxTokens: DEFAULT_MAX_TOKENS,
streamOutput: true, streamOutput: true,
hideMessages: false, hideMessages: false,
autoResetModel: false autoResetModel: false,
customParameters: []
} }
}) })
} }
@@ -116,7 +117,7 @@ const SettingsTab: FC<Props> = (props) => {
}, [assistant]) }, [assistant])
return ( return (
<Container> <Container className="settings-tab">
<SettingGroup style={{ marginTop: 10 }}> <SettingGroup style={{ marginTop: 10 }}>
<SettingSubtitle style={{ marginTop: 0 }}> <SettingSubtitle style={{ marginTop: 0 }}>
{t('settings.messages.model.title')}{' '} {t('settings.messages.model.title')}{' '}
@@ -390,6 +391,7 @@ const Container = styled(Scrollbar)`
padding: 0 10px; padding: 0 10px;
padding-right: 5px; padding-right: 5px;
padding-top: 2px; padding-top: 2px;
padding-bottom: 10px;
` `
const Label = styled.p` const Label = styled.p`
@@ -409,13 +411,11 @@ const SettingRowTitleSmall = styled(SettingRowTitle)`
` `
export const SettingGroup = styled.div<{ theme?: ThemeMode }>` export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 10px; padding: 0 5px;
width: 100%; width: 100%;
margin-top: 0; margin-top: 0;
border-radius: 8px; border-radius: 8px;
margin-bottom: 10px; margin-bottom: 10px;
border: 0.5px solid var(--color-border);
background: var(--color-group-background);
` `
export default SettingsTab export default SettingsTab

View File

@@ -10,11 +10,12 @@ import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic' import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService' import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store, { useAppSelector } from '@renderer/store' import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export' import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
@@ -35,46 +36,36 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { assistants } = useAssistants() const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation() const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const { showTopicTime, topicPosition } = useSettings() const { showTopicTime, topicPosition } = useSettings()
const borderRadius = showTopicTime ? 12 : 17 const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const onDeleteTopic = useCallback( const onDeleteTopic = useCallback(
(topic: Topic) => { async (topic: Topic) => {
if (generating) { await modelGenerating()
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const index = findIndex(assistant.topics, (t) => t.id === topic.id) const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1]) setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
removeTopic(topic) removeTopic(topic)
}, },
[assistant.topics, generating, removeTopic, setActiveTopic, t] [assistant.topics, removeTopic, setActiveTopic]
) )
const onMoveTopic = useCallback( const onMoveTopic = useCallback(
(topic: Topic, toAssistant: Assistant) => { async (topic: Topic, toAssistant: Assistant) => {
if (generating) { await modelGenerating()
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const index = findIndex(assistant.topics, (t) => t.id === topic.id) const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1]) setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
moveTopic(topic, toAssistant) moveTopic(topic, toAssistant)
}, },
[assistant.topics, generating, moveTopic, setActiveTopic, t] [assistant.topics, moveTopic, setActiveTopic]
) )
const onSwitchTopic = useCallback( const onSwitchTopic = useCallback(
(topic: Topic) => { async (topic: Topic) => {
if (generating) { await modelGenerating()
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
setActiveTopic(topic) setActiveTopic(topic)
}, },
[generating, setActiveTopic, t] [setActiveTopic]
) )
const onClearMessages = useCallback(() => { const onClearMessages = useCallback(() => {
@@ -186,7 +177,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
) )
return ( return (
<Container right={topicPosition === 'right'}> <Container right={topicPosition === 'right'} className="topics-tab">
<DragableList list={assistant.topics} onUpdate={updateTopics}> <DragableList list={assistant.topics} onUpdate={updateTopics}>
{(topic) => { {(topic) => {
const isActive = topic.id === activeTopic?.id const isActive = topic.id === activeTopic?.id
@@ -194,8 +185,8 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}> <Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem <TopicListItem
className={isActive ? 'active' : ''} className={isActive ? 'active' : ''}
style={{ borderRadius }} onClick={() => onSwitchTopic(topic)}
onClick={() => onSwitchTopic(topic)}> style={{ borderRadius }}>
<TopicName className="name">{topic.name.replace('`', '')}</TopicName> <TopicName className="name">{topic.name.replace('`', '')}</TopicName>
{showTopicTime && ( {showTopicTime && (
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime> <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
@@ -232,8 +223,9 @@ const Container = styled(Scrollbar)`
const TopicListItem = styled.div` const TopicListItem = styled.div`
padding: 7px 12px; padding: 7px 12px;
margin: 0 10px; margin-left: 10px;
border-radius: 16px; margin-right: 4px;
border-radius: var(--list-item-border-radius);
font-family: Ubuntu; font-family: Ubuntu;
font-size: 13px; font-size: 13px;
display: flex; display: flex;

View File

@@ -94,7 +94,7 @@ const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant,
}, [position, tab, topicPosition]) }, [position, tab, topicPosition])
return ( return (
<Container style={border}> <Container style={border} className="home-tabs">
{showTab && ( {showTab && (
<Segmented <Segmented
value={tab} value={tab}
@@ -125,7 +125,7 @@ const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant,
block block
/> />
)} )}
<TabContent> <TabContent className="home-tabs-content">
{tab === 'assistants' && ( {tab === 'assistants' && (
<Assistants <Assistants
activeAssistant={activeAssistant} activeAssistant={activeAssistant}

View File

@@ -1,9 +1,9 @@
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import VisionIcon from '@renderer/components/Icons/VisionIcon' import ModelTags from '@renderer/components/ModelTags'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import { isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { getProviderName } from '@renderer/services/ProviderService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { Button } from 'antd' import { Button } from 'antd'
import { FC } from 'react' import { FC } from 'react'
@@ -30,11 +30,17 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
} }
} }
const providerName = getProviderName(model?.provider)
return ( return (
<DropdownButton size="small" type="default" onClick={onSelectModel}> <DropdownButton size="small" type="default" onClick={onSelectModel}>
<ModelAvatar model={model} size={20} /> <ButtonContent>
<ModelName>{model ? model.name : t('button.select_model')}</ModelName> <ModelAvatar model={model} size={20} />
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />} <ModelName>
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
</ModelName>
<ModelTags model={model} showFree={false} />
</ButtonContent>
</DropdownButton> </DropdownButton>
) )
} }
@@ -49,8 +55,13 @@ const DropdownButton = styled(Button)`
border: 1px solid transparent; border: 1px solid transparent;
` `
const ButtonContent = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const ModelName = styled.span` const ModelName = styled.span`
margin-left: -2px;
font-weight: 500; font-weight: 500;
` `

View File

@@ -6,6 +6,7 @@ import {
GlobalOutlined, GlobalOutlined,
LinkOutlined, LinkOutlined,
PlusOutlined, PlusOutlined,
RedoOutlined,
SearchOutlined SearchOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
@@ -13,6 +14,7 @@ import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useKnowledge } from '@renderer/hooks/useKnowledge' import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { getProviderName } from '@renderer/services/ProviderService'
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types' import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
import { Alert, Button, Card, Divider, message, Tag, Typography, Upload } from 'antd' import { Alert, Button, Card, Divider, message, Tag, Typography, Upload } from 'antd'
import { FC } from 'react' import { FC } from 'react'
@@ -29,31 +31,7 @@ interface KnowledgeContentProps {
selectedBase: KnowledgeBase selectedBase: KnowledgeBase
} }
const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md', '.mdx'] const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md']
const FlexColumn = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const FlexAlignCenter = styled.div`
display: flex;
align-items: center;
gap: 16px;
`
const ClickableSpan = styled.span`
cursor: pointer;
`
const FileIcon = styled(FileTextOutlined)`
font-size: 16px;
`
const BottomSpacer = styled.div`
min-height: 20px;
`
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => { const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -66,6 +44,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
directoryItems, directoryItems,
addFiles, addFiles,
updateNoteContent, updateNoteContent,
refreshItem,
addUrl, addUrl,
addSitemap, addSitemap,
removeItem, removeItem,
@@ -74,11 +53,17 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
addDirectory addDirectory
} = useKnowledge(selectedBase.id || '') } = useKnowledge(selectedBase.id || '')
const providerName = getProviderName(base?.model.provider || '')
const disabled = !base?.version || !providerName
if (!base) { if (!base) {
return null return null
} }
const handleAddFile = () => { const handleAddFile = () => {
if (disabled) {
return
}
const input = document.createElement('input') const input = document.createElement('input')
input.type = 'file' input.type = 'file'
input.multiple = true input.multiple = true
@@ -91,6 +76,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
} }
const handleDrop = async (files: File[]) => { const handleDrop = async (files: File[]) => {
if (disabled) {
return
}
if (files) { if (files) {
const _files: FileType[] = files.map((file) => ({ const _files: FileType[] = files.map((file) => ({
id: file.name, id: file.name,
@@ -110,10 +99,14 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
} }
const handleAddUrl = async () => { const handleAddUrl = async () => {
if (disabled) {
return
}
const url = await PromptPopup.show({ const url = await PromptPopup.show({
title: t('knowledge_base.add_url'), title: t('knowledge.add_url'),
message: '', message: '',
inputPlaceholder: t('knowledge_base.url_placeholder'), inputPlaceholder: t('knowledge.url_placeholder'),
inputProps: { inputProps: {
maxLength: 1000, maxLength: 1000,
rows: 1 rows: 1
@@ -124,7 +117,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
try { try {
new URL(url) new URL(url)
if (urlItems.find((item) => item.content === url)) { if (urlItems.find((item) => item.content === url)) {
message.success(t('knowledge_base.url_added')) message.success(t('knowledge.url_added'))
return return
} }
addUrl(url) addUrl(url)
@@ -135,10 +128,14 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
} }
const handleAddSitemap = async () => { const handleAddSitemap = async () => {
if (disabled) {
return
}
const url = await PromptPopup.show({ const url = await PromptPopup.show({
title: t('knowledge_base.add_sitemap'), title: t('knowledge.add_sitemap'),
message: '', message: '',
inputPlaceholder: t('knowledge_base.sitemap_placeholder'), inputPlaceholder: t('knowledge.sitemap_placeholder'),
inputProps: { inputProps: {
maxLength: 1000, maxLength: 1000,
rows: 1 rows: 1
@@ -149,7 +146,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
try { try {
new URL(url) new URL(url)
if (sitemapItems.find((item) => item.content === url)) { if (sitemapItems.find((item) => item.content === url)) {
message.success(t('knowledge_base.sitemap_added')) message.success(t('knowledge.sitemap_added'))
return return
} }
addSitemap(url) addSitemap(url)
@@ -160,16 +157,28 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
} }
const handleAddNote = async () => { const handleAddNote = async () => {
if (disabled) {
return
}
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } }) const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
note && addNote(note) note && addNote(note)
} }
const handleEditNote = async (note: any) => { const handleEditNote = async (note: any) => {
if (disabled) {
return
}
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } }) const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
editedText && updateNoteContent(note.id, editedText) editedText && updateNoteContent(note.id, editedText)
} }
const handleAddDirectory = async () => { const handleAddDirectory = async () => {
if (disabled) {
return
}
const path = await window.api.file.selectFolder() const path = await window.api.file.selectFolder()
console.log('[KnowledgeContent] Selected directory:', path) console.log('[KnowledgeContent] Selected directory:', path)
path && addDirectory(path) path && addDirectory(path)
@@ -178,13 +187,16 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
return ( return (
<MainContent> <MainContent>
{!base?.version && ( {!base?.version && (
<Alert message={t('knowledge_base.not_support')} type="error" style={{ marginBottom: 20 }} showIcon /> <Alert message={t('knowledge.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
)}
{!providerName && (
<Alert message={t('knowledge.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
)} )}
<FileSection> <FileSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('files.title')}</Title> <Title level={5}>{t('files.title')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddFile}> <Button icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}>
{t('knowledge_base.add_file')} {t('knowledge.add_file')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<Dragger <Dragger
@@ -193,9 +205,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
multiple={true} multiple={true}
accept={fileTypes.join(',')} accept={fileTypes.join(',')}
style={{ marginTop: 10, background: 'transparent' }}> style={{ marginTop: 10, background: 'transparent' }}>
<p className="ant-upload-text">{t('knowledge_base.drag_file')}</p> <p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint"> <p className="ant-upload-hint">
{t('knowledge_base.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })} {t('knowledge.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })}
</p> </p>
</Dragger> </Dragger>
</FileSection> </FileSection>
@@ -211,7 +223,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ClickableSpan onClick={() => window.api.file.openPath(file.path)}>{file.origin_name}</ClickableSpan> <ClickableSpan onClick={() => window.api.file.openPath(file.path)}>{file.origin_name}</ClickableSpan>
</ItemInfo> </ItemInfo>
<FlexAlignCenter> <FlexAlignCenter>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} /> {item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} /> <Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter> </FlexAlignCenter>
</ItemContent> </ItemContent>
@@ -222,9 +237,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.directories')}</Title> <Title level={5}>{t('knowledge.directories')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddDirectory}> <Button icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}>
{t('knowledge_base.add_directory')} {t('knowledge.add_directory')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -238,7 +253,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</ClickableSpan> </ClickableSpan>
</ItemInfo> </ItemInfo>
<FlexAlignCenter> <FlexAlignCenter>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} /> {item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} /> <Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter> </FlexAlignCenter>
</ItemContent> </ItemContent>
@@ -249,9 +267,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.urls')}</Title> <Title level={5}>{t('knowledge.urls')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddUrl}> <Button icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}>
{t('knowledge_base.add_url')} {t('knowledge.add_url')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -265,7 +283,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</a> </a>
</ItemInfo> </ItemInfo>
<FlexAlignCenter> <FlexAlignCenter>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} /> {item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} /> <Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter> </FlexAlignCenter>
</ItemContent> </ItemContent>
@@ -276,9 +297,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.sitemaps')}</Title> <Title level={5}>{t('knowledge.sitemaps')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddSitemap}> <Button icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}>
{t('knowledge_base.add_sitemap')} {t('knowledge.add_sitemap')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -292,7 +313,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</a> </a>
</ItemInfo> </ItemInfo>
<FlexAlignCenter> <FlexAlignCenter>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} /> {item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} /> <Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter> </FlexAlignCenter>
</ItemContent> </ItemContent>
@@ -303,9 +327,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.notes')}</Title> <Title level={5}>{t('knowledge.notes')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddNote}> <Button icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}>
{t('knowledge_base.add_note')} {t('knowledge.add_note')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -317,7 +341,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</ItemInfo> </ItemInfo>
<FlexAlignCenter> <FlexAlignCenter>
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} /> <Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} /> <StatusIconWrapper>
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} /> <Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
</FlexAlignCenter> </FlexAlignCenter>
</ItemContent> </ItemContent>
@@ -329,15 +355,19 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<ModelInfo> <ModelInfo>
<label htmlFor="model-info">{t('knowledge_base.model_info')}</label> <label htmlFor="model-info">{t('knowledge.model_info')}</label>
<Tag color="blue">{base.model.name}</Tag> <Tag color="blue">{base.model.name}</Tag>
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag> <Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
<Tag color="purple">{base.model.provider}</Tag> {providerName && <Tag color="purple">{providerName}</Tag>}
</ModelInfo> </ModelInfo>
<IndexSection> <IndexSection>
<Button type="primary" onClick={() => KnowledgeSearchPopup.show({ base })} icon={<SearchOutlined />}> <Button
{t('knowledge_base.search')} type="primary"
onClick={() => KnowledgeSearchPopup.show({ base })}
icon={<SearchOutlined />}
disabled={disabled}>
{t('knowledge.search')}
</Button> </Button>
</IndexSection> </IndexSection>
@@ -440,4 +470,42 @@ const ModelInfo = styled.div`
} }
` `
const FlexColumn = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const FlexAlignCenter = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
const ClickableSpan = styled.span`
cursor: pointer;
`
const FileIcon = styled(FileTextOutlined)`
font-size: 16px;
`
const BottomSpacer = styled.div`
min-height: 20px;
`
const StatusIconWrapper = styled.div`
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
padding-top: 2px;
`
const RefreshIcon = styled(RedoOutlined)`
font-size: 15px !important;
color: var(--color-text-2);
`
export default KnowledgeContent export default KnowledgeContent

Some files were not shown because too many files have changed in this diff Show More