Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a046cf32ba | ||
|
|
66bc9cb3f9 | ||
|
|
247d1a1846 | ||
|
|
0e7fb2b19c | ||
|
|
8a94bb05ea | ||
|
|
bc454d4dec | ||
|
|
d388aeecfb | ||
|
|
3e33ee6cc5 | ||
|
|
1991df18d2 | ||
|
|
de3206b052 | ||
|
|
cb3ed42846 | ||
|
|
edbc8560cc | ||
|
|
56761d6f69 | ||
|
|
2b4cfe7cb1 | ||
|
|
6a5faa6610 | ||
|
|
84979a975c | ||
|
|
74740d7fcc | ||
|
|
dff04187be | ||
|
|
a0a13a4015 | ||
|
|
2ad6a1f24c | ||
|
|
cf7c0fc1fc | ||
|
|
4ecbf3edab | ||
|
|
83cc4ccec7 | ||
|
|
3998ad08de | ||
|
|
49a5bc7900 | ||
|
|
7633d70435 | ||
|
|
ad9fb9aa6d | ||
|
|
fc3d15fae8 | ||
|
|
c45fc2bbad | ||
|
|
270216f461 | ||
|
|
112e90c15c | ||
|
|
c579eff86e | ||
|
|
f9f5befc59 | ||
|
|
7271a86677 | ||
|
|
42ede42f62 | ||
|
|
ea7a42f736 | ||
|
|
d2836826e7 | ||
|
|
7d61af7170 | ||
|
|
3f4fa9b0ec | ||
|
|
1bdf6c7955 | ||
|
|
5d005cf5a7 | ||
|
|
1fbd727a7b | ||
|
|
c9813bb1e2 | ||
|
|
edac2004a0 | ||
|
|
a051f9fa44 | ||
|
|
a70e69caf9 | ||
|
|
4896db93fd | ||
|
|
2e7ecbc753 | ||
|
|
f68bd4d8d8 | ||
|
|
d0948e6f8a | ||
|
|
ac9017c031 | ||
|
|
de1d79abb8 | ||
|
|
ad577818dd | ||
|
|
bb50447a98 | ||
|
|
158f9bf1ad | ||
|
|
6a9bc103d7 | ||
|
|
529ec3612e | ||
|
|
d241c38c61 | ||
|
|
ee5ed8c565 | ||
|
|
dc73661678 | ||
|
|
ce973ce3a0 | ||
|
|
a0413158c8 | ||
|
|
6cb3b16451 | ||
|
|
08b0990cf9 | ||
|
|
10b9940edd | ||
|
|
4cbdd563e8 | ||
|
|
dba1f76db7 | ||
|
|
15fb605eb4 | ||
|
|
1bf147fa6a | ||
|
|
a782b2b4aa | ||
|
|
7f92cb59a6 | ||
|
|
6009ae84fb | ||
|
|
038aa2d5cc | ||
|
|
6384525e20 | ||
|
|
3fc7911c97 | ||
|
|
5f55d8c22c | ||
|
|
d9f7bcfc21 | ||
|
|
aa72794967 | ||
|
|
09e6756efe | ||
|
|
dde0400f0d | ||
|
|
1d3a01dd49 | ||
|
|
63cdc15bc2 | ||
|
|
b2818f8619 | ||
|
|
8ef9fb0216 | ||
|
|
63488e6fab | ||
|
|
6d9013f0a1 | ||
|
|
1a68587684 | ||
|
|
47c455b125 | ||
|
|
96124cf58e | ||
|
|
ef975add01 | ||
|
|
ed49066bab | ||
|
|
e7545c5a94 | ||
|
|
fc35df65b8 | ||
|
|
56ca81d245 | ||
|
|
6bc1f4b640 | ||
|
|
ccb216e76a | ||
|
|
60931b85ff | ||
|
|
dc1dbc7bb6 | ||
|
|
5d2efbd62b | ||
|
|
5337017648 | ||
|
|
c409256ae9 | ||
|
|
4ac608052c | ||
|
|
5e6aaabb23 | ||
|
|
8812daeeee | ||
|
|
13e3a8478c | ||
|
|
8687985ccb |
19
.github/workflows/release.yml
vendored
@@ -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 }}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 41 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 137 KiB |
BIN
build/logo.png
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 84 KiB |
47
build/nsis-installer.nsh
Normal 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
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -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
|
||||||
|
修复删除服务商导致的数据错误白屏问题
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: []
|
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js', 'chunk-AFE5XGNG.js', 'chunk-QIJABHCK.js']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
202
resources/cherry-studio/releases.html
Normal 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>
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/main/services/CacheService.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
63
src/main/services/GeminiService.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
src/preload/index.d.ts
vendored
@@ -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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
src/renderer/src/assets/images/apps/genspark.jpg
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/renderer/src/assets/images/apps/github-copilot.webp
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/renderer/src/assets/images/apps/grok.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/apps/hika.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/renderer/src/assets/images/apps/qwenlm.webp
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 84 KiB |
BIN
src/renderer/src/assets/images/providers/qwenlm.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
39
src/renderer/src/components/Icons/MinAppIcon.tsx
Normal 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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/renderer/src/components/ModelTags.tsx
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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 />}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/renderer/src/config/tools.ts
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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) => ({ '<': '<', '>': '>' })[char]!)
|
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[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>`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}, [])
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/renderer/src/hooks/useMinapps.ts
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
129
src/renderer/src/pages/files/ContentView.tsx
Normal 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)
|
||||||
@@ -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 {
|
||||||
|
|||||||
98
src/renderer/src/pages/files/GeminiFiles.tsx
Normal 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
|
||||||
@@ -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') }}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)' }} />
|
||||||
|
|||||||
155
src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx
Normal 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
|
||||||
26
src/renderer/src/pages/home/Inputbar/MentionModelsInput.tsx
Normal 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
|
||||||
@@ -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`
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/renderer/src/pages/home/Messages/NarrowLayout.tsx
Normal 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
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||