Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
2457c7b818
15
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
15
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@ -6,7 +6,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
Thank you for taking the time to fill out this bug report!
|
||||
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
@ -15,9 +16,11 @@ body:
|
||||
description: |
|
||||
Before submitting an issue, please make sure you have completed the following steps
|
||||
options:
|
||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
||||
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
|
||||
required: true
|
||||
- label: I have filled out the issue title correctly.
|
||||
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues) and [Closed Issues]( https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20), no similar issue was found.
|
||||
required: true
|
||||
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -45,7 +48,7 @@ body:
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
description: Please be as detailed as possible when describing the problem
|
||||
placeholder: Tell us what happened...
|
||||
validations:
|
||||
required: true
|
||||
@ -54,7 +57,7 @@ body:
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
@ -82,4 +85,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here
|
||||
description: Anything that gives us a better understanding of the problem you're experiencing
|
||||
|
||||
29
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
29
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@ -6,7 +6,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to suggest a new feature!
|
||||
Thank you for taking the time to submit a feature request!
|
||||
Before submitting this issue, please make sure you have reviewed the [Project Roadmap](https://docs.cherry-ai.com/cherrystudio/planning) and the [Feature Overview](https://docs.cherry-ai.com/cherrystudio/preview).
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
@ -15,9 +16,13 @@ body:
|
||||
description: |
|
||||
Before submitting an issue, please make sure you have completed the following steps
|
||||
options:
|
||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
||||
- label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue.
|
||||
required: true
|
||||
- label: I have filled out the issue title correctly.
|
||||
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues) and [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed) and did not find a similar suggestion.
|
||||
required: true
|
||||
- label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck."
|
||||
required: true
|
||||
- label: The latest version of Cherry Studio does not include the feature I am suggesting.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -44,28 +49,28 @@ body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: A clear and concise description of what the problem is
|
||||
placeholder: I'm always frustrated when...
|
||||
label: Is your feature request related to an existing issue?
|
||||
description: Please briefly describe the problem you are experiencing.
|
||||
placeholder: I often feel frustrated because...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen
|
||||
label: Desired Solution
|
||||
description: Please briefly describe what you would like to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered
|
||||
label: Alternative Solutions
|
||||
description: Please briefly describe any alternative solutions or features you have considered.
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here
|
||||
label: Additional Information
|
||||
description: Add any other context or screenshots related to your feature request.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/2_question.yml
vendored
20
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@ -1,12 +1,12 @@
|
||||
name: ❓ Question
|
||||
description: Ask a question or seek help
|
||||
title: '[Question]: '
|
||||
name: ❓ Discussion & Questions
|
||||
description: Seeking help, discussing issues, asking questions, etc...
|
||||
title: '[Discussion]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
|
||||
Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
@ -15,9 +15,9 @@ body:
|
||||
description: |
|
||||
Before submitting an issue, please make sure you have completed the following steps
|
||||
options:
|
||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
||||
- label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue.
|
||||
required: true
|
||||
- label: I have filled out the issue title correctly.
|
||||
- label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -45,8 +45,8 @@ body:
|
||||
id: question
|
||||
attributes:
|
||||
label: Your Question
|
||||
description: Please describe your question in detail
|
||||
placeholder: Please explain your question as clearly as possible...
|
||||
description: Please describe your issue in detail.
|
||||
placeholder: Please explain your issue as clearly as possible...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -68,9 +68,9 @@ body:
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How urgent is this question for you?
|
||||
description: How urgent is this issue for you?
|
||||
options:
|
||||
- Low (Can wait)
|
||||
- Low (Review when available)
|
||||
- Medium (Would like a response soon)
|
||||
- High (Blocking progress)
|
||||
validations:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
name: 🐛 错误报告
|
||||
name: 🐛 错误报告 (中文)
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['bug']
|
||||
@ -7,17 +7,20 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间填写此错误报告!
|
||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue 检查清单
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 正确填写了 Issue 标题。
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -45,7 +48,7 @@ body:
|
||||
id: description
|
||||
attributes:
|
||||
label: 错误描述
|
||||
description: 清晰简洁地描述错误是什么
|
||||
description: 描述问题时请尽可能详细
|
||||
placeholder: 告诉我们发生了什么...
|
||||
validations:
|
||||
required: true
|
||||
@ -54,7 +57,7 @@ body:
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 重现行为的步骤
|
||||
description: 提供详细的重现步骤,以便于我们可以准确地重现问题
|
||||
placeholder: |
|
||||
1. 转到 '...'
|
||||
2. 点击 '....'
|
||||
@ -82,4 +85,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 在此添加有关问题的任何其他上下文
|
||||
description: 任何能让我们对你所遇到的问题有更多了解的东西
|
||||
@ -1,4 +1,4 @@
|
||||
name: 💡 功能建议
|
||||
name: 💡 功能建议 (中文)
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['enhancement']
|
||||
@ -7,17 +7,22 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出新的功能建议!
|
||||
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue 检查清单
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 正确填写了 Issue 标题。
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||
required: true
|
||||
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -44,7 +49,7 @@ body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 您的功能建议是否与某个问题相关?
|
||||
label: 您的功能建议是否与某个问题/issue相关?
|
||||
description: 请简明扼要地描述您遇到的问题
|
||||
placeholder: 我总是感到沮丧,因为...
|
||||
validations:
|
||||
@ -1,6 +1,6 @@
|
||||
name: ❓ 提问
|
||||
description: 提出一个问题或寻求帮助
|
||||
title: '[问题]: '
|
||||
name: ❓ 讨论 & 提问 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
@ -15,9 +15,9 @@ body:
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 正确填写了 Issue 标题。
|
||||
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
@ -80,11 +80,9 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
增加服务商 LM Studio、魔搭、Perplexity、无问芯穹、DMXAPI
|
||||
提及功能支持上下按键循环选择模型
|
||||
小程序增加小艺
|
||||
增加Notion连接检测功能
|
||||
编辑模型弹窗搜索模型时,同时搜索模型的名字和ID
|
||||
编辑模型弹窗增加推理模型筛选按钮
|
||||
修复思考模型思考时间显示错误
|
||||
修复部分模型翻译出错
|
||||
消息分组支持网格模式
|
||||
知识库支持多选
|
||||
支持库增加 DRAFTS, EPUB
|
||||
知识库支持调节匹配度阈值
|
||||
添加 NotebookLM, Coze 小程序
|
||||
增加话题提示词
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.9.24",
|
||||
"version": "0.9.25",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@ -137,7 +137,7 @@
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-mathjax": "^6.0.0",
|
||||
"rehype-mathjax": "^7.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
|
||||
@ -2,6 +2,7 @@ export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||
export const bookExts = ['.epub']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
|
||||
22
src/main/loader/draftsExportLoader.ts
Normal file
22
src/main/loader/draftsExportLoader.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
import { JsonLoader } from '@llm-tools/embedjs'
|
||||
|
||||
/**
|
||||
* Drafts 应用导出的笔记文件加载器
|
||||
* 原始文件是一个 JSON 数组。每条笔记只保留 content、tags、modified_at 三个字段
|
||||
*/
|
||||
export class DraftsExportLoader extends JsonLoader {
|
||||
constructor(filePath: string) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const rawJson = JSON.parse(fileContent) as any[]
|
||||
const json = rawJson.map((item) => {
|
||||
return {
|
||||
content: item.content?.replace(/\n/g, '<br>'),
|
||||
tags: item.tags,
|
||||
modified_at: item.created_at
|
||||
}
|
||||
})
|
||||
super({ object: json })
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
import { LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||
import { LoaderReturn } from '@shared/config/types'
|
||||
import { FileType, KnowledgeBaseParams } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { DraftsExportLoader } from './draftsExportLoader'
|
||||
import { EpubLoader } from './epubLoader'
|
||||
import { OdLoader, OdType } from './odLoader'
|
||||
|
||||
// embedjs内置loader类型
|
||||
const commonExts = ['.pdf', '.csv', '.json', '.docx', '.pptx', '.xlsx', '.md']
|
||||
const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
|
||||
|
||||
export async function addOdLoader(
|
||||
ragApplication: RAGApplication,
|
||||
@ -89,7 +90,19 @@ export async function addFileLoader(
|
||||
} as LoaderReturn
|
||||
}
|
||||
|
||||
// DraftsExport类型 (file.ext会自动转换成小写)
|
||||
if (['.draftsexport'].includes(file.ext)) {
|
||||
const loaderReturn = await ragApplication.addLoader(new DraftsExportLoader(file.path) as any, forceReload)
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
}
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(file.path, 'utf-8')
|
||||
|
||||
// HTML类型
|
||||
if (['.html', '.htm'].includes(file.ext)) {
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
@ -108,6 +121,18 @@ export async function addFileLoader(
|
||||
}
|
||||
}
|
||||
|
||||
// JSON类型
|
||||
if (['.json'].includes(file.ext)) {
|
||||
const jsonObject = JSON.parse(fileContent)
|
||||
const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject }))
|
||||
return {
|
||||
entriesAdded: loaderReturn.entriesAdded,
|
||||
uniqueId: loaderReturn.uniqueId,
|
||||
uniqueIds: [loaderReturn.uniqueId],
|
||||
loaderType: loaderReturn.loaderType
|
||||
}
|
||||
}
|
||||
|
||||
// 文本类型
|
||||
const loaderReturn = await ragApplication.addLoader(
|
||||
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||
|
||||
@ -47,8 +47,8 @@ function formatShortcutKey(shortcut: string[]): string {
|
||||
|
||||
function handleZoom(delta: number) {
|
||||
return (window: BrowserWindow) => {
|
||||
const currentZoom = window.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + delta
|
||||
const currentZoom = configManager.getZoomFactor()
|
||||
const newZoom = Number((currentZoom + delta).toFixed(1))
|
||||
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
||||
window.webContents.setZoomFactor(newZoom)
|
||||
configManager.setZoomFactor(newZoom)
|
||||
@ -110,7 +110,9 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
|
||||
}
|
||||
|
||||
export function registerShortcuts(window: BrowserWindow) {
|
||||
window.once('ready-to-show', () => {
|
||||
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||
})
|
||||
|
||||
const register = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
|
||||
@ -64,6 +64,10 @@
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:has(+ ul) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
|
||||
@ -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 { AppLogo } from '@renderer/config/env'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
@ -49,7 +50,10 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
}
|
||||
|
||||
const onOpenLink = () => {
|
||||
window.api.openWebsite(app.url)
|
||||
if (webviewRef.current) {
|
||||
const currentUrl = webviewRef.current.getURL()
|
||||
window.api.openWebsite(currentUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const onTogglePin = () => {
|
||||
@ -236,6 +240,10 @@ export default class MinApp {
|
||||
await delay(0)
|
||||
}
|
||||
|
||||
if (!app.logo) {
|
||||
app.logo = AppLogo
|
||||
}
|
||||
|
||||
MinApp.app = app
|
||||
store.dispatch(setMinappShow(true))
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Input, Modal } from 'antd'
|
||||
import { TextAreaProps } from 'antd/es/input'
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { TopView } from '../TopView'
|
||||
@ -27,6 +27,7 @@ const PromptPopupContainer: React.FC<Props> = ({
|
||||
}) => {
|
||||
const [value, setValue] = useState(defaultValue)
|
||||
const [open, setOpen] = useState(true)
|
||||
const textAreaRef = useRef<any>(null)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
@ -41,17 +42,35 @@ const PromptPopupContainer: React.FC<Props> = ({
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
const handleAfterOpenChange = (visible: boolean) => {
|
||||
if (visible) {
|
||||
const textArea = textAreaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
textArea.focus()
|
||||
const length = textArea.value.length
|
||||
textArea.setSelectionRange(length, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PromptPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered>
|
||||
<Modal
|
||||
title={title}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
afterOpenChange={handleAfterOpenChange}
|
||||
centered>
|
||||
<Box mb={8}>{message}</Box>
|
||||
<Input.TextArea
|
||||
ref={textAreaRef}
|
||||
placeholder={inputPlaceholder}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
allowClear
|
||||
autoFocus
|
||||
onPressEnter={onOk}
|
||||
rows={1}
|
||||
{...inputProps}
|
||||
|
||||
@ -51,6 +51,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
setTimeout(resizeTextArea, 0)
|
||||
}, [])
|
||||
|
||||
const handleAfterOpenChange = (visible: boolean) => {
|
||||
if (visible) {
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
textArea.focus()
|
||||
const length = textArea.value.length
|
||||
textArea.setSelectionRange(length, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextEditPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
@ -65,6 +76,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
afterOpenChange={handleAfterOpenChange}
|
||||
centered>
|
||||
<TextArea
|
||||
ref={textareaRef}
|
||||
|
||||
@ -50,6 +50,7 @@ const Sidebar: FC = () => {
|
||||
|
||||
const onOpenDocs = () => {
|
||||
MinApp.start({
|
||||
id: 'docs',
|
||||
name: t('docs.title'),
|
||||
url: 'https://docs.cherry-ai.com/',
|
||||
logo: AppLogo
|
||||
@ -77,9 +78,11 @@ const Sidebar: FC = () => {
|
||||
</AppsContainer>
|
||||
)}
|
||||
</MainMenusContainer>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<Menus>
|
||||
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<Icon onClick={onOpenDocs}>
|
||||
<Icon
|
||||
onClick={onOpenDocs}
|
||||
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
|
||||
<QuestionCircleOutlined />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
@ -93,8 +96,14 @@ 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' : ''}>
|
||||
<StyledLink
|
||||
onClick={async () => {
|
||||
if (minappShow) {
|
||||
await MinApp.close()
|
||||
}
|
||||
await to(isLocalAi ? '/settings/assistant' : '/settings/provider')
|
||||
}}>
|
||||
<Icon className={pathname.startsWith('/settings') && !minappShow ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
@ -108,10 +117,11 @@ const MainMenus: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { pathname } = useLocation()
|
||||
const { sidebarIcons } = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
|
||||
|
||||
const iconMap = {
|
||||
assistants: <i className="iconfont icon-chat" />,
|
||||
@ -139,7 +149,13 @@ const MainMenus: FC = () => {
|
||||
|
||||
return (
|
||||
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => navigate(path)}>
|
||||
<StyledLink
|
||||
onClick={async () => {
|
||||
if (minappShow) {
|
||||
await MinApp.close()
|
||||
}
|
||||
navigate(path)
|
||||
}}>
|
||||
<Icon className={isActive}>{iconMap[icon]}</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
@ -150,6 +166,7 @@ const MainMenus: FC = () => {
|
||||
const PinnedApps: FC = () => {
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
const { minappShow } = useRuntime()
|
||||
|
||||
return (
|
||||
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
||||
@ -164,11 +181,12 @@ const PinnedApps: FC = () => {
|
||||
}
|
||||
}
|
||||
]
|
||||
const isActive = minappShow && MinApp.app?.id === app.id
|
||||
return (
|
||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Icon onClick={() => MinApp.start(app)}>
|
||||
<Icon onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
|
||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||
</Icon>
|
||||
</Dropdown>
|
||||
|
||||
@ -3,6 +3,7 @@ import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
|
||||
import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url'
|
||||
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'
|
||||
@ -18,6 +19,7 @@ import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url'
|
||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
||||
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
|
||||
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
|
||||
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
||||
@ -29,10 +31,8 @@ import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
||||
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url'
|
||||
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
||||
import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url'
|
||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
||||
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url'
|
||||
@ -171,7 +171,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'perplexity',
|
||||
name: 'perplexity',
|
||||
name: 'Perplexity',
|
||||
logo: PerplexityAppLogo,
|
||||
url: 'https://www.perplexity.ai/'
|
||||
},
|
||||
@ -306,7 +306,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
id: 'notebooklm',
|
||||
name: 'NotebookLM',
|
||||
logo: NotebookLMAppLogo,
|
||||
url: 'https://notebooklm.google.com/',
|
||||
url: 'https://notebooklm.google.com/'
|
||||
},
|
||||
{
|
||||
id: 'coze',
|
||||
|
||||
@ -161,7 +161,7 @@ export const VISION_REGEX = new RegExp(
|
||||
)
|
||||
|
||||
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
|
||||
export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\breasoner\b.*|.*-[rR]\d+.*)$/i
|
||||
export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*)$/i
|
||||
|
||||
export const EMBEDDING_REGEX =
|
||||
/(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
|
||||
@ -1326,34 +1326,70 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
],
|
||||
dmxapi: [
|
||||
{
|
||||
id: 'gpt-3.5-turbo',
|
||||
id: 'Qwen/Qwen2.5-7B-Instruct',
|
||||
provider: 'dmxapi',
|
||||
name: 'GPT-3.5-Turbo',
|
||||
group: 'OpenAI'
|
||||
name: 'Qwen/Qwen2.5-7B-Instruct',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'ERNIE-Speed-128K',
|
||||
provider: 'dmxapi',
|
||||
name: 'ERNIE-Speed-128K',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
provider: 'dmxapi',
|
||||
name: 'THUDM/glm-4-9b-chat',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flash',
|
||||
provider: 'dmxapi',
|
||||
name: 'glm-4-flash',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'hunyuan-lite',
|
||||
provider: 'dmxapi',
|
||||
name: 'hunyuan-lite',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'dmxapi',
|
||||
name: 'GPT-4o',
|
||||
name: 'gpt-4o',
|
||||
group: 'OpenAI'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'dmxapi',
|
||||
name: 'GPT-4o-Mini',
|
||||
name: 'gpt-4o-mini',
|
||||
group: 'OpenAI'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-reasoner',
|
||||
id: 'DMXAPI-DeepSeek-R1',
|
||||
provider: 'dmxapi',
|
||||
name: 'DeepSeek Reasoner',
|
||||
name: 'DMXAPI-DeepSeek-R1',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
id: 'DMXAPI-DeepSeek-V3',
|
||||
provider: 'dmxapi',
|
||||
name: 'DeepSeek Chat',
|
||||
name: 'DMXAPI-DeepSeek-V3',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
provider: 'dmxapi',
|
||||
name: 'claude-3-5-sonnet-20241022',
|
||||
group: 'Claude'
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.0-flash',
|
||||
provider: 'dmxapi',
|
||||
name: 'gemini-2.0-flash',
|
||||
group: 'Gemini'
|
||||
}
|
||||
],
|
||||
perplexity: [
|
||||
|
||||
@ -212,11 +212,11 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
dmxapi: {
|
||||
api: {
|
||||
url: 'https://api.dmxapi.com'
|
||||
url: 'https://www.dmxapi.com'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://dmxapi.com/',
|
||||
apiKey: 'https://www.dmxapi.com/token',
|
||||
official: 'https://www.dmxapi.com/register?aff=81aj/',
|
||||
apiKey: 'https://www.dmxapi.com/register?aff=81aj',
|
||||
docs: 'https://dmxapi.com/models.html#code-block',
|
||||
models: 'https://www.dmxapi.com/pricing'
|
||||
}
|
||||
@ -526,7 +526,7 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cloud.baidu.com/',
|
||||
apiKey: 'https://cloud.baidu.com/console/qianfan/apikey',
|
||||
apiKey: 'https://console.bce.baidu.com/iam/#/iam/apikey/list',
|
||||
docs: 'https://cloud.baidu.com/doc/index.html',
|
||||
models: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu'
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export function useAssistant(id: string) {
|
||||
|
||||
return {
|
||||
assistant,
|
||||
model: assistant?.model ?? defaultModel,
|
||||
model: assistant?.model ?? assistant?.defaultModel ?? defaultModel,
|
||||
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
||||
removeTopic: (topic: Topic) => {
|
||||
TopicManager.removeTopic(topic.id)
|
||||
|
||||
@ -307,16 +307,22 @@ export const useKnowledgeBases = () => {
|
||||
|
||||
// remove assistant knowledge_base
|
||||
const _assistants = assistants.map((assistant) => {
|
||||
if (assistant.knowledge_base?.id === baseId) {
|
||||
return { ...assistant, knowledge_base: undefined }
|
||||
if (assistant.knowledge_bases?.find((kb) => kb.id === baseId)) {
|
||||
return {
|
||||
...assistant,
|
||||
knowledge_bases: assistant.knowledge_bases.filter((kb) => kb.id !== baseId)
|
||||
}
|
||||
}
|
||||
return assistant
|
||||
})
|
||||
|
||||
// remove agent knowledge_base
|
||||
const _agents = agents.map((agent) => {
|
||||
if (agent.knowledge_base?.id === baseId) {
|
||||
return { ...agent, knowledge_base: undefined }
|
||||
if (agent.knowledge_bases?.find((kb) => kb.id === baseId)) {
|
||||
return {
|
||||
...agent,
|
||||
knowledge_bases: agent.knowledge_bases.filter((kb) => kb.id !== baseId)
|
||||
}
|
||||
}
|
||||
return agent
|
||||
})
|
||||
|
||||
@ -364,6 +364,7 @@
|
||||
"message.multi_model_style.fold": "Fold",
|
||||
"message.multi_model_style.horizontal": "Horizontal",
|
||||
"message.multi_model_style.vertical": "Vertical",
|
||||
"message.multi_model_style.grid": "Grid",
|
||||
"message.style": "Message style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain",
|
||||
@ -559,7 +560,9 @@
|
||||
},
|
||||
"data.title": "Data Directory",
|
||||
"notion.api_key": "Notion API Key",
|
||||
"notion.api_key_placeholder": "Enter Notion API Key",
|
||||
"notion.database_id": "Notion Database ID",
|
||||
"notion.database_id_placeholder": "Enter Notion Database ID",
|
||||
"notion.title": "Notion Configuration",
|
||||
"notion.check": {
|
||||
"button": "Check",
|
||||
@ -636,6 +639,10 @@
|
||||
"messages.input.title": "Input Settings",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input message",
|
||||
"messages.math_engine": "Math engine",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||
"messages.grid_popover_trigger.hover": "Hover to display",
|
||||
"messages.grid_popover_trigger.click": "Click to display",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.title": "Message Settings",
|
||||
@ -708,7 +715,7 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "Action",
|
||||
"alt_warning": "Mac does not support Option + letters as shortcuts",
|
||||
"alt_warning": "On Mac, Option key combinations only work with the Space key",
|
||||
"clear_shortcut": "Clear Shortcut",
|
||||
"clear_topic": "Clear Messages",
|
||||
"copy_last_message": "Copy Last Message",
|
||||
|
||||
@ -363,6 +363,7 @@
|
||||
"message.multi_model_style.fold": "折りたたむ",
|
||||
"message.multi_model_style.horizontal": "水平",
|
||||
"message.multi_model_style.vertical": "垂直",
|
||||
"message.multi_model_style.grid": "グリッド",
|
||||
"message.style": "メッセージスタイル",
|
||||
"message.style.bubble": "バブル",
|
||||
"message.style.plain": "プレーン",
|
||||
@ -559,7 +560,9 @@
|
||||
},
|
||||
"data.title": "データディレクトリ",
|
||||
"notion.api_key": "Notion APIキー",
|
||||
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||
"notion.database_id": "Notion データベースID",
|
||||
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
|
||||
"notion.title": "Notion 設定",
|
||||
"notion.check": {
|
||||
"button": "確認",
|
||||
@ -636,6 +639,10 @@
|
||||
"messages.input.title": "入力設定",
|
||||
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
|
||||
"messages.math_engine": "数式エンジン",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||
"messages.grid_popover_trigger.hover": "ホバーで表示",
|
||||
"messages.grid_popover_trigger.click": "クリックで表示",
|
||||
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
||||
"messages.model.title": "モデル設定",
|
||||
"messages.title": "メッセージ設定",
|
||||
@ -708,7 +715,7 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
"alt_warning": "MacではOption + 文字をショートカットとして使用できません",
|
||||
"alt_warning": "MacではOptionキーとの組み合わせは、スペースキーのみ使用可能です",
|
||||
"clear_shortcut": "ショートカットをクリア",
|
||||
"clear_topic": "メッセージを消去",
|
||||
"copy_last_message": "最後のメッセージをコピー",
|
||||
|
||||
@ -364,6 +364,7 @@
|
||||
"message.multi_model_style.fold": "Свернуть",
|
||||
"message.multi_model_style.horizontal": "Горизонтальный",
|
||||
"message.multi_model_style.vertical": "Вертикальный",
|
||||
"message.multi_model_style.grid": "клетчатый вид",
|
||||
"message.style": "Стиль сообщения",
|
||||
"message.style.bubble": "Пузырь",
|
||||
"message.style.plain": "Простой",
|
||||
@ -559,7 +560,9 @@
|
||||
},
|
||||
"data.title": "Каталог данных",
|
||||
"notion.api_key": "Ключ API Notion",
|
||||
"notion.api_key_placeholder": "Введите ключ API Notion",
|
||||
"notion.database_id": "ID базы данных Notion",
|
||||
"notion.database_id_placeholder": "Введите ID базы данных Notion",
|
||||
"notion.title": "Настройки Notion",
|
||||
"notion.check": {
|
||||
"button": "Проверить",
|
||||
@ -637,6 +640,10 @@
|
||||
"messages.math_engine": "Математический движок",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
||||
"messages.model.title": "Настройки модели",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||
"messages.grid_popover_trigger.hover": "Наведение для отображения",
|
||||
"messages.grid_popover_trigger.click": "Нажатие для отображения",
|
||||
"messages.title": "Настройки сообщений",
|
||||
"messages.use_serif_font": "Использовать serif шрифт",
|
||||
"model": "Модель по умолчанию",
|
||||
@ -707,7 +714,7 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "Действие",
|
||||
"alt_warning": "Mac не поддерживает Option + буквы как горячие клавиши",
|
||||
"alt_warning": "В Mac сочетания с клавишей Option работают только с пробелом",
|
||||
"clear_shortcut": "Очистить сочетание клавиш",
|
||||
"clear_topic": "Очистить все сообщения",
|
||||
"copy_last_message": "Копировать последнее сообщение",
|
||||
|
||||
@ -366,6 +366,7 @@
|
||||
"message.multi_model_style.fold": "折叠",
|
||||
"message.multi_model_style.horizontal": "水平",
|
||||
"message.multi_model_style.vertical": "垂直",
|
||||
"message.multi_model_style.grid": "网格",
|
||||
"message.style": "消息样式",
|
||||
"message.style.bubble": "气泡",
|
||||
"message.style.plain": "简洁",
|
||||
@ -559,7 +560,9 @@
|
||||
},
|
||||
"data.title": "数据目录",
|
||||
"notion.api_key": "Notion 密钥",
|
||||
"notion.database_id": "Notion 数据库ID",
|
||||
"notion.api_key_placeholder": "请输入Notion 密钥",
|
||||
"notion.database_id": "Notion 数据库 ID",
|
||||
"notion.database_id_placeholder": "请输入Notion 数据库 ID",
|
||||
"notion.title": "Notion 配置",
|
||||
"notion.check": {
|
||||
"button": "检查",
|
||||
@ -636,6 +639,10 @@
|
||||
"messages.input.title": "输入设置",
|
||||
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||
"messages.math_engine": "数学公式引擎",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
"messages.grid_popover_trigger": "网格详情触发",
|
||||
"messages.grid_popover_trigger.hover": "悬停显示",
|
||||
"messages.grid_popover_trigger.click": "点击显示",
|
||||
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||
"messages.model.title": "模型设置",
|
||||
"messages.title": "消息设置",
|
||||
@ -708,7 +715,7 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
"alt_warning": "Mac 系统不能使用 Option + 字母作为快捷键",
|
||||
"alt_warning": "Mac 系统中 Option 键只能与空格键组合使用",
|
||||
"clear_shortcut": "清除快捷键",
|
||||
"clear_topic": "清空消息",
|
||||
"copy_last_message": "复制上一条消息",
|
||||
|
||||
@ -364,6 +364,7 @@
|
||||
"message.multi_model_style.fold": "折疊",
|
||||
"message.multi_model_style.horizontal": "水平",
|
||||
"message.multi_model_style.vertical": "垂直",
|
||||
"message.multi_model_style.grid": "网格",
|
||||
"message.style": "消息樣式",
|
||||
"message.style.bubble": "氣泡",
|
||||
"message.style.plain": "簡潔",
|
||||
@ -557,7 +558,9 @@
|
||||
},
|
||||
"data.title": "數據目錄",
|
||||
"notion.api_key": "Notion 金鑰",
|
||||
"notion.api_key_placeholder": "請輸入Notion 密鑰",
|
||||
"notion.database_id": "Notion 資料庫 ID",
|
||||
"notion.database_id_placeholder": "請輸入Notion 資料庫 ID",
|
||||
"notion.title": "Notion 配置",
|
||||
"notion.check": {
|
||||
"button": "檢查",
|
||||
@ -635,6 +638,10 @@
|
||||
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
|
||||
"messages.input.title": "輸入設定",
|
||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||
"messages.grid_columns": "消息網格展示列數",
|
||||
"messages.grid_popover_trigger": "網格詳情觸發",
|
||||
"messages.grid_popover_trigger.hover": "懸停顯示",
|
||||
"messages.grid_popover_trigger.click": "點擊顯示",
|
||||
"messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||
"messages.model.title": "模型設定",
|
||||
"messages.title": "訊息設定",
|
||||
@ -707,7 +714,7 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
"alt_warning": "Mac 不能使用 Option + 字母作為快捷鍵",
|
||||
"alt_warning": "Mac 系統中 Option 鍵只能與空白鍵組合使用",
|
||||
"clear_shortcut": "清除快捷鍵",
|
||||
"clear_topic": "清除所有訊息",
|
||||
"copy_last_message": "複製上一条消息",
|
||||
|
||||
@ -9,7 +9,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Agent, KnowledgeBase } from '@renderer/types'
|
||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
@ -25,7 +25,7 @@ type FieldType = {
|
||||
id: string
|
||||
name: string
|
||||
prompt: string
|
||||
knowledge_base_id: string
|
||||
knowledge_base_id: string[]
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
@ -37,8 +37,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [emoji, setEmoji] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const knowledgeState = useAppSelector((state) => state.knowledge)
|
||||
const knowledgeOptions: SelectProps['options'] = []
|
||||
const showKnowledgeIcon = useSidebarIconShow('knowledge')
|
||||
const knowledgeOptions: SelectProps['options'] = []
|
||||
|
||||
knowledgeState.bases.forEach((base) => {
|
||||
knowledgeOptions.push({
|
||||
@ -57,7 +57,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const _agent: Agent = {
|
||||
id: uuid(),
|
||||
name: values.name,
|
||||
knowledge_base: knowledgeState.bases.find((t) => t.id === values.knowledge_base_id),
|
||||
knowledge_bases: values.knowledge_base_id
|
||||
.map((id) => knowledgeState.bases.find((t) => t.id === id))
|
||||
.filter((base): base is KnowledgeBase => base !== undefined),
|
||||
emoji: _emoji,
|
||||
prompt: values.prompt,
|
||||
defaultModel: getDefaultModel(),
|
||||
@ -156,10 +158,16 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
{showKnowledgeIcon && (
|
||||
<Form.Item name="knowledge_base_id" label={t('agents.add.knowledge_base')} rules={[{ required: false }]}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder={t('agents.add.knowledge_base.placeholder')}
|
||||
menuItemSelectedIcon={<CheckOutlined />}
|
||||
options={knowledgeOptions}
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@ -52,7 +52,6 @@ interface Props {
|
||||
|
||||
let _text = ''
|
||||
let _files: FileType[] = []
|
||||
let _base: KnowledgeBase | undefined
|
||||
|
||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
@ -83,7 +82,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
|
||||
|
||||
@ -104,7 +103,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
|
||||
_text = text
|
||||
_files = files
|
||||
_base = selectedKnowledgeBase
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
@ -124,8 +122,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
status: 'success'
|
||||
}
|
||||
|
||||
if (selectedKnowledgeBase) {
|
||||
message.knowledgeBaseIds = [selectedKnowledgeBase.id]
|
||||
if (selectedKnowledgeBases) {
|
||||
message.knowledgeBaseIds = selectedKnowledgeBases.map((base) => base.id)
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
@ -144,7 +142,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
setExpend(false)
|
||||
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files, mentionModels])
|
||||
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBases, files, mentionModels])
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
@ -458,14 +456,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKnowledgeBase(showKnowledgeIcon ? assistant.knowledge_base : undefined)
|
||||
}, [assistant.id, assistant.knowledge_base, showKnowledgeIcon])
|
||||
// if assistant knowledge bases are undefined return []
|
||||
setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : [])
|
||||
}, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon])
|
||||
|
||||
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
||||
|
||||
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
||||
updateAssistant({ ...assistant, knowledge_base: base })
|
||||
setSelectedKnowledgeBase(base)
|
||||
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
|
||||
updateAssistant({ ...assistant, knowledge_bases: bases })
|
||||
setSelectedKnowledgeBases(bases ?? [])
|
||||
}
|
||||
|
||||
const onMentionModel = (model: Model) => {
|
||||
@ -573,7 +572,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
</Tooltip>
|
||||
{showKnowledgeIcon && (
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
selectedBases={selectedKnowledgeBases}
|
||||
onSelect={handleKnowledgeBaseSelect}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={files.length > 0}
|
||||
|
||||
@ -1,71 +1,68 @@
|
||||
import { FileSearchOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, FileSearchOutlined } from '@ant-design/icons'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { KnowledgeBase } from '@renderer/types'
|
||||
import { Button, Popover, Tooltip } from 'antd'
|
||||
import { Popover, Select, SelectProps, Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
selectedBase?: KnowledgeBase
|
||||
onSelect: (base?: KnowledgeBase) => void
|
||||
selectedBases?: KnowledgeBase[]
|
||||
onSelect: (bases: KnowledgeBase[]) => void
|
||||
disabled?: boolean
|
||||
ToolbarButton?: any
|
||||
}
|
||||
|
||||
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
|
||||
const KnowledgeBaseSelector: FC<Props> = ({ selectedBases, onSelect }) => {
|
||||
const { t } = useTranslation()
|
||||
const knowledgeState = useAppSelector((state) => state.knowledge)
|
||||
const knowledgeOptions: SelectProps['options'] = knowledgeState.bases.map((base) => ({
|
||||
label: base.name,
|
||||
value: base.id
|
||||
}))
|
||||
|
||||
return (
|
||||
<SelectorContainer>
|
||||
{knowledgeState.bases.length === 0 ? (
|
||||
<EmptyMessage>{t('knowledge.no_bases')}</EmptyMessage>
|
||||
) : (
|
||||
<>
|
||||
{selectedBase && (
|
||||
<Button type="link" block onClick={() => onSelect(undefined)} style={{ textAlign: 'left' }}>
|
||||
{t('knowledge.clear_selection')}
|
||||
</Button>
|
||||
)}
|
||||
{knowledgeState.bases.map((base) => (
|
||||
<Button
|
||||
key={base.id}
|
||||
type={selectedBase?.id === base.id ? 'primary' : 'text'}
|
||||
block
|
||||
onClick={() => onSelect(base)}
|
||||
style={{ textAlign: 'left' }}>
|
||||
{base.name}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={selectedBases?.map((base) => base.id)}
|
||||
allowClear
|
||||
placeholder={t('agents.add.knowledge_base.placeholder')}
|
||||
menuItemSelectedIcon={<CheckOutlined />}
|
||||
options={knowledgeOptions}
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
onChange={(ids) => {
|
||||
const newSelected = knowledgeState.bases.filter((base) => ids.includes(base.id))
|
||||
onSelect(newSelected)
|
||||
}}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
)}
|
||||
</SelectorContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, ToolbarButton }) => {
|
||||
const KnowledgeBaseButton: FC<Props> = ({ selectedBases, onSelect, disabled, ToolbarButton }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (selectedBase) {
|
||||
return (
|
||||
<Tooltip placement="top" title={selectedBase.name} arrow>
|
||||
<ToolbarButton type="text" onClick={() => onSelect(undefined)}>
|
||||
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
|
||||
content={<KnowledgeBaseSelector selectedBases={selectedBases} onSelect={onSelect} />}
|
||||
overlayStyle={{ maxWidth: 400 }}
|
||||
trigger="click">
|
||||
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
|
||||
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||
<ToolbarButton type="text" disabled={disabled}>
|
||||
<FileSearchOutlined
|
||||
style={{ color: selectedBases && selectedBases?.length > 0 ? 'var(--color-link)' : 'var(--color-icon)' }}
|
||||
/>
|
||||
</ToolbarButton>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
|
||||
@ -28,6 +28,8 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const itemRefs = useRef<Array<HTMLDivElement | null>>([])
|
||||
// Add a new state to track if menu was dismissed
|
||||
const [menuDismissed, setMenuDismissed] = useState(false)
|
||||
|
||||
const setItemRef = (index: number, el: HTMLDivElement | null) => {
|
||||
itemRefs.current[index] = el
|
||||
@ -44,7 +46,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
|
||||
const handleModelSelect = (model: Model) => {
|
||||
// Check if model is already selected
|
||||
if (mentionModels.some((selected) => selected.id === model.id)) {
|
||||
if (mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(model))) {
|
||||
return
|
||||
}
|
||||
onSelect(model)
|
||||
@ -186,6 +188,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
setIsOpen(true)
|
||||
setSelectedIndex(0)
|
||||
setSearchText('')
|
||||
setMenuDismissed(false) // Reset dismissed flag when manually showing selector
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@ -209,7 +212,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
e.preventDefault()
|
||||
if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) {
|
||||
const selectedModel = flatModelItems[selectedIndex].model
|
||||
if (!mentionModels.some((selected) => selected.id === selectedModel.id)) {
|
||||
if (!mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(selectedModel))) {
|
||||
flatModelItems[selectedIndex].onClick()
|
||||
}
|
||||
setIsOpen(false)
|
||||
@ -218,6 +221,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
setSearchText('')
|
||||
setMenuDismissed(true) // Set dismissed flag when Escape is pressed
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,12 +234,16 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
|
||||
setIsOpen(false)
|
||||
setSearchText('')
|
||||
} else if (lastAtIndex !== -1) {
|
||||
// Get the text after @ for search
|
||||
setMenuDismissed(false) // Reset dismissed flag when @ is removed
|
||||
} else {
|
||||
// Only open menu if it wasn't explicitly dismissed
|
||||
if (!menuDismissed) {
|
||||
setIsOpen(true)
|
||||
const searchStr = textBeforeCursor.slice(lastAtIndex + 1)
|
||||
setSearchText(searchStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||
if (textArea) {
|
||||
@ -252,19 +260,15 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
textArea.removeEventListener('input', handleTextChange)
|
||||
}
|
||||
}
|
||||
}, [isOpen, selectedIndex, flatModelItems, mentionModels])
|
||||
|
||||
// Hide dropdown if no models available
|
||||
if (flatModelItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
}, [isOpen, selectedIndex, flatModelItems, mentionModels, menuDismissed])
|
||||
|
||||
const menu = (
|
||||
<div ref={menuRef} className="ant-dropdown-menu">
|
||||
{modelMenuItems.map((group, groupIndex) => {
|
||||
{flatModelItems.length > 0 ? (
|
||||
modelMenuItems.map((group, groupIndex) => {
|
||||
if (!group) return null
|
||||
|
||||
// Calculate the starting index for this group's items
|
||||
// Calculate starting index for items in this group
|
||||
const startIndex = modelMenuItems.slice(0, groupIndex).reduce((acc, g) => acc + (g?.children?.length || 0), 0)
|
||||
|
||||
return (
|
||||
@ -275,7 +279,9 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
<div
|
||||
key={item.key}
|
||||
ref={(el) => setItemRef(startIndex + idx, el)}
|
||||
className={`ant-dropdown-menu-item ${selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''}`}
|
||||
className={`ant-dropdown-menu-item ${
|
||||
selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''
|
||||
}`}
|
||||
onClick={item.onClick}>
|
||||
<span className="ant-dropdown-menu-item-icon">{item.icon}</span>
|
||||
{item.label}
|
||||
@ -284,7 +290,12 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
})
|
||||
) : (
|
||||
<div className="ant-dropdown-menu-item-group">
|
||||
<div className="ant-dropdown-menu-item no-results">{t('models.no_matches')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -334,6 +345,17 @@ const DropdownMenuStyle = createGlobalStyle`
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 8px 12px;
|
||||
color: var(--color-text-3);
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-group {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Flex, Tag } from 'antd'
|
||||
import { FC } from 'react'
|
||||
@ -13,14 +14,19 @@ const MentionModelsInput: FC<{
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getProviderName = (model: Model) => {
|
||||
const provider = providers.find((p) => p.models?.some((m) => m.id === model.id))
|
||||
const provider = providers.find((p) => p.id === model?.provider)
|
||||
return provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
|
||||
}
|
||||
|
||||
return (
|
||||
<Container gap="4px 0" wrap>
|
||||
{selectedModels.map((model) => (
|
||||
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
|
||||
<Tag
|
||||
bordered={false}
|
||||
color="processing"
|
||||
key={getModelUniqId(model)}
|
||||
closable
|
||||
onClose={() => onRemoveModel(model)}>
|
||||
@{model.name} ({getProviderName(model)})
|
||||
</Tag>
|
||||
))}
|
||||
|
||||
@ -240,6 +240,7 @@ const MessageContentContainer = styled.div`
|
||||
justify-content: space-between;
|
||||
margin-left: 46px;
|
||||
margin-top: 5px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
const MessageFooter = styled.div`
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
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 { Message, Topic } from '@renderer/types'
|
||||
import { Popover } from 'antd'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
import MessageItem from './Message'
|
||||
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||
|
||||
interface Props {
|
||||
messages: (Message & { index: number })[]
|
||||
@ -32,7 +29,7 @@ const MessageGroup: FC<Props> = ({
|
||||
onGetMessages,
|
||||
onDeleteGroupMessages
|
||||
}) => {
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings()
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
||||
@ -42,8 +39,9 @@ const MessageGroup: FC<Props> = ({
|
||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||
|
||||
const isGrouped = messageLength > 1
|
||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||
|
||||
const onDelete = async () => {
|
||||
const onDelete = useCallback(async () => {
|
||||
window.modal.confirm({
|
||||
title: t('message.group.delete.title'),
|
||||
content: t('message.group.delete.content'),
|
||||
@ -57,18 +55,75 @@ const MessageGroup: FC<Props> = ({
|
||||
askId && onDeleteGroupMessages?.(askId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [messages, onDeleteGroupMessages, t])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(messageLength - 1)
|
||||
}, [messageLength])
|
||||
|
||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||
|
||||
return (
|
||||
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
|
||||
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
|
||||
{messages.map((message, index) => (
|
||||
<GridContainer $count={messageLength} $layout={multiModelMessageStyle} $gridColumns={gridColumns}>
|
||||
{messages.map((message, index) => {
|
||||
const isGridGroupMessage = multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped
|
||||
if (isGridGroupMessage) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
$isInPopover={true}
|
||||
key={message.id}>
|
||||
<MessageItem
|
||||
isGrouped={isGrouped}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={{
|
||||
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
|
||||
}}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
}
|
||||
trigger={gridPopoverTrigger}
|
||||
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}
|
||||
key={message.id}>
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}>
|
||||
<MessageItem
|
||||
isGrouped={isGrouped}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={
|
||||
gridPopoverTrigger === 'hover' && isGrouped
|
||||
? {
|
||||
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15,
|
||||
overflow: isGrouped ? 'hidden' : 'auto',
|
||||
maxHeight: isGrouped ? '280px' : 'unset'
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSetMessages={onSetMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<MessageWrapper
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
@ -81,92 +136,63 @@ const MessageGroup: FC<Props> = ({
|
||||
topic={topic}
|
||||
index={message.index}
|
||||
hidePresetMessages={hidePresetMessages}
|
||||
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }}
|
||||
style={{ paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 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"
|
||||
<MessageGroupMenuBar
|
||||
multiModelMessageStyle={multiModelMessageStyle}
|
||||
setMultiModelMessageStyle={setMultiModelMessageStyle}
|
||||
messages={messages}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</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')};
|
||||
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
|
||||
`
|
||||
|
||||
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle }>`
|
||||
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
|
||||
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $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)},
|
||||
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
|
||||
minmax(400px, 1fr)
|
||||
);
|
||||
}
|
||||
overflow-y: auto;
|
||||
${({ $gridColumns, $layout, $count }) =>
|
||||
$layout === 'grid' &&
|
||||
css`
|
||||
grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
`}
|
||||
`
|
||||
|
||||
interface MessageWrapperProps {
|
||||
$layout: 'fold' | 'horizontal' | 'vertical'
|
||||
$layout: 'fold' | 'horizontal' | 'vertical' | 'grid'
|
||||
$selected: boolean
|
||||
$isGrouped: boolean
|
||||
$isInPopover?: boolean
|
||||
}
|
||||
|
||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
@ -176,10 +202,11 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
return props.$selected ? 'block' : 'none'
|
||||
}
|
||||
if (props.$layout === 'horizontal') {
|
||||
return 'inline-block'
|
||||
return 'inline-flex'
|
||||
}
|
||||
return 'block'
|
||||
}};
|
||||
|
||||
${({ $layout, $isGrouped }) => {
|
||||
if ($layout === 'horizontal' && $isGrouped) {
|
||||
return css`
|
||||
@ -187,82 +214,26 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
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;
|
||||
${({ $layout, $isInPopover, $isGrouped }) =>
|
||||
$layout === 'grid' && $isGrouped
|
||||
? css`
|
||||
max-height: ${$isInPopover ? '50vh' : '300px'};
|
||||
overflow-y: auto;
|
||||
border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'};
|
||||
padding: 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;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
: css`
|
||||
overflow-y: auto;
|
||||
border-radius: 6px;
|
||||
`}
|
||||
`
|
||||
|
||||
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
|
||||
export default memo(MessageGroup)
|
||||
|
||||
161
src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx
Normal file
161
src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import {
|
||||
ColumnHeightOutlined,
|
||||
ColumnWidthOutlined,
|
||||
DeleteOutlined,
|
||||
FolderOutlined,
|
||||
NumberOutlined
|
||||
} from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { Button, Segmented as AntdSegmented } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageGroupSettings from './MessageGroupSettings'
|
||||
|
||||
interface Props {
|
||||
multiModelMessageStyle: MultiModelMessageStyle
|
||||
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
|
||||
messages: Message[]
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
const MessageGroupMenuBar: FC<Props> = ({
|
||||
multiModelMessageStyle,
|
||||
setMultiModelMessageStyle,
|
||||
messages,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
onDelete
|
||||
}) => {
|
||||
return (
|
||||
<GroupMenuBar $layout={multiModelMessageStyle}>
|
||||
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
||||
<LayoutContainer>
|
||||
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
|
||||
<LayoutOption
|
||||
key={layout}
|
||||
$active={multiModelMessageStyle === layout}
|
||||
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
|
||||
{layout === 'fold' ? (
|
||||
<FolderOutlined />
|
||||
) : layout === 'horizontal' ? (
|
||||
<ColumnWidthOutlined />
|
||||
) : layout === 'vertical' ? (
|
||||
<ColumnHeightOutlined />
|
||||
) : (
|
||||
<NumberOutlined />
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||
</HStack>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</GroupMenuBar>
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
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 memo(MessageGroupMenuBar)
|
||||
@ -0,0 +1,59 @@
|
||||
import { SettingOutlined } from '@ant-design/icons'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { SettingDivider } from '@renderer/pages/settings'
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setGridColumns, setGridPopoverTrigger } from '@renderer/store/settings'
|
||||
import { Col, Row, Select, Slider } from 'antd'
|
||||
import { Popover } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const MessageGroupSettings: FC = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { gridColumns, gridPopoverTrigger } = useSettings()
|
||||
const [gridColumnsValue, setGridColumnsValue] = useState(gridColumns)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger={undefined}
|
||||
showArrow
|
||||
content={
|
||||
<div style={{ padding: 10 }}>
|
||||
<SettingRow>
|
||||
<div style={{ marginRight: 10 }}>{t('settings.messages.grid_popover_trigger')}</div>
|
||||
<Select
|
||||
value={gridPopoverTrigger || 'hover'}
|
||||
onChange={(value) => dispatch(setGridPopoverTrigger(value as 'hover' | 'click'))}
|
||||
size="small">
|
||||
<Select.Option value="hover">{t('settings.messages.grid_popover_trigger.hover')}</Select.Option>
|
||||
<Select.Option value="click">{t('settings.messages.grid_popover_trigger.click')}</Select.Option>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<div>{t('settings.messages.grid_columns')}</div>
|
||||
</SettingRow>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={24}>
|
||||
<Slider
|
||||
value={gridColumnsValue}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => setGridColumnsValue(value)}
|
||||
onChangeComplete={(value) => dispatch(setGridColumns(value))}
|
||||
min={2}
|
||||
max={6}
|
||||
step={1}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}>
|
||||
<SettingOutlined style={{ marginLeft: 15, cursor: 'pointer' }} />
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageGroupSettings
|
||||
@ -60,12 +60,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
|
||||
const isUserMessage = message.role === 'user'
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
const onCopy = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [message.content, t])
|
||||
},
|
||||
[message.content, t]
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
@ -195,14 +199,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
[message, onEdit, onNewBranch, t]
|
||||
)
|
||||
|
||||
const onRegenerate = async () => {
|
||||
const onRegenerate = async (e: React.MouseEvent | undefined) => {
|
||||
e?.stopPropagation?.()
|
||||
await modelGenerating()
|
||||
const selectedModel = isGrouped ? model : assistantModel
|
||||
const _message = resetAssistantMessage(message, selectedModel)
|
||||
onEditMessage?.(_message)
|
||||
}
|
||||
|
||||
const onMentionModel = async () => {
|
||||
const onMentionModel = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
await modelGenerating()
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
if (!selectedModel) return
|
||||
@ -216,9 +222,13 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
onEditMessage?.(_message)
|
||||
}
|
||||
|
||||
const onUseful = useCallback(() => {
|
||||
const onUseful = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onEditMessage?.({ ...message, useful: !message.useful })
|
||||
}, [message, onEditMessage])
|
||||
},
|
||||
[message, onEditMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
@ -270,13 +280,14 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
key: 'translate-close',
|
||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
}
|
||||
]
|
||||
],
|
||||
onClick: (e) => e.domEvent.stopPropagation()
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="topRight"
|
||||
arrow>
|
||||
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
||||
<ActionButton className="message-action-button">
|
||||
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||
<TranslationOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@ -298,14 +309,25 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={isGrouped ? () => onDeleteMessage?.(message) : undefined}>
|
||||
onClick={
|
||||
isGrouped
|
||||
? (e) => {
|
||||
e.stopPropagation()
|
||||
onDeleteMessage?.(message)
|
||||
}
|
||||
: (e) => e.stopPropagation()
|
||||
}>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton className="message-action-button">
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
|
||||
trigger={['click']}
|
||||
placement="topRight"
|
||||
arrow>
|
||||
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Collapse } from 'antd'
|
||||
import { FC, useEffect, useState, useMemo } from 'react'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BarLoader from 'react-spinners/BarLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Markdown from '../Markdown/Markdown'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@ -32,10 +33,12 @@ const MessageThought: FC<Props> = ({ message }) => {
|
||||
|
||||
const thinkingTime = message.metrics?.time_thinking_millsec || 0
|
||||
const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1)
|
||||
const isPaused = message.status === 'paused'
|
||||
|
||||
return (
|
||||
<CollapseContainer
|
||||
activeKey={activeKey}
|
||||
size="small"
|
||||
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
||||
className="message-thought-container"
|
||||
items={[
|
||||
@ -46,7 +49,7 @@ const MessageThought: FC<Props> = ({ message }) => {
|
||||
<TinkingText>
|
||||
{isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSeconds })}
|
||||
</TinkingText>
|
||||
{isThinking && <BarLoader color="#9254de" />}
|
||||
{isThinking && !isPaused && <BarLoader color="#9254de" />}
|
||||
</MessageTitleLabel>
|
||||
),
|
||||
children: (
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
@ -11,26 +12,30 @@ interface Props {
|
||||
|
||||
const Prompt: FC<Props> = ({ assistant, topic }) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const prompt = assistant.prompt || t('chat.default.description')
|
||||
const topicPrompt = topic?.prompt || ''
|
||||
const isDark = theme === 'dark'
|
||||
|
||||
if (!prompt && !topicPrompt) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })}>
|
||||
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })} $isDark={isDark}>
|
||||
<Text>{prompt}</Text>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled.div<{ $isDark: boolean }>`
|
||||
padding: 10px 20px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin: 4px 20px 0 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: 0.5px solid var(--color-border);
|
||||
background-color: ${({ $isDark }) => ($isDark ? 'var(--color-background-soft)' : 'transparent')};
|
||||
`
|
||||
|
||||
const Text = styled.div`
|
||||
|
||||
@ -283,6 +283,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
<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.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
@ -18,7 +18,7 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
|
||||
import { bookExts, documentExts, textExts } from '@shared/config/constant'
|
||||
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||
import { Alert, Button, Card, Divider, message, Tag, Tooltip, Typography, Upload } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -35,7 +35,7 @@ interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const fileTypes = [...bookExts, ...documentExts, ...textExts]
|
||||
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
|
||||
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -217,7 +217,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
style={{ marginTop: 10, background: 'transparent' }}>
|
||||
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
|
||||
<p className="ant-upload-hint">
|
||||
{t('knowledge.file_hint', { file_types: fileTypes.slice(0, 6).join(', ').replaceAll('.', '') })}
|
||||
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
|
||||
</p>
|
||||
</Dragger>
|
||||
</FileSection>
|
||||
|
||||
@ -16,18 +16,14 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
|
||||
const { t } = useTranslation()
|
||||
|
||||
const knowledgeState = useAppSelector((state) => state.knowledge)
|
||||
const knowledgeOptions: SelectProps['options'] = []
|
||||
|
||||
knowledgeState.bases.forEach((base) => {
|
||||
knowledgeOptions.push({
|
||||
const knowledgeOptions: SelectProps['options'] = knowledgeState.bases.map((base) => ({
|
||||
label: base.name,
|
||||
value: base.id
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
const onUpdate = (value) => {
|
||||
const knowledge_base = knowledgeState.bases.find((t) => t.id === value)
|
||||
const _assistant = { ...assistant, knowledge_base }
|
||||
const knowledge_bases = value.map((id) => knowledgeState.bases.find((b) => b.id === id))
|
||||
const _assistant = { ...assistant, knowledge_bases }
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
|
||||
@ -37,12 +33,18 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
|
||||
{t('common.knowledge_base')}
|
||||
</Box>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
defaultValue={assistant.knowledge_base?.id}
|
||||
value={assistant.knowledge_bases?.map((b) => b.id)}
|
||||
placeholder={t('agents.add.knowledge_base.placeholder')}
|
||||
menuItemSelectedIcon={<CheckOutlined />}
|
||||
options={knowledgeOptions}
|
||||
onChange={(value) => onUpdate(value)}
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { FileSearchOutlined, FolderOpenOutlined, InfoCircleOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { Client } from '@notionhq/client'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { backup, reset, restore } from '@renderer/services/BackupService'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@ -60,9 +61,23 @@ const NotionSettings: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotionTitleClick = () => {
|
||||
MinApp.start({
|
||||
id: 'notion-help',
|
||||
name: 'Notion Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.notion.title')}</SettingTitle>
|
||||
<SettingTitle style={{ justifyContent: 'flex-start', gap: 10 }}>
|
||||
{t('settings.data.notion.title')}
|
||||
<InfoCircleOutlined
|
||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer' }}
|
||||
onClick={handleNotionTitleClick}
|
||||
/>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
||||
@ -73,6 +88,7 @@ const NotionSettings: FC = () => {
|
||||
onChange={handleNotionDatabaseIdChange}
|
||||
onBlur={handleNotionDatabaseIdChange}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.notion.database_id_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
@ -86,6 +102,7 @@ const NotionSettings: FC = () => {
|
||||
onChange={handleNotionTokenChange}
|
||||
onBlur={handleNotionTokenChange}
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.notion.api_key_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleNotionConnectionCheck} style={{ width: 60 }}>
|
||||
{t('settings.data.notion.check.button')}
|
||||
|
||||
@ -23,50 +23,6 @@ interface MiniAppManagerProps {
|
||||
|
||||
type ListType = 'visible' | 'disabled'
|
||||
|
||||
// 添加 reorderLists 函数的接口定义
|
||||
interface ReorderListsParams {
|
||||
sourceList: MinAppType[]
|
||||
destList: MinAppType[]
|
||||
sourceIndex: number
|
||||
destIndex: number
|
||||
isSameList: boolean
|
||||
}
|
||||
|
||||
interface ReorderListsResult {
|
||||
sourceList: MinAppType[]
|
||||
destList: MinAppType[]
|
||||
}
|
||||
|
||||
// 添加 reorderLists 函数
|
||||
const reorderLists = ({
|
||||
sourceList,
|
||||
destList,
|
||||
sourceIndex,
|
||||
destIndex,
|
||||
isSameList
|
||||
}: ReorderListsParams): ReorderListsResult => {
|
||||
if (isSameList) {
|
||||
// 在同一列表内重新排序
|
||||
const newList = [...sourceList]
|
||||
const [removed] = newList.splice(sourceIndex, 1)
|
||||
newList.splice(destIndex, 0, removed)
|
||||
return {
|
||||
sourceList: newList,
|
||||
destList: destList
|
||||
}
|
||||
} else {
|
||||
// 在不同列表间移动
|
||||
const newSourceList = [...sourceList]
|
||||
const [removed] = newSourceList.splice(sourceIndex, 1)
|
||||
const newDestList = [...destList]
|
||||
newDestList.splice(destIndex, 0, removed)
|
||||
return {
|
||||
sourceList: newSourceList,
|
||||
destList: newDestList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
||||
visibleMiniApps,
|
||||
disabledMiniApps,
|
||||
@ -92,25 +48,35 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
||||
if (!result.destination) return
|
||||
|
||||
const { source, destination } = result
|
||||
const sourceList = source.droppableId as ListType
|
||||
const destList = destination.droppableId as ListType
|
||||
|
||||
if (source.droppableId === destination.droppableId) return
|
||||
if (source.droppableId === destination.droppableId) {
|
||||
// 在同一列表内重新排序
|
||||
const list = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
|
||||
const [removed] = list.splice(source.index, 1)
|
||||
list.splice(destination.index, 0, removed)
|
||||
|
||||
const newLists = reorderLists({
|
||||
sourceList: sourceList === 'visible' ? visibleMiniApps : disabledMiniApps,
|
||||
destList: destList === 'visible' ? visibleMiniApps : disabledMiniApps,
|
||||
sourceIndex: source.index,
|
||||
destIndex: destination.index,
|
||||
isSameList: sourceList === destList
|
||||
})
|
||||
if (source.droppableId === 'visible') {
|
||||
handleListUpdate(list, disabledMiniApps)
|
||||
} else {
|
||||
handleListUpdate(visibleMiniApps, list)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
handleListUpdate(
|
||||
sourceList === 'visible' ? newLists.sourceList : newLists.destList,
|
||||
sourceList === 'visible' ? newLists.destList : newLists.sourceList
|
||||
)
|
||||
// 在不同列表间移动
|
||||
const sourceList = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
|
||||
const destList = destination.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
|
||||
|
||||
const [removed] = sourceList.splice(source.index, 1)
|
||||
const targetList = destList.filter((app) => app.id !== removed.id)
|
||||
targetList.splice(destination.index, 0, removed)
|
||||
|
||||
const newVisibleMiniApps = destination.droppableId === 'visible' ? targetList : sourceList
|
||||
const newDisabledMiniApps = destination.droppableId === 'disabled' ? targetList : sourceList
|
||||
|
||||
handleListUpdate(newVisibleMiniApps, newDisabledMiniApps)
|
||||
},
|
||||
[disabledMiniApps, handleListUpdate, visibleMiniApps]
|
||||
[visibleMiniApps, disabledMiniApps, handleListUpdate]
|
||||
)
|
||||
|
||||
const onMoveMiniApp = useCallback(
|
||||
@ -153,7 +119,6 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
||||
<Droppable droppableId={listType}>
|
||||
{(provided: DroppableProvided) => (
|
||||
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
|
||||
<ScrollContainer>
|
||||
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
|
||||
<Draggable key={program.id} draggableId={String(program.id)} index={index}>
|
||||
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
|
||||
@ -163,7 +128,6 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
||||
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
</ScrollContainer>
|
||||
</ProgramList>
|
||||
)}
|
||||
</Droppable>
|
||||
@ -181,12 +145,6 @@ const AppLogo = styled.img`
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const ScrollContainer = styled.div`
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-right: 5px;
|
||||
`
|
||||
|
||||
const ProgramSection = styled.div`
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
@ -208,13 +166,29 @@ const ProgramList = styled.div`
|
||||
height: 365px;
|
||||
min-height: 365px;
|
||||
padding: 10px;
|
||||
padding-right: 5px;
|
||||
background: var(--color-background-soft);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
scroll-behavior: smooth;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-border-hover);
|
||||
}
|
||||
`
|
||||
|
||||
const ProgramItem = styled.div`
|
||||
|
||||
@ -59,7 +59,8 @@ const ShortcutSettings: FC = () => {
|
||||
const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
||||
const hasNonModifier = keys.some((key) => !['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
||||
|
||||
if (isMac && keys.includes('Alt')) {
|
||||
// only allows option + space
|
||||
if (isMac && keys[0] === 'Alt' && !['Space', undefined].includes(keys[1])) {
|
||||
window.message.warning({
|
||||
content: t('settings.shortcuts.alt_warning'),
|
||||
key: 'shortcut-alt-warning'
|
||||
|
||||
@ -5,6 +5,7 @@ import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import store from '@renderer/store'
|
||||
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { delay, isJSON, parseJSON } from '@renderer/utils'
|
||||
import { t } from 'i18next'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { CompletionsParams } from '.'
|
||||
@ -83,21 +84,35 @@ export default abstract class BaseProvider {
|
||||
return message.content
|
||||
}
|
||||
|
||||
const knowledgeId = message.knowledgeBaseIds[0]
|
||||
const base = store.getState().knowledge.bases.find((kb) => kb.id === knowledgeId)
|
||||
const bases = store.getState().knowledge.bases.filter((kb) => message.knowledgeBaseIds?.includes(kb.id))
|
||||
|
||||
if (!base) {
|
||||
if (!bases || bases.length === 0) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
const { referencesContent, referencesCount } = await getKnowledgeReferences(base, message)
|
||||
const allReferencesPromises = bases.map(async (base) => {
|
||||
const references = await getKnowledgeReferences(base, message)
|
||||
|
||||
// 如果知识库中未检索到内容则使用通用逻辑
|
||||
if (referencesCount === 0) {
|
||||
return {
|
||||
knowledgeBaseId: base.id,
|
||||
references
|
||||
}
|
||||
})
|
||||
const allReferences = (await Promise.all(allReferencesPromises))
|
||||
.filter((result) => result.references && result.references.length > 0)
|
||||
.flat()
|
||||
|
||||
if (allReferences.length === 0) {
|
||||
window.message.info({
|
||||
content: t('knowledge.no_match'),
|
||||
duration: 4,
|
||||
key: 'knowledge-base-no-match-info'
|
||||
})
|
||||
return message.content
|
||||
}
|
||||
const allReferencesContent = `\`\`\`json\n${JSON.stringify(allReferences, null, 2)}\n\`\`\``
|
||||
|
||||
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', referencesContent)
|
||||
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', allReferencesContent)
|
||||
}
|
||||
|
||||
protected getCustomParameters(assistant: Assistant) {
|
||||
|
||||
@ -3,7 +3,6 @@ import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@
|
||||
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||
import AiProvider from '@renderer/providers/AiProvider'
|
||||
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
|
||||
import { t } from 'i18next'
|
||||
import { take } from 'lodash'
|
||||
|
||||
import { getProviderByModel } from './AssistantService'
|
||||
@ -91,14 +90,6 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
|
||||
return item.score >= threshold
|
||||
})
|
||||
)
|
||||
if (searchResults.length === 0) {
|
||||
window.message.info({
|
||||
content: t('knowledge.no_match'),
|
||||
duration: 4,
|
||||
key: 'knowledge-base-no-match-info'
|
||||
})
|
||||
return { referencesContent: '', referencesCount: 0 }
|
||||
}
|
||||
|
||||
const _searchResults = await Promise.all(
|
||||
searchResults.map(async (item) => {
|
||||
@ -121,7 +112,5 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
|
||||
})
|
||||
)
|
||||
|
||||
const referencesContent = `\`\`\`json\n${JSON.stringify(references, null, 2)}\n\`\`\``
|
||||
|
||||
return { referencesContent, referencesCount: references.length }
|
||||
return references
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import store from '@renderer/store'
|
||||
import { Model } from '@renderer/types'
|
||||
import { t } from 'i18next'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
export const getModelUniqId = (m?: Model) => {
|
||||
@ -17,5 +18,13 @@ export const hasModel = (m?: Model) => {
|
||||
}
|
||||
|
||||
export function getModelName(model?: Model) {
|
||||
return model?.name || model?.id || ''
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === model?.provider)
|
||||
const modelName = model?.name || model?.id || ''
|
||||
|
||||
if (provider) {
|
||||
const providerName = provider?.isSystem ? t(`provider.${provider.id}`) : provider?.name
|
||||
return `${modelName} | ${providerName}`
|
||||
}
|
||||
|
||||
return modelName
|
||||
}
|
||||
|
||||
@ -152,7 +152,7 @@ const initialState: LlmState = {
|
||||
name: 'DMXAPI',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.dmxapi.com',
|
||||
apiHost: 'https://www.dmxapi.com',
|
||||
models: SYSTEM_MODELS.dmxapi,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
|
||||
@ -1071,6 +1071,8 @@ const migrateConfig = {
|
||||
state.minapps.enabled.push(coze)
|
||||
}
|
||||
}
|
||||
state.settings.gridColumns = 2
|
||||
state.settings.gridPopoverTrigger = 'hover'
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,8 @@ export interface SettingsState {
|
||||
mathEngine: 'MathJax' | 'KaTeX'
|
||||
messageStyle: 'plain' | 'bubble'
|
||||
codeStyle: CodeStyleVarious
|
||||
gridColumns: number
|
||||
gridPopoverTrigger: 'hover' | 'click'
|
||||
// webdav 配置 host, user, pass, path
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
@ -69,7 +71,7 @@ export interface SettingsState {
|
||||
notionApiKey: string | null
|
||||
}
|
||||
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold'
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||
|
||||
const initialState: SettingsState = {
|
||||
showAssistants: true,
|
||||
@ -99,6 +101,8 @@ const initialState: SettingsState = {
|
||||
mathEngine: 'KaTeX',
|
||||
messageStyle: 'plain',
|
||||
codeStyle: 'auto',
|
||||
gridColumns: 2,
|
||||
gridPopoverTrigger: 'hover',
|
||||
webdavHost: '',
|
||||
webdavUser: '',
|
||||
webdavPass: '',
|
||||
@ -224,6 +228,12 @@ const settingsSlice = createSlice({
|
||||
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
||||
state.mathEngine = action.payload
|
||||
},
|
||||
setGridColumns: (state, action: PayloadAction<number>) => {
|
||||
state.gridColumns = action.payload
|
||||
},
|
||||
setGridPopoverTrigger: (state, action: PayloadAction<'hover' | 'click'>) => {
|
||||
state.gridPopoverTrigger = action.payload
|
||||
},
|
||||
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
|
||||
state.messageStyle = action.payload
|
||||
},
|
||||
@ -265,7 +275,7 @@ const settingsSlice = createSlice({
|
||||
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
|
||||
state.enableQuickAssistant = action.payload
|
||||
},
|
||||
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => {
|
||||
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold' | 'grid'>) => {
|
||||
state.multiModelMessageStyle = action.payload
|
||||
},
|
||||
setNotionDatabaseID: (state, action: PayloadAction<string>) => {
|
||||
@ -310,6 +320,8 @@ export const {
|
||||
setCodeShowLineNumbers,
|
||||
setCodeCollapsible,
|
||||
setMathEngine,
|
||||
setGridColumns,
|
||||
setGridPopoverTrigger,
|
||||
setMessageStyle,
|
||||
setCodeStyle,
|
||||
setTranslateModelPrompt,
|
||||
|
||||
@ -5,7 +5,7 @@ export type Assistant = {
|
||||
id: string
|
||||
name: string
|
||||
prompt: string
|
||||
knowledge_base?: KnowledgeBase
|
||||
knowledge_bases?: KnowledgeBase[]
|
||||
topics: Topic[]
|
||||
type: string
|
||||
emoji?: string
|
||||
|
||||
@ -50,7 +50,7 @@ const Inputbar: FC = () => {
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase>()
|
||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>()
|
||||
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
@ -274,8 +274,8 @@ const Inputbar: FC = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
||||
setSelectedKnowledgeBase(base)
|
||||
const handleKnowledgeBaseSelect = (bases: KnowledgeBase[]) => {
|
||||
setSelectedKnowledgeBase(bases?.[0])
|
||||
}
|
||||
|
||||
return (
|
||||
@ -317,7 +317,7 @@ const Inputbar: FC = () => {
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
selectedBases={selectedKnowledgeBase ? [selectedKnowledgeBase] : []}
|
||||
onSelect={handleKnowledgeBaseSelect}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={files.length > 0}
|
||||
|
||||
@ -8,7 +8,7 @@ import { uuid } from '@renderer/utils'
|
||||
import { Divider } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -25,6 +25,8 @@ const HomeWindow: FC = () => {
|
||||
const [clipboardText, setClipboardText] = useState('')
|
||||
const [selectedText, setSelectedText] = useState('')
|
||||
const [text, setText] = useState('')
|
||||
const [lastClipboardText, setLastClipboardText] = useState<string | null>(null)
|
||||
const textChange = useState(() => {})[1]
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { defaultModel: model } = useDefaultModel()
|
||||
const { language } = useSettings()
|
||||
@ -35,9 +37,12 @@ const HomeWindow: FC = () => {
|
||||
const content = (referenceText === text ? text : `${referenceText}\n\n${text}`).trim()
|
||||
|
||||
const onReadClipboard = useCallback(async () => {
|
||||
const text = await navigator.clipboard.readText()
|
||||
const text = await navigator.clipboard.readText().catch(() => null)
|
||||
if (text && text !== lastClipboardText) {
|
||||
setLastClipboardText(text)
|
||||
setClipboardText(text.trim())
|
||||
}, [])
|
||||
}
|
||||
}, [lastClipboardText])
|
||||
|
||||
useEffect(() => {
|
||||
onReadClipboard()
|
||||
@ -50,9 +55,10 @@ const HomeWindow: FC = () => {
|
||||
const onCloseWindow = () => window.api.miniWindow.hide()
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const isEnterPressed = e.keyCode == 13
|
||||
const isEnterPressed = e.code === 'Enter'
|
||||
const isBackspacePressed = e.code === 'Backspace'
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
if (e.code === 'Escape') {
|
||||
setText('')
|
||||
setRoute('home')
|
||||
route === 'home' && onCloseWindow()
|
||||
@ -67,6 +73,18 @@ const HomeWindow: FC = () => {
|
||||
setTimeout(() => setText(''), 100)
|
||||
}
|
||||
}
|
||||
|
||||
if (isBackspacePressed) {
|
||||
textChange(() => {
|
||||
if (text.length === 0) {
|
||||
clearClipboard()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setText(e.target.value)
|
||||
}
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
@ -95,7 +113,6 @@ const HomeWindow: FC = () => {
|
||||
const clearClipboard = () => {
|
||||
setClipboardText('')
|
||||
setSelectedText('')
|
||||
navigator.clipboard.writeText('')
|
||||
}
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
@ -132,7 +149,7 @@ const HomeWindow: FC = () => {
|
||||
referenceText={referenceText}
|
||||
placeholder={t('miniwindow.input.placeholder.empty', { model: model.name })}
|
||||
handleKeyDown={handleKeyDown}
|
||||
setText={setText}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
</>
|
||||
@ -171,7 +188,7 @@ const HomeWindow: FC = () => {
|
||||
: t('miniwindow.input.placeholder.empty', { model: model.name })
|
||||
}
|
||||
handleKeyDown={handleKeyDown}
|
||||
setText={setText}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { Input as AntdInput } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import React, { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface InputBarProps {
|
||||
@ -10,10 +10,10 @@ interface InputBarProps {
|
||||
referenceText: string
|
||||
placeholder: string
|
||||
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
|
||||
setText: (text: string) => void
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown, setText }) => {
|
||||
const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown, handleChange }) => {
|
||||
const { generating } = useRuntime()
|
||||
return (
|
||||
<InputWrapper>
|
||||
@ -24,7 +24,7 @@ const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown,
|
||||
bordered={false}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={handleChange}
|
||||
disabled={generating}
|
||||
/>
|
||||
</InputWrapper>
|
||||
|
||||
270
yarn.lock
270
yarn.lock
@ -126,17 +126,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@asamuzakjp/dom-selector@npm:^2.0.1":
|
||||
version: 2.0.2
|
||||
resolution: "@asamuzakjp/dom-selector@npm:2.0.2"
|
||||
dependencies:
|
||||
bidi-js: "npm:^1.0.3"
|
||||
css-tree: "npm:^2.3.1"
|
||||
is-potential-custom-element-name: "npm:^1.0.1"
|
||||
checksum: 10c0/54d9afa3d654a98fcf2e45c53ea330237e513877f130f8c8c17611c603c8d50cb18f937e1b0bcc08f0030443a9c8479dcad9cebff02766025e2df2754459c647
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2":
|
||||
version: 7.26.2
|
||||
resolution: "@babel/code-frame@npm:7.26.2"
|
||||
@ -3079,7 +3068,7 @@ __metadata:
|
||||
redux: "npm:^5.0.1"
|
||||
redux-persist: "npm:^6.0.0"
|
||||
rehype-katex: "npm:^7.0.1"
|
||||
rehype-mathjax: "npm:^6.0.0"
|
||||
rehype-mathjax: "npm:^7.0.0"
|
||||
rehype-raw: "npm:^7.0.0"
|
||||
remark-gfm: "npm:^4.0.0"
|
||||
remark-math: "npm:^6.0.0"
|
||||
@ -3747,15 +3736,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bidi-js@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "bidi-js@npm:1.0.3"
|
||||
dependencies:
|
||||
require-from-string: "npm:^2.0.2"
|
||||
checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bindings@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "bindings@npm:1.5.0"
|
||||
@ -4645,25 +4625,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-tree@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "css-tree@npm:2.3.1"
|
||||
dependencies:
|
||||
mdn-data: "npm:2.0.30"
|
||||
source-map-js: "npm:^1.0.1"
|
||||
checksum: 10c0/6f8c1a11d5e9b14bf02d10717fc0351b66ba12594166f65abfbd8eb8b5b490dd367f5c7721db241a3c792d935fc6751fbc09f7e1598d421477ad9fadc30f4f24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cssstyle@npm:^4.0.1":
|
||||
version: 4.1.0
|
||||
resolution: "cssstyle@npm:4.1.0"
|
||||
dependencies:
|
||||
rrweb-cssom: "npm:^0.7.1"
|
||||
checksum: 10c0/05c6597e5d3e0ec6b15221f2c0ce9a0443a46cc50a6089a3ba9ee1ac27f83ff86a445a8f95435137dadd859f091fc61b6d342abaf396d3c910471b5b33cfcbfa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "csstype@npm:3.1.3"
|
||||
@ -4703,16 +4664,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"data-urls@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "data-urls@npm:5.0.0"
|
||||
dependencies:
|
||||
whatwg-mimetype: "npm:^4.0.0"
|
||||
whatwg-url: "npm:^14.0.0"
|
||||
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"data-view-buffer@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "data-view-buffer@npm:1.0.2"
|
||||
@ -4799,13 +4750,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decimal.js@npm:^10.4.3":
|
||||
version: 10.4.3
|
||||
resolution: "decimal.js@npm:10.4.3"
|
||||
checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decode-named-character-reference@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "decode-named-character-reference@npm:1.0.2"
|
||||
@ -7244,15 +7188,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-encoding-sniffer@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "html-encoding-sniffer@npm:4.0.0"
|
||||
dependencies:
|
||||
whatwg-encoding: "npm:^3.1.1"
|
||||
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-parse-stringify@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "html-parse-stringify@npm:3.0.1"
|
||||
@ -7391,7 +7326,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2":
|
||||
"https-proxy-agent@npm:^7.0.1":
|
||||
version: 7.0.6
|
||||
resolution: "https-proxy-agent@npm:7.0.6"
|
||||
dependencies:
|
||||
@ -7445,15 +7380,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
|
||||
version: 0.6.3
|
||||
resolution: "iconv-lite@npm:0.6.3"
|
||||
dependencies:
|
||||
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
|
||||
checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:^0.4.4":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
@ -7463,6 +7389,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:^0.6.2":
|
||||
version: 0.6.3
|
||||
resolution: "iconv-lite@npm:0.6.3"
|
||||
dependencies:
|
||||
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
|
||||
checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
@ -7883,13 +7818,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-potential-custom-element-name@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "is-potential-custom-element-name@npm:1.0.1"
|
||||
checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-regex@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "is-regex@npm:1.2.1"
|
||||
@ -8162,40 +8090,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsdom@npm:^23.0.0":
|
||||
version: 23.2.0
|
||||
resolution: "jsdom@npm:23.2.0"
|
||||
dependencies:
|
||||
"@asamuzakjp/dom-selector": "npm:^2.0.1"
|
||||
cssstyle: "npm:^4.0.1"
|
||||
data-urls: "npm:^5.0.0"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
form-data: "npm:^4.0.0"
|
||||
html-encoding-sniffer: "npm:^4.0.0"
|
||||
http-proxy-agent: "npm:^7.0.0"
|
||||
https-proxy-agent: "npm:^7.0.2"
|
||||
is-potential-custom-element-name: "npm:^1.0.1"
|
||||
parse5: "npm:^7.1.2"
|
||||
rrweb-cssom: "npm:^0.6.0"
|
||||
saxes: "npm:^6.0.0"
|
||||
symbol-tree: "npm:^3.2.4"
|
||||
tough-cookie: "npm:^4.1.3"
|
||||
w3c-xmlserializer: "npm:^5.0.0"
|
||||
webidl-conversions: "npm:^7.0.0"
|
||||
whatwg-encoding: "npm:^3.1.1"
|
||||
whatwg-mimetype: "npm:^4.0.0"
|
||||
whatwg-url: "npm:^14.0.0"
|
||||
ws: "npm:^8.16.0"
|
||||
xml-name-validator: "npm:^5.0.0"
|
||||
peerDependencies:
|
||||
canvas: ^2.11.2
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
checksum: 10c0/b062af50f7be59d914ba75236b7817c848ef3cd007aea1d6b8020a41eb263b7d5bd2652298106e9756b56892f773d990598778d02adab7d0d0d8e58726fc41d3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsesc@npm:^3.0.2":
|
||||
version: 3.1.0
|
||||
resolution: "jsesc@npm:3.1.0"
|
||||
@ -9128,13 +9022,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdn-data@npm:2.0.30":
|
||||
version: 2.0.30
|
||||
resolution: "mdn-data@npm:2.0.30"
|
||||
checksum: 10c0/a2c472ea16cee3911ae742593715aa4c634eb3d4b9f1e6ada0902aa90df13dcbb7285d19435f3ff213ebaa3b2e0c0265c1eb0e3fb278fda7f8919f046a410cd9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdurl@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdurl@npm:2.0.0"
|
||||
@ -10701,7 +10588,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse5@npm:^7.0.0, parse5@npm:^7.1.2":
|
||||
"parse5@npm:^7.0.0":
|
||||
version: 7.2.1
|
||||
resolution: "parse5@npm:7.2.1"
|
||||
dependencies:
|
||||
@ -11159,7 +11046,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"psl@npm:^1.1.28, psl@npm:^1.1.33":
|
||||
"psl@npm:^1.1.28":
|
||||
version: 1.15.0
|
||||
resolution: "psl@npm:1.15.0"
|
||||
dependencies:
|
||||
@ -12157,19 +12044,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rehype-mathjax@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "rehype-mathjax@npm:6.0.0"
|
||||
"rehype-mathjax@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "rehype-mathjax@npm:7.0.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^3.0.0"
|
||||
"@types/mathjax": "npm:^0.0.40"
|
||||
hast-util-from-dom: "npm:^5.0.0"
|
||||
hast-util-to-text: "npm:^4.0.0"
|
||||
jsdom: "npm:^23.0.0"
|
||||
hastscript: "npm:^9.0.0"
|
||||
mathjax-full: "npm:^3.0.0"
|
||||
unified: "npm:^11.0.0"
|
||||
unist-util-visit-parents: "npm:^6.0.0"
|
||||
checksum: 10c0/f6e0e5a0ed177cfacb9c23ec7603e5ac27937018cf7eb42b8db615eac9b121748686012a200db5ee6de1ca0cd5c8ba7d3aaec28090f01cf6321957a867f1bb78
|
||||
checksum: 10c0/bd05b8495316877f4c555bef127c463c0f198f11790523e082307a701234556bc84d2483508531bcc1d2e5c85c609a85dfe7980e8d7fea0c9c5e4f5298c80945
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -12571,20 +12457,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rrweb-cssom@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "rrweb-cssom@npm:0.6.0"
|
||||
checksum: 10c0/3d9d90d53c2349ea9c8509c2690df5a4ef930c9cf8242aeb9425d4046f09d712bb01047e00da0e1c1dab5db35740b3d78fd45c3e7272f75d3724a563f27c30a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rrweb-cssom@npm:^0.7.1":
|
||||
version: 0.7.1
|
||||
resolution: "rrweb-cssom@npm:0.7.1"
|
||||
checksum: 10c0/127b8ca6c8aac45e2755abbae6138d4a813b1bedc2caabf79466ae83ab3cfc84b5bfab513b7033f0aa4561c7753edf787d0dd01163ceacdee2e8eb1b6bf7237e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"run-parallel@npm:^1.1.9":
|
||||
version: 1.2.0
|
||||
resolution: "run-parallel@npm:1.2.0"
|
||||
@ -12682,15 +12554,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"saxes@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "saxes@npm:6.0.0"
|
||||
dependencies:
|
||||
xmlchars: "npm:^2.2.0"
|
||||
checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"scheduler@npm:^0.23.2":
|
||||
version: 0.23.2
|
||||
resolution: "scheduler@npm:0.23.2"
|
||||
@ -13028,7 +12891,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
|
||||
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "source-map-js@npm:1.2.1"
|
||||
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
|
||||
@ -13539,13 +13402,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"symbol-tree@npm:^3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "symbol-tree@npm:3.2.4"
|
||||
checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"synckit@npm:^0.9.1":
|
||||
version: 0.9.2
|
||||
resolution: "synckit@npm:0.9.2"
|
||||
@ -13813,18 +13669,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tough-cookie@npm:^4.1.3":
|
||||
version: 4.1.4
|
||||
resolution: "tough-cookie@npm:4.1.4"
|
||||
dependencies:
|
||||
psl: "npm:^1.1.33"
|
||||
punycode: "npm:^2.1.1"
|
||||
universalify: "npm:^0.2.0"
|
||||
url-parse: "npm:^1.5.3"
|
||||
checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tough-cookie@npm:~2.5.0":
|
||||
version: 2.5.0
|
||||
resolution: "tough-cookie@npm:2.5.0"
|
||||
@ -13835,15 +13679,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tr46@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "tr46@npm:5.0.0"
|
||||
dependencies:
|
||||
punycode: "npm:^2.3.1"
|
||||
checksum: 10c0/1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tr46@npm:~0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "tr46@npm:0.0.3"
|
||||
@ -14229,13 +14064,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "universalify@npm:0.2.0"
|
||||
checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "universalify@npm:2.0.1"
|
||||
@ -14284,7 +14112,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"url-parse@npm:^1.5.10, url-parse@npm:^1.5.3":
|
||||
"url-parse@npm:^1.5.10":
|
||||
version: 1.5.10
|
||||
resolution: "url-parse@npm:1.5.10"
|
||||
dependencies:
|
||||
@ -14499,15 +14327,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"w3c-xmlserializer@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "w3c-xmlserializer@npm:5.0.0"
|
||||
dependencies:
|
||||
xml-name-validator: "npm:^5.0.0"
|
||||
checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-namespaces@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "web-namespaces@npm:2.0.1"
|
||||
@ -14557,39 +14376,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webidl-conversions@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "webidl-conversions@npm:7.0.0"
|
||||
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-encoding@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "whatwg-encoding@npm:3.1.1"
|
||||
dependencies:
|
||||
iconv-lite: "npm:0.6.3"
|
||||
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-mimetype@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "whatwg-mimetype@npm:4.0.0"
|
||||
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-url@npm:^14.0.0":
|
||||
version: 14.1.0
|
||||
resolution: "whatwg-url@npm:14.1.0"
|
||||
dependencies:
|
||||
tr46: "npm:^5.0.0"
|
||||
webidl-conversions: "npm:^7.0.0"
|
||||
checksum: 10c0/f00104f1c67ce086ba8ffedab529cbbd9aefd8c0a6555320026de7aeff31f91c38680f95818b140a7c9cc657cde3781e567835dda552ddb1e2b8faaba0ac3cb6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-url@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "whatwg-url@npm:5.0.0"
|
||||
@ -14769,7 +14555,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^8.13.0, ws@npm:^8.16.0":
|
||||
"ws@npm:^8.13.0":
|
||||
version: 8.18.0
|
||||
resolution: "ws@npm:8.18.0"
|
||||
peerDependencies:
|
||||
@ -14816,13 +14602,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xml-name-validator@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "xml-name-validator@npm:5.0.0"
|
||||
checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xml-parse-from-string@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "xml-parse-from-string@npm:1.0.1"
|
||||
@ -14888,13 +14667,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xmlchars@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "xmlchars@npm:2.2.0"
|
||||
checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xmldom-sre@npm:0.1.31":
|
||||
version: 0.1.31
|
||||
resolution: "xmldom-sre@npm:0.1.31"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user