Merge remote-tracking branch 'origin/main'

This commit is contained in:
denislov 2025-02-18 20:15:37 +08:00
commit 2457c7b818
57 changed files with 1023 additions and 755 deletions

View File

@ -6,7 +6,8 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: checkboxes
id: checklist id: checklist
@ -15,9 +16,11 @@ body:
description: | description: |
Before submitting an issue, please make sure you have completed the following steps Before submitting an issue, please make sure you have completed the following steps
options: 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 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 required: true
- type: dropdown - type: dropdown
@ -45,7 +48,7 @@ body:
id: description id: description
attributes: attributes:
label: Bug Description 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... placeholder: Tell us what happened...
validations: validations:
required: true required: true
@ -54,7 +57,7 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: Steps To Reproduce 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: | placeholder: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
@ -82,4 +85,4 @@ body:
id: additional id: additional
attributes: attributes:
label: Additional Context 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

View File

@ -6,7 +6,8 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: checkboxes
id: checklist id: checklist
@ -15,9 +16,13 @@ body:
description: | description: |
Before submitting an issue, please make sure you have completed the following steps Before submitting an issue, please make sure you have completed the following steps
options: 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 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 required: true
- type: dropdown - type: dropdown
@ -44,28 +49,28 @@ body:
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:
label: Is your feature request related to a problem? label: Is your feature request related to an existing issue?
description: A clear and concise description of what the problem is description: Please briefly describe the problem you are experiencing.
placeholder: I'm always frustrated when... placeholder: I often feel frustrated because...
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: solution id: solution
attributes: attributes:
label: Describe the solution you'd like label: Desired Solution
description: A clear and concise description of what you want to happen description: Please briefly describe what you would like to happen.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: alternatives id: alternatives
attributes: attributes:
label: Describe alternatives you've considered label: Alternative Solutions
description: A clear and concise description of any alternative solutions or features you've considered description: Please briefly describe any alternative solutions or features you have considered.
- type: textarea - type: textarea
id: additional id: additional
attributes: attributes:
label: Additional Context label: Additional Information
description: Add any other context or screenshots about the feature request here description: Add any other context or screenshots related to your feature request.

View File

@ -1,12 +1,12 @@
name: Question name: Discussion & Questions
description: Ask a question or seek help description: Seeking help, discussing issues, asking questions, etc...
title: '[Question]: ' title: '[Discussion]: '
labels: ['question'] labels: ['question']
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: checkboxes
id: checklist id: checklist
@ -15,9 +15,9 @@ body:
description: | description: |
Before submitting an issue, please make sure you have completed the following steps Before submitting an issue, please make sure you have completed the following steps
options: 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 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 required: true
- type: dropdown - type: dropdown
@ -45,8 +45,8 @@ body:
id: question id: question
attributes: attributes:
label: Your Question label: Your Question
description: Please describe your question in detail description: Please describe your issue in detail.
placeholder: Please explain your question as clearly as possible... placeholder: Please explain your issue as clearly as possible...
validations: validations:
required: true required: true
@ -68,9 +68,9 @@ body:
id: priority id: priority
attributes: attributes:
label: Priority label: Priority
description: How urgent is this question for you? description: How urgent is this issue for you?
options: options:
- Low (Can wait) - Low (Review when available)
- Medium (Would like a response soon) - Medium (Would like a response soon)
- High (Blocking progress) - High (Blocking progress)
validations: validations:

View File

@ -1,4 +1,4 @@
name: 🐛 错误报告 name: 🐛 错误报告 (中文)
description: 创建一个报告以帮助我们改进 description: 创建一个报告以帮助我们改进
title: '[错误]: ' title: '[错误]: '
labels: ['bug'] labels: ['bug']
@ -7,17 +7,20 @@ body:
attributes: attributes:
value: | value: |
感谢您花时间填写此错误报告! 感谢您花时间填写此错误报告!
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes - type: checkboxes
id: checklist id: checklist
attributes: attributes:
label: Issue 检查清单 label: 提交前检查
description: | description: |
在提交 Issue 前请确保您已经完成了以下所有步骤 在提交 Issue 前请确保您已经完成了以下所有步骤
options: options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题 - label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true 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 required: true
- type: dropdown - type: dropdown
@ -45,7 +48,7 @@ body:
id: description id: description
attributes: attributes:
label: 错误描述 label: 错误描述
description: 清晰简洁地描述错误是什么 description: 描述问题时请尽可能详细
placeholder: 告诉我们发生了什么... placeholder: 告诉我们发生了什么...
validations: validations:
required: true required: true
@ -54,7 +57,7 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: 重现步骤 label: 重现步骤
description: 重现行为的步骤 description: 提供详细的重现步骤,以便于我们可以准确地重现问题
placeholder: | placeholder: |
1. 转到 '...' 1. 转到 '...'
2. 点击 '....' 2. 点击 '....'
@ -82,4 +85,4 @@ body:
id: additional id: additional
attributes: attributes:
label: 附加信息 label: 附加信息
description: 在此添加有关问题的任何其他上下文 description: 任何能让我们对你所遇到的问题有更多了解的东西

View File

@ -1,4 +1,4 @@
name: 💡 功能建议 name: 💡 功能建议 (中文)
description: 为项目提出新的想法 description: 为项目提出新的想法
title: '[功能]: ' title: '[功能]: '
labels: ['enhancement'] labels: ['enhancement']
@ -7,17 +7,22 @@ body:
attributes: attributes:
value: | value: |
感谢您花时间提出新的功能建议! 感谢您花时间提出新的功能建议!
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
- type: checkboxes - type: checkboxes
id: checklist id: checklist
attributes: attributes:
label: Issue 检查清单 label: 提交前检查
description: | description: |
在提交 Issue 前请确保您已经完成了以下所有步骤 在提交 Issue 前请确保您已经完成了以下所有步骤
options: options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题 - label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true 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 required: true
- type: dropdown - type: dropdown
@ -44,7 +49,7 @@ body:
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:
label: 您的功能建议是否与某个问题相关? label: 您的功能建议是否与某个问题/issue相关?
description: 请简明扼要地描述您遇到的问题 description: 请简明扼要地描述您遇到的问题
placeholder: 我总是感到沮丧,因为... placeholder: 我总是感到沮丧,因为...
validations: validations:

View File

@ -1,6 +1,6 @@
name: 提问 name: 讨论 & 提问 (中文)
description: 提出一个问题或寻求帮助 description: 寻求帮助、讨论问题、提出疑问等...
title: '[问题]: ' title: '[讨论]: '
labels: ['question'] labels: ['question']
body: body:
- type: markdown - type: markdown
@ -15,9 +15,9 @@ body:
description: | description: |
在提交 Issue 前请确保您已经完成了以下所有步骤 在提交 Issue 前请确保您已经完成了以下所有步骤
options: options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题 - label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true required: true
- label: 正确填写了 Issue 标题 - label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议
required: true required: true
- type: dropdown - type: dropdown

View File

@ -80,11 +80,9 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
增加服务商 LM Studio、魔搭、Perplexity、无问芯穹、DMXAPI 消息分组支持网格模式
提及功能支持上下按键循环选择模型 知识库支持多选
小程序增加小艺 支持库增加 DRAFTS, EPUB
增加Notion连接检测功能 知识库支持调节匹配度阈值
编辑模型弹窗搜索模型时同时搜索模型的名字和ID 添加 NotebookLM, Coze 小程序
编辑模型弹窗增加推理模型筛选按钮 增加话题提示词
修复思考模型思考时间显示错误
修复部分模型翻译出错

View File

@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.9.24", "version": "0.9.25",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@ -137,7 +137,7 @@
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^7.0.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",

View File

