Compare commits
53 Commits
v0.9.11
...
feat/setti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c66f0e41a | ||
|
|
fd1629e004 | ||
|
|
790caae2ab | ||
|
|
7f7300e6dc | ||
|
|
4464992873 | ||
|
|
37d1c250d2 | ||
|
|
e9c51579a2 | ||
|
|
aec2952780 | ||
|
|
95a1bdac72 | ||
|
|
306cb04ef0 | ||
|
|
dc9444a9d4 | ||
|
|
ad9fefe902 | ||
|
|
e07d4838a9 | ||
|
|
30d070040c | ||
|
|
f335699958 | ||
|
|
b1bc576e3f | ||
|
|
a6f086e3be | ||
|
|
084da9ebab | ||
|
|
57aef23741 | ||
|
|
900b11bdf7 | ||
|
|
8aec8a60b3 | ||
|
|
a566b0e91a | ||
|
|
4d201059ad | ||
|
|
00d91ecf01 | ||
|
|
462ac39897 | ||
|
|
3fa1e8c842 | ||
|
|
d32a76c087 | ||
|
|
9e9fd37bda | ||
|
|
dd464db594 | ||
|
|
ccac5358f4 | ||
|
|
e72e324155 | ||
|
|
28c18b6651 | ||
|
|
3d432d810f | ||
|
|
21ad28ee62 | ||
|
|
f7db1289e4 | ||
|
|
f5c547cdb2 | ||
|
|
9160cee919 | ||
|
|
298bb8be29 | ||
|
|
b800c64fed | ||
|
|
504d7b88d4 | ||
|
|
713d6dba8f | ||
|
|
a6833d5994 | ||
|
|
d850fd315a | ||
|
|
c04fd62bec | ||
|
|
f86a274cd3 | ||
|
|
798a6e8c3e | ||
|
|
749353f460 | ||
|
|
c510f5dcce | ||
|
|
46b314303c | ||
|
|
b01aca9066 | ||
|
|
725f81c165 | ||
|
|
c0e25879e5 | ||
|
|
4c22c404ca |
@@ -15,3 +15,203 @@ index 50c3c4064af17bc4c7c46554d8f2419b3afceb0e..632c9b2e04d2e0e3bb09ef1cd8f29d25
|
||||
}
|
||||
static getInstance() {
|
||||
return RAGEmbedding.singleton;
|
||||
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
|
||||
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..87002be39e7305a02e2a607b0c0d95cbbc359f9d 100644
|
||||
--- a/src/loaders/local-path-loader.d.ts
|
||||
+++ b/src/loaders/local-path-loader.d.ts
|
||||
@@ -1,19 +1,29 @@
|
||||
-import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
+import { BaseLoader } from "@llm-tools/embedjs-interfaces";
|
||||
export declare class LocalPathLoader extends BaseLoader<{
|
||||
- type: 'LocalPathLoader';
|
||||
+ type: "LocalPathLoader";
|
||||
}> {
|
||||
- private readonly debug;
|
||||
- private readonly path;
|
||||
- constructor({ path }: {
|
||||
- path: string;
|
||||
- });
|
||||
- getUnfilteredChunks(): AsyncGenerator<{
|
||||
- metadata: {
|
||||
- type: "LocalPathLoader";
|
||||
- originalPath: string;
|
||||
- source: string;
|
||||
- };
|
||||
- pageContent: string;
|
||||
- }, void, unknown>;
|
||||
- private recursivelyAddPath;
|
||||
+ private readonly debug;
|
||||
+ private readonly path;
|
||||
+ constructor({
|
||||
+ path,
|
||||
+ chunkSize,
|
||||
+ chunkOverlap,
|
||||
+ }: {
|
||||
+ path: string;
|
||||
+ chunkSize?: number;
|
||||
+ chunkOverlap?: number;
|
||||
+ });
|
||||
+ getUnfilteredChunks(): AsyncGenerator<
|
||||
+ {
|
||||
+ metadata: {
|
||||
+ type: "LocalPathLoader";
|
||||
+ originalPath: string;
|
||||
+ source: string;
|
||||
+ };
|
||||
+ pageContent: string;
|
||||
+ },
|
||||
+ void,
|
||||
+ unknown
|
||||
+ >;
|
||||
+ private recursivelyAddPath;
|
||||
}
|
||||
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
|
||||
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..fd0fe1951c73da315b0c9bf4a8f33effbadb9f8f 100644
|
||||
--- a/src/loaders/local-path-loader.js
|
||||
+++ b/src/loaders/local-path-loader.js
|
||||
@@ -8,8 +8,8 @@ import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
export class LocalPathLoader extends BaseLoader {
|
||||
debug = createDebugMessages('embedjs:loader:LocalPathLoader');
|
||||
path;
|
||||
- constructor({ path }) {
|
||||
- super(`LocalPathLoader_${md5(path)}`, { path });
|
||||
+ constructor({ path, chunkSize, chunkOverlap}) {
|
||||
+ super(`LocalPathLoader_${md5(path)}`, { path }, chunkSize ?? 1000, chunkOverlap ?? 0);
|
||||
this.path = path;
|
||||
}
|
||||
async *getUnfilteredChunks() {
|
||||
@@ -36,10 +36,12 @@ export class LocalPathLoader extends BaseLoader {
|
||||
const extension = currentPath.split('.').pop().toLowerCase();
|
||||
if (extension === 'md' || extension === 'mdx')
|
||||
mime = 'text/markdown';
|
||||
+ if (extension === 'txt')
|
||||
+ mime = 'text/plain';
|
||||
this.debug(`File '${this.path}' mime type updated to 'text/markdown'`);
|
||||
}
|
||||
try {
|
||||
- const loader = await createLoaderFromMimeType(currentPath, mime);
|
||||
+ const loader = await createLoaderFromMimeType(currentPath, mime, this.chunkSize, this.chunkOverlap);
|
||||
for await (const result of await loader.getUnfilteredChunks()) {
|
||||
yield {
|
||||
pageContent: result.pageContent,
|
||||
diff --git a/src/util/mime.d.ts b/src/util/mime.d.ts
|
||||
index 57f56a1b8edc98366af9f84d671676c41c2f01ca..f53856fa9c78afbeee9e085c7ed0b3a131f8ee5a 100644
|
||||
--- a/src/util/mime.d.ts
|
||||
+++ b/src/util/mime.d.ts
|
||||
@@ -1,2 +1,7 @@
|
||||
-import { BaseLoader } from '@llm-tools/embedjs-interfaces';
|
||||
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
|
||||
+import { BaseLoader } from "@llm-tools/embedjs-interfaces";
|
||||
+export declare function createLoaderFromMimeType(
|
||||
+ loaderData: string,
|
||||
+ mimeType: string,
|
||||
+ chunkSize?: number,
|
||||
+ chunkOverlap?: number
|
||||
+): Promise<BaseLoader>;
|
||||
diff --git a/src/util/mime.js b/src/util/mime.js
|
||||
index 9af30bd5b8cf42985f547073a4c19756292c33a3..54ae20343131a533ab70236d3060b6accc8f6126 100644
|
||||
--- a/src/util/mime.js
|
||||
+++ b/src/util/mime.js
|
||||
@@ -1,7 +1,9 @@
|
||||
import mime from 'mime';
|
||||
import createDebugMessages from 'debug';
|
||||
import { TextLoader } from '../loaders/text-loader.js';
|
||||
-export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
+import fs from 'node:fs';
|
||||
+
|
||||
+export async function createLoaderFromMimeType(loaderData, mimeType, chunkSize, chunkOverlap) {
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')(`Incoming mime type '${mimeType}'`);
|
||||
switch (mimeType) {
|
||||
case 'application/msword':
|
||||
@@ -10,7 +12,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load docx files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported DocxLoader');
|
||||
- return new DocxLoader({ filePathOrUrl: loaderData });
|
||||
+ return new DocxLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/vnd.ms-excel':
|
||||
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
|
||||
@@ -18,21 +20,21 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load excel files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported ExcelLoader');
|
||||
- return new ExcelLoader({ filePathOrUrl: loaderData });
|
||||
+ return new ExcelLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/pdf': {
|
||||
const { PdfLoader } = await import('@llm-tools/embedjs-loader-pdf').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-pdf` needs to be installed to load PDF files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PdfLoader');
|
||||
- return new PdfLoader({ filePathOrUrl: loaderData });
|
||||
+ return new PdfLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
|
||||
const { PptLoader } = await import('@llm-tools/embedjs-loader-msoffice').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load pptx files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PptLoader');
|
||||
- return new PptLoader({ filePathOrUrl: loaderData });
|
||||
+ return new PptLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/plain': {
|
||||
const fineType = mime.getType(loaderData);
|
||||
@@ -42,24 +44,26 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
+ }
|
||||
+ else{
|
||||
+ const content = fs.readFileSync(loaderData, 'utf-8');
|
||||
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
|
||||
}
|
||||
- else
|
||||
- return new TextLoader({ text: loaderData });
|
||||
}
|
||||
case 'application/csv': {
|
||||
const { CsvLoader } = await import('@llm-tools/embedjs-loader-csv').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
|
||||
- return new CsvLoader({ filePathOrUrl: loaderData });
|
||||
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/html': {
|
||||
const { WebLoader } = await import('@llm-tools/embedjs-loader-web').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-web` needs to be installed to load web documents');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported WebLoader');
|
||||
- return new WebLoader({ urlOrContent: loaderData });
|
||||
+ return new WebLoader({ urlOrContent: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/xml': {
|
||||
const { SitemapLoader } = await import('@llm-tools/embedjs-loader-sitemap').catch(() => {
|
||||
@@ -67,14 +71,14 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported SitemapLoader');
|
||||
if (await SitemapLoader.test(loaderData)) {
|
||||
- return new SitemapLoader({ url: loaderData });
|
||||
+ return new SitemapLoader({ url: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
//This is not a Sitemap but is still XML
|
||||
const { XmlLoader } = await import('@llm-tools/embedjs-loader-xml').catch(() => {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-xml` needs to be installed to load XML documents');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported XmlLoader');
|
||||
- return new XmlLoader({ filePathOrUrl: loaderData });
|
||||
+ return new XmlLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case 'text/x-markdown':
|
||||
case 'text/markdown': {
|
||||
@@ -82,7 +86,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
|
||||
throw new Error('Package `@llm-tools/embedjs-loader-markdown` needs to be installed to load markdown files');
|
||||
});
|
||||
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported MarkdownLoader');
|
||||
- return new MarkdownLoader({ filePathOrUrl: loaderData });
|
||||
+ return new MarkdownLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
|
||||
}
|
||||
case undefined:
|
||||
throw new Error(`MIME type could not be detected. Please file an issue if you think this is a bug.`);
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||
|
||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)
|
||||
👏 Join [Telegram Group](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/C3xrXWjY) | [QQ Group](https://qm.qq.com/q/pQPuHMjUeQ)
|
||||
|
||||
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||
@@ -23,6 +23,8 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
|
||||
# 🌟 Key Features
|
||||
|
||||

|
||||
|
||||
1. **Diverse LLM Provider Support**:
|
||||
|
||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
|
||||
|
||||
👏 [Telegramグループ](https://t.me/CherryStudioAI)に参加しましょう
|
||||
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/C3xrXWjY) | [QQグループ](https://qm.qq.com/q/pQPuHMjUeQ)
|
||||
|
||||
❤️ Cherry Studioをお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||
@@ -23,6 +23,8 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
|
||||
|
||||
# 🌟 主な機能
|
||||
|
||||

|
||||
|
||||
1. **多様な LLM サービス対応**:
|
||||
|
||||
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
|
||||

|
||||
|
||||
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
|
||||
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)
|
||||
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/C3xrXWjY) | [QQ 群](https://qm.qq.com/q/pQPuHMjUeQ)
|
||||
|
||||
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
|
||||
|
||||
# 🌠 界面
|
||||
|
||||
@@ -23,6 +23,8 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
||||
|
||||
# 🌟 主要特性
|
||||
|
||||

|
||||
|
||||
1. **多样化 LLM 服务支持**:
|
||||
|
||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||
|
||||
@@ -80,9 +80,4 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
新增快捷助手弹窗
|
||||
翻译默认使用流输出
|
||||
小程序弹窗顶部增加固定按钮 @ousugo
|
||||
Gemini 安全设置更新 @magicdmer
|
||||
智能体页面性能优化 @magicdmer
|
||||
修复 WebDAV 不能自动备份问题
|
||||
错误修复
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.17",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
|
||||
@@ -87,7 +87,8 @@ export const textExts = [
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java' // Java 代码文件
|
||||
'.java', // Java 代码文件
|
||||
'.cs' // C# 代码文件
|
||||
]
|
||||
|
||||
export const ZOOM_SHORTCUTS = [
|
||||
|
||||
@@ -10,12 +10,14 @@ import AppUpdater from './services/AppUpdater'
|
||||
import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileService from './services/FileService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { getResourcePath } from './utils'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
@@ -31,6 +33,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
appPath: app.getAppPath(),
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
|
||||
appDataPath: app.getPath('userData'),
|
||||
resourcesPath: getResourcePath(),
|
||||
logsPath: log.transports.file.getFile().path
|
||||
}))
|
||||
|
||||
@@ -130,6 +133,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('file:copy', fileManager.copyFile)
|
||||
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
|
||||
|
||||
// fs
|
||||
ipcMain.handle('fs:read', FileService.readFile)
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
windowService.createMinappWindow({
|
||||
|
||||
7
src/main/services/FileService.ts
Normal file
7
src/main/services/FileService.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
export default class FileService {
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
|
||||
return fs.readFileSync(path, 'utf8')
|
||||
}
|
||||
}
|
||||
@@ -388,7 +388,7 @@ class FileStorage {
|
||||
}
|
||||
|
||||
// 如果URL中有文件名,使用URL中的文件名
|
||||
const urlFilename = url.split('/').pop()
|
||||
const urlFilename = url.split('/').pop()?.split('?')[0]
|
||||
if (urlFilename && urlFilename.includes('.')) {
|
||||
filename = urlFilename
|
||||
}
|
||||
|
||||
@@ -83,54 +83,103 @@ class KnowledgeService {
|
||||
|
||||
if (item.type === 'directory') {
|
||||
const directory = item.content as string
|
||||
return await ragApplication.addLoader(new LocalPathLoader({ path: directory }), forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new LocalPathLoader({ path: directory, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'url') {
|
||||
const content = item.content as string
|
||||
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, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type === 'sitemap') {
|
||||
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, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'note') {
|
||||
const content = item.content as string
|
||||
return await ragApplication.addLoader(new TextLoader({ text: content }), forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
const file = item.content as FileType
|
||||
|
||||
if (file.ext === '.pdf') {
|
||||
return await ragApplication.addLoader(new PdfLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new PdfLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (file.ext === '.docx') {
|
||||
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new DocxLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (file.ext === '.pptx') {
|
||||
return await ragApplication.addLoader(new PptLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new PptLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
if (file.ext === '.xlsx') {
|
||||
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new ExcelLoader({
|
||||
filePathOrUrl: file.path,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}) as any,
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(file.path, 'utf-8')
|
||||
|
||||
return await ragApplication.addLoader(new TextLoader({ text: fileContent }), forceReload)
|
||||
return await ragApplication.addLoader(
|
||||
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
|
||||
forceReload
|
||||
)
|
||||
}
|
||||
|
||||
return { entriesAdded: 0, uniqueId: '', loaderType: '' }
|
||||
|
||||
@@ -14,7 +14,6 @@ export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private miniWindow: BrowserWindow | null = null
|
||||
private isQuitting: boolean = false
|
||||
private wasFullScreen: boolean = false
|
||||
private selectionMenuWindow: BrowserWindow | null = null
|
||||
private lastSelectedText: string = ''
|
||||
@@ -199,30 +198,25 @@ export class WindowService {
|
||||
}
|
||||
|
||||
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
|
||||
// 监听应用退出事件
|
||||
app.on('before-quit', () => {
|
||||
this.isQuitting = true
|
||||
})
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
const notInTray = !configManager.getTray()
|
||||
// 如果已经触发退出,直接退出
|
||||
if (app.isQuitting) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// Windows and Linux
|
||||
// 没有开启托盘,且是Windows或Linux系统,直接退出
|
||||
const notInTray = !configManager.getTray()
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// Mac
|
||||
if (!this.isQuitting) {
|
||||
if (this.wasFullScreen) {
|
||||
// 如果是全屏状态,直接退出
|
||||
this.isQuitting = true
|
||||
app.quit()
|
||||
} else {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
}
|
||||
// 如果是全屏状态,直接退出
|
||||
if (this.wasFullScreen) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,3 +34,11 @@ export function debounce(func: (...args: any[]) => void, wait: number, immediate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dumpPersistState() {
|
||||
const persistState = JSON.parse(localStorage.getItem('persist:cherry-studio') || '{}')
|
||||
for (const key in persistState) {
|
||||
persistState[key] = JSON.parse(persistState[key])
|
||||
}
|
||||
return JSON.stringify(persistState)
|
||||
}
|
||||
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@@ -56,6 +56,9 @@ declare global {
|
||||
copy: (fileId: string, destPath: string) => Promise<void>
|
||||
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
|
||||
}
|
||||
fs: {
|
||||
read: (path: string) => Promise<string>
|
||||
}
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ const api = {
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath),
|
||||
binaryFile: (fileId: string) => ipcRenderer.invoke('file:binaryFile', fileId)
|
||||
},
|
||||
fs: {
|
||||
read: (path: string) => ipcRenderer.invoke('fs:read', path)
|
||||
},
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
@@ -37,7 +36,6 @@ function App(): JSX.Element {
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
|
||||
4
src/renderer/src/assets/images/apps/flowith.svg
Normal file
4
src/renderer/src/assets/images/apps/flowith.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="464" height="464" viewBox="0 0 464 464" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="464" height="464" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M243 127C235.268 127 229 133.268 229 141V322C229 329.732 235.268 336 243 336H283C290.732 336 297 329.732 297 322V141C297 133.268 290.732 127 283 127H243ZM167.562 128C163.762 128 160.317 129.518 157.805 131.978C157.787 131.995 157.759 131.977 157.767 131.954C157.775 131.93 157.743 131.913 157.727 131.933L157.311 132.486C156.679 133.171 156.115 133.92 155.629 134.722C154.303 136.486 153.139 138.365 152.152 140.338L88.8745 266.857L85.2894 274.899C85.2249 275.037 85.1626 275.177 85.1027 275.318L84.7141 276.189C84.7086 276.201 84.7223 276.213 84.7339 276.206C84.745 276.2 84.7583 276.211 84.7541 276.223C84.2654 277.639 84 279.16 84 280.742L84 322.399C84 330.067 90.2354 336.284 97.9271 336.284H139.708C147.4 336.284 153.635 330.067 153.635 322.399V266.857L153.636 252.97C153.636 222.295 178.577 197.428 209.344 197.428C217.035 197.428 223.271 191.211 223.271 183.542V141.886C223.271 134.217 217.035 128 209.344 128H167.562ZM304.5 301.57C304.5 282.398 320.088 266.856 339.318 266.856C358.547 266.856 374.135 282.398 374.135 301.57C374.135 320.742 358.547 336.284 339.318 336.284C320.088 336.284 304.5 320.742 304.5 301.57Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/renderer/src/assets/images/models/bge.webp
Normal file
BIN
src/renderer/src/assets/images/models/bge.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -234,6 +234,9 @@ body,
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
.message-thought-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.message-user {
|
||||
color: var(--chat-text-user);
|
||||
.markdown,
|
||||
@@ -246,6 +249,13 @@ body,
|
||||
background-color: var(--color-white-soft);
|
||||
}
|
||||
}
|
||||
.group-message-wrapper {
|
||||
background-color: var(--color-background);
|
||||
.message-content-container {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-background-mute);
|
||||
}
|
||||
}
|
||||
code {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import store from '@renderer/store'
|
||||
@@ -31,6 +32,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
useBridge()
|
||||
|
||||
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||
const canPinned = DEFAULT_MIN_APPS.some((i) => i.id === app?.id)
|
||||
|
||||
const onClose = async (_delay = 0.3) => {
|
||||
setOpen(false)
|
||||
@@ -63,9 +65,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
<Button onClick={onReload}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
|
||||
<PushpinOutlined style={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
{canPinned && (
|
||||
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
|
||||
<PushpinOutlined style={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
)}
|
||||
{canOpenExternalLink && (
|
||||
<Button onClick={onOpenLink}>
|
||||
<ExportOutlined />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import systemAgents from '@renderer/config/agents.json'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSystemAgents } from '@renderer/pages/agents'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Agent, Assistant } from '@renderer/types'
|
||||
@@ -28,6 +28,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const systemAgents = useSystemAgents()
|
||||
|
||||
const agents = useMemo(() => {
|
||||
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||
@@ -48,7 +49,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
return [newAgent, ...filtered]
|
||||
}
|
||||
return filtered
|
||||
}, [assistants, defaultAssistant, searchText, userAgents])
|
||||
}, [assistants, defaultAssistant, searchText, systemAgents, userAgents])
|
||||
|
||||
const onCreateAssistant = async (agent: Agent) => {
|
||||
let assistant: Assistant
|
||||
@@ -120,7 +121,11 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
key={agent.id}
|
||||
onClick={() => onCreateAssistant(agent)}
|
||||
className={agent.id === 'default' ? 'default' : ''}>
|
||||
<HStack alignItems="center" gap={5}>
|
||||
<HStack
|
||||
alignItems="center"
|
||||
gap={5}
|
||||
style={{ overflow: 'hidden', maxWidth: '100%' }}
|
||||
className="text-nowrap">
|
||||
{agent.emoji} {agent.name}
|
||||
</HStack>
|
||||
{agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
|
||||
@@ -149,6 +154,7 @@ const AgentItem = styled.div`
|
||||
user-select: none;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
&.default {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
|
||||
66
src/renderer/src/components/Popups/SettingsPopup.tsx
Normal file
66
src/renderer/src/components/Popups/SettingsPopup.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import SettingsPage, { SettingsTab } from '@renderer/pages/settings/SettingsPage'
|
||||
import { Modal } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import styled, { createGlobalStyle } from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
actionButton?: React.ReactNode
|
||||
activeTab?: SettingsTab
|
||||
}
|
||||
|
||||
const SettingsPopup: FC<Props> = (props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab | undefined>(props.activeTab)
|
||||
|
||||
const onOpen = () => {
|
||||
if (props.activeTab) {
|
||||
setActiveTab(props.activeTab)
|
||||
}
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={onOpen}>{props.actionButton}</div>
|
||||
<GlobalStyle />
|
||||
<StyledModal
|
||||
transitionName="ant-move-down"
|
||||
width="80vw"
|
||||
title={null}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
footer={null}>
|
||||
<SettingsPage activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
</StyledModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
.ant-modal-mask {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: transparent !important;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledModal = styled(Modal)`
|
||||
min-width: 900px;
|
||||
max-width: 1300px;
|
||||
padding-bottom: 0;
|
||||
|
||||
.ant-modal-content {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.ant-modal-close {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
`
|
||||
|
||||
export default SettingsPopup
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||
import { UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Tooltip } from 'antd'
|
||||
@@ -18,14 +18,13 @@ import styled from 'styled-components'
|
||||
import DragableList from '../DragableList'
|
||||
import MinAppIcon from '../Icons/MinAppIcon'
|
||||
import MinApp from '../MinApp'
|
||||
import SettingsPopup from '../Popups/SettingsPopup'
|
||||
import UserPopup from '../Popups/UserPopup'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const avatar = useAvatar()
|
||||
const { minappShow } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { windowStyle, sidebarIcons } = useSettings()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { pinned } = useMinapps()
|
||||
@@ -37,11 +36,6 @@ const Sidebar: FC = () => {
|
||||
|
||||
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
|
||||
|
||||
const to = async (path: string) => {
|
||||
await modelGenerating()
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
id="app-sidebar"
|
||||
@@ -73,13 +67,15 @@ const Sidebar: FC = () => {
|
||||
)}
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<SettingsPopup
|
||||
actionButton={
|
||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<Icon>
|
||||
<i className="iconfont icon-setting" />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</Menus>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
|
||||
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
|
||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
|
||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
|
||||
import FlowithAppLogo from '@renderer/assets/images/apps/flowith.svg?url'
|
||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png?url'
|
||||
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg?url'
|
||||
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?url'
|
||||
@@ -252,7 +253,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
id: 'grok',
|
||||
name: 'Grok',
|
||||
logo: GrokAppLogo,
|
||||
url: 'https://x.com/i/grok',
|
||||
url: 'https://grok.com',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
@@ -260,6 +261,13 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
name: 'QwenLM',
|
||||
logo: QwenlmAppLogo,
|
||||
url: 'https://qwenlm.ai/'
|
||||
},
|
||||
{
|
||||
id: 'flowith',
|
||||
name: 'Flowith',
|
||||
logo: FlowithAppLogo,
|
||||
url: 'https://www.flowith.io/',
|
||||
bodered: true
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import AisingaporeModelLogo from '@renderer/assets/images/models/aisingapore.png
|
||||
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
|
||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
|
||||
import BgeModelLogo from '@renderer/assets/images/models/bge.webp'
|
||||
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.webp'
|
||||
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.webp'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
|
||||
@@ -251,7 +252,8 @@ export function getModelLogo(modelId: string) {
|
||||
rakutenai: isLight ? RakutenaiModelLogo : RakutenaiModelLogoDark,
|
||||
ibm: isLight ? IbmModelLogo : IbmModelLogoDark,
|
||||
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
|
||||
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark
|
||||
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
|
||||
'bge-': BgeModelLogo
|
||||
}
|
||||
|
||||
for (const key in logoMap) {
|
||||
@@ -441,10 +443,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'DeepSeek Chat'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-coder',
|
||||
id: 'deepseek-reasoner',
|
||||
provider: 'deepseek',
|
||||
name: 'DeepSeek Coder',
|
||||
group: 'DeepSeek Coder'
|
||||
name: 'DeepSeek Reasoner',
|
||||
group: 'DeepSeek Reasoner'
|
||||
}
|
||||
],
|
||||
together: [
|
||||
@@ -758,6 +760,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
provider: 'minimax',
|
||||
name: 'abab5.5s',
|
||||
group: 'abab5'
|
||||
},
|
||||
{
|
||||
id: 'minimax-text-01',
|
||||
provider: 'minimax',
|
||||
name: 'minimax-01',
|
||||
group: 'minimax-01'
|
||||
}
|
||||
],
|
||||
hyperbolic: [
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isLocalAi } from '@renderer/config/env'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useEffect } from 'react'
|
||||
@@ -71,6 +71,7 @@ export function useAppInit() {
|
||||
// set files path
|
||||
window.api.getAppInfo().then((info) => {
|
||||
dispatch(setFilesPath(info.filesPath))
|
||||
dispatch(setResourcesPath(info.resourcesPath))
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
@@ -7,9 +8,9 @@ export const useMinapps = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
minapps: enabled,
|
||||
disabled,
|
||||
pinned,
|
||||
minapps: enabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
disabled: disabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
pinned: pinned.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
updateMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setMinApps(minapps))
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { orderBy } from 'lodash'
|
||||
import { useCallback } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
@@ -58,7 +59,7 @@ export const useShortcut = (
|
||||
|
||||
export function useShortcuts() {
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
return { shortcuts }
|
||||
return { shortcuts: orderBy(shortcuts, 'system', 'desc') }
|
||||
}
|
||||
|
||||
export function useShortcutDisplay(key: string) {
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||
"default.name": "⭐️ Default Assistant",
|
||||
"default.topic.name": "Default Topic",
|
||||
"input.clear": "Clear",
|
||||
"input.clear": "Clear {{Command}}",
|
||||
"input.clear.content": "Do you want to clear all messages of the current topic?",
|
||||
"input.clear.title": "Clear all messages?",
|
||||
"input.collapse": "Collapse",
|
||||
"input.context_count.tip": "Context Count",
|
||||
"input.estimated_tokens.tip": "Estimated tokens",
|
||||
"input.expand": "Expand",
|
||||
"input.new.context": "Clear Context",
|
||||
"input.new.context": "Clear Context {{Command}}",
|
||||
"input.new_topic": "New Topic {{Command}}",
|
||||
"input.pause": "Pause",
|
||||
"input.placeholder": "Type your message here...",
|
||||
@@ -86,6 +86,7 @@
|
||||
"message.new.branch.created": "New Branch Created",
|
||||
"message.regenerate.model": "Switch Model",
|
||||
"message.new.context": "New Context",
|
||||
"message.useful": "Helpful",
|
||||
"save": "Save",
|
||||
"settings.code_collapsible": "Code block collapsible",
|
||||
"settings.context_count": "Context",
|
||||
@@ -113,7 +114,9 @@
|
||||
"topics.move_to": "Move to",
|
||||
"topics.title": "Topics",
|
||||
"translate": "Translate",
|
||||
"resend": "Resend"
|
||||
"resend": "Resend",
|
||||
"thinking": "Thinking",
|
||||
"deeply_thought": "Deeply thought ({{secounds}} seconds)"
|
||||
},
|
||||
"common": {
|
||||
"and": "and",
|
||||
@@ -242,6 +245,7 @@
|
||||
"error.enter.api.key": "Please enter your API key first",
|
||||
"error.enter.model": "Please select a model first",
|
||||
"error.enter.name": "Please enter the name of the knowledge base",
|
||||
"error.chunk_overlap_too_large": "Chunk overlap cannot be greater than chunk size",
|
||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||
"message.code_style": "Code style",
|
||||
@@ -250,6 +254,10 @@
|
||||
"message.style": "Message style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain",
|
||||
"message.multi_model_style": "Multi-model answer style",
|
||||
"message.multi_model_style.horizontal": "Horizontal",
|
||||
"message.multi_model_style.vertical": "Vertical",
|
||||
"message.multi_model_style.fold": "Fold",
|
||||
"reset.confirm.content": "Are you sure you want to clear all data?",
|
||||
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
||||
"reset.double.confirm.title": "DATA LOST !!!",
|
||||
@@ -262,7 +270,9 @@
|
||||
"upgrade.success.title": "Upgrade successfully",
|
||||
"regenerate.confirm": "Regenerating will replace current message",
|
||||
"copy.success": "Copied!",
|
||||
"error.get_embedding_dimensions": "Failed to get embedding dimensions"
|
||||
"error.get_embedding_dimensions": "Failed to get embedding dimensions",
|
||||
"group.delete.title": "Delete Group Message",
|
||||
"group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "MinApp",
|
||||
@@ -375,7 +385,10 @@
|
||||
"webdav.path": "WebDAV Path",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "Auto Backup",
|
||||
"webdav.minute": "Minute",
|
||||
"webdav.minutes": "Minutes",
|
||||
"webdav.hour": "Hour",
|
||||
"webdav.hours": "Hours",
|
||||
"webdav.restore.button": "Restore from WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV User",
|
||||
@@ -526,7 +539,9 @@
|
||||
"toggle_show_topics": "Toggle Topics",
|
||||
"copy_last_message": "Copy Last Message",
|
||||
"search_message": "Search Message",
|
||||
"mini_window": "Quick Assistant"
|
||||
"mini_window": "Quick Assistant",
|
||||
"clear_topic": "Clear Messages",
|
||||
"toggle_new_context": "Clear Context"
|
||||
},
|
||||
"theme.auto": "Auto",
|
||||
"theme.dark": "Dark",
|
||||
@@ -611,7 +626,10 @@
|
||||
"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",
|
||||
"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",
|
||||
"chunk_size": "Chunk Size",
|
||||
"chunk_overlap": "Chunk Overlap",
|
||||
"not_set": "Not Set"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "Pinned",
|
||||
@@ -665,7 +683,8 @@
|
||||
"footer": {
|
||||
"esc": "Press ESC {{action}}",
|
||||
"esc_close": "close the window",
|
||||
"esc_back": "back"
|
||||
"esc_back": "back",
|
||||
"copy_last_message": "Press C to copy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
|
||||
"default.name": "⭐️ デフォルトアシスタント",
|
||||
"default.topic.name": "デフォルトトピック",
|
||||
"input.clear": "クリア",
|
||||
"input.clear": "クリア {{Command}}",
|
||||
"input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?",
|
||||
"input.clear.title": "すべてのメッセージをクリアしますか?",
|
||||
"input.collapse": "折りたたむ",
|
||||
"input.context_count.tip": "コンテキスト数",
|
||||
"input.estimated_tokens.tip": "推定トークン数",
|
||||
"input.expand": "展開",
|
||||
"input.new.context": "コンテキストをクリア",
|
||||
"input.new.context": "コンテキストをクリア {{Command}}",
|
||||
"input.new_topic": "新しいトピック {{Command}}",
|
||||
"input.pause": "一時停止",
|
||||
"input.placeholder": "ここにメッセージを入力...",
|
||||
@@ -86,6 +86,7 @@
|
||||
"message.new.branch.created": "新しいブランチが作成されました",
|
||||
"message.regenerate.model": "モデルを切り替え",
|
||||
"message.new.context": "新しいコンテキスト",
|
||||
"message.useful": "役立つ",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "コードブロックを折りたたむ",
|
||||
"settings.context_count": "コンテキスト",
|
||||
@@ -113,7 +114,9 @@
|
||||
"topics.move_to": "移動先",
|
||||
"topics.title": "トピック",
|
||||
"translate": "翻訳",
|
||||
"resend": "再送信"
|
||||
"resend": "再送信",
|
||||
"thinking": "思考中...",
|
||||
"deeply_thought": "深く考えています({{secounds}} 秒)"
|
||||
},
|
||||
"common": {
|
||||
"and": "と",
|
||||
@@ -241,6 +244,7 @@
|
||||
"error.enter.api.host": "APIホストを入力してください",
|
||||
"error.enter.api.key": "APIキーを入力してください",
|
||||
"error.enter.model": "モデルを選択してください",
|
||||
"error.chunk_overlap_too_large": "チャンクの重なりは、チャンクサイズを超えることはできません",
|
||||
"error.invalid.proxy.url": "無効なプロキシURL",
|
||||
"error.invalid.webdav": "無効なWebDAV設定",
|
||||
"message.code_style": "コードスタイル",
|
||||
@@ -249,6 +253,10 @@
|
||||
"message.style": "メッセージスタイル",
|
||||
"message.style.bubble": "バブル",
|
||||
"message.style.plain": "プレーン",
|
||||
"message.multi_model_style": "複数モデル回答スタイル",
|
||||
"message.multi_model_style.horizontal": "水平",
|
||||
"message.multi_model_style.vertical": "垂直",
|
||||
"message.multi_model_style.fold": "折りたたむ",
|
||||
"reset.confirm.content": "すべてのデータをリセットしてもよろしいですか?",
|
||||
"reset.double.confirm.content": "すべてのデータが失われます。続行しますか?",
|
||||
"reset.double.confirm.title": "データが失われます!!!",
|
||||
@@ -260,7 +268,10 @@
|
||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||
"upgrade.success.title": "アップグレードに成功しました",
|
||||
"regenerate.confirm": "再生成すると現在のメッセージが置き換えられます",
|
||||
"copy.success": "コピーしました!"
|
||||
"copy.success": "コピーしました!",
|
||||
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
|
||||
"group.delete.title": "分組メッセージを削除",
|
||||
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "ミニアプリ",
|
||||
@@ -374,6 +385,7 @@
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "自動バックアップ",
|
||||
"webdav.minutes": "分",
|
||||
"webdav.hours": "時間",
|
||||
"webdav.restore.button": "WebDAVから復元",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAVユーザー",
|
||||
@@ -511,7 +523,9 @@
|
||||
"toggle_show_topics": "トピックの表示を切り替え",
|
||||
"copy_last_message": "最後のメッセージをコピー",
|
||||
"search_message": "メッセージを検索",
|
||||
"mini_window": "クイックアシスタント"
|
||||
"mini_window": "クイックアシスタント",
|
||||
"clear_topic": "メッセージを消去",
|
||||
"toggle_new_context": "コンテキストをクリア"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "ダークテーマ",
|
||||
@@ -596,7 +610,10 @@
|
||||
"model_info": "モデル情報",
|
||||
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
|
||||
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
|
||||
"source": "ソース"
|
||||
"source": "ソース",
|
||||
"chunk_size": "チャンクサイズ",
|
||||
"chunk_overlap": "チャンクの重なり",
|
||||
"not_set": "未設定"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "固定済み",
|
||||
@@ -650,7 +667,8 @@
|
||||
"footer": {
|
||||
"esc": "ESC キーを押して{{action}}",
|
||||
"esc_close": "ウィンドウを閉じる",
|
||||
"esc_back": "戻る"
|
||||
"esc_back": "戻る",
|
||||
"copy_last_message": "C キーを押してコピー"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
|
||||
"default.name": "⭐️ Ассистент по умолчанию",
|
||||
"default.topic.name": "Топик по умолчанию",
|
||||
"input.clear": "Очистить",
|
||||
"input.clear": "Очистить {{Command}}",
|
||||
"input.clear.content": "Хотите очистить все сообщения текущего топика?",
|
||||
"input.clear.title": "Очистить все сообщения?",
|
||||
"input.collapse": "Свернуть",
|
||||
"input.context_count.tip": "Количество контекстов",
|
||||
"input.estimated_tokens.tip": "Затраты токенов",
|
||||
"input.expand": "Развернуть",
|
||||
"input.new.context": "Очистить контекст",
|
||||
"input.new.context": "Очистить контекст {{Command}}",
|
||||
"input.new_topic": "Новый топик {{Command}}",
|
||||
"input.pause": "Остановить",
|
||||
"input.placeholder": "Введите ваше сообщение здесь...",
|
||||
@@ -86,6 +86,7 @@
|
||||
"message.new.branch.created": "Новая ветка создана",
|
||||
"message.regenerate.model": "Переключить модель",
|
||||
"message.new.context": "Новый контекст",
|
||||
"message.useful": "Полезно",
|
||||
"save": "Сохранить",
|
||||
"settings.code_collapsible": "Блок кода свернут",
|
||||
"settings.context_count": "Контекст",
|
||||
@@ -113,7 +114,9 @@
|
||||
"topics.move_to": "Переместить в",
|
||||
"topics.title": "Топики",
|
||||
"translate": "Перевести",
|
||||
"resend": "Переотправить"
|
||||
"resend": "Переотправить",
|
||||
"thinking": "Мыслим",
|
||||
"deeply_thought": "Мыслим ({{secounds}} секунд)"
|
||||
},
|
||||
"common": {
|
||||
"and": "и",
|
||||
@@ -242,6 +245,7 @@
|
||||
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
|
||||
"error.enter.model": "Пожалуйста, выберите модель",
|
||||
"error.enter.name": "Пожалуйста, введите название базы знаний",
|
||||
"error.chunk_overlap_too_large": "Перекрытие фрагментов не может быть больше размера фрагмента.",
|
||||
"error.invalid.proxy.url": "Неверный URL прокси",
|
||||
"error.invalid.webdav": "Неверные настройки WebDAV",
|
||||
"message.code_style": "Стиль кода",
|
||||
@@ -250,6 +254,10 @@
|
||||
"message.style": "Стиль сообщения",
|
||||
"message.style.bubble": "Пузырь",
|
||||
"message.style.plain": "Простой",
|
||||
"message.multi_model_style": "Стиль ответов от нескольких моделей",
|
||||
"message.multi_model_style.horizontal": "Горизонтальный",
|
||||
"message.multi_model_style.vertical": "Вертикальный",
|
||||
"message.multi_model_style.fold": "Свернуть",
|
||||
"reset.confirm.content": "Вы уверены, что хотите очистить все данные?",
|
||||
"reset.double.confirm.content": "Все данные будут утеряны, хотите продолжить?",
|
||||
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
|
||||
@@ -262,7 +270,9 @@
|
||||
"upgrade.success.title": "Обновление успешно",
|
||||
"regenerate.confirm": "Перегенерация заменит текущее сообщение",
|
||||
"copy.success": "Скопировано!",
|
||||
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
|
||||
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
|
||||
"group.delete.title": "Удалить группу сообщений",
|
||||
"group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "Встроенные приложения",
|
||||
@@ -376,6 +386,7 @@
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "Автоматическое резервное копирование",
|
||||
"webdav.minutes": "минут",
|
||||
"webdav.hours": "часов",
|
||||
"webdav.restore.button": "Восстановление с WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "Пользователь WebDAV",
|
||||
@@ -525,7 +536,9 @@
|
||||
"toggle_show_topics": "Переключить отображение топиков",
|
||||
"copy_last_message": "Копировать последнее сообщение",
|
||||
"search_message": "Поиск сообщения",
|
||||
"mini_window": "Быстрый помощник"
|
||||
"mini_window": "Быстрый помощник",
|
||||
"clear_topic": "Очистить все сообщения",
|
||||
"toggle_new_context": "Очистить контекст"
|
||||
},
|
||||
"theme.auto": "Автоматически",
|
||||
"theme.dark": "Темная",
|
||||
@@ -610,7 +623,10 @@
|
||||
"model_info": "Модель информации",
|
||||
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
|
||||
"no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
|
||||
"source": "Источник"
|
||||
"source": "Источник",
|
||||
"chunk_size": "Размер фрагмента",
|
||||
"chunk_overlap": "Перекрытие фрагмента",
|
||||
"not_set": "Не установлено"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "Закреплено",
|
||||
@@ -664,7 +680,8 @@
|
||||
"footer": {
|
||||
"esc": "Нажмите ESC {{action}}",
|
||||
"esc_close": "закрытия окна",
|
||||
"esc_back": "возвращения"
|
||||
"esc_back": "возвращения",
|
||||
"copy_last_message": "Нажмите C для копирования"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||
"default.name": "⭐️ 默认助手",
|
||||
"default.topic.name": "默认话题",
|
||||
"input.clear": "清空消息",
|
||||
"input.clear": "清空消息 {{Command}}",
|
||||
"input.clear.content": "确定要清除当前会话所有消息吗?",
|
||||
"input.clear.title": "清空消息",
|
||||
"input.collapse": "收起",
|
||||
"input.context_count.tip": "上下文数",
|
||||
"input.estimated_tokens.tip": "预估 token 数",
|
||||
"input.expand": "展开",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.new.context": "清除上下文 {{Command}}",
|
||||
"input.new_topic": "新话题 {{Command}}",
|
||||
"input.pause": "暂停",
|
||||
"input.placeholder": "在这里输入消息...",
|
||||
@@ -86,6 +86,7 @@
|
||||
"message.new.branch.created": "新分支已创建",
|
||||
"message.regenerate.model": "切换模型",
|
||||
"message.new.context": "清除上下文",
|
||||
"message.useful": "有用",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "代码块可折叠",
|
||||
"settings.context_count": "上下文数",
|
||||
@@ -113,7 +114,9 @@
|
||||
"topics.move_to": "移动到",
|
||||
"topics.title": "话题",
|
||||
"translate": "翻译",
|
||||
"resend": "重新发送"
|
||||
"resend": "重新发送",
|
||||
"thinking": "思考中",
|
||||
"deeply_thought": "已深度思考(用时 {{secounds}} 秒)"
|
||||
},
|
||||
"common": {
|
||||
"and": "和",
|
||||
@@ -243,6 +246,7 @@
|
||||
"error.enter.api.key": "请输入您的 API 密钥",
|
||||
"error.enter.model": "请选择一个模型",
|
||||
"error.enter.name": "请输入知识库名称",
|
||||
"error.chunk_overlap_too_large": "分段重叠不能大于分段大小",
|
||||
"error.invalid.proxy.url": "无效的代理地址",
|
||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||
"message.code_style": "代码风格",
|
||||
@@ -251,6 +255,10 @@
|
||||
"message.style": "消息样式",
|
||||
"message.style.bubble": "气泡",
|
||||
"message.style.plain": "简洁",
|
||||
"message.multi_model_style": "多模型回答样式",
|
||||
"message.multi_model_style.horizontal": "水平",
|
||||
"message.multi_model_style.vertical": "垂直",
|
||||
"message.multi_model_style.fold": "折叠",
|
||||
"reset.confirm.content": "确定要重置所有数据吗?",
|
||||
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
||||
"reset.double.confirm.title": "数据丢失!!!",
|
||||
@@ -263,7 +271,9 @@
|
||||
"upgrade.success.title": "升级成功",
|
||||
"regenerate.confirm": "重新生成会覆盖当前消息",
|
||||
"copy.success": "复制成功",
|
||||
"error.get_embedding_dimensions": "获取嵌入维度失败"
|
||||
"error.get_embedding_dimensions": "获取嵌入维度失败",
|
||||
"group.delete.title": "删除分组消息",
|
||||
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序",
|
||||
@@ -377,6 +387,7 @@
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "自动备份",
|
||||
"webdav.minutes": "分钟",
|
||||
"webdav.hours": "小时",
|
||||
"webdav.restore.button": "从 WebDAV 恢复",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 用户名",
|
||||
@@ -514,7 +525,9 @@
|
||||
"toggle_show_topics": "切换话题显示",
|
||||
"copy_last_message": "复制上一条消息",
|
||||
"search_message": "搜索消息",
|
||||
"mini_window": "快捷助手"
|
||||
"mini_window": "快捷助手",
|
||||
"clear_topic": "清空消息",
|
||||
"toggle_new_context": "清除上下文"
|
||||
},
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.dark": "深色主题",
|
||||
@@ -599,7 +612,10 @@
|
||||
"model_info": "模型信息",
|
||||
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
|
||||
"no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
|
||||
"source": "来源"
|
||||
"source": "来源",
|
||||
"chunk_size": "分段大小",
|
||||
"chunk_overlap": "重叠大小",
|
||||
"not_set": "未设置"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "已固定",
|
||||
@@ -653,7 +669,8 @@
|
||||
"footer": {
|
||||
"esc": "按 ESC {{action}}",
|
||||
"esc_close": "关闭窗口",
|
||||
"esc_back": "返回"
|
||||
"esc_back": "返回",
|
||||
"copy_last_message": "按 C 键复制"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
||||
"default.name": "⭐️ 預設助手",
|
||||
"default.topic.name": "預設話題",
|
||||
"input.clear": "清除",
|
||||
"input.clear": "清除 {{Command}}",
|
||||
"input.clear.content": "您想要清除當前話題的所有訊息嗎?",
|
||||
"input.clear.title": "清除所有訊息?",
|
||||
"input.collapse": "收起",
|
||||
"input.context_count.tip": "上下文數量",
|
||||
"input.estimated_tokens.tip": "預估 Token 數",
|
||||
"input.expand": "展開",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.new.context": "清除上下文 {{Command}}",
|
||||
"input.new_topic": "新話題 {{Command}}",
|
||||
"input.pause": "暫停",
|
||||
"input.placeholder": "在此輸入您的訊息...",
|
||||
@@ -86,6 +86,7 @@
|
||||
"message.new.branch.created": "新分支已建立",
|
||||
"message.regenerate.model": "切換模型",
|
||||
"message.new.context": "新上下文",
|
||||
"message.useful": "有用",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "代码块可折叠",
|
||||
"settings.context_count": "上下文",
|
||||
@@ -113,7 +114,9 @@
|
||||
"topics.move_to": "移動到",
|
||||
"topics.title": "話題",
|
||||
"translate": "翻譯",
|
||||
"resend": "重新發送"
|
||||
"resend": "重新發送",
|
||||
"thinking": "思考中",
|
||||
"deeply_thought": "已深度思考(用時 {{secounds}} 秒)"
|
||||
},
|
||||
"common": {
|
||||
"and": "與",
|
||||
@@ -242,6 +245,7 @@
|
||||
"error.enter.api.key": "請先輸入您的 API 密鑰",
|
||||
"error.enter.model": "請先選擇一個模型",
|
||||
"error.enter.name": "請先輸入知識庫名稱",
|
||||
"error.chunk_overlap_too_large": "分段重疊不能大於分段大小",
|
||||
"error.invalid.proxy.url": "無效的代理 URL",
|
||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||
"message.code_style": "程式碼風格",
|
||||
@@ -250,6 +254,10 @@
|
||||
"message.style": "消息樣式",
|
||||
"message.style.bubble": "氣泡",
|
||||
"message.style.plain": "簡潔",
|
||||
"message.multi_model_style": "多模型回答樣式",
|
||||
"message.multi_model_style.horizontal": "水平",
|
||||
"message.multi_model_style.vertical": "垂直",
|
||||
"message.multi_model_style.fold": "折疊",
|
||||
"reset.confirm.content": "確定要清除所有資料嗎?",
|
||||
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
|
||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||
@@ -262,7 +270,9 @@
|
||||
"upgrade.success.title": "升級成功",
|
||||
"regenerate.confirm": "重新生成會覆蓋當前訊息",
|
||||
"copy.success": "複製成功",
|
||||
"error.get_embedding_dimensions": "獲取嵌入維度失敗"
|
||||
"error.get_embedding_dimensions": "獲取嵌入維度失敗",
|
||||
"group.delete.title": "刪除分組消息",
|
||||
"group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序",
|
||||
@@ -376,6 +386,7 @@
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "自動備份",
|
||||
"webdav.minutes": "分鐘",
|
||||
"webdav.hours": "小時",
|
||||
"webdav.restore.button": "從 WebDAV 恢復",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 使用者名稱",
|
||||
@@ -513,7 +524,9 @@
|
||||
"toggle_show_topics": "切換話題顯示",
|
||||
"copy_last_message": "複製上一条消息",
|
||||
"search_message": "搜索消息",
|
||||
"mini_window": "快捷助手"
|
||||
"mini_window": "快捷助手",
|
||||
"clear_topic": "清除所有訊息",
|
||||
"toggle_new_context": "清除上下文"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "深色主題",
|
||||
@@ -598,7 +611,10 @@
|
||||
"model_info": "模型信息",
|
||||
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
|
||||
"no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
|
||||
"source": "來源"
|
||||
"source": "來源",
|
||||
"chunk_size": "分段大小",
|
||||
"chunk_overlap": "重疊大小",
|
||||
"not_set": "未設置"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "已固定",
|
||||
@@ -652,7 +668,8 @@
|
||||
"footer": {
|
||||
"esc": "按 ESC {{action}}",
|
||||
"esc_close": "關閉窗口",
|
||||
"esc_back": "返回"
|
||||
"esc_back": "返回",
|
||||
"copy_last_message": "按 C 鍵複製"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import SystemAgents from '@renderer/config/agents.json'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
@@ -12,35 +11,26 @@ import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { getAgentsFromSystemAgents, useSystemAgents } from '.'
|
||||
import { groupTranslations } from './agentGroupTranslations'
|
||||
import AgentCard from './components/AgentCard'
|
||||
import MyAgents from './components/MyAgents'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const getAgentsFromSystemAgents = () => {
|
||||
const agents: Agent[] = []
|
||||
for (let i = 0; i < SystemAgents.length; i++) {
|
||||
for (let j = 0; j < SystemAgents[i].group.length; j++) {
|
||||
const agent = { ...SystemAgents[i], group: SystemAgents[i].group[j], topics: [], type: 'agent' } as Agent
|
||||
agents.push(agent)
|
||||
}
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
let _agentGroups: Record<string, Agent[]> = {}
|
||||
|
||||
const AgentsPage: FC = () => {
|
||||
const [search, setSearch] = useState('')
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const systemAgents = useSystemAgents()
|
||||
|
||||
const agentGroups = useMemo(() => {
|
||||
if (Object.keys(_agentGroups).length === 0) {
|
||||
_agentGroups = groupBy(getAgentsFromSystemAgents(), 'group')
|
||||
_agentGroups = groupBy(getAgentsFromSystemAgents(systemAgents), 'group')
|
||||
}
|
||||
return _agentGroups
|
||||
}, [])
|
||||
}, [systemAgents])
|
||||
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
@@ -102,7 +92,7 @@ const AgentsPage: FC = () => {
|
||||
[t]
|
||||
)
|
||||
|
||||
const getAgentFromSystemAgent = (agent: (typeof SystemAgents)[number]) => {
|
||||
const getAgentFromSystemAgent = (agent: (typeof systemAgents)[number]) => {
|
||||
return {
|
||||
...omit(agent, 'group'),
|
||||
name: agent.name,
|
||||
@@ -292,13 +282,14 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
|
||||
user-select: none;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
outline: none !important;
|
||||
.ant-tabs-tab-btn {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
outline: none !important;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-text) !important;
|
||||
|
||||
33
src/renderer/src/pages/agents/index.ts
Normal file
33
src/renderer/src/pages/agents/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
let _agents: Agent[] = []
|
||||
|
||||
export const getAgentsFromSystemAgents = (systemAgents: any) => {
|
||||
const agents: Agent[] = []
|
||||
for (let i = 0; i < systemAgents.length; i++) {
|
||||
for (let j = 0; j < systemAgents[i].group.length; j++) {
|
||||
const agent = { ...systemAgents[i], group: systemAgents[i].group[j], topics: [], type: 'agent' } as Agent
|
||||
agents.push(agent)
|
||||
}
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
export function useSystemAgents() {
|
||||
const [agents, setAgents] = useState<Agent[]>(_agents)
|
||||
const { resourcesPath } = useRuntime()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
if (_agents.length > 0) return
|
||||
const agents = await window.api.fs.read(resourcesPath + '/data/agents.json')
|
||||
_agents = JSON.parse(agents) as Agent[]
|
||||
setAgents(_agents)
|
||||
})
|
||||
}, [resourcesPath])
|
||||
|
||||
return agents
|
||||
}
|
||||
@@ -97,6 +97,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
[estimateTextTokens, showInputEstimatedTokens, text]
|
||||
)
|
||||
const newTopicShortcut = useShortcutDisplay('new_topic')
|
||||
const newContextShortcut = useShortcutDisplay('toggle_new_context')
|
||||
const cleanTopicShortcut = useShortcutDisplay('clear_topic')
|
||||
const inputEmpty = isEmpty(text.trim()) && files.length === 0
|
||||
|
||||
_text = text
|
||||
@@ -137,7 +139,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
|
||||
setText('')
|
||||
setFiles([])
|
||||
setMentionModels([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
@@ -188,7 +189,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
return setExpend(false)
|
||||
return onToggleExpended()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,25 +282,31 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
|
||||
const onPaste = useCallback(
|
||||
async (event: ClipboardEvent) => {
|
||||
for (const file of event.clipboardData?.files || []) {
|
||||
event.preventDefault()
|
||||
const clipboardText = event.clipboardData?.getData('text')
|
||||
if (clipboardText) {
|
||||
// Prioritize the text when pasting.
|
||||
// handled by the default event
|
||||
} else {
|
||||
for (const file of event.clipboardData?.files || []) {
|
||||
event.preventDefault()
|
||||
|
||||
if (file.path === '') {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const tempFilePath = await window.api.file.create(file.name)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
await window.api.file.write(tempFilePath, uint8Array)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
break
|
||||
if (file.path === '') {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const tempFilePath = await window.api.file.create(file.name)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
await window.api.file.write(tempFilePath, uint8Array)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file.path) {
|
||||
if (supportExts.includes(getFileExtension(file.path))) {
|
||||
const selectedFile = await window.api.file.get(file.path)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
if (file.path) {
|
||||
if (supportExts.includes(getFileExtension(file.path))) {
|
||||
const selectedFile = await window.api.file.get(file.path)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +362,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('clear_topic', () => {
|
||||
clearTopic()
|
||||
})
|
||||
|
||||
useShortcut('toggle_new_context', () => {
|
||||
onNewContext()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||
const unsubscribes = [
|
||||
@@ -468,14 +483,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} 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')}>
|
||||
okText={t('chat.input.clear.title')}>
|
||||
<ToolbarButton type="text">
|
||||
<ClearOutlined />
|
||||
</ToolbarButton>
|
||||
@@ -500,11 +515,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
/>
|
||||
)}
|
||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<PicCenterOutlined />
|
||||
</Tooltip>
|
||||
</ToolbarButton>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
||||
@@ -25,19 +26,28 @@ interface Props {
|
||||
index?: number
|
||||
total?: number
|
||||
hidePresetMessages?: boolean
|
||||
style?: React.CSSProperties
|
||||
isGrouped?: boolean
|
||||
onGetMessages?: () => Message[]
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => void
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
}
|
||||
|
||||
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
|
||||
isBubbleStyle ? (isAssistantMessage ? 'var(--chat-background-assistant)' : 'var(--chat-background-user)') : undefined
|
||||
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) => {
|
||||
return isBubbleStyle
|
||||
? isAssistantMessage
|
||||
? 'var(--chat-background-assistant)'
|
||||
: 'var(--chat-background-user)'
|
||||
: undefined
|
||||
}
|
||||
|
||||
const MessageItem: FC<Props> = ({
|
||||
message: _message,
|
||||
topic,
|
||||
index,
|
||||
hidePresetMessages,
|
||||
isGrouped,
|
||||
style,
|
||||
onDeleteMessage,
|
||||
onSetMessages,
|
||||
onGetMessages
|
||||
@@ -45,7 +55,7 @@ const MessageItem: FC<Props> = ({
|
||||
const [message, setMessage] = useState(_message)
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(message.modelId)
|
||||
const model = useModel(getMessageModelId(message)) || message.model
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -123,7 +133,7 @@ const MessageItem: FC<Props> = ({
|
||||
onResponse: (msg) => {
|
||||
setMessage(msg)
|
||||
if (msg.status !== 'pending') {
|
||||
const _messages = messages.map((m) => (m.id === msg.id ? msg : m))
|
||||
const _messages = onGetMessages().map((m) => (m.id === msg.id ? msg : m))
|
||||
onSetMessages(_messages)
|
||||
db.topics.update(topic.id, { messages: _messages })
|
||||
}
|
||||
@@ -157,8 +167,8 @@ const MessageItem: FC<Props> = ({
|
||||
'message-user': !isAssistantMessage
|
||||
})}
|
||||
ref={messageContainerRef}
|
||||
style={isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : undefined}>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} key={message.modelId} />
|
||||
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} key={getMessageModelId(message)} />
|
||||
<MessageContentContainer
|
||||
className="message-content-container"
|
||||
style={{ fontFamily, fontSize, background: messageBackground }}>
|
||||
@@ -179,6 +189,7 @@ const MessageItem: FC<Props> = ({
|
||||
index={index}
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
isGrouped={isGrouped}
|
||||
setModel={setModel}
|
||||
onEditMessage={onEditMessage}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
@@ -194,7 +205,6 @@ const MessageItem: FC<Props> = ({
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px 20px 0 20px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
&.message-highlight {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { getBriefInfo } from '@renderer/utils'
|
||||
import { withMessageThought } from '@renderer/utils/formats'
|
||||
import { Divider, Flex } from 'antd'
|
||||
import React from 'react'
|
||||
import React, { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
@@ -11,12 +12,16 @@ import Markdown from '../Markdown/Markdown'
|
||||
import MessageAttachments from './MessageAttachments'
|
||||
import MessageError from './MessageError'
|
||||
import MessageSearchResults from './MessageSearchResults'
|
||||
import MessageThought from './MessageThought'
|
||||
|
||||
const MessageContent: React.FC<{
|
||||
interface Props {
|
||||
message: Message
|
||||
model?: Model
|
||||
}> = ({ message, model }) => {
|
||||
}
|
||||
|
||||
const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
const { t } = useTranslation()
|
||||
const message = withMessageThought(_message)
|
||||
|
||||
if (message.status === 'sending') {
|
||||
return (
|
||||
@@ -36,13 +41,14 @@ const MessageContent: React.FC<{
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="8px" wrap>
|
||||
<Fragment>
|
||||
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
|
||||
{message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)}
|
||||
</Flex>
|
||||
<MessageThought message={message} />
|
||||
<Markdown message={message} />
|
||||
{message.translatedContent && (
|
||||
<>
|
||||
<Fragment>
|
||||
<Divider style={{ margin: 0, marginBottom: 10 }}>
|
||||
<TranslationOutlined />
|
||||
</Divider>
|
||||
@@ -51,11 +57,11 @@ const MessageContent: React.FC<{
|
||||
) : (
|
||||
<Markdown message={{ ...message, content: message.translatedContent }} />
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
)}
|
||||
<MessageAttachments message={message} />
|
||||
<MessageSearchResults message={message} />
|
||||
</>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
268
src/renderer/src/pages/home/Messages/MessageGroup.tsx
Normal file
268
src/renderer/src/pages/home/Messages/MessageGroup.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import { ColumnHeightOutlined, ColumnWidthOutlined, DeleteOutlined, FolderOutlined } from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import { Message, Model, Topic } from '@renderer/types'
|
||||
import { Button, Segmented as AntdSegmented } from 'antd'
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
import MessageItem from './Message'
|
||||
|
||||
interface Props {
|
||||
messages: (Message & { index: number })[]
|
||||
topic?: Topic
|
||||
hidePresetMessages?: boolean
|
||||
onGetMessages?: () => Message[]
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onDeleteGroupMessages?: (askId: string) => Promise<void>
|
||||
}
|
||||
|
||||
const MessageGroup: FC<Props> = ({
|
||||
messages,
|
||||
topic,
|
||||
hidePresetMessages,
|
||||
onDeleteMessage,
|
||||
onSetMessages,
|
||||
onGetMessages,
|
||||
onDeleteGroupMessages
|
||||
}) => {
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
||||
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
|
||||
|
||||
const messageLength = messages.length
|
||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||
|
||||
const isGrouped = messageLength > 1
|
||||
|
||||
const onDelete = async () => {
|
||||
window.modal.confirm({
|
||||
title: t('message.group.delete.title'),
|
||||
content: t('message.group.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
okText: t('common.delete'),
|
||||
onOk: () => {
|
||||
const askId = messages[0].askId
|
||||
askId && onDeleteGroupMessages?.(askId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(messageLength - 1)
|
||||
}, [messageLength])
|
||||
|
||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||
|
||||
return (
|
||||
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
|
||||
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
|
||||
{messages.map((message, index) => (
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}
|
||||
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
||||
<MessageItem
|
||||
isGrouped={isGrouped}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
))}
|
||||
</GridContainer>
|
||||
{isGrouped && (
|
||||
<GroupMenuBar className="group-menu-bar" $layout={multiModelMessageStyle}>
|
||||
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
||||
<LayoutContainer>
|
||||
{['fold', 'vertical', 'horizontal'].map((layout) => (
|
||||
<LayoutOption
|
||||
key={layout}
|
||||
active={multiModelMessageStyle === layout}
|
||||
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
|
||||
{layout === 'fold' ? (
|
||||
<FolderOutlined />
|
||||
) : layout === 'horizontal' ? (
|
||||
<ColumnWidthOutlined />
|
||||
) : (
|
||||
<ColumnHeightOutlined />
|
||||
)}
|
||||
</LayoutOption>
|
||||
))}
|
||||
</LayoutContainer>
|
||||
{multiModelMessageStyle === 'fold' && (
|
||||
<ModelsContainer>
|
||||
<Segmented
|
||||
value={selectedIndex.toString()}
|
||||
onChange={(value) => {
|
||||
setSelectedIndex(Number(value))
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
|
||||
}}
|
||||
options={messages.map((message, index) => ({
|
||||
label: (
|
||||
<SegmentedLabel>
|
||||
<ModelAvatar model={message.model as Model} size={20} />
|
||||
<ModelName>{message.model?.name}</ModelName>
|
||||
</SegmentedLabel>
|
||||
),
|
||||
value: index.toString()
|
||||
}))}
|
||||
size="small"
|
||||
/>
|
||||
</ModelsContainer>
|
||||
)}
|
||||
</HStack>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</GroupMenuBar>
|
||||
)}
|
||||
</GroupContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
|
||||
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && $layout === 'horizontal' ? '15px' : '0')};
|
||||
`
|
||||
|
||||
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle }>`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
|
||||
minmax(550px, 1fr)
|
||||
);
|
||||
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: repeat(
|
||||
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
|
||||
minmax(400px, 1fr)
|
||||
);
|
||||
}
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
interface MessageWrapperProps {
|
||||
$layout: 'fold' | 'horizontal' | 'vertical'
|
||||
$selected: boolean
|
||||
$isGrouped: boolean
|
||||
}
|
||||
|
||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
width: 100%;
|
||||
display: ${(props) => {
|
||||
if (props.$layout === 'fold') {
|
||||
return props.$selected ? 'block' : 'none'
|
||||
}
|
||||
if (props.$layout === 'horizontal') {
|
||||
return 'inline-block'
|
||||
}
|
||||
return 'block'
|
||||
}};
|
||||
${({ $layout, $isGrouped }) => {
|
||||
if ($layout === 'horizontal' && $isGrouped) {
|
||||
return css`
|
||||
border: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
}
|
||||
return ''
|
||||
}}
|
||||
`
|
||||
|
||||
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
border: 0.5px solid var(--color-border);
|
||||
height: 40px;
|
||||
margin-left: ${({ $layout }) => ($layout === 'horizontal' ? '0' : '40px')};
|
||||
transition: all 0.3s ease;
|
||||
`
|
||||
|
||||
const LayoutContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: row;
|
||||
`
|
||||
|
||||
const LayoutOption = styled.div<{ active: boolean }>`
|
||||
cursor: pointer;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'transparent')};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'var(--color-hover)')};
|
||||
}
|
||||
`
|
||||
|
||||
const ModelsContainer = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Segmented = styled(AntdSegmented)`
|
||||
.ant-segmented-item {
|
||||
background-color: transparent !important;
|
||||
transition: none !important;
|
||||
&:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
.ant-segmented-thumb,
|
||||
.ant-segmented-item-selected {
|
||||
background-color: transparent !important;
|
||||
border: 0.5px solid var(--color-border);
|
||||
transition: none !important;
|
||||
}
|
||||
`
|
||||
|
||||
const SegmentedLabel = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 0;
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
export default MessageGroup
|
||||
@@ -5,6 +5,8 @@ import { getModelLogo } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelName } from '@renderer/services/ModelService'
|
||||
import { Assistant, Message, Model } from '@renderer/types'
|
||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar } from 'antd'
|
||||
@@ -31,13 +33,19 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
|
||||
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, message.modelId), [message.modelId])
|
||||
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, getMessageModelId(message)), [message])
|
||||
|
||||
const getUserName = useCallback(() => {
|
||||
if (isLocalAi && message.role !== 'user') return APP_NAME
|
||||
if (message.role === 'assistant') return model?.name || model?.id || message.modelId || ''
|
||||
if (isLocalAi && message.role !== 'user') {
|
||||
return APP_NAME
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
return getModelName(model) || getMessageModelId(message) || ''
|
||||
}
|
||||
|
||||
return userName || t('common.you')
|
||||
}, [message.modelId, message.role, model?.id, model?.name, t, userName])
|
||||
}, [message, model, t, userName])
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMinappIcon = sidebarIcons.visible.includes('minapp')
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ForkOutlined,
|
||||
LikeFilled,
|
||||
LikeOutlined,
|
||||
MenuOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SaveOutlined,
|
||||
@@ -11,13 +13,16 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { resetAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils'
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -27,11 +32,12 @@ interface Props {
|
||||
assistantModel?: Model
|
||||
model?: Model
|
||||
index?: number
|
||||
isGrouped?: boolean
|
||||
isLastMessage: boolean
|
||||
isAssistantMessage: boolean
|
||||
setModel: (model: Model) => void
|
||||
onEditMessage?: (message: Message) => void
|
||||
onDeleteMessage?: (message: Message) => void
|
||||
onDeleteMessage?: (message: Message) => Promise<void>
|
||||
onGetMessages?: () => Message[]
|
||||
}
|
||||
|
||||
@@ -39,11 +45,11 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const {
|
||||
message,
|
||||
index,
|
||||
isGrouped,
|
||||
model,
|
||||
isLastMessage,
|
||||
isAssistantMessage,
|
||||
assistantModel,
|
||||
setModel,
|
||||
onEditMessage,
|
||||
onDeleteMessage,
|
||||
onGetMessages
|
||||
@@ -53,7 +59,6 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
|
||||
const isUserMessage = message.role === 'user'
|
||||
const canRegenerate = isLastMessage && isAssistantMessage
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
||||
@@ -62,14 +67,6 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [message.content, t])
|
||||
|
||||
const onRegenerate = useCallback(
|
||||
(model: Model) => {
|
||||
setModel(model)
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
|
||||
},
|
||||
[setModel]
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||
@@ -82,6 +79,21 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const onResend = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
const _messages = onGetMessages?.() || []
|
||||
const groupdMessages = _messages.filter((m) => m.askId === message.id)
|
||||
|
||||
// Resend all groupd messages
|
||||
if (!isEmpty(groupdMessages)) {
|
||||
for (const assistantMessage of groupdMessages) {
|
||||
const _model = assistantMessage.model || assistantModel
|
||||
EventEmitter.emit(
|
||||
EVENT_NAMES.RESEND_MESSAGE + ':' + assistantMessage.id,
|
||||
resetAssistantMessage(assistantMessage, _model)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If there is no groupd message, resend next message
|
||||
const index = _messages.findIndex((m) => m.id === message.id)
|
||||
const nextIndex = index + 1
|
||||
const nextMessage = _messages[nextIndex]
|
||||
@@ -91,35 +103,42 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
...nextMessage,
|
||||
content: '',
|
||||
status: 'sending',
|
||||
modelId: assistantModel?.id || model?.id,
|
||||
model: assistantModel || model,
|
||||
translatedContent: undefined
|
||||
})
|
||||
}
|
||||
|
||||
// If next message is not exist or next message role is user, delete current message and resend
|
||||
if (!nextMessage || nextMessage.role === 'user') {
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { ...message, id: uuid() })
|
||||
onDeleteMessage?.(message)
|
||||
}
|
||||
}, [assistantModel?.id, message, model?.id, onDeleteMessage, onGetMessages])
|
||||
}, [assistantModel, message, model, onDeleteMessage, onGetMessages])
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
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>
|
||||
)
|
||||
children: (props) => {
|
||||
const onPress = () => {
|
||||
props.onOk?.()
|
||||
resendMessage = true
|
||||
}
|
||||
return message.role === 'user' ? (
|
||||
<ReSendButton
|
||||
icon={<i className="iconfont icon-ic_send" style={{ color: 'var(--color-primary)' }} />}
|
||||
onClick={onPress}>
|
||||
{t('chat.resend')}
|
||||
</ReSendButton>
|
||||
) : null
|
||||
}
|
||||
})
|
||||
|
||||
editedText && onEditMessage?.({ ...message, content: editedText })
|
||||
if (editedText) {
|
||||
await onEditMessage?.({ ...message, content: editedText })
|
||||
}
|
||||
|
||||
resendMessage && onResend()
|
||||
}, [message, onEditMessage, onResend, t])
|
||||
|
||||
@@ -132,8 +151,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
setIsTranslating(true)
|
||||
|
||||
try {
|
||||
const translatedText = await translateText(message.content, language)
|
||||
onEditMessage?.({ ...message, translatedContent: translatedText })
|
||||
await translateText(message.content, language, (text) =>
|
||||
onEditMessage?.({ ...message, translatedContent: text })
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
window.message.error({
|
||||
@@ -175,22 +195,23 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
[message, onEdit, onNewBranch, t]
|
||||
)
|
||||
|
||||
const onAtModelRegenerate = async () => {
|
||||
const onRegenerate = async () => {
|
||||
await modelGenerating()
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
selectedModel && onRegenerate(selectedModel)
|
||||
if (!selectedModel) return
|
||||
|
||||
const _message: Message = resetAssistantMessage(message, selectedModel)
|
||||
|
||||
if (message.askId && message.model) {
|
||||
return EventEmitter.emit(EVENT_NAMES.APPEND_MESSAGE, { ..._message, id: uuid() })
|
||||
}
|
||||
|
||||
onEditMessage?.(_message)
|
||||
}
|
||||
|
||||
const onDeleteAndRegenerate = async () => {
|
||||
await modelGenerating()
|
||||
onEditMessage?.({
|
||||
...message,
|
||||
content: '',
|
||||
status: 'sending',
|
||||
modelId: assistantModel?.id || model?.id,
|
||||
translatedContent: undefined
|
||||
})
|
||||
}
|
||||
const onUseful = useCallback(() => {
|
||||
onEditMessage?.({ ...message, useful: !message.useful })
|
||||
}, [message, onEditMessage])
|
||||
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
@@ -208,23 +229,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{isAssistantMessage && (
|
||||
<Popconfirm
|
||||
title={t('message.regenerate.confirm')}
|
||||
okButtonProps={{ danger: true }}
|
||||
destroyTooltipOnHide
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={onDeleteAndRegenerate}>
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button">
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
|
||||
<i className="iconfont icon-at"></i>
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onRegenerate}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -232,36 +239,11 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
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')
|
||||
},
|
||||
...TranslateLanguageOptions.map((item) => ({
|
||||
label: item.emoji + ' ' + item.label,
|
||||
key: item.value,
|
||||
onClick: () => handleTranslate(item.value)
|
||||
})),
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
@@ -279,13 +261,23 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onUseful}>
|
||||
{message.useful ? <LikeFilled /> : <LikeOutlined />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
disabled={isGrouped}
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => onDeleteMessage?.(message)}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton className="message-action-button">
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={isGrouped ? () => onDeleteMessage?.(message) : undefined}>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
|
||||
61
src/renderer/src/pages/home/Messages/MessageThought.tsx
Normal file
61
src/renderer/src/pages/home/Messages/MessageThought.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Message } from '@renderer/types'
|
||||
import { Collapse } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const MessageThought: FC<Props> = ({ message }) => {
|
||||
const isThinking = !message.content
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!message.reasoning_content) {
|
||||
return null
|
||||
}
|
||||
|
||||
const thinkingTime = message.metrics?.time_thinking_millsec || 0
|
||||
const thinkingTimeSecounds = (thinkingTime / 1000).toFixed(1)
|
||||
|
||||
return (
|
||||
<CollapseContainer
|
||||
className="message-thought-container"
|
||||
items={[
|
||||
{
|
||||
key: 'thought',
|
||||
label: (
|
||||
<MessageTitleLabel>
|
||||
<TinkingText>
|
||||
{isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSecounds })}
|
||||
</TinkingText>
|
||||
{isThinking && <BarLoader color="#9254de" />}
|
||||
</MessageTitleLabel>
|
||||
),
|
||||
children: <ReactMarkdown>{message.reasoning_content}</ReactMarkdown>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CollapseContainer = styled(Collapse)`
|
||||
margin-bottom: 15px;
|
||||
`
|
||||
|
||||
const MessageTitleLabel = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
gap: 15px;
|
||||
`
|
||||
|
||||
const TinkingText = styled.span`
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
export default MessageThought
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
filterMessages,
|
||||
getAssistantMessage,
|
||||
getContextCount,
|
||||
getGroupedMessages,
|
||||
getUserMessage
|
||||
} from '@renderer/services/MessagesService'
|
||||
import { estimateHistoryTokens } from '@renderer/services/TokenService'
|
||||
@@ -25,7 +26,7 @@ import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Suggestions from '../components/Suggestions'
|
||||
import MessageItem from './Message'
|
||||
import MessageGroup from './MessageGroup'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import Prompt from './Prompt'
|
||||
|
||||
@@ -35,39 +36,6 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
interface LoaderProps {
|
||||
$loading: boolean
|
||||
}
|
||||
|
||||
const LoaderContainer = styled.div<LoaderProps>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
background: var(--color-background);
|
||||
opacity: ${(props) => (props.$loading ? 1 : 0)};
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const ScrollContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
`
|
||||
|
||||
interface ContainerProps {
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
const Container = styled(Scrollbar)<ContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 10px 0;
|
||||
padding-bottom: 20px;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||
@@ -79,6 +47,8 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings()
|
||||
|
||||
const groupedMessages = getGroupedMessages(displayMessages)
|
||||
|
||||
const INITIAL_MESSAGES_COUNT = 20
|
||||
const LOAD_MORE_COUNT = 20
|
||||
|
||||
@@ -98,14 +68,18 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const onSendMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const assistantMessages: Message[] = []
|
||||
|
||||
if (message.mentions?.length) {
|
||||
message.mentions.forEach((m) => {
|
||||
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
|
||||
assistantMessage.model = m
|
||||
assistantMessage.askId = message.id
|
||||
assistantMessages.push(assistantMessage)
|
||||
})
|
||||
} else {
|
||||
assistantMessages.push(getAssistantMessage({ assistant, topic }))
|
||||
const assistantMessage = getAssistantMessage({ assistant, topic })
|
||||
assistantMessage.askId = message.id
|
||||
assistantMessages.push(assistantMessage)
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
@@ -119,6 +93,17 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
[assistant, scrollToBottom, topic]
|
||||
)
|
||||
|
||||
const onAppendMessage = useCallback(
|
||||
(message: Message) => {
|
||||
setMessages((prev) => {
|
||||
const messages = prev.concat([message])
|
||||
db.topics.put({ id: topic.id, messages })
|
||||
return messages
|
||||
})
|
||||
},
|
||||
[topic.id]
|
||||
)
|
||||
|
||||
const autoRenameTopic = useCallback(async () => {
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
|
||||
@@ -143,12 +128,25 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
}, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic])
|
||||
|
||||
const onDeleteMessage = useCallback(
|
||||
(message: Message) => {
|
||||
async (message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
setDisplayMessages(_messages)
|
||||
db.topics.update(topic.id, { messages: _messages })
|
||||
deleteMessageFiles(message)
|
||||
await db.topics.update(topic.id, { messages: _messages })
|
||||
await deleteMessageFiles(message)
|
||||
},
|
||||
[messages, topic.id]
|
||||
)
|
||||
|
||||
const onDeleteGroupMessages = useCallback(
|
||||
async (askId: string) => {
|
||||
const _messages = messages.filter((m) => m.askId !== askId && m.id !== askId)
|
||||
setMessages(_messages)
|
||||
setDisplayMessages(_messages)
|
||||
await db.topics.update(topic.id, { messages: _messages })
|
||||
for (const message of _messages) {
|
||||
await deleteMessageFiles(message)
|
||||
}
|
||||
},
|
||||
[messages, topic.id]
|
||||
)
|
||||
@@ -160,13 +158,13 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage),
|
||||
EventEmitter.on(EVENT_NAMES.APPEND_MESSAGE, onAppendMessage),
|
||||
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async () => {
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
|
||||
const lastUserMessage = last(filterMessages(messages).filter((m) => m.role === 'user'))
|
||||
lastUserMessage &&
|
||||
onSendMessage({ ...lastUserMessage, id: uuid(), modelId: model.id, model: model, mentions: [model] })
|
||||
lastUserMessage && onSendMessage({ ...lastUserMessage, id: uuid(), model: model, mentions: [model] })
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
||||
@@ -214,7 +212,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
setActiveTopic(newTopic)
|
||||
autoRenameTopic()
|
||||
|
||||
// 由于复制了消<EFBFBD><EFBFBD><EFBFBD>,消息中附带的文件的总数变了,需要更新
|
||||
// 由于复制了消息,消息中附带的文件的总数变了,需要更新
|
||||
const filesArr = branchMessages.map((m) => m.files)
|
||||
const files = flatten(filesArr).filter(Boolean)
|
||||
files.map(async (f) => {
|
||||
@@ -229,6 +227,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
assistant,
|
||||
autoRenameTopic,
|
||||
messages,
|
||||
onAppendMessage,
|
||||
onDeleteMessage,
|
||||
onSendMessage,
|
||||
scrollToBottom,
|
||||
@@ -293,7 +292,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
style={{ maxWidth }}
|
||||
key={assistant.id}
|
||||
ref={containerRef}
|
||||
right={topicPosition === 'left'}>
|
||||
$right={topicPosition === 'left'}>
|
||||
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
|
||||
<Suggestions assistant={assistant} messages={messages} />
|
||||
<InfiniteScroll
|
||||
@@ -307,15 +306,15 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
{displayMessages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
{Object.entries(groupedMessages).map(([key, messages]) => (
|
||||
<MessageGroup
|
||||
key={key}
|
||||
messages={messages}
|
||||
topic={topic}
|
||||
index={index}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onDeleteGroupMessages={onDeleteGroupMessages}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
))}
|
||||
@@ -327,4 +326,38 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
)
|
||||
}
|
||||
|
||||
interface LoaderProps {
|
||||
$loading: boolean
|
||||
}
|
||||
|
||||
const LoaderContainer = styled.div<LoaderProps>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
background: var(--color-background);
|
||||
opacity: ${(props) => (props.$loading ? 1 : 0)};
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const ScrollContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
interface ContainerProps {
|
||||
$right?: boolean
|
||||
}
|
||||
|
||||
const Container = styled(Scrollbar)<ContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 10px 0;
|
||||
padding-bottom: 20px;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
export default Messages
|
||||
|
||||
@@ -27,7 +27,6 @@ const Prompt: FC<Props> = ({ assistant }) => {
|
||||
const Container = styled.div`
|
||||
padding: 10px 20px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-bottom: 20px;
|
||||
margin: 4px 20px 0 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
setMathEngine,
|
||||
setMessageFont,
|
||||
setMessageStyle,
|
||||
setMultiModelMessageStyle,
|
||||
setPasteLongTextAsFile,
|
||||
setPasteLongTextThreshold,
|
||||
setRenderInputMessageAsMarkdown,
|
||||
@@ -64,7 +65,8 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
codeCollapsible,
|
||||
mathEngine,
|
||||
autoTranslateWithSpace,
|
||||
pasteLongTextThreshold
|
||||
pasteLongTextThreshold,
|
||||
multiModelMessageStyle
|
||||
} = useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||
@@ -255,6 +257,19 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
|
||||
<Select
|
||||
size="small"
|
||||
value={multiModelMessageStyle}
|
||||
onChange={(value) => dispatch(setMultiModelMessageStyle(value))}
|
||||
style={{ width: 135 }}>
|
||||
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
|
||||
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
|
||||
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||
<Select
|
||||
|
||||
@@ -361,6 +361,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
{providerName && <Tag color="purple">{providerName}</Tag>}
|
||||
</ModelInfo>
|
||||
|
||||
<ModelInfo>
|
||||
<label htmlFor="model-info">{t('knowledge.chunk_size')}</label>
|
||||
<Tag color="green">{base.chunkSize || t('knowledge.not_set')}</Tag>
|
||||
<label htmlFor="model-info">{t('knowledge.chunk_overlap')}</label>
|
||||
<Tag color="orange">{base.chunkOverlap || t('knowledge.not_set')}</Tag>
|
||||
</ModelInfo>
|
||||
|
||||
<IndexSection>
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -6,7 +6,7 @@ import AiProvider from '@renderer/providers/AiProvider'
|
||||
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Form, Input, Modal, Select } from 'antd'
|
||||
import { Form, Input, InputNumber, Modal, Select } from 'antd'
|
||||
import { find, sortBy } from 'lodash'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useState } from 'react'
|
||||
@@ -19,6 +19,8 @@ interface ShowParams {
|
||||
interface FormData {
|
||||
name: string
|
||||
model: string
|
||||
chunkSize?: number
|
||||
chunkOverlap?: number
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
@@ -81,6 +83,8 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
name: values.name,
|
||||
model: selectedModel,
|
||||
dimensions,
|
||||
chunkSize: values.chunkSize,
|
||||
chunkOverlap: values.chunkOverlap,
|
||||
items: [],
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
@@ -131,6 +135,27 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
rules={[{ required: true, message: t('message.error.enter.model') }]}>
|
||||
<Select style={{ width: '100%' }} options={selectOptions} placeholder={t('settings.models.empty')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="chunkSize" label={t('knowledge.chunk_size')}>
|
||||
<InputNumber style={{ width: '100%' }} min={1} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="chunkOverlap"
|
||||
label={t('knowledge.chunk_overlap')}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('chunkSize') > value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject(new Error(t('message.error.chunk_overlap_too_large')))
|
||||
}
|
||||
})
|
||||
]}
|
||||
dependencies={['chunkSize']}>
|
||||
<InputNumber style={{ width: '100%' }} min={0} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ const AboutSettings: FC = () => {
|
||||
}
|
||||
|
||||
const mailto = async () => {
|
||||
const email = 'kangfenmao@qq.com'
|
||||
const email = 'support@cherry-ai.com'
|
||||
const subject = `${APP_NAME} Feedback`
|
||||
const version = (await window.api.getAppInfo()).version
|
||||
const platform = window.electron.process.platform
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { Assistant, AssistantMessage, AssistantSettings } from '@renderer/types'
|
||||
import { Button, Card, Col, Divider, Form as FormAntd, FormInstance, Row, Space, Switch } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { FC, useRef, useState } from 'react'
|
||||
import { FC, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -19,7 +19,15 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
|
||||
const [messages, setMessagess] = useState<AssistantMessage[]>(assistant?.messages || [])
|
||||
const [hideMessages, setHideMessages] = useState(assistant?.settings?.hideMessages || false)
|
||||
|
||||
const showSaveButton = (assistant?.messages || []).length !== messages.length
|
||||
const showSaveButton = useMemo(() => {
|
||||
const originalMessages = assistant?.messages || []
|
||||
if (originalMessages.length !== messages.length) return true
|
||||
|
||||
return messages.some((msg, index) => {
|
||||
const originalMsg = originalMessages[index]
|
||||
return !originalMsg || msg.content.trim() !== originalMsg.content.trim()
|
||||
})
|
||||
}, [messages, assistant?.messages])
|
||||
|
||||
const onSave = () => {
|
||||
// 检查是否有空对话组
|
||||
|
||||
@@ -40,7 +40,7 @@ const WebDavSettings: FC = () => {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const { webdavSync } = useRuntime()
|
||||
|
||||
@@ -168,12 +168,19 @@ const WebDavSettings: FC = () => {
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
|
||||
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
|
||||
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
|
||||
<Select.Option value={1}>1 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={1}>
|
||||
1 {i18n.language === 'en-US' ? t('settings.data.webdav.minute') : t('settings.data.webdav.minutes')}
|
||||
</Select.Option>
|
||||
<Select.Option value={5}>5 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={15}>15 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={30}>30 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={60}>60 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={120}>120 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={60}>
|
||||
1 {i18n.language === 'en-US' ? t('settings.data.webdav.hour') : t('settings.data.webdav.hours')}
|
||||
</Select.Option>
|
||||
<Select.Option value={120}>2 {t('settings.data.webdav.hours')}</Select.Option>
|
||||
<Select.Option value={360}>6 {t('settings.data.webdav.hours')}</Select.Option>
|
||||
<Select.Option value={720}>12 {t('settings.data.webdav.hours')}</Select.Option>
|
||||
<Select.Option value={1440}>24 {t('settings.data.webdav.hours')}</Select.Option>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
{webdavSync && syncInterval > 0 && (
|
||||
|
||||
@@ -67,8 +67,22 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
setApiHost(provider.apiHost)
|
||||
}, [provider])
|
||||
|
||||
const onUpdateApiKey = () => updateProvider({ ...provider, apiKey })
|
||||
const onUpdateApiHost = () => updateProvider({ ...provider, apiHost })
|
||||
const onUpdateApiKey = () => {
|
||||
if (apiKey.trim()) {
|
||||
updateProvider({ ...provider, apiKey })
|
||||
} else {
|
||||
setApiKey(provider.apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateApiHost = () => {
|
||||
if (apiHost.trim()) {
|
||||
updateProvider({ ...provider, apiHost })
|
||||
} else {
|
||||
setApiHost(provider.apiHost)
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateApiVersion = () => updateProvider({ ...provider, apiVersion })
|
||||
const onManageModel = () => EditModelsPopup.show({ provider })
|
||||
const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add.add_model'), provider })
|
||||
|
||||
@@ -164,7 +164,7 @@ const ProviderListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: calc(var(--settings-width) + 10px);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
height: calc(75vh - var(--navbar-height));
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
@@ -180,19 +180,18 @@ const ProviderListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
padding: 8px 8px;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 0.5px solid transparent;
|
||||
&:hover {
|
||||
background: var(--color-background-soft);
|
||||
background: var(--color-primary-mute);
|
||||
}
|
||||
&.active {
|
||||
background: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
background: var(--color-primary-mute);
|
||||
color: var(--color-primary);
|
||||
font-weight: bold !important;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -7,11 +7,10 @@ import {
|
||||
SaveOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { FC } from 'react'
|
||||
import { Breadcrumb, Button, Menu } from 'antd'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AboutSettings from './AboutSettings'
|
||||
@@ -23,94 +22,145 @@ import ProvidersList from './ProviderSettings'
|
||||
import QuickAssistantSettings from './QuickAssistantSettings'
|
||||
import ShortcutSettings from './ShortcutSettings'
|
||||
|
||||
const SettingsPage: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { t } = useTranslation()
|
||||
export type SettingsTab =
|
||||
| 'provider'
|
||||
| 'model'
|
||||
| 'general'
|
||||
| 'display'
|
||||
| 'data'
|
||||
| 'quickAssistant'
|
||||
| 'shortcut'
|
||||
| 'about'
|
||||
|
||||
const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SettingMenus>
|
||||
{!isLocalAi && (
|
||||
<>
|
||||
<MenuItemLink to="/settings/provider">
|
||||
<MenuItem className={isRoute('/settings/provider')}>
|
||||
<CloudOutlined />
|
||||
{t('settings.provider.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/model">
|
||||
<MenuItem className={isRoute('/settings/model')}>
|
||||
<i className="iconfont icon-ai-model" />
|
||||
{t('settings.model')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
</>
|
||||
)}
|
||||
<MenuItemLink to="/settings/general">
|
||||
<MenuItem className={isRoute('/settings/general')}>
|
||||
<SettingOutlined />
|
||||
{t('settings.general')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/display">
|
||||
<MenuItem className={isRoute('/settings/display')}>
|
||||
<LayoutOutlined />
|
||||
{t('settings.display.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/shortcut">
|
||||
<MenuItem className={isRoute('/settings/shortcut')}>
|
||||
<MacCommandOutlined />
|
||||
{t('settings.shortcuts.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/quickAssistant">
|
||||
<MenuItem className={isRoute('/settings/quickAssistant')}>
|
||||
<RocketOutlined />
|
||||
{t('settings.quickAssistant.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/data">
|
||||
<MenuItem className={isRoute('/settings/data')}>
|
||||
<SaveOutlined />
|
||||
{t('settings.data.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/about">
|
||||
<MenuItem className={isRoute('/settings/about')}>
|
||||
<InfoCircleOutlined />
|
||||
{t('settings.about')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
</SettingMenus>
|
||||
<SettingContent>
|
||||
<Routes>
|
||||
<Route path="provider" element={<ProvidersList />} />
|
||||
<Route path="model" element={<ModelSettings />} />
|
||||
<Route path="general/*" element={<GeneralSettings />} />
|
||||
<Route path="display" element={<DisplaySettings />} />
|
||||
<Route path="data/*" element={<DataSettings />} />
|
||||
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
|
||||
<Route path="shortcut" element={<ShortcutSettings />} />
|
||||
<Route path="about" element={<AboutSettings />} />
|
||||
</Routes>
|
||||
</SettingContent>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
interface Props {
|
||||
activeTab?: SettingsTab
|
||||
onTabChange?: (tab: SettingsTab) => void
|
||||
}
|
||||
interface MenuItem {
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
key: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`
|
||||
const SettingsPage: FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const activeTab = props.activeTab || 'provider'
|
||||
|
||||
const settingMenus = useMemo<MenuItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: t('settings.provider.title'),
|
||||
icon: <CloudOutlined />,
|
||||
key: 'provider',
|
||||
enabled: !isLocalAi
|
||||
},
|
||||
{
|
||||
label: t('settings.model'),
|
||||
icon: <i className="iconfont icon-ai-model" />,
|
||||
key: 'model',
|
||||
enabled: !isLocalAi
|
||||
},
|
||||
{
|
||||
label: t('settings.general'),
|
||||
icon: <SettingOutlined />,
|
||||
key: 'general',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
label: t('settings.display.title'),
|
||||
icon: <LayoutOutlined />,
|
||||
key: 'display',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
label: t('settings.shortcuts.title'),
|
||||
icon: <MacCommandOutlined />,
|
||||
key: 'shortcut',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
label: t('settings.quickAssistant.title'),
|
||||
icon: <RocketOutlined />,
|
||||
key: 'quickAssistant',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
label: t('settings.data.title'),
|
||||
icon: <SaveOutlined />,
|
||||
key: 'data',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
label: t('settings.about'),
|
||||
icon: <InfoCircleOutlined />,
|
||||
key: 'about',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
const breadcrumbItems = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: t('settings.title')
|
||||
},
|
||||
{
|
||||
title: settingMenus.find((item) => item.key === activeTab)?.label
|
||||
}
|
||||
]
|
||||
}, [t, activeTab, settingMenus])
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'provider':
|
||||
return <ProvidersList />
|
||||
case 'model':
|
||||
return <ModelSettings />
|
||||
case 'general':
|
||||
return <GeneralSettings />
|
||||
case 'display':
|
||||
return <DisplaySettings />
|
||||
case 'data':
|
||||
return <DataSettings />
|
||||
case 'quickAssistant':
|
||||
return <QuickAssistantSettings />
|
||||
case 'shortcut':
|
||||
return <ShortcutSettings />
|
||||
case 'about':
|
||||
return <AboutSettings />
|
||||
default:
|
||||
return <GeneralSettings />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<MenuContainer $isCollapsed={collapsed}>
|
||||
<Title>{t('settings.title')}</Title>
|
||||
<Menu
|
||||
mode="inline"
|
||||
onClick={(e) => props.onTabChange?.(e.key as SettingsTab)}
|
||||
selectedKeys={[activeTab]}
|
||||
items={settingMenus.filter((item) => item.enabled)}
|
||||
inlineCollapsed={collapsed}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<SettingContent>
|
||||
<SettingHeader>
|
||||
<CollapseButton shape="circle" type="text" onClick={() => setCollapsed(!collapsed)} $isCollapsed={collapsed}>
|
||||
<i className="iconfont icon-hide-sidebar" />
|
||||
</CollapseButton>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
</SettingHeader>
|
||||
{renderContent()}
|
||||
</SettingContent>
|
||||
</ContentContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -118,57 +168,40 @@ const ContentContainer = styled.div`
|
||||
flex-direction: row;
|
||||
`
|
||||
|
||||
const SettingMenus = styled.ul`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--settings-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const MenuItemLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const MenuItem = styled.li`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 0.5px solid transparent;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
const MenuContainer = styled.div<{ $isCollapsed: boolean }>`
|
||||
width: ${({ $isCollapsed }) => ($isCollapsed ? '80px' : '160px')};
|
||||
background-color: var(--color-background-mute);
|
||||
transition: width 0.3s ease-in-out;
|
||||
position: relative;
|
||||
.ant-menu-light {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
const CollapseButton = styled(Button)<{ $isCollapsed: boolean }>`
|
||||
color: var(--color-icon);
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
opacity: 0.7;
|
||||
margin-left: -1px;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
transform: rotate(${({ $isCollapsed }) => ($isCollapsed ? '180deg' : '0deg')});
|
||||
}
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 16px 24px;
|
||||
`
|
||||
|
||||
const SettingContent = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const SettingHeader = styled.div`
|
||||
padding: 4px 8px;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ClearOutlined, UndoOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { useShortcuts } from '@renderer/hooks/useShortcuts'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { initialState, resetShortcuts, toggleShortcut, updateShortcut } from '@renderer/store/shortcuts'
|
||||
import { Shortcut } from '@renderer/types'
|
||||
import { Button, Input, InputRef, Switch, Table as AntTable, Tooltip } from 'antd'
|
||||
@@ -17,7 +18,7 @@ const ShortcutSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
const { shortcuts } = useShortcuts()
|
||||
const inputRefs = useRef<Record<string, InputRef>>({})
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
height: calc(75vh - var(--navbar-height));
|
||||
padding: 16px;
|
||||
padding-top: 15px;
|
||||
overflow-y: scroll;
|
||||
font-family: Ubuntu;
|
||||
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import SettingsPopup from '@renderer/components/Popups/SettingsPopup'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import db from '@renderer/databases'
|
||||
@@ -55,7 +56,7 @@ const TranslatePage: FC = () => {
|
||||
content: text,
|
||||
assistantId: assistant.id,
|
||||
topicId: uuid(),
|
||||
modelId: translateModel.id,
|
||||
model: translateModel,
|
||||
createdAt: new Date().toISOString(),
|
||||
type: 'text',
|
||||
status: 'sending'
|
||||
@@ -90,9 +91,10 @@ const TranslatePage: FC = () => {
|
||||
|
||||
if (translateModel) {
|
||||
return (
|
||||
<Link to="/settings/model" style={{ color: 'var(--color-text-2)' }}>
|
||||
<SettingOutlined />
|
||||
</Link>
|
||||
<SettingsPopup
|
||||
activeTab="model"
|
||||
actionButton={<Button type="text" shape="circle" icon={<SettingOutlined />} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
||||
import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import store from '@renderer/store'
|
||||
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { delay, isJSON } from '@renderer/utils'
|
||||
import { delay, isJSON, parseJSON } from '@renderer/utils'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { CompletionsParams } from '.'
|
||||
@@ -98,9 +98,15 @@ export default abstract class BaseProvider {
|
||||
}
|
||||
if (param.type === 'json') {
|
||||
const value = param.value as string
|
||||
return { ...acc, [param.name]: isJSON(value) ? JSON.parse(value) : value }
|
||||
return {
|
||||
...acc,
|
||||
[param.name]: isJSON(value) ? parseJSON(value) : value
|
||||
}
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[param.name]: param.value
|
||||
}
|
||||
return { ...acc, [param.name]: param.value }
|
||||
}, {}) || {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -114,9 +114,9 @@ export default class GeminiProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
private getSafetySettings(modelId: string): SafetySetting[] {
|
||||
const safetyThreshold = modelId.includes('gemini-exp-')
|
||||
? HarmBlockThreshold.BLOCK_NONE
|
||||
: ('OFF' as HarmBlockThreshold)
|
||||
const safetyThreshold = modelId.includes('gemini-2.0-flash-exp')
|
||||
? ('OFF' as HarmBlockThreshold)
|
||||
: HarmBlockThreshold.BLOCK_NONE
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -117,6 +117,20 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
} as ChatCompletionMessageParam
|
||||
}
|
||||
|
||||
private getTemperature(assistant: Assistant, model: Model) {
|
||||
const isOpenAIo1 = model.id.startsWith('o1')
|
||||
|
||||
if (isOpenAIo1) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (model.provider === 'deepseek' && model.id === 'deepseek-reasoner') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return assistant?.settings?.temperature
|
||||
}
|
||||
|
||||
async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
@@ -128,6 +142,12 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
const _messages = filterContextMessages(takeRight(messages, contextCount + 1))
|
||||
onFilterMessages(_messages)
|
||||
|
||||
if (model.id === 'deepseek-reasoner') {
|
||||
if (_messages[0]?.role !== 'user') {
|
||||
userMessages.push({ role: 'user', content: '' })
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of _messages) {
|
||||
userMessages.push(await this.getMessageParam(message, model))
|
||||
}
|
||||
@@ -142,6 +162,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
let time_first_token_millsec = 0
|
||||
let time_first_content_millsec = 0
|
||||
const start_time_millsec = new Date().getTime()
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
@@ -150,7 +171,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
messages: [isOpenAIo1 ? undefined : systemMessage, ...userMessages].filter(
|
||||
Boolean
|
||||
) as ChatCompletionMessageParam[],
|
||||
temperature: isOpenAIo1 ? 1 : assistant?.settings?.temperature,
|
||||
temperature: this.getTemperature(assistant, model),
|
||||
top_p: assistant?.settings?.topP,
|
||||
max_tokens: maxTokens,
|
||||
keep_alive: this.keepAliveTime,
|
||||
@@ -176,17 +197,28 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||
break
|
||||
}
|
||||
|
||||
if (time_first_token_millsec == 0) {
|
||||
time_first_token_millsec = new Date().getTime() - start_time_millsec
|
||||
}
|
||||
|
||||
if (time_first_content_millsec == 0 && chunk.choices[0]?.delta?.content) {
|
||||
time_first_content_millsec = new Date().getTime()
|
||||
}
|
||||
|
||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||
const time_thinking_millsec = time_first_content_millsec ? time_first_content_millsec - start_time_millsec : 0
|
||||
|
||||
onChunk({
|
||||
text: chunk.choices[0]?.delta?.content || '',
|
||||
// @ts-ignore key is not typed
|
||||
reasoning_content: chunk.choices[0]?.delta?.reasoning_content || '',
|
||||
usage: chunk.usage,
|
||||
metrics: {
|
||||
completion_tokens: chunk.usage?.completion_tokens,
|
||||
time_completion_millsec,
|
||||
time_first_token_millsec
|
||||
time_first_token_millsec,
|
||||
time_thinking_millsec
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
5
src/renderer/src/providers/index.d.ts
vendored
5
src/renderer/src/providers/index.d.ts
vendored
@@ -1,8 +1,9 @@
|
||||
import type { GroundingMetadata } from '@google/generative-ai'
|
||||
import type { Assistant, Metrics } from '@renderer/types'
|
||||
import type { Assistant, Message, Metrics } from '@renderer/types'
|
||||
|
||||
interface ChunkCallbackData {
|
||||
text?: string
|
||||
reasoning_content?: string
|
||||
usage?: OpenAI.Completions.CompletionUsage
|
||||
metrics?: Metrics
|
||||
search?: GroundingMetadata
|
||||
@@ -11,6 +12,6 @@ interface ChunkCallbackData {
|
||||
interface CompletionsParams {
|
||||
messages: Message[]
|
||||
assistant: Assistant
|
||||
onChunk: ({ text, usage, metrics, search }: ChunkCallbackData) => void
|
||||
onChunk: ({ text, reasoning_content, usage, metrics, search }: ChunkCallbackData) => void
|
||||
onFilterMessages: (messages: Message[]) => void
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
getTranslateModel
|
||||
} from './AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from './EventService'
|
||||
import { filterMessages } from './MessagesService'
|
||||
import { filterMessages, filterUsefulMessages } from './MessagesService'
|
||||
import { estimateMessagesUsage } from './TokenService'
|
||||
|
||||
export async function fetchChatCompletion({
|
||||
@@ -53,14 +53,18 @@ export async function fetchChatCompletion({
|
||||
let _messages: Message[] = []
|
||||
|
||||
await AI.completions({
|
||||
messages,
|
||||
messages: filterUsefulMessages(messages),
|
||||
assistant,
|
||||
onFilterMessages: (messages) => (_messages = messages),
|
||||
onChunk: ({ text, usage, metrics, search }) => {
|
||||
onChunk: ({ text, reasoning_content, usage, metrics, search }) => {
|
||||
message.content = message.content + text || ''
|
||||
message.usage = usage
|
||||
message.metrics = metrics
|
||||
|
||||
if (reasoning_content) {
|
||||
message.reasoning_content = (message.reasoning_content || '') + reasoning_content
|
||||
}
|
||||
|
||||
if (search) {
|
||||
message.metadata = { groundingMetadata: search }
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function addAssistantMessagesToTopic({ assistant, topic }: { assist
|
||||
topicId: topic.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'success',
|
||||
modelId: assistant.defaultModel?.id || defaultModel.id,
|
||||
model: assistant.defaultModel || defaultModel,
|
||||
type: 'text',
|
||||
isPreset: true
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export const EventEmitter = new Emittery()
|
||||
|
||||
export const EVENT_NAMES = {
|
||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||
APPEND_MESSAGE: 'APPEND_MESSAGE',
|
||||
RECEIVE_MESSAGE: 'RECEIVE_MESSAGE',
|
||||
AI_AUTO_RENAME: 'AI_AUTO_RENAME',
|
||||
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
||||
|
||||
@@ -22,7 +22,9 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
||||
dimensions: base.dimensions,
|
||||
apiKey: aiProvider.getApiKey() || 'secret',
|
||||
apiVersion: provider.apiVersion,
|
||||
baseURL: host
|
||||
baseURL: host,
|
||||
chunkSize: base.chunkSize,
|
||||
chunkOverlap: base.chunkOverlap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { DEFAULT_CONTEXTCOUNT } from '@renderer/config/constant'
|
||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { isEmpty, takeRight } from 'lodash'
|
||||
import { isEmpty, remove, takeRight } from 'lodash'
|
||||
import { NavigateFunction } from 'react-router'
|
||||
|
||||
import { getAssistantById, getDefaultModel } from './AssistantService'
|
||||
@@ -86,7 +86,7 @@ export function getUserMessage({
|
||||
content: content || '',
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
modelId: model.id,
|
||||
model,
|
||||
createdAt: new Date().toISOString(),
|
||||
type,
|
||||
status: 'success'
|
||||
@@ -103,9 +103,68 @@ export function getAssistantMessage({ assistant, topic }: { assistant: Assistant
|
||||
content: '',
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
modelId: model.id,
|
||||
model,
|
||||
createdAt: new Date().toISOString(),
|
||||
type: 'text',
|
||||
status: 'sending'
|
||||
}
|
||||
}
|
||||
|
||||
export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
const _messages = messages
|
||||
const groupedMessages = getGroupedMessages(messages)
|
||||
|
||||
Object.entries(groupedMessages).forEach(([key, messages]) => {
|
||||
if (key.startsWith('assistant')) {
|
||||
const usefulMessage = messages.find((m) => m.useful === true)
|
||||
if (usefulMessage) {
|
||||
messages.forEach((m) => {
|
||||
if (m.id !== usefulMessage.id) {
|
||||
remove(_messages, (o) => o.id === m.id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
messages?.slice(0, -1).forEach((m) => {
|
||||
remove(_messages, (o) => o.id === m.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
while (_messages.length > 0 && _messages[_messages.length - 1].role === 'assistant') {
|
||||
_messages.pop()
|
||||
}
|
||||
|
||||
return _messages
|
||||
}
|
||||
|
||||
export function getGroupedMessages(messages: Message[]): { [key: string]: (Message & { index: number })[] } {
|
||||
const groups: { [key: string]: (Message & { index: number })[] } = {}
|
||||
messages.forEach((message, index) => {
|
||||
const key = message.askId ? 'assistant' + message.askId : 'user' + message.id
|
||||
if (key && !groups[key]) {
|
||||
groups[key] = []
|
||||
}
|
||||
groups[key].unshift({ ...message, index })
|
||||
})
|
||||
return groups
|
||||
}
|
||||
|
||||
export function getMessageModelId(message: Message) {
|
||||
return message?.model?.id || message.modelId
|
||||
}
|
||||
|
||||
export function resetAssistantMessage(message: Message, model?: Model): Message {
|
||||
return {
|
||||
...message,
|
||||
model: model || message.model,
|
||||
content: '',
|
||||
status: 'sending',
|
||||
translatedContent: undefined,
|
||||
reasoning_content: undefined,
|
||||
usage: undefined,
|
||||
metrics: undefined,
|
||||
metadata: undefined,
|
||||
useful: undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,7 @@ export const hasModel = (m?: Model) => {
|
||||
|
||||
return allModels.find((model) => model.id === m?.id)
|
||||
}
|
||||
|
||||
export function getModelName(model?: Model) {
|
||||
return model?.name || model?.id || ''
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getDefaultTopic } from './AssistantService'
|
||||
import { getDefaultTranslateAssistant } from './AssistantService'
|
||||
import { getUserMessage } from './MessagesService'
|
||||
|
||||
export const translateText = async (text: string, targetLanguage: string) => {
|
||||
export const translateText = async (text: string, targetLanguage: string, onResponse?: (text: string) => void) => {
|
||||
const translateModel = store.getState().llm.translateModel
|
||||
|
||||
if (!translateModel) {
|
||||
@@ -25,7 +25,7 @@ export const translateText = async (text: string, targetLanguage: string) => {
|
||||
content: text
|
||||
})
|
||||
|
||||
const translatedText = await fetchTranslate({ message, assistant })
|
||||
const translatedText = await fetchTranslate({ message, assistant, onResponse })
|
||||
|
||||
const trimmedText = translatedText.trim()
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 58,
|
||||
version: 61,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -12,6 +12,15 @@ import { createMigrate } from 'redux-persist'
|
||||
import { RootState } from '.'
|
||||
import { DEFAULT_SIDEBAR_ICONS } from './settings'
|
||||
|
||||
// remove logo base64 data to reduce the size of the state
|
||||
function removeMiniAppIconsFromState(state: RootState) {
|
||||
if (state.minapps) {
|
||||
state.minapps.enabled = state.minapps.enabled.map((app) => ({ ...app, logo: undefined }))
|
||||
state.minapps.disabled = state.minapps.disabled.map((app) => ({ ...app, logo: undefined }))
|
||||
state.minapps.pinned = state.minapps.pinned.map((app) => ({ ...app, logo: undefined }))
|
||||
}
|
||||
}
|
||||
|
||||
const migrateConfig = {
|
||||
'2': (state: RootState) => {
|
||||
return {
|
||||
@@ -805,7 +814,7 @@ const migrateConfig = {
|
||||
state.llm.providers.push({
|
||||
id: 'qwenlm',
|
||||
name: 'QwenLM',
|
||||
type: 'openai',
|
||||
type: 'qwenlm',
|
||||
apiKey: '',
|
||||
apiHost: 'https://chat.qwenlm.ai/api/',
|
||||
models: SYSTEM_MODELS.qwenlm,
|
||||
@@ -825,20 +834,7 @@ const migrateConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
if (state.minapps) {
|
||||
state.minapps.enabled = state.minapps.enabled.map((app) => {
|
||||
const _app = DEFAULT_MIN_APPS.find((m) => m.id === app.id)
|
||||
return _app || app
|
||||
})
|
||||
state.minapps.disabled = state.minapps.disabled.map((app) => {
|
||||
const _app = DEFAULT_MIN_APPS.find((m) => m.id === app.id)
|
||||
return _app || app
|
||||
})
|
||||
state.minapps.pinned = state.minapps.pinned.map((app) => {
|
||||
const _app = DEFAULT_MIN_APPS.find((m) => m.id === app.id)
|
||||
return _app || app
|
||||
})
|
||||
}
|
||||
removeMiniAppIconsFromState(state)
|
||||
|
||||
state.llm.providers.forEach((provider) => {
|
||||
if (provider.id === 'qwenlm') {
|
||||
@@ -849,6 +845,49 @@ const migrateConfig = {
|
||||
state.settings.enableQuickAssistant = false
|
||||
state.settings.clickTrayToShowQuickAssistant = true
|
||||
|
||||
return state
|
||||
},
|
||||
'58': (state: RootState) => {
|
||||
if (state.shortcuts) {
|
||||
state.shortcuts.shortcuts.push(
|
||||
{
|
||||
key: 'clear_topic',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'L'],
|
||||
editable: true,
|
||||
enabled: true,
|
||||
system: false
|
||||
},
|
||||
{
|
||||
key: 'toggle_new_context',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'R'],
|
||||
editable: true,
|
||||
enabled: true,
|
||||
system: false
|
||||
}
|
||||
)
|
||||
}
|
||||
return state
|
||||
},
|
||||
'59': (state: RootState) => {
|
||||
if (state.minapps) {
|
||||
const flowith = DEFAULT_MIN_APPS.find((app) => app.id === 'flowith')
|
||||
if (flowith) {
|
||||
state.minapps.enabled.push(flowith)
|
||||
}
|
||||
}
|
||||
removeMiniAppIconsFromState(state)
|
||||
return state
|
||||
},
|
||||
'60': (state: RootState) => {
|
||||
state.settings.multiModelMessageStyle = 'fold'
|
||||
return state
|
||||
},
|
||||
'61': (state: RootState) => {
|
||||
state.llm.providers.forEach((provider) => {
|
||||
if (provider.id === 'qwenlm') {
|
||||
provider.type = 'qwenlm'
|
||||
}
|
||||
})
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ const minAppsSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
setMinApps: (state, action: PayloadAction<MinAppType[]>) => {
|
||||
state.enabled = action.payload
|
||||
state.enabled = action.payload.map((app) => ({ ...app, logo: undefined }))
|
||||
},
|
||||
addMinApp: (state, action: PayloadAction<MinAppType>) => {
|
||||
state.enabled.push(action.payload)
|
||||
},
|
||||
setDisabledMinApps: (state, action: PayloadAction<MinAppType[]>) => {
|
||||
state.disabled = action.payload
|
||||
state.disabled = action.payload.map((app) => ({ ...app, logo: undefined }))
|
||||
},
|
||||
setPinnedMinApps: (state, action: PayloadAction<MinAppType[]>) => {
|
||||
state.pinned = action.payload
|
||||
state.pinned = action.payload.map((app) => ({ ...app, logo: undefined }))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface RuntimeState {
|
||||
minappShow: boolean
|
||||
searching: boolean
|
||||
filesPath: string
|
||||
resourcesPath: string
|
||||
update: UpdateState
|
||||
webdavSync: WebDAVSyncState
|
||||
}
|
||||
@@ -32,6 +33,7 @@ const initialState: RuntimeState = {
|
||||
minappShow: false,
|
||||
searching: false,
|
||||
filesPath: '',
|
||||
resourcesPath: '',
|
||||
update: {
|
||||
info: null,
|
||||
checking: false,
|
||||
@@ -65,6 +67,9 @@ const runtimeSlice = createSlice({
|
||||
setFilesPath: (state, action: PayloadAction<string>) => {
|
||||
state.filesPath = action.payload
|
||||
},
|
||||
setResourcesPath: (state, action: PayloadAction<string>) => {
|
||||
state.resourcesPath = action.payload
|
||||
},
|
||||
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
||||
state.update = { ...state.update, ...action.payload }
|
||||
},
|
||||
@@ -80,6 +85,7 @@ export const {
|
||||
setMinappShow,
|
||||
setSearching,
|
||||
setFilesPath,
|
||||
setResourcesPath,
|
||||
setUpdateState,
|
||||
setWebDAVSyncState
|
||||
} = runtimeSlice.actions
|
||||
|
||||
@@ -63,8 +63,11 @@ export interface SettingsState {
|
||||
narrowMode: boolean
|
||||
enableQuickAssistant: boolean
|
||||
clickTrayToShowQuickAssistant: boolean
|
||||
multiModelMessageStyle: MultiModelMessageStyle
|
||||
}
|
||||
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold'
|
||||
|
||||
const initialState: SettingsState = {
|
||||
showAssistants: true,
|
||||
showTopics: true,
|
||||
@@ -109,7 +112,8 @@ const initialState: SettingsState = {
|
||||
},
|
||||
narrowMode: false,
|
||||
enableQuickAssistant: false,
|
||||
clickTrayToShowQuickAssistant: false
|
||||
clickTrayToShowQuickAssistant: false,
|
||||
multiModelMessageStyle: 'fold'
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -133,6 +137,7 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setLanguage: (state, action: PayloadAction<LanguageVarious>) => {
|
||||
state.language = action.payload
|
||||
window.electron.ipcRenderer.send('miniwindow-reload')
|
||||
},
|
||||
setProxyMode: (state, action: PayloadAction<'system' | 'custom' | 'none'>) => {
|
||||
state.proxyMode = action.payload
|
||||
@@ -250,6 +255,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
|
||||
state.enableQuickAssistant = action.payload
|
||||
},
|
||||
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => {
|
||||
state.multiModelMessageStyle = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -297,7 +305,8 @@ export const {
|
||||
setSidebarIcons,
|
||||
setNarrowMode,
|
||||
setClickTrayToShowQuickAssistant,
|
||||
setEnableQuickAssistant
|
||||
setEnableQuickAssistant,
|
||||
setMultiModelMessageStyle
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -17,6 +17,13 @@ const initialState: ShortcutsState = {
|
||||
enabled: true,
|
||||
system: true
|
||||
},
|
||||
{
|
||||
key: 'mini_window',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'E'],
|
||||
editable: true,
|
||||
enabled: false,
|
||||
system: true
|
||||
},
|
||||
{
|
||||
key: 'new_topic',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'N'],
|
||||
@@ -53,11 +60,18 @@ const initialState: ShortcutsState = {
|
||||
system: false
|
||||
},
|
||||
{
|
||||
key: 'mini_window',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'E'],
|
||||
key: 'clear_topic',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'L'],
|
||||
editable: true,
|
||||
enabled: false,
|
||||
system: true
|
||||
enabled: true,
|
||||
system: false
|
||||
},
|
||||
{
|
||||
key: 'toggle_new_context',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'K'],
|
||||
editable: true,
|
||||
enabled: true,
|
||||
system: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -47,11 +47,13 @@ export type Message = {
|
||||
assistantId: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
reasoning_content?: string
|
||||
translatedContent?: string
|
||||
topicId: string
|
||||
createdAt: string
|
||||
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
||||
modelId?: string
|
||||
model?: Model
|
||||
files?: FileType[]
|
||||
images?: string[]
|
||||
usage?: OpenAI.Completions.CompletionUsage
|
||||
@@ -60,17 +62,19 @@ export type Message = {
|
||||
type: 'text' | '@' | 'clear'
|
||||
isPreset?: boolean
|
||||
mentions?: Model[]
|
||||
model?: Model
|
||||
metadata?: {
|
||||
// Gemini
|
||||
groundingMetadata?: any
|
||||
}
|
||||
askId?: string
|
||||
useful?: boolean
|
||||
}
|
||||
|
||||
export type Metrics = {
|
||||
completion_tokens?: number
|
||||
time_completion_millsec?: number
|
||||
time_first_token_millsec?: number
|
||||
time_thinking_millsec?: number
|
||||
}
|
||||
|
||||
export type Topic = {
|
||||
@@ -137,7 +141,7 @@ export interface Painting {
|
||||
export type MinAppType = {
|
||||
id?: string | number
|
||||
name: string
|
||||
logo: string
|
||||
logo?: string
|
||||
url: string
|
||||
bodered?: boolean
|
||||
background?: string
|
||||
@@ -187,6 +191,7 @@ export type AppInfo = {
|
||||
isPackaged: boolean
|
||||
appPath: string
|
||||
appDataPath: string
|
||||
resourcesPath: string
|
||||
filesPath: string
|
||||
logsPath: string
|
||||
}
|
||||
@@ -227,6 +232,8 @@ export interface KnowledgeBase {
|
||||
created_at: number
|
||||
updated_at: number
|
||||
version: number
|
||||
chunkSize?: number
|
||||
chunkOverlap?: number
|
||||
}
|
||||
|
||||
export type KnowledgeBaseParams = {
|
||||
@@ -236,6 +243,8 @@ export type KnowledgeBaseParams = {
|
||||
apiKey: string
|
||||
apiVersion?: string
|
||||
baseURL: string
|
||||
chunkSize?: number
|
||||
chunkOverlap?: number
|
||||
}
|
||||
|
||||
export type GenerateImageParams = {
|
||||
|
||||
@@ -80,3 +80,22 @@ export function withGeminiGrounding(message: Message) {
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export function withMessageThought(message: Message) {
|
||||
const content = message.content
|
||||
|
||||
const thinkPattern = /<think>(.*?)<\/think>/s
|
||||
const matches = content.match(thinkPattern)
|
||||
|
||||
if (matches) {
|
||||
const reasoning_content = matches[1].trim()
|
||||
const remainingContent = content.replace(thinkPattern, '').trim()
|
||||
if (reasoning_content) {
|
||||
message.reasoning_content = reasoning_content
|
||||
message.content = remainingContent
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -26,6 +26,18 @@ export function isJSON(str: any): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseJSON(str: string) {
|
||||
if (str === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const delay = (seconds: number) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
||||
import MessageErrorBoundary from '@renderer/pages/home/Messages/MessageErrorBoundary'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { getDefaultAssistant, getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { Message } from '@renderer/types'
|
||||
import { isMiniWindow } from '@renderer/utils'
|
||||
import { Dispatch, FC, memo, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@@ -24,7 +25,7 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
|
||||
|
||||
const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetMessages, onGetMessages }) => {
|
||||
const [message, setMessage] = useState(_message)
|
||||
const model = useModel(message.modelId)
|
||||
const model = useModel(getMessageModelId(message))
|
||||
const isBubbleStyle = true
|
||||
const { messageFont, fontSize } = useSettings()
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { Assistant, Message } from '@renderer/types'
|
||||
import { last } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -49,7 +49,7 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant.id, onSendMessage])
|
||||
|
||||
useShortcut('copy_last_message', () => {
|
||||
useHotkeys('c', () => {
|
||||
const lastMessage = last(messages)
|
||||
if (lastMessage) {
|
||||
navigator.clipboard.writeText(lastMessage.content)
|
||||
|
||||
@@ -7,7 +7,8 @@ import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Divider } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -28,11 +29,10 @@ const HomeWindow: FC = () => {
|
||||
const { defaultModel: model } = useDefaultModel()
|
||||
const { language } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const textRef = useRef(text)
|
||||
|
||||
const referenceText = selectedText || clipboardText || text
|
||||
|
||||
textRef.current = referenceText === text ? text : `${referenceText}\n\n${text}`
|
||||
const content = (referenceText === text ? text : `${referenceText}\n\n${text}`).trim()
|
||||
|
||||
const onReadClipboard = useCallback(async () => {
|
||||
const text = await navigator.clipboard.readText()
|
||||
@@ -59,23 +59,25 @@ const HomeWindow: FC = () => {
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (text.trim() === '') {
|
||||
return
|
||||
if (content) {
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
setTimeout(() => setText(''), 100)
|
||||
}
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
setTimeout(() => setText(''), 100)
|
||||
}
|
||||
}
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (prompt?: string) => {
|
||||
const text = textRef.current.trim()
|
||||
if (isEmpty(content)) {
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: prompt ? `${prompt}\n\n${text}` : text,
|
||||
content: prompt ? `${prompt}\n\n${content}` : content,
|
||||
assistantId: defaultAssistant.id,
|
||||
topicId: defaultAssistant.topics[0].id || uuid(),
|
||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
@@ -85,7 +87,7 @@ const HomeWindow: FC = () => {
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
}, 0)
|
||||
},
|
||||
[defaultAssistant]
|
||||
[content, defaultAssistant.id, defaultAssistant.topics]
|
||||
)
|
||||
|
||||
const clearClipboard = () => {
|
||||
@@ -104,12 +106,8 @@ const HomeWindow: FC = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('show-mini-window', () => {
|
||||
onReadClipboard()
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.on('show-mini-window', onReadClipboard)
|
||||
window.electron.ipcRenderer.on('selection-action', (_, { action, selectedText }) => {
|
||||
console.debug('[HomeWindow] selection-action', action, selectedText)
|
||||
selectedText && setSelectedText(selectedText)
|
||||
action && setRoute(action)
|
||||
action === 'chat' && onSendMessage()
|
||||
@@ -176,7 +174,7 @@ const HomeWindow: FC = () => {
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
|
||||
<Main>
|
||||
<FeatureMenus setRoute={setRoute} onSendMessage={onSendMessage} />
|
||||
<FeatureMenus setRoute={setRoute} onSendMessage={onSendMessage} text={content} />
|
||||
</Main>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Footer
|
||||
|
||||
@@ -1,57 +1,90 @@
|
||||
import { BulbOutlined, FileTextOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { Col } from 'antd'
|
||||
import { Dispatch, FC, SetStateAction } from 'react'
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface FeatureMenusProps {
|
||||
text: string
|
||||
setRoute: Dispatch<SetStateAction<'translate' | 'summary' | 'chat' | 'explanation' | 'home'>>
|
||||
onSendMessage: (prompt?: string) => void
|
||||
}
|
||||
|
||||
const FeatureMenus: FC<FeatureMenusProps> = ({ setRoute, onSendMessage }) => {
|
||||
const FeatureMenus: FC<FeatureMenusProps> = ({ text, setRoute, onSendMessage }) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <MessageOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.chat'),
|
||||
active: true,
|
||||
onClick: () => {
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
const features = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <MessageOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.chat'),
|
||||
active: true,
|
||||
onClick: () => {
|
||||
if (text) {
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <TranslationOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.translate'),
|
||||
onClick: () => text && setRoute('translate')
|
||||
},
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.summary'),
|
||||
onClick: () => {
|
||||
if (text) {
|
||||
setRoute('summary')
|
||||
onSendMessage(t('prompts.summarize'))
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <BulbOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.explanation'),
|
||||
onClick: () => {
|
||||
if (text) {
|
||||
setRoute('explanation')
|
||||
onSendMessage(t('prompts.explanation'))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <TranslationOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.translate'),
|
||||
onClick: () => setRoute('translate')
|
||||
},
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.summary'),
|
||||
onClick: () => {
|
||||
setRoute('summary')
|
||||
onSendMessage(t('prompts.summarize'))
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <BulbOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.explanation'),
|
||||
onClick: () => {
|
||||
setRoute('explanation')
|
||||
onSendMessage(t('prompts.explanation'))
|
||||
],
|
||||
[onSendMessage, setRoute, t, text]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : features.length - 1))
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setSelectedIndex((prev) => (prev < features.length - 1 ? prev + 1 : 0))
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
features[selectedIndex].onClick?.()
|
||||
break
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [features, selectedIndex])
|
||||
|
||||
return (
|
||||
<FeatureList>
|
||||
<FeatureListWrapper>
|
||||
{features.map((feature, index) => (
|
||||
<Col span={24} key={index}>
|
||||
<FeatureItem onClick={feature.onClick} className={feature.active ? 'active' : ''}>
|
||||
<FeatureItem onClick={feature.onClick} className={index === selectedIndex ? 'active' : ''}>
|
||||
<FeatureIcon>{feature.icon}</FeatureIcon>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
</FeatureItem>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CopyOutlined, LoginOutlined } from '@ant-design/icons'
|
||||
import { Tag } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -11,11 +13,18 @@ const Footer: FC<FooterProps> = ({ route, onExit }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<WindowFooter onClick={() => onExit()}>
|
||||
<WindowFooter>
|
||||
<FooterText className="nodrag">
|
||||
{t('miniwindow.footer.esc', {
|
||||
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
||||
})}
|
||||
<Tag bordered={false} icon={<LoginOutlined />} onClick={() => onExit()}>
|
||||
{t('miniwindow.footer.esc', {
|
||||
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
||||
})}
|
||||
</Tag>
|
||||
{route !== 'home' && (
|
||||
<Tag bordered={false} icon={<CopyOutlined />}>
|
||||
{t('miniwindow.footer.copy_last_message')}
|
||||
</Tag>
|
||||
)}
|
||||
</FooterText>
|
||||
</WindowFooter>
|
||||
)
|
||||
@@ -29,7 +38,11 @@ const WindowFooter = styled.div`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const FooterText = styled.span`
|
||||
const FooterText = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
@@ -24,7 +24,7 @@ const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown,
|
||||
bordered={false}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setText(e.target.value.trim())}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={generating}
|
||||
/>
|
||||
</InputWrapper>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||
import { Select, Space } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -44,7 +45,7 @@ const Translate: FC<Props> = ({ text }) => {
|
||||
content: text,
|
||||
assistantId: assistant.id,
|
||||
topicId: uuid(),
|
||||
modelId: translateModel.id,
|
||||
model: translateModel,
|
||||
createdAt: new Date().toISOString(),
|
||||
type: 'text',
|
||||
status: 'sending'
|
||||
@@ -71,6 +72,10 @@ const Translate: FC<Props> = ({ text }) => {
|
||||
translate()
|
||||
}, [translate])
|
||||
|
||||
useHotkeys('c', () => {
|
||||
navigator.clipboard.writeText(result)
|
||||
})
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<MenuContainer>
|
||||
|
||||
@@ -1693,7 +1693,7 @@ __metadata:
|
||||
|
||||
"@llm-tools/embedjs@patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch":
|
||||
version: 0.1.25
|
||||
resolution: "@llm-tools/embedjs@patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch::version=0.1.25&hash=7b05b5"
|
||||
resolution: "@llm-tools/embedjs@patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch::version=0.1.25&hash=3b8a9c"
|
||||
dependencies:
|
||||
"@langchain/textsplitters": "npm:^0.1.0"
|
||||
"@llm-tools/embedjs-interfaces": "npm:0.1.25"
|
||||
@@ -1703,7 +1703,7 @@ __metadata:
|
||||
md5: "npm:^2.3.0"
|
||||
mime: "npm:^4.0.6"
|
||||
stream-mime-type: "npm:^2.0.0"
|
||||
checksum: 10c0/d0a37a5c7232571a71eff7e90ff4ba612bf33022a6eccd933c3a778844320f427a936d0851aae00092e34407c8c2f3555fe4444c6f2139f978ecfdd42fd89375
|
||||
checksum: 10c0/3ef5fb0068e662d9fc3ff794c0c200fca91fba548d1989a628ad2c3576e3f97838f3abca683adc77b1774d57e09c6d155c1c4b9d69eb20aac26bd274148f72a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user