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
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Thank you for taking the time to fill out this bug report!
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes
id: checklist
@ -15,9 +16,11 @@ body:
description: |
Before submitting an issue, please make sure you have completed the following steps
options:
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
required: true
- label: I have filled out the issue title correctly.
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues) and [Closed Issues]( https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20), no similar issue was found.
required: true
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
required: true
- type: dropdown
@ -45,7 +48,7 @@ body:
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is
description: Please be as detailed as possible when describing the problem
placeholder: Tell us what happened...
validations:
required: true
@ -54,7 +57,7 @@ body:
id: reproduction
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior
description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately
placeholder: |
1. Go to '...'
2. Click on '....'
@ -82,4 +85,4 @@ body:
id: additional
attributes:
label: Additional Context
description: Add any other context about the problem here
description: Anything that gives us a better understanding of the problem you're experiencing

View File

@ -6,7 +6,8 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a new feature!
Thank you for taking the time to submit a feature request!
Before submitting this issue, please make sure you have reviewed the [Project Roadmap](https://docs.cherry-ai.com/cherrystudio/planning) and the [Feature Overview](https://docs.cherry-ai.com/cherrystudio/preview).
- type: checkboxes
id: checklist
@ -15,9 +16,13 @@ body:
description: |
Before submitting an issue, please make sure you have completed the following steps
options:
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
- label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue.
required: true
- label: I have filled out the issue title correctly.
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues) and [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed) and did not find a similar suggestion.
required: true
- label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck."
required: true
- label: The latest version of Cherry Studio does not include the feature I am suggesting.
required: true
- type: dropdown
@ -44,28 +49,28 @@ body:
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem?
description: A clear and concise description of what the problem is
placeholder: I'm always frustrated when...
label: Is your feature request related to an existing issue?
description: Please briefly describe the problem you are experiencing.
placeholder: I often feel frustrated because...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen
label: Desired Solution
description: Please briefly describe what you would like to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered
label: Alternative Solutions
description: Please briefly describe any alternative solutions or features you have considered.
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here
label: Additional Information
description: Add any other context or screenshots related to your feature request.

View File

@ -1,12 +1,12 @@
name: Question
description: Ask a question or seek help
title: '[Question]: '
name: Discussion & Questions
description: Seeking help, discussing issues, asking questions, etc...
title: '[Discussion]: '
labels: ['question']
body:
- type: markdown
attributes:
value: |
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
- type: checkboxes
id: checklist
@ -15,9 +15,9 @@ body:
description: |
Before submitting an issue, please make sure you have completed the following steps
options:
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
- label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue.
required: true
- label: I have filled out the issue title correctly.
- label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features.
required: true
- type: dropdown
@ -45,8 +45,8 @@ body:
id: question
attributes:
label: Your Question
description: Please describe your question in detail
placeholder: Please explain your question as clearly as possible...
description: Please describe your issue in detail.
placeholder: Please explain your issue as clearly as possible...
validations:
required: true
@ -68,9 +68,9 @@ body:
id: priority
attributes:
label: Priority
description: How urgent is this question for you?
description: How urgent is this issue for you?
options:
- Low (Can wait)
- Low (Review when available)
- Medium (Would like a response soon)
- High (Blocking progress)
validations:

View File

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

View File

@ -1,4 +1,4 @@
name: 💡 功能建议
name: 💡 功能建议 (中文)
description: 为项目提出新的想法
title: '[功能]: '
labels: ['enhancement']
@ -7,17 +7,22 @@ body:
attributes:
value: |
感谢您花时间提出新的功能建议!
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
- type: checkboxes
id: checklist
attributes:
label: Issue 检查清单
label: 提交前检查
description: |
在提交 Issue 前请确保您已经完成了以下所有步骤
options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题
- label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true
- label: 正确填写了 Issue 标题。
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
required: true
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
required: true
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
required: true
- type: dropdown
@ -44,7 +49,7 @@ body:
- type: textarea
id: problem
attributes:
label: 您的功能建议是否与某个问题相关?
label: 您的功能建议是否与某个问题/issue相关?
description: 请简明扼要地描述您遇到的问题
placeholder: 我总是感到沮丧,因为...
validations:

View File

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

View File

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