@ -2,6 +2,7 @@ export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'] export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac'] export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods'] export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const thirdPartyApplicationExts = ['.draftsExport']
export const bookExts = ['.epub'] export const bookExts = ['.epub']
export const textExts = [ export const textExts = [
'.txt', // 普通文本文件 '.txt', // 普通文本文件

View File

@ -0,0 +1,22 @@
import * as fs from 'node:fs'
import { JsonLoader } from '@llm-tools/embedjs'
/**
* Drafts
* JSON contenttagsmodified_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 })
}
}

View File

@ -1,17 +1,18 @@
import * as fs from 'node:fs' 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 type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
import { WebLoader } from '@llm-tools/embedjs-loader-web' import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { LoaderReturn } from '@shared/config/types' import { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams } from '@types' import { FileType, KnowledgeBaseParams } from '@types'
import Logger from 'electron-log' import Logger from 'electron-log'
import { DraftsExportLoader } from './draftsExportLoader'
import { EpubLoader } from './epubLoader' import { EpubLoader } from './epubLoader'
import { OdLoader, OdType } from './odLoader' import { OdLoader, OdType } from './odLoader'
// embedjs内置loader类型 // embedjs内置loader类型
const commonExts = ['.pdf', '.csv', '.json', '.docx', '.pptx', '.xlsx', '.md'] const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
export async function addOdLoader( export async function addOdLoader(
ragApplication: RAGApplication, ragApplication: RAGApplication,
@ -89,7 +90,19 @@ export async function addFileLoader(
} as LoaderReturn } 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') const fileContent = fs.readFileSync(file.path, 'utf-8')
// HTML类型 // HTML类型
if (['.html', '.htm'].includes(file.ext)) { if (['.html', '.htm'].includes(file.ext)) {
const loaderReturn = await ragApplication.addLoader( 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( const loaderReturn = await ragApplication.addLoader(
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any, new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,

View File

@ -47,8 +47,8 @@ function formatShortcutKey(shortcut: string[]): string {
function handleZoom(delta: number) { function handleZoom(delta: number) {
return (window: BrowserWindow) => { return (window: BrowserWindow) => {
const currentZoom = window.webContents.getZoomFactor() const currentZoom = configManager.getZoomFactor()
const newZoom = currentZoom + delta const newZoom = Number((currentZoom + delta).toFixed(1))
if (newZoom >= 0.1 && newZoom <= 5.0) { if (newZoom >= 0.1 && newZoom <= 5.0) {
window.webContents.setZoomFactor(newZoom) window.webContents.setZoomFactor(newZoom)
configManager.setZoomFactor(newZoom) configManager.setZoomFactor(newZoom)
@ -110,7 +110,9 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
} }
export function registerShortcuts(window: BrowserWindow) { export function registerShortcuts(window: BrowserWindow) {
window.webContents.setZoomFactor(configManager.getZoomFactor()) window.once('ready-to-show', () => {
window.webContents.setZoomFactor(configManager.getZoomFactor())
})
const register = () => { const register = () => {
if (window.isDestroyed()) return if (window.isDestroyed()) return

View File

@ -6,6 +6,7 @@
<meta <meta
http-equiv="Content-Security-Policy" 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:" /> 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> <style>
html, html,

View File

@ -64,6 +64,10 @@
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
} }
&:has(+ ul) {
margin-bottom: 0;
}
} }
ul { ul {

View File

@ -1,6 +1,7 @@
/* eslint-disable react/no-unknown-property */ /* eslint-disable react/no-unknown-property */
import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons' import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { AppLogo } from '@renderer/config/env'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useBridge } from '@renderer/hooks/useBridge' import { useBridge } from '@renderer/hooks/useBridge'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
@ -49,7 +50,10 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
} }
const onOpenLink = () => { const onOpenLink = () => {
window.api.openWebsite(app.url) if (webviewRef.current) {
const currentUrl = webviewRef.current.getURL()
window.api.openWebsite(currentUrl)
}
} }
const onTogglePin = () => { const onTogglePin = () => {
@ -236,6 +240,10 @@ export default class MinApp {
await delay(0) await delay(0)
} }
if (!app.logo) {
app.logo = AppLogo
}
MinApp.app = app MinApp.app = app
store.dispatch(setMinappShow(true)) store.dispatch(setMinappShow(true))

View File

@ -1,6 +1,6 @@
import { Input, Modal } from 'antd' import { Input, Modal } from 'antd'
import { TextAreaProps } from 'antd/es/input' import { TextAreaProps } from 'antd/es/input'
import { useState } from 'react' import { useRef, useState } from 'react'
import { Box } from '../Layout' import { Box } from '../Layout'
import { TopView } from '../TopView' import { TopView } from '../TopView'
@ -27,6 +27,7 @@ const PromptPopupContainer: React.FC<Props> = ({
}) => { }) => {
const [value, setValue] = useState(defaultValue) const [value, setValue] = useState(defaultValue)
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const textAreaRef = useRef<any>(null)
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
@ -41,17 +42,35 @@ const PromptPopupContainer: React.FC<Props> = ({
resolve(null) 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 PromptPopup.hide = onCancel
return ( 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> <Box mb={8}>{message}</Box>
<Input.TextArea <Input.TextArea
ref={textAreaRef}
placeholder={inputPlaceholder} placeholder={inputPlaceholder}
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
allowClear allowClear
autoFocus
onPressEnter={onOk} onPressEnter={onOk}
rows={1} rows={1}
{...inputProps} {...inputProps}

View File

@ -51,6 +51,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
setTimeout(resizeTextArea, 0) 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 TextEditPopup.hide = onCancel
return ( return (
@ -65,6 +76,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
onOk={onOk} onOk={onOk}
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
afterOpenChange={handleAfterOpenChange}
centered> centered>
<TextArea <TextArea
ref={textareaRef} ref={textareaRef}

View File

@ -50,6 +50,7 @@ const Sidebar: FC = () => {
const onOpenDocs = () => { const onOpenDocs = () => {
MinApp.start({ MinApp.start({
id: 'docs',
name: t('docs.title'), name: t('docs.title'),
url: 'https://docs.cherry-ai.com/', url: 'https://docs.cherry-ai.com/',
logo: AppLogo logo: AppLogo
@ -77,9 +78,11 @@ const Sidebar: FC = () => {
</AppsContainer> </AppsContainer>
)} )}
</MainMenusContainer> </MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus>
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right"> <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 /> <QuestionCircleOutlined />
</Icon> </Icon>
</Tooltip> </Tooltip>
@ -93,8 +96,14 @@ const Sidebar: FC = () => {
</Icon> </Icon>
</Tooltip> </Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}> <StyledLink
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}> 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" /> <i className="iconfont icon-setting" />
</Icon> </Icon>
</StyledLink> </StyledLink>
@ -108,10 +117,11 @@ const MainMenus: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { pathname } = useLocation() const { pathname } = useLocation()
const { sidebarIcons } = useSettings() const { sidebarIcons } = useSettings()
const { minappShow } = useRuntime()
const navigate = useNavigate() const navigate = useNavigate()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '') const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
const iconMap = { const iconMap = {
assistants: <i className="iconfont icon-chat" />, assistants: <i className="iconfont icon-chat" />,
@ -139,7 +149,13 @@ const MainMenus: FC = () => {
return ( return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right"> <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> <Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
@ -150,6 +166,7 @@ const MainMenus: FC = () => {
const PinnedApps: FC = () => { const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps() const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation() const { t } = useTranslation()
const { minappShow } = useRuntime()
return ( return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}> <DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
@ -164,11 +181,12 @@ const PinnedApps: FC = () => {
} }
} }
] ]
const isActive = minappShow && MinApp.app?.id === app.id
return ( return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right"> <Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink> <StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}> <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 }} /> <MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon> </Icon>
</Dropdown> </Dropdown>

View File

@ -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 BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url' import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?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 DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url' import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?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 MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url' import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?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 PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url' import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?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 WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url' import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?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 YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.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 ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url' import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url' import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url'
@ -171,7 +171,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
}, },
{ {
id: 'perplexity', id: 'perplexity',
name: 'perplexity', name: 'Perplexity',
logo: PerplexityAppLogo, logo: PerplexityAppLogo,
url: 'https://www.perplexity.ai/' url: 'https://www.perplexity.ai/'
}, },
@ -306,7 +306,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
id: 'notebooklm', id: 'notebooklm',
name: 'NotebookLM', name: 'NotebookLM',
logo: NotebookLMAppLogo, logo: NotebookLMAppLogo,
url: 'https://notebooklm.google.com/', url: 'https://notebooklm.google.com/'
}, },
{ {
id: 'coze', id: 'coze',

View File

@ -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 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 = export const EMBEDDING_REGEX =
/(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i /(?:^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: [ dmxapi: [
{ {
id: 'gpt-3.5-turbo', id: 'Qwen/Qwen2.5-7B-Instruct',
provider: 'dmxapi', provider: 'dmxapi',
name: 'GPT-3.5-Turbo', name: 'Qwen/Qwen2.5-7B-Instruct',
group: 'OpenAI' 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', id: 'gpt-4o',
provider: 'dmxapi', provider: 'dmxapi',
name: 'GPT-4o', name: 'gpt-4o',
group: 'OpenAI' group: 'OpenAI'
}, },
{ {
id: 'gpt-4o-mini', id: 'gpt-4o-mini',
provider: 'dmxapi', provider: 'dmxapi',
name: 'GPT-4o-Mini', name: 'gpt-4o-mini',
group: 'OpenAI' group: 'OpenAI'
}, },
{ {
id: 'deepseek-reasoner', id: 'DMXAPI-DeepSeek-R1',
provider: 'dmxapi', provider: 'dmxapi',
name: 'DeepSeek Reasoner', name: 'DMXAPI-DeepSeek-R1',
group: 'DeepSeek' group: 'DeepSeek'
}, },
{ {
id: 'deepseek-chat', id: 'DMXAPI-DeepSeek-V3',
provider: 'dmxapi', provider: 'dmxapi',
name: 'DeepSeek Chat', name: 'DMXAPI-DeepSeek-V3',
group: 'DeepSeek' 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: [ perplexity: [

View File

@ -212,11 +212,11 @@ export const PROVIDER_CONFIG = {
}, },
dmxapi: { dmxapi: {
api: { api: {
url: 'https://api.dmxapi.com' url: 'https://www.dmxapi.com'
}, },
websites: { websites: {
official: 'https://dmxapi.com/', official: 'https://www.dmxapi.com/register?aff=81aj/',
apiKey: 'https://www.dmxapi.com/token', apiKey: 'https://www.dmxapi.com/register?aff=81aj',
docs: 'https://dmxapi.com/models.html#code-block', docs: 'https://dmxapi.com/models.html#code-block',
models: 'https://www.dmxapi.com/pricing' models: 'https://www.dmxapi.com/pricing'
} }
@ -526,7 +526,7 @@ export const PROVIDER_CONFIG = {
}, },
websites: { websites: {
official: 'https://cloud.baidu.com/', 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', docs: 'https://cloud.baidu.com/doc/index.html',
models: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu' models: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu'
} }

View File

@ -44,7 +44,7 @@ export function useAssistant(id: string) {
return { return {
assistant, assistant,
model: assistant?.model ?? defaultModel, model: assistant?.model ?? assistant?.defaultModel ?? defaultModel,
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })), addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => { removeTopic: (topic: Topic) => {
TopicManager.removeTopic(topic.id) TopicManager.removeTopic(topic.id)

View File

@ -307,16 +307,22 @@ export const useKnowledgeBases = () => {
// remove assistant knowledge_base // remove assistant knowledge_base
const _assistants = assistants.map((assistant) => { const _assistants = assistants.map((assistant) => {
if (assistant.knowledge_base?.id === baseId) { if (assistant.knowledge_bases?.find((kb) => kb.id === baseId)) {
return { ...assistant, knowledge_base: undefined } return {
...assistant,
knowledge_bases: assistant.knowledge_bases.filter((kb) => kb.id !== baseId)
}
} }
return assistant return assistant
}) })
// remove agent knowledge_base // remove agent knowledge_base
const _agents = agents.map((agent) => { const _agents = agents.map((agent) => {
if (agent.knowledge_base?.id === baseId) { if (agent.knowledge_bases?.find((kb) => kb.id === baseId)) {
return { ...agent, knowledge_base: undefined } return {
...agent,
knowledge_bases: agent.knowledge_bases.filter((kb) => kb.id !== baseId)
}
} }
return agent return agent
}) })

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "Fold", "message.multi_model_style.fold": "Fold",
"message.multi_model_style.horizontal": "Horizontal", "message.multi_model_style.horizontal": "Horizontal",
"message.multi_model_style.vertical": "Vertical", "message.multi_model_style.vertical": "Vertical",
"message.multi_model_style.grid": "Grid",
"message.style": "Message style", "message.style": "Message style",
"message.style.bubble": "Bubble", "message.style.bubble": "Bubble",
"message.style.plain": "Plain", "message.style.plain": "Plain",
@ -559,7 +560,9 @@
}, },
"data.title": "Data Directory", "data.title": "Data Directory",
"notion.api_key": "Notion API Key", "notion.api_key": "Notion API Key",
"notion.api_key_placeholder": "Enter Notion API Key",
"notion.database_id": "Notion Database ID", "notion.database_id": "Notion Database ID",
"notion.database_id_placeholder": "Enter Notion Database ID",
"notion.title": "Notion Configuration", "notion.title": "Notion Configuration",
"notion.check": { "notion.check": {
"button": "Check", "button": "Check",
@ -636,6 +639,10 @@
"messages.input.title": "Input Settings", "messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input message", "messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math engine", "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.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.model.title": "Model Settings", "messages.model.title": "Model Settings",
"messages.title": "Message Settings", "messages.title": "Message Settings",
@ -708,7 +715,7 @@
}, },
"shortcuts": { "shortcuts": {
"action": "Action", "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_shortcut": "Clear Shortcut",
"clear_topic": "Clear Messages", "clear_topic": "Clear Messages",
"copy_last_message": "Copy Last Message", "copy_last_message": "Copy Last Message",

View File

@ -363,6 +363,7 @@
"message.multi_model_style.fold": "折りたたむ", "message.multi_model_style.fold": "折りたたむ",
"message.multi_model_style.horizontal": "水平", "message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直", "message.multi_model_style.vertical": "垂直",
"message.multi_model_style.grid": "グリッド",
"message.style": "メッセージスタイル", "message.style": "メッセージスタイル",
"message.style.bubble": "バブル", "message.style.bubble": "バブル",
"message.style.plain": "プレーン", "message.style.plain": "プレーン",
@ -559,7 +560,9 @@
}, },
"data.title": "データディレクトリ", "data.title": "データディレクトリ",
"notion.api_key": "Notion APIキー", "notion.api_key": "Notion APIキー",
"notion.api_key_placeholder": "Notion APIキーを入力してください",
"notion.database_id": "Notion データベースID", "notion.database_id": "Notion データベースID",
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
"notion.title": "Notion 設定", "notion.title": "Notion 設定",
"notion.check": { "notion.check": {
"button": "確認", "button": "確認",
@ -636,6 +639,10 @@
"messages.input.title": "入力設定", "messages.input.title": "入力設定",
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング", "messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"messages.math_engine": "数式エンジン", "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.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
"messages.model.title": "モデル設定", "messages.model.title": "モデル設定",
"messages.title": "メッセージ設定", "messages.title": "メッセージ設定",
@ -708,7 +715,7 @@
}, },
"shortcuts": { "shortcuts": {
"action": "操作", "action": "操作",
"alt_warning": "MacではOption + 文字をショートカットとして使用できません", "alt_warning": "MacではOptionキーとの組み合わせは、スペースキーのみ使用可能です",
"clear_shortcut": "ショートカットをクリア", "clear_shortcut": "ショートカットをクリア",
"clear_topic": "メッセージを消去", "clear_topic": "メッセージを消去",
"copy_last_message": "最後のメッセージをコピー", "copy_last_message": "最後のメッセージをコピー",

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "Свернуть", "message.multi_model_style.fold": "Свернуть",
"message.multi_model_style.horizontal": "Горизонтальный", "message.multi_model_style.horizontal": "Горизонтальный",
"message.multi_model_style.vertical": "Вертикальный", "message.multi_model_style.vertical": "Вертикальный",
"message.multi_model_style.grid": "клетчатый вид",
"message.style": "Стиль сообщения", "message.style": "Стиль сообщения",
"message.style.bubble": "Пузырь", "message.style.bubble": "Пузырь",
"message.style.plain": "Простой", "message.style.plain": "Простой",
@ -559,7 +560,9 @@
}, },
"data.title": "Каталог данных", "data.title": "Каталог данных",
"notion.api_key": "Ключ API Notion", "notion.api_key": "Ключ API Notion",
"notion.api_key_placeholder": "Введите ключ API Notion",
"notion.database_id": "ID базы данных Notion", "notion.database_id": "ID базы данных Notion",
"notion.database_id_placeholder": "Введите ID базы данных Notion",
"notion.title": "Настройки Notion", "notion.title": "Настройки Notion",
"notion.check": { "notion.check": {
"button": "Проверить", "button": "Проверить",
@ -637,6 +640,10 @@
"messages.math_engine": "Математический движок", "messages.math_engine": "Математический движок",
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
"messages.model.title": "Настройки модели", "messages.model.title": "Настройки модели",
"messages.grid_columns": "Количество столбцов сетки сообщений",
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
"messages.grid_popover_trigger.hover": "Наведение для отображения",
"messages.grid_popover_trigger.click": "Нажатие для отображения",
"messages.title": "Настройки сообщений", "messages.title": "Настройки сообщений",
"messages.use_serif_font": "Использовать serif шрифт", "messages.use_serif_font": "Использовать serif шрифт",
"model": "Модель по умолчанию", "model": "Модель по умолчанию",
@ -707,7 +714,7 @@
}, },
"shortcuts": { "shortcuts": {
"action": "Действие", "action": "Действие",
"alt_warning": "Mac не поддерживает Option + буквы как горячие клавиши", "alt_warning": "В Mac сочетания с клавишей Option работают только с пробелом",
"clear_shortcut": "Очистить сочетание клавиш", "clear_shortcut": "Очистить сочетание клавиш",
"clear_topic": "Очистить все сообщения", "clear_topic": "Очистить все сообщения",
"copy_last_message": "Копировать последнее сообщение", "copy_last_message": "Копировать последнее сообщение",

View File

@ -366,6 +366,7 @@
"message.multi_model_style.fold": "折叠", "message.multi_model_style.fold": "折叠",
"message.multi_model_style.horizontal": "水平", "message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直", "message.multi_model_style.vertical": "垂直",
"message.multi_model_style.grid": "网格",
"message.style": "消息样式", "message.style": "消息样式",
"message.style.bubble": "气泡", "message.style.bubble": "气泡",
"message.style.plain": "简洁", "message.style.plain": "简洁",
@ -559,7 +560,9 @@
}, },
"data.title": "数据目录", "data.title": "数据目录",
"notion.api_key": "Notion 密钥", "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.title": "Notion 配置",
"notion.check": { "notion.check": {
"button": "检查", "button": "检查",
@ -636,6 +639,10 @@
"messages.input.title": "输入设置", "messages.input.title": "输入设置",
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息", "messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
"messages.math_engine": "数学公式引擎", "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.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
"messages.model.title": "模型设置", "messages.model.title": "模型设置",
"messages.title": "消息设置", "messages.title": "消息设置",
@ -708,7 +715,7 @@
}, },
"shortcuts": { "shortcuts": {
"action": "操作", "action": "操作",
"alt_warning": "Mac 系统不能使用 Option + 字母作为快捷键", "alt_warning": "Mac 系统中 Option 键只能与空格键组合使用",
"clear_shortcut": "清除快捷键", "clear_shortcut": "清除快捷键",
"clear_topic": "清空消息", "clear_topic": "清空消息",
"copy_last_message": "复制上一条消息", "copy_last_message": "复制上一条消息",

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "折疊", "message.multi_model_style.fold": "折疊",
"message.multi_model_style.horizontal": "水平", "message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直", "message.multi_model_style.vertical": "垂直",
"message.multi_model_style.grid": "网格",
"message.style": "消息樣式", "message.style": "消息樣式",
"message.style.bubble": "氣泡", "message.style.bubble": "氣泡",
"message.style.plain": "簡潔", "message.style.plain": "簡潔",
@ -557,7 +558,9 @@
}, },
"data.title": "數據目錄", "data.title": "數據目錄",
"notion.api_key": "Notion 金鑰", "notion.api_key": "Notion 金鑰",
"notion.api_key_placeholder": "請輸入Notion 密鑰",
"notion.database_id": "Notion 資料庫 ID", "notion.database_id": "Notion 資料庫 ID",
"notion.database_id_placeholder": "請輸入Notion 資料庫 ID",
"notion.title": "Notion 配置", "notion.title": "Notion 配置",
"notion.check": { "notion.check": {
"button": "檢查", "button": "檢查",
@ -635,6 +638,10 @@
"messages.input.show_estimated_tokens": "顯示預估 Token 數", "messages.input.show_estimated_tokens": "顯示預估 Token 數",
"messages.input.title": "輸入設定", "messages.input.title": "輸入設定",
"messages.math_engine": "Markdown 渲染輸入訊息", "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.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
"messages.model.title": "模型設定", "messages.model.title": "模型設定",
"messages.title": "訊息設定", "messages.title": "訊息設定",
@ -707,7 +714,7 @@
}, },
"shortcuts": { "shortcuts": {
"action": "操作", "action": "操作",
"alt_warning": "Mac 不能使用 Option + 字母作為快捷鍵", "alt_warning": "Mac 系統中 Option 鍵只能與空白鍵組合使用",
"clear_shortcut": "清除快捷鍵", "clear_shortcut": "清除快捷鍵",
"clear_topic": "清除所有訊息", "clear_topic": "清除所有訊息",
"copy_last_message": "複製上一条消息", "copy_last_message": "複製上一条消息",

View File

@ -9,7 +9,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { fetchGenerate } from '@renderer/services/ApiService' import { fetchGenerate } from '@renderer/services/ApiService'
import { getDefaultModel } from '@renderer/services/AssistantService' import { getDefaultModel } from '@renderer/services/AssistantService'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { Agent } from '@renderer/types' import { Agent, KnowledgeBase } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils' import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd' import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
@ -25,7 +25,7 @@ type FieldType = {
id: string id: string
name: string name: string
prompt: string prompt: string
knowledge_base_id: string knowledge_base_id: string[]
} }
const PopupContainer: React.FC<Props> = ({ resolve }) => { const PopupContainer: React.FC<Props> = ({ resolve }) => {
@ -37,8 +37,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [emoji, setEmoji] = useState('') const [emoji, setEmoji] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const knowledgeState = useAppSelector((state) => state.knowledge) const knowledgeState = useAppSelector((state) => state.knowledge)
const knowledgeOptions: SelectProps['options'] = []
const showKnowledgeIcon = useSidebarIconShow('knowledge') const showKnowledgeIcon = useSidebarIconShow('knowledge')
const knowledgeOptions: SelectProps['options'] = []
knowledgeState.bases.forEach((base) => { knowledgeState.bases.forEach((base) => {
knowledgeOptions.push({ knowledgeOptions.push({
@ -57,7 +57,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const _agent: Agent = { const _agent: Agent = {
id: uuid(), id: uuid(),
name: values.name, 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, emoji: _emoji,
prompt: values.prompt, prompt: values.prompt,
defaultModel: getDefaultModel(), defaultModel: getDefaultModel(),
@ -156,10 +158,16 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
{showKnowledgeIcon && ( {showKnowledgeIcon && (
<Form.Item name="knowledge_base_id" label={t('agents.add.knowledge_base')} rules={[{ required: false }]}> <Form.Item name="knowledge_base_id" label={t('agents.add.knowledge_base')} rules={[{ required: false }]}>
<Select <Select
mode="multiple"
allowClear allowClear
placeholder={t('agents.add.knowledge_base.placeholder')} placeholder={t('agents.add.knowledge_base.placeholder')}
menuItemSelectedIcon={<CheckOutlined />} menuItemSelectedIcon={<CheckOutlined />}
options={knowledgeOptions} options={knowledgeOptions}
filterOption={(input, option) =>
String(option?.label ?? '')
.toLowerCase()
.includes(input.toLowerCase())
}
/> />
</Form.Item> </Form.Item>
)} )}

View File

@ -52,7 +52,6 @@ interface Props {
let _text = '' let _text = ''
let _files: FileType[] = [] let _files: FileType[] = []
let _base: KnowledgeBase | undefined
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => { const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
@ -83,7 +82,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const [spaceClickCount, setSpaceClickCount] = useState(0) const [spaceClickCount, setSpaceClickCount] = useState(0)
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>()
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base) const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
const [mentionModels, setMentionModels] = useState<Model[]>([]) const [mentionModels, setMentionModels] = useState<Model[]>([])
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false) const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
@ -104,7 +103,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
_text = text _text = text
_files = files _files = files
_base = selectedKnowledgeBase
const sendMessage = useCallback(async () => { const sendMessage = useCallback(async () => {
await modelGenerating() await modelGenerating()
@ -124,8 +122,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
status: 'success' status: 'success'
} }
if (selectedKnowledgeBase) { if (selectedKnowledgeBases) {
message.knowledgeBaseIds = [selectedKnowledgeBase.id] message.knowledgeBaseIds = selectedKnowledgeBases.map((base) => base.id)
} }
if (files.length > 0) { if (files.length > 0) {
@ -144,7 +142,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setTimeout(() => resizeTextArea(), 0) setTimeout(() => resizeTextArea(), 0)
setExpend(false) setExpend(false)
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files, mentionModels]) }, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBases, files, mentionModels])
const translate = async () => { const translate = async () => {
if (isTranslating) { if (isTranslating) {
@ -458,14 +456,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
}, []) }, [])
useEffect(() => { useEffect(() => {
setSelectedKnowledgeBase(showKnowledgeIcon ? assistant.knowledge_base : undefined) // if assistant knowledge bases are undefined return []
}, [assistant.id, assistant.knowledge_base, showKnowledgeIcon]) setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : [])
}, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon])
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1 const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => { const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
updateAssistant({ ...assistant, knowledge_base: base }) updateAssistant({ ...assistant, knowledge_bases: bases })
setSelectedKnowledgeBase(base) setSelectedKnowledgeBases(bases ?? [])
} }
const onMentionModel = (model: Model) => { const onMentionModel = (model: Model) => {
@ -573,7 +572,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
</Tooltip> </Tooltip>
{showKnowledgeIcon && ( {showKnowledgeIcon && (
<KnowledgeBaseButton <KnowledgeBaseButton
selectedBase={selectedKnowledgeBase} selectedBases={selectedKnowledgeBases}
onSelect={handleKnowledgeBaseSelect} onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton} ToolbarButton={ToolbarButton}
disabled={files.length > 0} disabled={files.length > 0}

View File

@ -1,71 +1,68 @@
import { FileSearchOutlined } from '@ant-design/icons' import { CheckOutlined, FileSearchOutlined } from '@ant-design/icons'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { KnowledgeBase } from '@renderer/types' import { KnowledgeBase } from '@renderer/types'
import { Button, Popover, Tooltip } from 'antd' import { Popover, Select, SelectProps, Tooltip } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
selectedBase?: KnowledgeBase selectedBases?: KnowledgeBase[]
onSelect: (base?: KnowledgeBase) => void onSelect: (bases: KnowledgeBase[]) => void
disabled?: boolean disabled?: boolean
ToolbarButton?: any ToolbarButton?: any
} }
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => { const KnowledgeBaseSelector: FC<Props> = ({ selectedBases, onSelect }) => {
const { t } = useTranslation() const { t } = useTranslation()
const knowledgeState = useAppSelector((state) => state.knowledge) const knowledgeState = useAppSelector((state) => state.knowledge)
const knowledgeOptions: SelectProps['options'] = knowledgeState.bases.map((base) => ({
label: base.name,
value: base.id
}))
return ( return (
<SelectorContainer> <SelectorContainer>
{knowledgeState.bases.length === 0 ? ( {knowledgeState.bases.length === 0 ? (
<EmptyMessage>{t('knowledge.no_bases')}</EmptyMessage> <EmptyMessage>{t('knowledge.no_bases')}</EmptyMessage>
) : ( ) : (
<> <Select
{selectedBase && ( mode="multiple"
<Button type="link" block onClick={() => onSelect(undefined)} style={{ textAlign: 'left' }}> value={selectedBases?.map((base) => base.id)}
{t('knowledge.clear_selection')} allowClear
</Button> placeholder={t('agents.add.knowledge_base.placeholder')}
)} menuItemSelectedIcon={<CheckOutlined />}
{knowledgeState.bases.map((base) => ( options={knowledgeOptions}
<Button filterOption={(input, option) =>
key={base.id} String(option?.label ?? '')
type={selectedBase?.id === base.id ? 'primary' : 'text'} .toLowerCase()
block .includes(input.toLowerCase())
onClick={() => onSelect(base)} }
style={{ textAlign: 'left' }}> onChange={(ids) => {
{base.name} const newSelected = knowledgeState.bases.filter((base) => ids.includes(base.id))
</Button> onSelect(newSelected)
))} }}
</> style={{ width: '200px' }}
/>
)} )}
</SelectorContainer> </SelectorContainer>
) )
} }
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, ToolbarButton }) => { const KnowledgeBaseButton: FC<Props> = ({ selectedBases, onSelect, disabled, ToolbarButton }) => {
const { t } = useTranslation() 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 ( return (
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow> <Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
<Popover <Popover
placement="top" placement="top"
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />} content={<KnowledgeBaseSelector selectedBases={selectedBases} onSelect={onSelect} />}
overlayStyle={{ maxWidth: 400 }} overlayStyle={{ maxWidth: 400 }}
trigger="click"> trigger="click">
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}> <ToolbarButton type="text" disabled={disabled}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} /> <FileSearchOutlined
style={{ color: selectedBases && selectedBases?.length > 0 ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton> </ToolbarButton>
</Popover> </Popover>
</Tooltip> </Tooltip>

View File

@ -28,6 +28,8 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const itemRefs = useRef<Array<HTMLDivElement | null>>([]) 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) => { const setItemRef = (index: number, el: HTMLDivElement | null) => {
itemRefs.current[index] = el itemRefs.current[index] = el
@ -44,7 +46,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
const handleModelSelect = (model: Model) => { const handleModelSelect = (model: Model) => {
// Check if model is already selected // Check if model is already selected
if (mentionModels.some((selected) => selected.id === model.id)) { if (mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(model))) {
return return
} }
onSelect(model) onSelect(model)
@ -186,6 +188,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
setIsOpen(true) setIsOpen(true)
setSelectedIndex(0) setSelectedIndex(0)
setSearchText('') setSearchText('')
setMenuDismissed(false) // Reset dismissed flag when manually showing selector
} }
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@ -209,7 +212,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
e.preventDefault() e.preventDefault()
if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) { if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) {
const selectedModel = flatModelItems[selectedIndex].model const selectedModel = flatModelItems[selectedIndex].model
if (!mentionModels.some((selected) => selected.id === selectedModel.id)) { if (!mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(selectedModel))) {
flatModelItems[selectedIndex].onClick() flatModelItems[selectedIndex].onClick()
} }
setIsOpen(false) setIsOpen(false)
@ -218,6 +221,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
setIsOpen(false) setIsOpen(false)
setSearchText('') setSearchText('')
setMenuDismissed(true) // Set dismissed flag when Escape is pressed
} }
} }
@ -230,10 +234,14 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) { if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
setIsOpen(false) setIsOpen(false)
setSearchText('') setSearchText('')
} else if (lastAtIndex !== -1) { setMenuDismissed(false) // Reset dismissed flag when @ is removed
// Get the text after @ for search } else {
const searchStr = textBeforeCursor.slice(lastAtIndex + 1) // Only open menu if it wasn't explicitly dismissed
setSearchText(searchStr) if (!menuDismissed) {
setIsOpen(true)
const searchStr = textBeforeCursor.slice(lastAtIndex + 1)
setSearchText(searchStr)
}
} }
} }
@ -252,39 +260,42 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
textArea.removeEventListener('input', handleTextChange) textArea.removeEventListener('input', handleTextChange)
} }
} }
}, [isOpen, selectedIndex, flatModelItems, mentionModels]) }, [isOpen, selectedIndex, flatModelItems, mentionModels, menuDismissed])
// Hide dropdown if no models available
if (flatModelItems.length === 0) {
return null
}
const menu = ( const menu = (
<div ref={menuRef} className="ant-dropdown-menu"> <div ref={menuRef} className="ant-dropdown-menu">
{modelMenuItems.map((group, groupIndex) => { {flatModelItems.length > 0 ? (
if (!group) return null 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) const startIndex = modelMenuItems.slice(0, groupIndex).reduce((acc, g) => acc + (g?.children?.length || 0), 0)
return ( return (
<div key={group.key} className="ant-dropdown-menu-item-group"> <div key={group.key} className="ant-dropdown-menu-item-group">
<div className="ant-dropdown-menu-item-group-title">{group.label}</div> <div className="ant-dropdown-menu-item-group-title">{group.label}</div>
<div> <div>
{group.children.map((item, idx) => ( {group.children.map((item, idx) => (
<div <div
key={item.key} key={item.key}
ref={(el) => setItemRef(startIndex + idx, el)} ref={(el) => setItemRef(startIndex + idx, el)}
className={`ant-dropdown-menu-item ${selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''}`} className={`ant-dropdown-menu-item ${
onClick={item.onClick}> selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''
<span className="ant-dropdown-menu-item-icon">{item.icon}</span> }`}
{item.label} onClick={item.onClick}>
</div> <span className="ant-dropdown-menu-item-icon">{item.icon}</span>
))} {item.label}
</div>
))}
</div>
</div> </div>
</div> )
) })
})} ) : (
<div className="ant-dropdown-menu-item-group">
<div className="ant-dropdown-menu-item no-results">{t('models.no_matches')}</div>
</div>
)}
</div> </div>
) )
@ -334,6 +345,17 @@ const DropdownMenuStyle = createGlobalStyle`
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: transparent; 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 { .ant-dropdown-menu-item-group {

View File

@ -1,4 +1,5 @@
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Flex, Tag } from 'antd' import { Flex, Tag } from 'antd'
import { FC } from 'react' import { FC } from 'react'
@ -13,14 +14,19 @@ const MentionModelsInput: FC<{
const { t } = useTranslation() const { t } = useTranslation()
const getProviderName = (model: Model) => { 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 provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
} }
return ( return (
<Container gap="4px 0" wrap> <Container gap="4px 0" wrap>
{selectedModels.map((model) => ( {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)}) @{model.name} ({getProviderName(model)})
</Tag> </Tag>
))} ))}

View File

@ -240,6 +240,7 @@ const MessageContentContainer = styled.div`
justify-content: space-between; justify-content: space-between;
margin-left: 46px; margin-left: 46px;
margin-top: 5px; margin-top: 5px;
overflow-y: auto;
` `
const MessageFooter = styled.div` const MessageFooter = styled.div`

View File

@ -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 Scrollbar from '@renderer/components/Scrollbar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings' import { MultiModelMessageStyle } from '@renderer/store/settings'
import { Message, Model, Topic } from '@renderer/types' import { Message, Topic } from '@renderer/types'
import { Button, Segmented as AntdSegmented } from 'antd' import { Popover } from 'antd'
import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react' import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import MessageItem from './Message' import MessageItem from './Message'
import MessageGroupMenuBar from './MessageGroupMenuBar'
interface Props { interface Props {
messages: (Message & { index: number })[] messages: (Message & { index: number })[]
@ -32,7 +29,7 @@ const MessageGroup: FC<Props> = ({
onGetMessages, onGetMessages,
onDeleteGroupMessages onDeleteGroupMessages
}) => { }) => {
const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings() const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const [multiModelMessageStyle, setMultiModelMessageStyle] = const [multiModelMessageStyle, setMultiModelMessageStyle] =
@ -42,8 +39,9 @@ const MessageGroup: FC<Props> = ({
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1) const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
const isGrouped = messageLength > 1 const isGrouped = messageLength > 1
const isHorizontal = multiModelMessageStyle === 'horizontal'
const onDelete = async () => { const onDelete = useCallback(async () => {
window.modal.confirm({ window.modal.confirm({
title: t('message.group.delete.title'), title: t('message.group.delete.title'),
content: t('message.group.delete.content'), content: t('message.group.delete.content'),
@ -57,116 +55,144 @@ const MessageGroup: FC<Props> = ({
askId && onDeleteGroupMessages?.(askId) askId && onDeleteGroupMessages?.(askId)
} }
}) })
} }, [messages, onDeleteGroupMessages, t])
useEffect(() => { useEffect(() => {
setSelectedIndex(messageLength - 1) setSelectedIndex(messageLength - 1)
}, [messageLength]) }, [messageLength])
const isHorizontal = multiModelMessageStyle === 'horizontal'
return ( return (
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}> <GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}> <GridContainer $count={messageLength} $layout={multiModelMessageStyle} $gridColumns={gridColumns}>
{messages.map((message, index) => ( {messages.map((message, index) => {
<MessageWrapper const isGridGroupMessage = multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped
$layout={multiModelMessageStyle} if (isGridGroupMessage) {
$selected={index === selectedIndex} return (
$isGrouped={isGrouped} <Popover
key={message.id} content={
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}> <MessageWrapper
<MessageItem $layout={multiModelMessageStyle}
isGrouped={isGrouped} $selected={index === selectedIndex}
message={message} $isGrouped={isGrouped}
topic={topic} $isInPopover={true}
index={message.index} key={message.id}>
hidePresetMessages={hidePresetMessages} <MessageItem
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }} isGrouped={isGrouped}
onSetMessages={onSetMessages} message={message}
onDeleteMessage={onDeleteMessage} topic={topic}
onGetMessages={onGetMessages} index={message.index}
/> hidePresetMessages={hidePresetMessages}
</MessageWrapper> 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}
$isGrouped={isGrouped}
key={message.id}
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
<MessageItem
isGrouped={isGrouped}
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={{ paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15 }}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
)
})}
</GridContainer> </GridContainer>
{isGrouped && ( {isGrouped && (
<GroupMenuBar className="group-menu-bar" $layout={multiModelMessageStyle}> <MessageGroupMenuBar
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}> multiModelMessageStyle={multiModelMessageStyle}
<LayoutContainer> setMultiModelMessageStyle={setMultiModelMessageStyle}
{['fold', 'vertical', 'horizontal'].map((layout) => ( messages={messages}
<LayoutOption selectedIndex={selectedIndex}
key={layout} setSelectedIndex={setSelectedIndex}
active={multiModelMessageStyle === layout} onDelete={onDelete}
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}> />
{layout === 'fold' ? (
<FolderOutlined />
) : layout === 'horizontal' ? (
<ColumnWidthOutlined />
) : (
<ColumnHeightOutlined />
)}
</LayoutOption>
))}
</LayoutContainer>
{multiModelMessageStyle === 'fold' && (
<ModelsContainer>
<Segmented
value={selectedIndex.toString()}
onChange={(value) => {
setSelectedIndex(Number(value))
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
}}
options={messages.map((message, index) => ({
label: (
<SegmentedLabel>
<ModelAvatar model={message.model as Model} size={20} />
<ModelName>{message.model?.name}</ModelName>
</SegmentedLabel>
),
value: index.toString()
}))}
size="small"
/>
</ModelsContainer>
)}
</HStack>
<Button
type="text"
size="small"
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
onClick={onDelete}
/>
</GroupMenuBar>
)} )}
</GroupContainer> </GroupContainer>
) )
} }
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>` 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%; width: 100%;
display: grid; display: grid;
grid-template-columns: repeat( grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)}, ${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(550px, 1fr) minmax(550px, 1fr)
); );
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')}; gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
@media (max-width: 800px) { @media (max-width: 800px) {
grid-template-columns: repeat( grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)}, ${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
minmax(400px, 1fr) minmax(400px, 1fr)
); );
} }
overflow-y: auto; 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 { interface MessageWrapperProps {
$layout: 'fold' | 'horizontal' | 'vertical' $layout: 'fold' | 'horizontal' | 'vertical' | 'grid'
$selected: boolean $selected: boolean
$isGrouped: boolean $isGrouped: boolean
$isInPopover?: boolean
} }
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>` const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
@ -176,10 +202,11 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
return props.$selected ? 'block' : 'none' return props.$selected ? 'block' : 'none'
} }
if (props.$layout === 'horizontal') { if (props.$layout === 'horizontal') {
return 'inline-block' return 'inline-flex'
} }
return 'block' return 'block'
}}; }};
${({ $layout, $isGrouped }) => { ${({ $layout, $isGrouped }) => {
if ($layout === 'horizontal' && $isGrouped) { if ($layout === 'horizontal' && $isGrouped) {
return css` return css`
@ -187,82 +214,26 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
padding: 10px; padding: 10px;
border-radius: 6px; border-radius: 6px;
max-height: 600px; max-height: 600px;
overflow-y: auto;
margin-bottom: 10px; margin-bottom: 10px;
` `
} }
return '' return ''
}} }}
${({ $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;
background-color: var(--color-background);
`
: css`
overflow-y: auto;
border-radius: 6px;
`}
` `
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>` export default memo(MessageGroup)
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 6px 10px;
border-radius: 6px;
margin-top: 10px;
justify-content: space-between;
overflow: hidden;
border: 0.5px solid var(--color-border);
height: 40px;
margin-left: ${({ $layout }) => ($layout === 'horizontal' ? '0' : '40px')};
transition: all 0.3s ease;
`
const LayoutContainer = styled.div`
display: flex;
gap: 10px;
flex-direction: row;
`
const LayoutOption = styled.div<{ active: boolean }>`
cursor: pointer;
padding: 2px 10px;
border-radius: 4px;
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'transparent')};
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'var(--color-hover)')};
}
`
const ModelsContainer = styled(Scrollbar)`
display: flex;
flex-direction: column;
justify-content: space-between;
&::-webkit-scrollbar {
display: none;
}
`
const Segmented = styled(AntdSegmented)`
.ant-segmented-item {
background-color: transparent !important;
transition: none !important;
&:hover {
background: transparent !important;
}
}
.ant-segmented-thumb,
.ant-segmented-item-selected {
background-color: transparent !important;
border: 0.5px solid var(--color-border);
transition: none !important;
}
`
const SegmentedLabel = styled.div`
display: flex;
align-items: center;
gap: 5px;
padding: 3px 0;
`
const ModelName = styled.span`
font-weight: 500;
font-size: 12px;
`
export default MessageGroup

View 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)

View File

@ -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

View File

@ -60,12 +60,16 @@ const MessageMenubar: FC<Props> = (props) => {
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
const onCopy = useCallback(() => { const onCopy = useCallback(
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content)) (e: React.MouseEvent) => {
window.message.success({ content: t('message.copied'), key: 'copy-message' }) e.stopPropagation()
setCopied(true) navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
setTimeout(() => setCopied(false), 2000) window.message.success({ content: t('message.copied'), key: 'copy-message' })
}, [message.content, t]) setCopied(true)
setTimeout(() => setCopied(false), 2000)
},
[message.content, t]
)
const onNewBranch = useCallback(async () => { const onNewBranch = useCallback(async () => {
await modelGenerating() await modelGenerating()
@ -195,14 +199,16 @@ const MessageMenubar: FC<Props> = (props) => {
[message, onEdit, onNewBranch, t] [message, onEdit, onNewBranch, t]
) )
const onRegenerate = async () => { const onRegenerate = async (e: React.MouseEvent | undefined) => {
e?.stopPropagation?.()
await modelGenerating() await modelGenerating()
const selectedModel = isGrouped ? model : assistantModel const selectedModel = isGrouped ? model : assistantModel
const _message = resetAssistantMessage(message, selectedModel) const _message = resetAssistantMessage(message, selectedModel)
onEditMessage?.(_message) onEditMessage?.(_message)
} }
const onMentionModel = async () => { const onMentionModel = async (e: React.MouseEvent) => {
e.stopPropagation()
await modelGenerating() await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model }) const selectedModel = await SelectModelPopup.show({ model })
if (!selectedModel) return if (!selectedModel) return
@ -216,9 +222,13 @@ const MessageMenubar: FC<Props> = (props) => {
onEditMessage?.(_message) onEditMessage?.(_message)
} }
const onUseful = useCallback(() => { const onUseful = useCallback(
onEditMessage?.({ ...message, useful: !message.useful }) (e: React.MouseEvent) => {
}, [message, onEditMessage]) e.stopPropagation()
onEditMessage?.({ ...message, useful: !message.useful })
},
[message, onEditMessage]
)
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
@ -270,13 +280,14 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'translate-close', key: 'translate-close',
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined }) onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
} }
] ],
onClick: (e) => e.domEvent.stopPropagation()
}} }}
trigger={['click']} trigger={['click']}
placement="topRight" placement="topRight"
arrow> arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}> <Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton className="message-action-button"> <ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<TranslationOutlined /> <TranslationOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -298,14 +309,25 @@ const MessageMenubar: FC<Props> = (props) => {
<Tooltip title={t('common.delete')} mouseEnterDelay={1}> <Tooltip title={t('common.delete')} mouseEnterDelay={1}>
<ActionButton <ActionButton
className="message-action-button" className="message-action-button"
onClick={isGrouped ? () => onDeleteMessage?.(message) : undefined}> onClick={
isGrouped
? (e) => {
e.stopPropagation()
onDeleteMessage?.(message)
}
: (e) => e.stopPropagation()
}>
<DeleteOutlined /> <DeleteOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
</Popconfirm> </Popconfirm>
{!isUserMessage && ( {!isUserMessage && (
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow> <Dropdown
<ActionButton className="message-action-button"> menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight"
arrow>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<MenuOutlined /> <MenuOutlined />
</ActionButton> </ActionButton>
</Dropdown> </Dropdown>

View File

@ -1,11 +1,12 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types' import { Message } from '@renderer/types'
import { Collapse } from 'antd' import { Collapse } from 'antd'
import { FC, useEffect, useState, useMemo } from 'react' import { FC, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BarLoader from 'react-spinners/BarLoader' import BarLoader from 'react-spinners/BarLoader'
import styled from 'styled-components' import styled from 'styled-components'
import Markdown from '../Markdown/Markdown' import Markdown from '../Markdown/Markdown'
import { useSettings } from '@renderer/hooks/useSettings'
interface Props { interface Props {
message: Message message: Message
@ -32,10 +33,12 @@ const MessageThought: FC<Props> = ({ message }) => {
const thinkingTime = message.metrics?.time_thinking_millsec || 0 const thinkingTime = message.metrics?.time_thinking_millsec || 0
const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1) const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1)
const isPaused = message.status === 'paused'
return ( return (
<CollapseContainer <CollapseContainer
activeKey={activeKey} activeKey={activeKey}
size="small"
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))} onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
className="message-thought-container" className="message-thought-container"
items={[ items={[
@ -46,7 +49,7 @@ const MessageThought: FC<Props> = ({ message }) => {
<TinkingText> <TinkingText>
{isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSeconds })} {isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSeconds })}
</TinkingText> </TinkingText>
{isThinking && <BarLoader color="#9254de" />} {isThinking && !isPaused && <BarLoader color="#9254de" />}
</MessageTitleLabel> </MessageTitleLabel>
), ),
children: ( children: (

View File

@ -1,3 +1,4 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { FC } from 'react' import { FC } from 'react'
@ -11,26 +12,30 @@ interface Props {
const Prompt: FC<Props> = ({ assistant, topic }) => { const Prompt: FC<Props> = ({ assistant, topic }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme()
const prompt = assistant.prompt || t('chat.default.description') const prompt = assistant.prompt || t('chat.default.description')
const topicPrompt = topic?.prompt || '' const topicPrompt = topic?.prompt || ''
const isDark = theme === 'dark'
if (!prompt && !topicPrompt) { if (!prompt && !topicPrompt) {
return null return null
} }
return ( return (
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })}> <Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })} $isDark={isDark}>
<Text>{prompt}</Text> <Text>{prompt}</Text>
</Container> </Container>
) )
} }
const Container = styled.div` const Container = styled.div<{ $isDark: boolean }>`
padding: 10px 20px; padding: 10px 20px;
background-color: var(--color-background-soft);
margin: 4px 20px 0 20px; margin: 4px 20px 0 20px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
background-color: ${({ $isDark }) => ($isDark ? 'var(--color-background-soft)' : 'transparent')};
` `
const Text = styled.div` const Text = styled.div`

View File

@ -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="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="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="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />

View File

@ -18,7 +18,7 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { getProviderName } from '@renderer/services/ProviderService' import { getProviderName } from '@renderer/services/ProviderService'
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types' 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 { Alert, Button, Card, Divider, message, Tag, Tooltip, Typography, Upload } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -35,7 +35,7 @@ interface KnowledgeContentProps {
selectedBase: KnowledgeBase selectedBase: KnowledgeBase
} }
const fileTypes = [...bookExts, ...documentExts, ...textExts] const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => { const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -217,7 +217,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
style={{ marginTop: 10, background: 'transparent' }}> style={{ marginTop: 10, background: 'transparent' }}>
<p className="ant-upload-text">{t('knowledge.drag_file')}</p> <p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint"> <p className="ant-upload-hint">
{t('knowledge.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> </p>
</Dragger> </Dragger>
</FileSection> </FileSection>

View File

@ -16,18 +16,14 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
const { t } = useTranslation() const { t } = useTranslation()
const knowledgeState = useAppSelector((state) => state.knowledge) const knowledgeState = useAppSelector((state) => state.knowledge)
const knowledgeOptions: SelectProps['options'] = [] const knowledgeOptions: SelectProps['options'] = knowledgeState.bases.map((base) => ({
label: base.name,
knowledgeState.bases.forEach((base) => { value: base.id
knowledgeOptions.push({ }))
label: base.name,
value: base.id
})
})
const onUpdate = (value) => { const onUpdate = (value) => {
const knowledge_base = knowledgeState.bases.find((t) => t.id === value) const knowledge_bases = value.map((id) => knowledgeState.bases.find((b) => b.id === id))
const _assistant = { ...assistant, knowledge_base } const _assistant = { ...assistant, knowledge_bases }
updateAssistant(_assistant) updateAssistant(_assistant)
} }
@ -37,12 +33,18 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
{t('common.knowledge_base')} {t('common.knowledge_base')}
</Box> </Box>
<Select <Select
mode="multiple"
allowClear allowClear
defaultValue={assistant.knowledge_base?.id} value={assistant.knowledge_bases?.map((b) => b.id)}
placeholder={t('agents.add.knowledge_base.placeholder')} placeholder={t('agents.add.knowledge_base.placeholder')}
menuItemSelectedIcon={<CheckOutlined />} menuItemSelectedIcon={<CheckOutlined />}
options={knowledgeOptions} options={knowledgeOptions}
onChange={(value) => onUpdate(value)} onChange={(value) => onUpdate(value)}
filterOption={(input, option) =>
String(option?.label ?? '')
.toLowerCase()
.includes(input.toLowerCase())
}
/> />
</Container> </Container>
) )

View File

@ -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 { Client } from '@notionhq/client'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { backup, reset, restore } from '@renderer/services/BackupService' import { backup, reset, restore } from '@renderer/services/BackupService'
import { RootState, useAppDispatch } from '@renderer/store' 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 ( return (
<SettingGroup theme={theme}> <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 /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
@ -73,6 +88,7 @@ const NotionSettings: FC = () => {
onChange={handleNotionDatabaseIdChange} onChange={handleNotionDatabaseIdChange}
onBlur={handleNotionDatabaseIdChange} onBlur={handleNotionDatabaseIdChange}
style={{ width: 315 }} style={{ width: 315 }}
placeholder={t('settings.data.notion.database_id_placeholder')}
/> />
</HStack> </HStack>
</SettingRow> </SettingRow>
@ -86,6 +102,7 @@ const NotionSettings: FC = () => {
onChange={handleNotionTokenChange} onChange={handleNotionTokenChange}
onBlur={handleNotionTokenChange} onBlur={handleNotionTokenChange}
style={{ width: 250 }} style={{ width: 250 }}
placeholder={t('settings.data.notion.api_key_placeholder')}
/> />
<Button onClick={handleNotionConnectionCheck} style={{ width: 60 }}> <Button onClick={handleNotionConnectionCheck} style={{ width: 60 }}>
{t('settings.data.notion.check.button')} {t('settings.data.notion.check.button')}

View File

@ -23,50 +23,6 @@ interface MiniAppManagerProps {
type ListType = 'visible' | 'disabled' 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> = ({ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
visibleMiniApps, visibleMiniApps,
disabledMiniApps, disabledMiniApps,
@ -92,25 +48,35 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
if (!result.destination) return if (!result.destination) return
const { source, destination } = result 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({ if (source.droppableId === 'visible') {
sourceList: sourceList === 'visible' ? visibleMiniApps : disabledMiniApps, handleListUpdate(list, disabledMiniApps)
destList: destList === 'visible' ? visibleMiniApps : disabledMiniApps, } else {
sourceIndex: source.index, handleListUpdate(visibleMiniApps, list)
destIndex: destination.index, }
isSameList: sourceList === destList return
}) }
handleListUpdate( // 在不同列表间移动
sourceList === 'visible' ? newLists.sourceList : newLists.destList, const sourceList = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
sourceList === 'visible' ? newLists.destList : newLists.sourceList 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( const onMoveMiniApp = useCallback(
@ -153,17 +119,15 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
<Droppable droppableId={listType}> <Droppable droppableId={listType}>
{(provided: DroppableProvided) => ( {(provided: DroppableProvided) => (
<ProgramList ref={provided.innerRef} {...provided.droppableProps}> <ProgramList ref={provided.innerRef} {...provided.droppableProps}>
<ScrollContainer> {(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => ( <Draggable key={program.id} draggableId={String(program.id)} index={index}>
<Draggable key={program.id} draggableId={String(program.id)} index={index}> {(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)} </Draggable>
</Draggable> ))}
))} {disabledMiniApps.length === 0 && listType === 'disabled' && (
{disabledMiniApps.length === 0 && listType === 'disabled' && ( <EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder> )}
)} {provided.placeholder}
{provided.placeholder}
</ScrollContainer>
</ProgramList> </ProgramList>
)} )}
</Droppable> </Droppable>
@ -181,12 +145,6 @@ const AppLogo = styled.img`
object-fit: contain; object-fit: contain;
` `
const ScrollContainer = styled.div`
overflow-y: auto;
height: 100%;
padding-right: 5px;
`
const ProgramSection = styled.div` const ProgramSection = styled.div`
display: flex; display: flex;
gap: 20px; gap: 20px;
@ -208,13 +166,29 @@ const ProgramList = styled.div`
height: 365px; height: 365px;
min-height: 365px; min-height: 365px;
padding: 10px; padding: 10px;
padding-right: 5px;
background: var(--color-background-soft); background: var(--color-background-soft);
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
display: flex; overflow-y: auto;
flex-direction: column;
overflow-y: hidden; 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` const ProgramItem = styled.div`

View File

@ -59,7 +59,8 @@ const ShortcutSettings: FC = () => {
const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key)) const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
const hasNonModifier = 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({ window.message.warning({
content: t('settings.shortcuts.alt_warning'), content: t('settings.shortcuts.alt_warning'),
key: 'shortcut-alt-warning' key: 'shortcut-alt-warning'

View File

@ -5,6 +5,7 @@ import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
import store from '@renderer/store' import store from '@renderer/store'
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types' import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
import { delay, isJSON, parseJSON } from '@renderer/utils' import { delay, isJSON, parseJSON } from '@renderer/utils'
import { t } from 'i18next'
import OpenAI from 'openai' import OpenAI from 'openai'
import { CompletionsParams } from '.' import { CompletionsParams } from '.'
@ -83,21 +84,35 @@ export default abstract class BaseProvider {
return message.content return message.content
} }
const knowledgeId = message.knowledgeBaseIds[0] const bases = store.getState().knowledge.bases.filter((kb) => message.knowledgeBaseIds?.includes(kb.id))
const base = store.getState().knowledge.bases.find((kb) => kb.id === knowledgeId)
if (!base) { if (!bases || bases.length === 0) {
return message.content return message.content
} }
const { referencesContent, referencesCount } = await getKnowledgeReferences(base, message) const allReferencesPromises = bases.map(async (base) => {
const references = await getKnowledgeReferences(base, message)
// 如果知识库中未检索到内容则使用通用逻辑 return {
if (referencesCount === 0) { 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 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) { protected getCustomParameters(assistant: Assistant) {

View File

@ -3,7 +3,6 @@ import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@
import { getEmbeddingMaxContext } from '@renderer/config/embedings' import { getEmbeddingMaxContext } from '@renderer/config/embedings'
import AiProvider from '@renderer/providers/AiProvider' import AiProvider from '@renderer/providers/AiProvider'
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types' import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
import { t } from 'i18next'
import { take } from 'lodash' import { take } from 'lodash'
import { getProviderByModel } from './AssistantService' import { getProviderByModel } from './AssistantService'
@ -91,14 +90,6 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
return item.score >= threshold 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( const _searchResults = await Promise.all(
searchResults.map(async (item) => { 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 references
return { referencesContent, referencesCount: references.length }
} }

View File

@ -1,5 +1,6 @@
import store from '@renderer/store' import store from '@renderer/store'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { t } from 'i18next'
import { pick } from 'lodash' import { pick } from 'lodash'
export const getModelUniqId = (m?: Model) => { export const getModelUniqId = (m?: Model) => {
@ -17,5 +18,13 @@ export const hasModel = (m?: Model) => {
} }
export function getModelName(model?: 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
} }

View File

@ -152,7 +152,7 @@ const initialState: LlmState = {
name: 'DMXAPI', name: 'DMXAPI',
type: 'openai', type: 'openai',
apiKey: '', apiKey: '',
apiHost: 'https://api.dmxapi.com', apiHost: 'https://www.dmxapi.com',
models: SYSTEM_MODELS.dmxapi, models: SYSTEM_MODELS.dmxapi,
isSystem: true, isSystem: true,
enabled: false enabled: false

View File

@ -1071,6 +1071,8 @@ const migrateConfig = {
state.minapps.enabled.push(coze) state.minapps.enabled.push(coze)
} }
} }
state.settings.gridColumns = 2
state.settings.gridPopoverTrigger = 'hover'
return state return state
} }
} }

View File

@ -44,6 +44,8 @@ export interface SettingsState {
mathEngine: 'MathJax' | 'KaTeX' mathEngine: 'MathJax' | 'KaTeX'
messageStyle: 'plain' | 'bubble' messageStyle: 'plain' | 'bubble'
codeStyle: CodeStyleVarious codeStyle: CodeStyleVarious
gridColumns: number
gridPopoverTrigger: 'hover' | 'click'
// webdav 配置 host, user, pass, path // webdav 配置 host, user, pass, path
webdavHost: string webdavHost: string
webdavUser: string webdavUser: string
@ -69,7 +71,7 @@ export interface SettingsState {
notionApiKey: string | null notionApiKey: string | null
} }
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
const initialState: SettingsState = { const initialState: SettingsState = {
showAssistants: true, showAssistants: true,
@ -99,6 +101,8 @@ const initialState: SettingsState = {
mathEngine: 'KaTeX', mathEngine: 'KaTeX',
messageStyle: 'plain', messageStyle: 'plain',
codeStyle: 'auto', codeStyle: 'auto',
gridColumns: 2,
gridPopoverTrigger: 'hover',
webdavHost: '', webdavHost: '',
webdavUser: '', webdavUser: '',
webdavPass: '', webdavPass: '',
@ -224,6 +228,12 @@ const settingsSlice = createSlice({
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => { setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
state.mathEngine = action.payload 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'>) => { setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
state.messageStyle = action.payload state.messageStyle = action.payload
}, },
@ -265,7 +275,7 @@ const settingsSlice = createSlice({
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => { setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
state.enableQuickAssistant = action.payload state.enableQuickAssistant = action.payload
}, },
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => { setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold' | 'grid'>) => {
state.multiModelMessageStyle = action.payload state.multiModelMessageStyle = action.payload
}, },
setNotionDatabaseID: (state, action: PayloadAction<string>) => { setNotionDatabaseID: (state, action: PayloadAction<string>) => {
@ -310,6 +320,8 @@ export const {
setCodeShowLineNumbers, setCodeShowLineNumbers,
setCodeCollapsible, setCodeCollapsible,
setMathEngine, setMathEngine,
setGridColumns,
setGridPopoverTrigger,
setMessageStyle, setMessageStyle,
setCodeStyle, setCodeStyle,
setTranslateModelPrompt, setTranslateModelPrompt,

View File

@ -5,7 +5,7 @@ export type Assistant = {
id: string id: string
name: string name: string
prompt: string prompt: string
knowledge_base?: KnowledgeBase knowledge_bases?: KnowledgeBase[]
topics: Topic[] topics: Topic[]
type: string type: string
emoji?: string emoji?: string

View File

@ -50,7 +50,7 @@ const Inputbar: FC = () => {
const [spaceClickCount, setSpaceClickCount] = useState(0) const [spaceClickCount, setSpaceClickCount] = useState(0)
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>()
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase>() const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>()
const isVision = useMemo(() => isVisionModel(model), [model]) const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
@ -274,8 +274,8 @@ const Inputbar: FC = () => {
} }
}, []) }, [])
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => { const handleKnowledgeBaseSelect = (bases: KnowledgeBase[]) => {
setSelectedKnowledgeBase(base) setSelectedKnowledgeBase(bases?.[0])
} }
return ( return (
@ -317,7 +317,7 @@ const Inputbar: FC = () => {
</Popconfirm> </Popconfirm>
</Tooltip> </Tooltip>
<KnowledgeBaseButton <KnowledgeBaseButton
selectedBase={selectedKnowledgeBase} selectedBases={selectedKnowledgeBase ? [selectedKnowledgeBase] : []}
onSelect={handleKnowledgeBaseSelect} onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton} ToolbarButton={ToolbarButton}
disabled={files.length > 0} disabled={files.length > 0}

View File

@ -8,7 +8,7 @@ import { uuid } from '@renderer/utils'
import { Divider } from 'antd' import { Divider } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isEmpty } from 'lodash' 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 { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -25,6 +25,8 @@ const HomeWindow: FC = () => {
const [clipboardText, setClipboardText] = useState('') const [clipboardText, setClipboardText] = useState('')
const [selectedText, setSelectedText] = useState('') const [selectedText, setSelectedText] = useState('')
const [text, setText] = useState('') const [text, setText] = useState('')
const [lastClipboardText, setLastClipboardText] = useState<string | null>(null)
const textChange = useState(() => {})[1]
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { defaultModel: model } = useDefaultModel() const { defaultModel: model } = useDefaultModel()
const { language } = useSettings() const { language } = useSettings()
@ -35,9 +37,12 @@ const HomeWindow: FC = () => {
const content = (referenceText === text ? text : `${referenceText}\n\n${text}`).trim() const content = (referenceText === text ? text : `${referenceText}\n\n${text}`).trim()
const onReadClipboard = useCallback(async () => { const onReadClipboard = useCallback(async () => {
const text = await navigator.clipboard.readText() const text = await navigator.clipboard.readText().catch(() => null)
setClipboardText(text.trim()) if (text && text !== lastClipboardText) {
}, []) setLastClipboardText(text)
setClipboardText(text.trim())
}
}, [lastClipboardText])
useEffect(() => { useEffect(() => {
onReadClipboard() onReadClipboard()
@ -50,9 +55,10 @@ const HomeWindow: FC = () => {
const onCloseWindow = () => window.api.miniWindow.hide() const onCloseWindow = () => window.api.miniWindow.hide()
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { 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('') setText('')
setRoute('home') setRoute('home')
route === 'home' && onCloseWindow() route === 'home' && onCloseWindow()
@ -67,6 +73,18 @@ const HomeWindow: FC = () => {
setTimeout(() => setText(''), 100) setTimeout(() => setText(''), 100)
} }
} }
if (isBackspacePressed) {
textChange(() => {
if (text.length === 0) {
clearClipboard()
}
})
}
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value)
} }
const onSendMessage = useCallback( const onSendMessage = useCallback(
@ -95,7 +113,6 @@ const HomeWindow: FC = () => {
const clearClipboard = () => { const clearClipboard = () => {
setClipboardText('') setClipboardText('')
setSelectedText('') setSelectedText('')
navigator.clipboard.writeText('')
} }
useHotkeys('esc', () => { useHotkeys('esc', () => {
@ -132,7 +149,7 @@ const HomeWindow: FC = () => {
referenceText={referenceText} referenceText={referenceText}
placeholder={t('miniwindow.input.placeholder.empty', { model: model.name })} placeholder={t('miniwindow.input.placeholder.empty', { model: model.name })}
handleKeyDown={handleKeyDown} handleKeyDown={handleKeyDown}
setText={setText} handleChange={handleChange}
/> />
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
</> </>
@ -171,7 +188,7 @@ const HomeWindow: FC = () => {
: t('miniwindow.input.placeholder.empty', { model: model.name }) : t('miniwindow.input.placeholder.empty', { model: model.name })
} }
handleKeyDown={handleKeyDown} handleKeyDown={handleKeyDown}
setText={setText} handleChange={handleChange}
/> />
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} /> <ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />

View File

@ -1,7 +1,7 @@
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { Input as AntdInput } from 'antd' import { Input as AntdInput } from 'antd'
import { FC } from 'react' import React, { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface InputBarProps { interface InputBarProps {
@ -10,10 +10,10 @@ interface InputBarProps {
referenceText: string referenceText: string
placeholder: string placeholder: string
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void 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() const { generating } = useRuntime()
return ( return (
<InputWrapper> <InputWrapper>
@ -24,7 +24,7 @@ const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown,
bordered={false} bordered={false}
autoFocus autoFocus
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onChange={(e) => setText(e.target.value)} onChange={handleChange}
disabled={generating} disabled={generating}
/> />
</InputWrapper> </InputWrapper>

270
yarn.lock
View File

@ -126,17 +126,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@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 version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2" resolution: "@babel/code-frame@npm:7.26.2"
@ -3079,7 +3068,7 @@ __metadata:
redux: "npm:^5.0.1" redux: "npm:^5.0.1"
redux-persist: "npm:^6.0.0" redux-persist: "npm:^6.0.0"
rehype-katex: "npm:^7.0.1" rehype-katex: "npm:^7.0.1"
rehype-mathjax: "npm:^6.0.0" rehype-mathjax: "npm:^7.0.0"
rehype-raw: "npm:^7.0.0" rehype-raw: "npm:^7.0.0"
remark-gfm: "npm:^4.0.0" remark-gfm: "npm:^4.0.0"
remark-math: "npm:^6.0.0" remark-math: "npm:^6.0.0"
@ -3747,15 +3736,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "bindings@npm:^1.5.0":
version: 1.5.0 version: 1.5.0
resolution: "bindings@npm:1.5.0" resolution: "bindings@npm:1.5.0"
@ -4645,25 +4625,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3":
version: 3.1.3 version: 3.1.3
resolution: "csstype@npm:3.1.3" resolution: "csstype@npm:3.1.3"
@ -4703,16 +4664,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "data-view-buffer@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "data-view-buffer@npm:1.0.2" resolution: "data-view-buffer@npm:1.0.2"
@ -4799,13 +4750,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "decode-named-character-reference@npm:^1.0.0":
version: 1.0.2 version: 1.0.2
resolution: "decode-named-character-reference@npm:1.0.2" resolution: "decode-named-character-reference@npm:1.0.2"
@ -7244,15 +7188,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "html-parse-stringify@npm:^3.0.1":
version: 3.0.1 version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1" resolution: "html-parse-stringify@npm:3.0.1"
@ -7391,7 +7326,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6" resolution: "https-proxy-agent@npm:7.0.6"
dependencies: dependencies:
@ -7445,15 +7380,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "iconv-lite@npm:^0.4.4":
version: 0.4.24 version: 0.4.24
resolution: "iconv-lite@npm:0.4.24" resolution: "iconv-lite@npm:0.4.24"
@ -7463,6 +7389,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
@ -7883,13 +7818,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "is-regex@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "is-regex@npm:1.2.1" resolution: "is-regex@npm:1.2.1"
@ -8162,40 +8090,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "jsesc@npm:^3.0.2":
version: 3.1.0 version: 3.1.0
resolution: "jsesc@npm:3.1.0" resolution: "jsesc@npm:3.1.0"
@ -9128,13 +9022,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "mdurl@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "mdurl@npm:2.0.0" resolution: "mdurl@npm:2.0.0"
@ -10701,7 +10588,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse5@npm:^7.0.0, parse5@npm:^7.1.2": "parse5@npm:^7.0.0":
version: 7.2.1 version: 7.2.1
resolution: "parse5@npm:7.2.1" resolution: "parse5@npm:7.2.1"
dependencies: dependencies:
@ -11159,7 +11046,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"psl@npm:^1.1.28, psl@npm:^1.1.33": "psl@npm:^1.1.28":
version: 1.15.0 version: 1.15.0
resolution: "psl@npm:1.15.0" resolution: "psl@npm:1.15.0"
dependencies: dependencies:
@ -12157,19 +12044,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rehype-mathjax@npm:^6.0.0": "rehype-mathjax@npm:^7.0.0":
version: 6.0.0 version: 7.0.0
resolution: "rehype-mathjax@npm:6.0.0" resolution: "rehype-mathjax@npm:7.0.0"
dependencies: dependencies:
"@types/hast": "npm:^3.0.0" "@types/hast": "npm:^3.0.0"
"@types/mathjax": "npm:^0.0.40" "@types/mathjax": "npm:^0.0.40"
hast-util-from-dom: "npm:^5.0.0"
hast-util-to-text: "npm:^4.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" mathjax-full: "npm:^3.0.0"
unified: "npm:^11.0.0" unified: "npm:^11.0.0"
unist-util-visit-parents: "npm:^6.0.0" unist-util-visit-parents: "npm:^6.0.0"
checksum: 10c0/f6e0e5a0ed177cfacb9c23ec7603e5ac27937018cf7eb42b8db615eac9b121748686012a200db5ee6de1ca0cd5c8ba7d3aaec28090f01cf6321957a867f1bb78 checksum: 10c0/bd05b8495316877f4c555bef127c463c0f198f11790523e082307a701234556bc84d2483508531bcc1d2e5c85c609a85dfe7980e8d7fea0c9c5e4f5298c80945
languageName: node languageName: node
linkType: hard linkType: hard
@ -12571,20 +12457,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "run-parallel@npm:^1.1.9":
version: 1.2.0 version: 1.2.0
resolution: "run-parallel@npm:1.2.0" resolution: "run-parallel@npm:1.2.0"
@ -12682,15 +12554,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "scheduler@npm:^0.23.2":
version: 0.23.2 version: 0.23.2
resolution: "scheduler@npm:0.23.2" resolution: "scheduler@npm:0.23.2"
@ -13028,7 +12891,7 @@ __metadata:
languageName: node languageName: node
linkType: hard 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 version: 1.2.1
resolution: "source-map-js@npm:1.2.1" resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@ -13539,13 +13402,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "synckit@npm:^0.9.1":
version: 0.9.2 version: 0.9.2
resolution: "synckit@npm:0.9.2" resolution: "synckit@npm:0.9.2"
@ -13813,18 +13669,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "tough-cookie@npm:~2.5.0":
version: 2.5.0 version: 2.5.0
resolution: "tough-cookie@npm:2.5.0" resolution: "tough-cookie@npm:2.5.0"
@ -13835,15 +13679,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "tr46@npm:~0.0.3":
version: 0.0.3 version: 0.0.3
resolution: "tr46@npm:0.0.3" resolution: "tr46@npm:0.0.3"
@ -14229,13 +14064,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "universalify@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "universalify@npm:2.0.1" resolution: "universalify@npm:2.0.1"
@ -14284,7 +14112,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"url-parse@npm:^1.5.10, url-parse@npm:^1.5.3": "url-parse@npm:^1.5.10":
version: 1.5.10 version: 1.5.10
resolution: "url-parse@npm:1.5.10" resolution: "url-parse@npm:1.5.10"
dependencies: dependencies:
@ -14499,15 +14327,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "web-namespaces@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "web-namespaces@npm:2.0.1" resolution: "web-namespaces@npm:2.0.1"
@ -14557,39 +14376,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "whatwg-url@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "whatwg-url@npm:5.0.0" resolution: "whatwg-url@npm:5.0.0"
@ -14769,7 +14555,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ws@npm:^8.13.0, ws@npm:^8.16.0": "ws@npm:^8.13.0":
version: 8.18.0 version: 8.18.0
resolution: "ws@npm:8.18.0" resolution: "ws@npm:8.18.0"
peerDependencies: peerDependencies:
@ -14816,13 +14602,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "xml-parse-from-string@npm:^1.0.0":
version: 1.0.1 version: 1.0.1
resolution: "xml-parse-from-string@npm:1.0.1" resolution: "xml-parse-from-string@npm:1.0.1"
@ -14888,13 +14667,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "xmldom-sre@npm:0.1.31":
version: 0.1.31 version: 0.1.31
resolution: "xmldom-sre@npm:0.1.31" resolution: "xmldom-sre@npm:0.1.31"