View File

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

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 audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const thirdPartyApplicationExts = ['.draftsExport']
export const bookExts = ['.epub']
export const textExts = [
'.txt', // 普通文本文件

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 { LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams } from '@types'
import Logger from 'electron-log'
import { DraftsExportLoader } from './draftsExportLoader'
import { EpubLoader } from './epubLoader'
import { OdLoader, OdType } from './odLoader'
// embedjs内置loader类型
const commonExts = ['.pdf', '.csv', '.json', '.docx', '.pptx', '.xlsx', '.md']
const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
export async function addOdLoader(
ragApplication: RAGApplication,
@ -89,7 +90,19 @@ export async function addFileLoader(
} as LoaderReturn
}
// DraftsExport类型 (file.ext会自动转换成小写)
if (['.draftsexport'].includes(file.ext)) {
const loaderReturn = await ragApplication.addLoader(new DraftsExportLoader(file.path) as any, forceReload)
return {
entriesAdded: loaderReturn.entriesAdded,
uniqueId: loaderReturn.uniqueId,
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
}
}
const fileContent = fs.readFileSync(file.path, 'utf-8')
// HTML类型
if (['.html', '.htm'].includes(file.ext)) {
const loaderReturn = await ragApplication.addLoader(
@ -108,6 +121,18 @@ export async function addFileLoader(
}
}
// JSON类型
if (['.json'].includes(file.ext)) {
const jsonObject = JSON.parse(fileContent)
const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject }))
return {
entriesAdded: loaderReturn.entriesAdded,
uniqueId: loaderReturn.uniqueId,
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
}
}
// 文本类型
const loaderReturn = await ragApplication.addLoader(
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,

View File

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

View File

@ -6,6 +6,7 @@
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style>
html,

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Input, Modal } from 'antd'
import { TextAreaProps } from 'antd/es/input'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { Box } from '../Layout'
import { TopView } from '../TopView'
@ -27,6 +27,7 @@ const PromptPopupContainer: React.FC<Props> = ({
}) => {
const [value, setValue] = useState(defaultValue)
const [open, setOpen] = useState(true)
const textAreaRef = useRef<any>(null)
const onOk = () => {
setOpen(false)
@ -41,17 +42,35 @@ const PromptPopupContainer: React.FC<Props> = ({
resolve(null)
}
const handleAfterOpenChange = (visible: boolean) => {
if (visible) {
const textArea = textAreaRef.current?.resizableTextArea?.textArea
if (textArea) {
textArea.focus()
const length = textArea.value.length
textArea.setSelectionRange(length, length)
}
}
}
PromptPopup.hide = onCancel
return (
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered>
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
afterOpenChange={handleAfterOpenChange}
centered>
<Box mb={8}>{message}</Box>
<Input.TextArea
ref={textAreaRef}
placeholder={inputPlaceholder}
value={value}
onChange={(e) => setValue(e.target.value)}
allowClear
autoFocus
onPressEnter={onOk}
rows={1}
{...inputProps}

View File

@ -51,6 +51,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
setTimeout(resizeTextArea, 0)
}, [])
const handleAfterOpenChange = (visible: boolean) => {
if (visible) {
const textArea = textareaRef.current?.resizableTextArea?.textArea
if (textArea) {
textArea.focus()
const length = textArea.value.length
textArea.setSelectionRange(length, length)
}
}
}
TextEditPopup.hide = onCancel
return (
@ -65,6 +76,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
afterOpenChange={handleAfterOpenChange}
centered>
<TextArea
ref={textareaRef}

View File

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

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

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 REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\breasoner\b.*|.*-[rR]\d+.*)$/i
export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*)$/i
export const EMBEDDING_REGEX =
/(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
@ -1326,34 +1326,70 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
],
dmxapi: [
{
id: 'gpt-3.5-turbo',
id: 'Qwen/Qwen2.5-7B-Instruct',
provider: 'dmxapi',
name: 'GPT-3.5-Turbo',
group: 'OpenAI'
name: 'Qwen/Qwen2.5-7B-Instruct',
group: '免费模型'
},
{
id: 'ERNIE-Speed-128K',
provider: 'dmxapi',
name: 'ERNIE-Speed-128K',
group: '免费模型'
},
{
id: 'THUDM/glm-4-9b-chat',
provider: 'dmxapi',
name: 'THUDM/glm-4-9b-chat',
group: '免费模型'
},
{
id: 'glm-4-flash',
provider: 'dmxapi',
name: 'glm-4-flash',
group: '免费模型'
},
{
id: 'hunyuan-lite',
provider: 'dmxapi',
name: 'hunyuan-lite',
group: '免费模型'
},
{
id: 'gpt-4o',
provider: 'dmxapi',
name: 'GPT-4o',
name: 'gpt-4o',
group: 'OpenAI'
},
{
id: 'gpt-4o-mini',
provider: 'dmxapi',
name: 'GPT-4o-Mini',
name: 'gpt-4o-mini',
group: 'OpenAI'
},
{
id: 'deepseek-reasoner',
id: 'DMXAPI-DeepSeek-R1',
provider: 'dmxapi',
name: 'DeepSeek Reasoner',
name: 'DMXAPI-DeepSeek-R1',
group: 'DeepSeek'
},
{
id: 'deepseek-chat',
id: 'DMXAPI-DeepSeek-V3',
provider: 'dmxapi',
name: 'DeepSeek Chat',
name: 'DMXAPI-DeepSeek-V3',
group: 'DeepSeek'
},
{
id: 'claude-3-5-sonnet-20241022',
provider: 'dmxapi',
name: 'claude-3-5-sonnet-20241022',
group: 'Claude'
},
{
id: 'gemini-2.0-flash',
provider: 'dmxapi',
name: 'gemini-2.0-flash',
group: 'Gemini'
}
],
perplexity: [

View File

@ -212,11 +212,11 @@ export const PROVIDER_CONFIG = {
},
dmxapi: {
api: {
url: 'https://api.dmxapi.com'
url: 'https://www.dmxapi.com'
},
websites: {
official: 'https://dmxapi.com/',
apiKey: 'https://www.dmxapi.com/token',
official: 'https://www.dmxapi.com/register?aff=81aj/',
apiKey: 'https://www.dmxapi.com/register?aff=81aj',
docs: 'https://dmxapi.com/models.html#code-block',
models: 'https://www.dmxapi.com/pricing'
}
@ -526,7 +526,7 @@ export const PROVIDER_CONFIG = {
},
websites: {
official: 'https://cloud.baidu.com/',
apiKey: 'https://cloud.baidu.com/console/qianfan/apikey',
apiKey: 'https://console.bce.baidu.com/iam/#/iam/apikey/list',
docs: 'https://cloud.baidu.com/doc/index.html',
models: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu'
}

View File

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

View File

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

View File

@ -364,6 +364,7 @@
"message.multi_model_style.fold": "Fold",
"message.multi_model_style.horizontal": "Horizontal",
"message.multi_model_style.vertical": "Vertical",
"message.multi_model_style.grid": "Grid",
"message.style": "Message style",
"message.style.bubble": "Bubble",
"message.style.plain": "Plain",
@ -559,7 +560,9 @@
},
"data.title": "Data Directory",
"notion.api_key": "Notion API Key",
"notion.api_key_placeholder": "Enter Notion API Key",
"notion.database_id": "Notion Database ID",
"notion.database_id_placeholder": "Enter Notion Database ID",
"notion.title": "Notion Configuration",
"notion.check": {
"button": "Check",
@ -636,6 +639,10 @@
"messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math engine",
"messages.grid_columns": "Message grid display columns",
"messages.grid_popover_trigger": "Grid detail trigger",
"messages.grid_popover_trigger.hover": "Hover to display",
"messages.grid_popover_trigger.click": "Click to display",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.model.title": "Model Settings",
"messages.title": "Message Settings",
@ -708,7 +715,7 @@
},
"shortcuts": {
"action": "Action",
"alt_warning": "Mac does not support Option + letters as shortcuts",
"alt_warning": "On Mac, Option key combinations only work with the Space key",
"clear_shortcut": "Clear Shortcut",
"clear_topic": "Clear Messages",
"copy_last_message": "Copy Last Message",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -28,6 +28,8 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
const menuRef = useRef<HTMLDivElement>(null)
const [searchText, setSearchText] = useState('')
const itemRefs = useRef<Array<HTMLDivElement | null>>([])
// Add a new state to track if menu was dismissed
const [menuDismissed, setMenuDismissed] = useState(false)
const setItemRef = (index: number, el: HTMLDivElement | null) => {
itemRefs.current[index] = el
@ -44,7 +46,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
const handleModelSelect = (model: Model) => {
// Check if model is already selected
if (mentionModels.some((selected) => selected.id === model.id)) {
if (mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(model))) {
return
}
onSelect(model)
@ -186,6 +188,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
setIsOpen(true)
setSelectedIndex(0)
setSearchText('')
setMenuDismissed(false) // Reset dismissed flag when manually showing selector
}
const handleKeyDown = (e: KeyboardEvent) => {
@ -209,7 +212,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
e.preventDefault()
if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) {
const selectedModel = flatModelItems[selectedIndex].model
if (!mentionModels.some((selected) => selected.id === selectedModel.id)) {
if (!mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(selectedModel))) {
flatModelItems[selectedIndex].onClick()
}
setIsOpen(false)
@ -218,6 +221,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
} else if (e.key === 'Escape') {
setIsOpen(false)
setSearchText('')
setMenuDismissed(true) // Set dismissed flag when Escape is pressed
}
}
@ -230,12 +234,16 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
setIsOpen(false)
setSearchText('')
} else if (lastAtIndex !== -1) {
// Get the text after @ for search
setMenuDismissed(false) // Reset dismissed flag when @ is removed
} else {
// Only open menu if it wasn't explicitly dismissed
if (!menuDismissed) {
setIsOpen(true)
const searchStr = textBeforeCursor.slice(lastAtIndex + 1)
setSearchText(searchStr)
}
}
}
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
if (textArea) {
@ -252,19 +260,15 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
textArea.removeEventListener('input', handleTextChange)
}
}
}, [isOpen, selectedIndex, flatModelItems, mentionModels])
// Hide dropdown if no models available
if (flatModelItems.length === 0) {
return null
}
}, [isOpen, selectedIndex, flatModelItems, mentionModels, menuDismissed])
const menu = (
<div ref={menuRef} className="ant-dropdown-menu">
{modelMenuItems.map((group, groupIndex) => {
{flatModelItems.length > 0 ? (
modelMenuItems.map((group, groupIndex) => {
if (!group) return null
// Calculate the starting index for this group's items
// Calculate starting index for items in this group
const startIndex = modelMenuItems.slice(0, groupIndex).reduce((acc, g) => acc + (g?.children?.length || 0), 0)
return (
@ -275,7 +279,9 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
<div
key={item.key}
ref={(el) => setItemRef(startIndex + idx, el)}
className={`ant-dropdown-menu-item ${selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''}`}
className={`ant-dropdown-menu-item ${
selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''
}`}
onClick={item.onClick}>
<span className="ant-dropdown-menu-item-icon">{item.icon}</span>
{item.label}
@ -284,7 +290,12 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
</div>
</div>
)
})}
})
) : (
<div className="ant-dropdown-menu-item-group">
<div className="ant-dropdown-menu-item no-results">{t('models.no_matches')}</div>
</div>
)}
</div>
)
@ -334,6 +345,17 @@ const DropdownMenuStyle = createGlobalStyle`
&::-webkit-scrollbar-track {
background: transparent;
}
.no-results {
padding: 8px 12px;
color: var(--color-text-3);
cursor: default;
font-size: 14px;
&:hover {
background: none;
}
}
}
.ant-dropdown-menu-item-group {

View File

@ -1,4 +1,5 @@
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import { Flex, Tag } from 'antd'
import { FC } from 'react'
@ -13,14 +14,19 @@ const MentionModelsInput: FC<{
const { t } = useTranslation()
const getProviderName = (model: Model) => {
const provider = providers.find((p) => p.models?.some((m) => m.id === model.id))
const provider = providers.find((p) => p.id === model?.provider)
return provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
}
return (
<Container gap="4px 0" wrap>
{selectedModels.map((model) => (
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
<Tag
bordered={false}
color="processing"
key={getModelUniqId(model)}
closable
onClose={() => onRemoveModel(model)}>
@{model.name} ({getProviderName(model)})
</Tag>
))}

View File

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

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

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

View File

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

View File

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

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

View File

@ -18,7 +18,7 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge'
import FileManager from '@renderer/services/FileManager'
import { getProviderName } from '@renderer/services/ProviderService'
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
import { bookExts, documentExts, textExts } from '@shared/config/constant'
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
import { Alert, Button, Card, Divider, message, Tag, Tooltip, Typography, Upload } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -35,7 +35,7 @@ interface KnowledgeContentProps {
selectedBase: KnowledgeBase
}
const fileTypes = [...bookExts, ...documentExts, ...textExts]
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
const { t } = useTranslation()
@ -217,7 +217,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
style={{ marginTop: 10, background: 'transparent' }}>
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint">
{t('knowledge.file_hint', { file_types: fileTypes.slice(0, 6).join(', ').replaceAll('.', '') })}
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
</p>
</Dragger>
</FileSection>

View File

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

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 { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/context/ThemeProvider'
import { backup, reset, restore } from '@renderer/services/BackupService'
import { RootState, useAppDispatch } from '@renderer/store'
@ -60,9 +61,23 @@ const NotionSettings: FC = () => {
})
}
const handleNotionTitleClick = () => {
MinApp.start({
id: 'notion-help',
name: 'Notion Help',
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
})
}
return (
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.notion.title')}</SettingTitle>
<SettingTitle style={{ justifyContent: 'flex-start', gap: 10 }}>
{t('settings.data.notion.title')}
<InfoCircleOutlined
style={{ color: 'var(--color-text-2)', cursor: 'pointer' }}
onClick={handleNotionTitleClick}
/>
</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
@ -73,6 +88,7 @@ const NotionSettings: FC = () => {
onChange={handleNotionDatabaseIdChange}
onBlur={handleNotionDatabaseIdChange}
style={{ width: 315 }}
placeholder={t('settings.data.notion.database_id_placeholder')}
/>
</HStack>
</SettingRow>
@ -86,6 +102,7 @@ const NotionSettings: FC = () => {
onChange={handleNotionTokenChange}
onBlur={handleNotionTokenChange}
style={{ width: 250 }}
placeholder={t('settings.data.notion.api_key_placeholder')}
/>
<Button onClick={handleNotionConnectionCheck} style={{ width: 60 }}>
{t('settings.data.notion.check.button')}

View File

@ -23,50 +23,6 @@ interface MiniAppManagerProps {
type ListType = 'visible' | 'disabled'
// 添加 reorderLists 函数的接口定义
interface ReorderListsParams {
sourceList: MinAppType[]
destList: MinAppType[]
sourceIndex: number
destIndex: number
isSameList: boolean
}
interface ReorderListsResult {
sourceList: MinAppType[]
destList: MinAppType[]
}
// 添加 reorderLists 函数
const reorderLists = ({
sourceList,
destList,
sourceIndex,
destIndex,
isSameList
}: ReorderListsParams): ReorderListsResult => {
if (isSameList) {
// 在同一列表内重新排序
const newList = [...sourceList]
const [removed] = newList.splice(sourceIndex, 1)
newList.splice(destIndex, 0, removed)
return {
sourceList: newList,
destList: destList
}
} else {
// 在不同列表间移动
const newSourceList = [...sourceList]
const [removed] = newSourceList.splice(sourceIndex, 1)
const newDestList = [...destList]
newDestList.splice(destIndex, 0, removed)
return {
sourceList: newSourceList,
destList: newDestList
}
}
}
const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
visibleMiniApps,
disabledMiniApps,
@ -92,25 +48,35 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
if (!result.destination) return
const { source, destination } = result
const sourceList = source.droppableId as ListType
const destList = destination.droppableId as ListType
if (source.droppableId === destination.droppableId) return
if (source.droppableId === destination.droppableId) {
// 在同一列表内重新排序
const list = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
const [removed] = list.splice(source.index, 1)
list.splice(destination.index, 0, removed)
const newLists = reorderLists({
sourceList: sourceList === 'visible' ? visibleMiniApps : disabledMiniApps,
destList: destList === 'visible' ? visibleMiniApps : disabledMiniApps,
sourceIndex: source.index,
destIndex: destination.index,
isSameList: sourceList === destList
})
if (source.droppableId === 'visible') {
handleListUpdate(list, disabledMiniApps)
} else {
handleListUpdate(visibleMiniApps, list)
}
return
}
handleListUpdate(
sourceList === 'visible' ? newLists.sourceList : newLists.destList,
sourceList === 'visible' ? newLists.destList : newLists.sourceList
)
// 在不同列表间移动
const sourceList = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
const destList = destination.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
const [removed] = sourceList.splice(source.index, 1)
const targetList = destList.filter((app) => app.id !== removed.id)
targetList.splice(destination.index, 0, removed)
const newVisibleMiniApps = destination.droppableId === 'visible' ? targetList : sourceList
const newDisabledMiniApps = destination.droppableId === 'disabled' ? targetList : sourceList
handleListUpdate(newVisibleMiniApps, newDisabledMiniApps)
},
[disabledMiniApps, handleListUpdate, visibleMiniApps]
[visibleMiniApps, disabledMiniApps, handleListUpdate]
)
const onMoveMiniApp = useCallback(
@ -153,7 +119,6 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
<Droppable droppableId={listType}>
{(provided: DroppableProvided) => (
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
<ScrollContainer>
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
<Draggable key={program.id} draggableId={String(program.id)} index={index}>
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
@ -163,7 +128,6 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
)}
{provided.placeholder}
</ScrollContainer>
</ProgramList>
)}
</Droppable>
@ -181,12 +145,6 @@ const AppLogo = styled.img`
object-fit: contain;
`
const ScrollContainer = styled.div`
overflow-y: auto;
height: 100%;
padding-right: 5px;
`
const ProgramSection = styled.div`
display: flex;
gap: 20px;
@ -208,13 +166,29 @@ const ProgramList = styled.div`
height: 365px;
min-height: 365px;
padding: 10px;
padding-right: 5px;
background: var(--color-background-soft);
border-radius: 8px;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
overflow-y: hidden;
overflow-y: auto;
scroll-behavior: smooth;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--color-border-hover);
}
`
const ProgramItem = styled.div`

View File

@ -59,7 +59,8 @@ const ShortcutSettings: FC = () => {
const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
const hasNonModifier = keys.some((key) => !['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
if (isMac && keys.includes('Alt')) {
// only allows option + space
if (isMac && keys[0] === 'Alt' && !['Space', undefined].includes(keys[1])) {
window.message.warning({
content: t('settings.shortcuts.alt_warning'),
key: 'shortcut-alt-warning'

View File

@ -5,6 +5,7 @@ import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
import store from '@renderer/store'
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
import { delay, isJSON, parseJSON } from '@renderer/utils'
import { t } from 'i18next'
import OpenAI from 'openai'
import { CompletionsParams } from '.'
@ -83,21 +84,35 @@ export default abstract class BaseProvider {
return message.content
}
const knowledgeId = message.knowledgeBaseIds[0]
const base = store.getState().knowledge.bases.find((kb) => kb.id === knowledgeId)
const bases = store.getState().knowledge.bases.filter((kb) => message.knowledgeBaseIds?.includes(kb.id))
if (!base) {
if (!bases || bases.length === 0) {
return message.content
}
const { referencesContent, referencesCount } = await getKnowledgeReferences(base, message)
const allReferencesPromises = bases.map(async (base) => {
const references = await getKnowledgeReferences(base, message)
// 如果知识库中未检索到内容则使用通用逻辑
if (referencesCount === 0) {
return {
knowledgeBaseId: base.id,
references
}
})
const allReferences = (await Promise.all(allReferencesPromises))
.filter((result) => result.references && result.references.length > 0)
.flat()
if (allReferences.length === 0) {
window.message.info({
content: t('knowledge.no_match'),
duration: 4,
key: 'knowledge-base-no-match-info'
})
return message.content
}
const allReferencesContent = `\`\`\`json\n${JSON.stringify(allReferences, null, 2)}\n\`\`\``
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', referencesContent)
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', allReferencesContent)
}
protected getCustomParameters(assistant: Assistant) {

View File

@ -3,7 +3,6 @@ import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
import AiProvider from '@renderer/providers/AiProvider'
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
import { t } from 'i18next'
import { take } from 'lodash'
import { getProviderByModel } from './AssistantService'
@ -91,14 +90,6 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
return item.score >= threshold
})
)
if (searchResults.length === 0) {
window.message.info({
content: t('knowledge.no_match'),
duration: 4,
key: 'knowledge-base-no-match-info'
})
return { referencesContent: '', referencesCount: 0 }
}
const _searchResults = await Promise.all(
searchResults.map(async (item) => {
@ -121,7 +112,5 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
})
)
const referencesContent = `\`\`\`json\n${JSON.stringify(references, null, 2)}\n\`\`\``
return { referencesContent, referencesCount: references.length }
return references
}

View File

@ -1,5 +1,6 @@
import store from '@renderer/store'
import { Model } from '@renderer/types'
import { t } from 'i18next'
import { pick } from 'lodash'
export const getModelUniqId = (m?: Model) => {
@ -17,5 +18,13 @@ export const hasModel = (m?: Model) => {
}
export function getModelName(model?: Model) {
return model?.name || model?.id || ''
const provider = store.getState().llm.providers.find((p) => p.id === model?.provider)
const modelName = model?.name || model?.id || ''
if (provider) {
const providerName = provider?.isSystem ? t(`provider.${provider.id}`) : provider?.name
return `${modelName} | ${providerName}`
}
return modelName
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

270
yarn.lock
View File

@ -126,17 +126,6 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^2.0.1":
version: 2.0.2
resolution: "@asamuzakjp/dom-selector@npm:2.0.2"
dependencies:
bidi-js: "npm:^1.0.3"
css-tree: "npm:^2.3.1"
is-potential-custom-element-name: "npm:^1.0.1"
checksum: 10c0/54d9afa3d654a98fcf2e45c53ea330237e513877f130f8c8c17611c603c8d50cb18f937e1b0bcc08f0030443a9c8479dcad9cebff02766025e2df2754459c647
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2":
version: 7.26.2
resolution: "@babel/code-frame@npm:7.26.2"
@ -3079,7 +3068,7 @@ __metadata:
redux: "npm:^5.0.1"
redux-persist: "npm:^6.0.0"
rehype-katex: "npm:^7.0.1"
rehype-mathjax: "npm:^6.0.0"
rehype-mathjax: "npm:^7.0.0"
rehype-raw: "npm:^7.0.0"
remark-gfm: "npm:^4.0.0"
remark-math: "npm:^6.0.0"
@ -3747,15 +3736,6 @@ __metadata:
languageName: node
linkType: hard
"bidi-js@npm:^1.0.3":
version: 1.0.3
resolution: "bidi-js@npm:1.0.3"
dependencies:
require-from-string: "npm:^2.0.2"
checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
languageName: node
linkType: hard
"bindings@npm:^1.5.0":
version: 1.5.0
resolution: "bindings@npm:1.5.0"
@ -4645,25 +4625,6 @@ __metadata:
languageName: node
linkType: hard
"css-tree@npm:^2.3.1":
version: 2.3.1
resolution: "css-tree@npm:2.3.1"
dependencies:
mdn-data: "npm:2.0.30"
source-map-js: "npm:^1.0.1"
checksum: 10c0/6f8c1a11d5e9b14bf02d10717fc0351b66ba12594166f65abfbd8eb8b5b490dd367f5c7721db241a3c792d935fc6751fbc09f7e1598d421477ad9fadc30f4f24
languageName: node
linkType: hard
"cssstyle@npm:^4.0.1":
version: 4.1.0
resolution: "cssstyle@npm:4.1.0"
dependencies:
rrweb-cssom: "npm:^0.7.1"
checksum: 10c0/05c6597e5d3e0ec6b15221f2c0ce9a0443a46cc50a6089a3ba9ee1ac27f83ff86a445a8f95435137dadd859f091fc61b6d342abaf396d3c910471b5b33cfcbfa
languageName: node
linkType: hard
"csstype@npm:3.1.3, csstype@npm:^3.0.2, csstype@npm:^3.1.3":
version: 3.1.3
resolution: "csstype@npm:3.1.3"
@ -4703,16 +4664,6 @@ __metadata:
languageName: node
linkType: hard
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
dependencies:
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad
languageName: node
linkType: hard
"data-view-buffer@npm:^1.0.2":
version: 1.0.2
resolution: "data-view-buffer@npm:1.0.2"
@ -4799,13 +4750,6 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.4.3
resolution: "decimal.js@npm:10.4.3"
checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee
languageName: node
linkType: hard
"decode-named-character-reference@npm:^1.0.0":
version: 1.0.2
resolution: "decode-named-character-reference@npm:1.0.2"
@ -7244,15 +7188,6 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
dependencies:
whatwg-encoding: "npm:^3.1.1"
checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140
languageName: node
linkType: hard
"html-parse-stringify@npm:^3.0.1":
version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1"
@ -7391,7 +7326,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2":
"https-proxy-agent@npm:^7.0.1":
version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6"
dependencies:
@ -7445,15 +7380,6 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1
languageName: node
linkType: hard
"iconv-lite@npm:^0.4.4":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@ -7463,6 +7389,15 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
safer-buffer: "npm:>= 2.1.2 < 3.0.0"
checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1
languageName: node
linkType: hard
"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
@ -7883,13 +7818,6 @@ __metadata:
languageName: node
linkType: hard
"is-potential-custom-element-name@npm:^1.0.1":
version: 1.0.1
resolution: "is-potential-custom-element-name@npm:1.0.1"
checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
languageName: node
linkType: hard
"is-regex@npm:^1.2.1":
version: 1.2.1
resolution: "is-regex@npm:1.2.1"
@ -8162,40 +8090,6 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^23.0.0":
version: 23.2.0
resolution: "jsdom@npm:23.2.0"
dependencies:
"@asamuzakjp/dom-selector": "npm:^2.0.1"
cssstyle: "npm:^4.0.1"
data-urls: "npm:^5.0.0"
decimal.js: "npm:^10.4.3"
form-data: "npm:^4.0.0"
html-encoding-sniffer: "npm:^4.0.0"
http-proxy-agent: "npm:^7.0.0"
https-proxy-agent: "npm:^7.0.2"
is-potential-custom-element-name: "npm:^1.0.1"
parse5: "npm:^7.1.2"
rrweb-cssom: "npm:^0.6.0"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^4.1.3"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
whatwg-encoding: "npm:^3.1.1"
whatwg-mimetype: "npm:^4.0.0"
whatwg-url: "npm:^14.0.0"
ws: "npm:^8.16.0"
xml-name-validator: "npm:^5.0.0"
peerDependencies:
canvas: ^2.11.2
peerDependenciesMeta:
canvas:
optional: true
checksum: 10c0/b062af50f7be59d914ba75236b7817c848ef3cd007aea1d6b8020a41eb263b7d5bd2652298106e9756b56892f773d990598778d02adab7d0d0d8e58726fc41d3
languageName: node
linkType: hard
"jsesc@npm:^3.0.2":
version: 3.1.0
resolution: "jsesc@npm:3.1.0"
@ -9128,13 +9022,6 @@ __metadata:
languageName: node
linkType: hard
"mdn-data@npm:2.0.30":
version: 2.0.30
resolution: "mdn-data@npm:2.0.30"
checksum: 10c0/a2c472ea16cee3911ae742593715aa4c634eb3d4b9f1e6ada0902aa90df13dcbb7285d19435f3ff213ebaa3b2e0c0265c1eb0e3fb278fda7f8919f046a410cd9
languageName: node
linkType: hard
"mdurl@npm:^2.0.0":
version: 2.0.0
resolution: "mdurl@npm:2.0.0"
@ -10701,7 +10588,7 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.0.0, parse5@npm:^7.1.2":
"parse5@npm:^7.0.0":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
dependencies:
@ -11159,7 +11046,7 @@ __metadata:
languageName: node
linkType: hard
"psl@npm:^1.1.28, psl@npm:^1.1.33":
"psl@npm:^1.1.28":
version: 1.15.0
resolution: "psl@npm:1.15.0"
dependencies:
@ -12157,19 +12044,18 @@ __metadata:
languageName: node
linkType: hard
"rehype-mathjax@npm:^6.0.0":
version: 6.0.0
resolution: "rehype-mathjax@npm:6.0.0"
"rehype-mathjax@npm:^7.0.0":
version: 7.0.0
resolution: "rehype-mathjax@npm:7.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/mathjax": "npm:^0.0.40"
hast-util-from-dom: "npm:^5.0.0"
hast-util-to-text: "npm:^4.0.0"
jsdom: "npm:^23.0.0"
hastscript: "npm:^9.0.0"
mathjax-full: "npm:^3.0.0"
unified: "npm:^11.0.0"
unist-util-visit-parents: "npm:^6.0.0"
checksum: 10c0/f6e0e5a0ed177cfacb9c23ec7603e5ac27937018cf7eb42b8db615eac9b121748686012a200db5ee6de1ca0cd5c8ba7d3aaec28090f01cf6321957a867f1bb78
checksum: 10c0/bd05b8495316877f4c555bef127c463c0f198f11790523e082307a701234556bc84d2483508531bcc1d2e5c85c609a85dfe7980e8d7fea0c9c5e4f5298c80945
languageName: node
linkType: hard
@ -12571,20 +12457,6 @@ __metadata:
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.6.0":
version: 0.6.0
resolution: "rrweb-cssom@npm:0.6.0"
checksum: 10c0/3d9d90d53c2349ea9c8509c2690df5a4ef930c9cf8242aeb9425d4046f09d712bb01047e00da0e1c1dab5db35740b3d78fd45c3e7272f75d3724a563f27c30a3
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.7.1":
version: 0.7.1
resolution: "rrweb-cssom@npm:0.7.1"
checksum: 10c0/127b8ca6c8aac45e2755abbae6138d4a813b1bedc2caabf79466ae83ab3cfc84b5bfab513b7033f0aa4561c7753edf787d0dd01163ceacdee2e8eb1b6bf7237e
languageName: node
linkType: hard
"run-parallel@npm:^1.1.9":
version: 1.2.0
resolution: "run-parallel@npm:1.2.0"
@ -12682,15 +12554,6 @@ __metadata:
languageName: node
linkType: hard
"saxes@npm:^6.0.0":
version: 6.0.0
resolution: "saxes@npm:6.0.0"
dependencies:
xmlchars: "npm:^2.2.0"
checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
languageName: node
linkType: hard
"scheduler@npm:^0.23.2":
version: 0.23.2
resolution: "scheduler@npm:0.23.2"
@ -13028,7 +12891,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@ -13539,13 +13402,6 @@ __metadata:
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.4":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
languageName: node
linkType: hard
"synckit@npm:^0.9.1":
version: 0.9.2
resolution: "synckit@npm:0.9.2"
@ -13813,18 +13669,6 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^4.1.3":
version: 4.1.4
resolution: "tough-cookie@npm:4.1.4"
dependencies:
psl: "npm:^1.1.33"
punycode: "npm:^2.1.1"
universalify: "npm:^0.2.0"
url-parse: "npm:^1.5.3"
checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45
languageName: node
linkType: hard
"tough-cookie@npm:~2.5.0":
version: 2.5.0
resolution: "tough-cookie@npm:2.5.0"
@ -13835,15 +13679,6 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^5.0.0":
version: 5.0.0
resolution: "tr46@npm:5.0.0"
dependencies:
punycode: "npm:^2.3.1"
checksum: 10c0/1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
@ -14229,13 +14064,6 @@ __metadata:
languageName: node
linkType: hard
"universalify@npm:^0.2.0":
version: 0.2.0
resolution: "universalify@npm:0.2.0"
checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe
languageName: node
linkType: hard
"universalify@npm:^2.0.0":
version: 2.0.1
resolution: "universalify@npm:2.0.1"
@ -14284,7 +14112,7 @@ __metadata:
languageName: node
linkType: hard
"url-parse@npm:^1.5.10, url-parse@npm:^1.5.3":
"url-parse@npm:^1.5.10":
version: 1.5.10
resolution: "url-parse@npm:1.5.10"
dependencies:
@ -14499,15 +14327,6 @@ __metadata:
languageName: node
linkType: hard
"w3c-xmlserializer@npm:^5.0.0":
version: 5.0.0
resolution: "w3c-xmlserializer@npm:5.0.0"
dependencies:
xml-name-validator: "npm:^5.0.0"
checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
languageName: node
linkType: hard
"web-namespaces@npm:^2.0.0":
version: 2.0.1
resolution: "web-namespaces@npm:2.0.1"
@ -14557,39 +14376,6 @@ __metadata:
languageName: node
linkType: hard
"webidl-conversions@npm:^7.0.0":
version: 7.0.0
resolution: "webidl-conversions@npm:7.0.0"
checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
dependencies:
iconv-lite: "npm:0.6.3"
checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0":
version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0"
checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0":
version: 14.1.0
resolution: "whatwg-url@npm:14.1.0"
dependencies:
tr46: "npm:^5.0.0"
webidl-conversions: "npm:^7.0.0"
checksum: 10c0/f00104f1c67ce086ba8ffedab529cbbd9aefd8c0a6555320026de7aeff31f91c38680f95818b140a7c9cc657cde3781e567835dda552ddb1e2b8faaba0ac3cb6
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
@ -14769,7 +14555,7 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.13.0, ws@npm:^8.16.0":
"ws@npm:^8.13.0":
version: 8.18.0
resolution: "ws@npm:8.18.0"
peerDependencies:
@ -14816,13 +14602,6 @@ __metadata:
languageName: node
linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"
checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
languageName: node
linkType: hard
"xml-parse-from-string@npm:^1.0.0":
version: 1.0.1
resolution: "xml-parse-from-string@npm:1.0.1"
@ -14888,13 +14667,6 @@ __metadata:
languageName: node
linkType: hard
"xmlchars@npm:^2.2.0":
version: 2.2.0
resolution: "xmlchars@npm:2.2.0"
checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
languageName: node
linkType: hard
"xmldom-sre@npm:0.1.31":
version: 0.1.31
resolution: "xmldom-sre@npm:0.1.31"