feat: Optimize QuickPanel (#4604)

* feat(QuickPanel): enhance close action options and improve input handling

- Added 'enter_empty' as a new close action option for QuickPanel.
- Refactored input handling to include a delay before clearing search text after panel closure.
- Updated keyboard event handling to prevent default actions for specific keys.
- Improved styling for selected and focused states in QuickPanel components.
- Enhanced AttachmentPreview to utilize a separate FileNameRender component for better readability and functionality.

* feat(AttachmentPreview): enhance file icon rendering and styling

* feat(CustomTag): add closable functionality and improve styling

- Enhanced CustomTag component to support closable tags with an onClose callback.
- Updated styling for better visual integration and added hover effects for the close icon.
- Refactored usage of CustomTag in AttachmentPreview, KnowledgeBaseInput, and MentionModelsInput components for consistency.

* feat(SelectModelPopup, QuickPanel): update tag component and enhance search functionality

* feat(Inputbar, SettingsTab): add enable quick panel triggers setting and update translations

* feat(QuickPanel): integrate color library for dynamic styling and update package dependencies
This commit is contained in:
Teo 2025-04-09 17:00:34 +08:00 committed by GitHub
parent f7f7d2bde8
commit a0be911dc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 363 additions and 139 deletions

View File

@ -72,6 +72,7 @@
"@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16",
"color": "^5.0.0",
"diff": "^7.0.0",
"docx": "^9.0.2",
"electron-log": "^5.1.5",

View File

@ -1,3 +1,4 @@
import { CloseOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
@ -8,13 +9,16 @@ interface CustomTagProps {
color: string
size?: number
tooltip?: string
closable?: boolean
onClose?: () => void
}
const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, tooltip }) => {
const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, tooltip, closable = false, onClose }) => {
return (
<Tooltip title={tooltip} placement="top">
<Tag $color={color} $size={size}>
<Tag $color={color} $size={size} $closable={closable}>
{icon && icon} {children}
{closable && <CloseIcon $size={size} $color={color} onClick={onClose} />}
</Tag>
</Tooltip>
)
@ -22,19 +26,42 @@ const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, toolt
export default CustomTag
const Tag = styled.div<{ $color: string; $size: number }>`
const Tag = styled.div<{ $color: string; $size: number; $closable: boolean }>`
display: inline-flex;
align-items: center;
gap: 4px;
padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px;
padding-right: ${({ $closable, $size }) => ($closable ? $size * 1.8 : $size * 0.8)}px;
border-radius: 99px;
color: ${({ $color }) => $color};
background-color: ${({ $color }) => $color + '20'};
font-size: ${({ $size }) => $size}px;
line-height: 1;
white-space: nowrap;
position: relative;
.iconfont {
font-size: ${({ $size }) => $size}px;
color: ${({ $color }) => $color};
}
`
const CloseIcon = styled(CloseOutlined)<{ $size: number; $color: string }>`
cursor: pointer;
font-size: ${({ $size }) => $size * 0.8}px;
color: ${({ $color }) => $color};
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: ${({ $size }) => $size * 0.2}px;
top: ${({ $size }) => $size * 0.2}px;
bottom: ${({ $size }) => $size * 0.2}px;
border-radius: 99px;
transition: all 0.2s ease;
aspect-ratio: 1;
line-height: 1;
&:hover {
background-color: #da8a8a;
color: #ffffff;
}
`

View File

@ -23,6 +23,7 @@ interface ModelTagsProps {
showToolsCalling?: boolean
size?: number
showLabel?: boolean
style?: React.CSSProperties
}
const ModelTagsWithLabel: FC<ModelTagsProps> = ({
@ -31,7 +32,8 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
showReasoning = true,
showToolsCalling = true,
size = 12,
showLabel = true
showLabel = true,
style
}) => {
const { t } = useTranslation()
const [_showLabel, _setShowLabel] = useState(showLabel)
@ -64,7 +66,7 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
}, [showLabel])
return (
<Container ref={containerRef}>
<Container ref={containerRef} style={style}>
{isVisionModel(model) && (
<CustomTag
size={size}

View File

@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { HStack } from '../Layout'
import ModelTags from '../ModelTags'
import ModelTagsWithLabel from '../ModelTagsWithLabel'
import Scrollbar from '../Scrollbar'
type MenuItem = Required<MenuProps>['items'][number]
@ -130,7 +130,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
label: (
<ModelItem>
<ModelNameRow>
<span>{m?.name}</span> <ModelTags model={m} />
<span>{m?.name}</span> <ModelTagsWithLabel model={m} size={11} showLabel={false} />
</ModelNameRow>
<PinIcon
onClick={(e) => {
@ -184,7 +184,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
<span>
{m.model?.name} | {m.provider.isSystem ? t(`provider.${m.provider.id}`) : m.provider.name}
</span>{' '}
<ModelTags model={m.model} />
<ModelTagsWithLabel model={m.model} size={11} showLabel={false} />
</ModelNameRow>
<PinIcon
onClick={(e) => {
@ -481,6 +481,10 @@ const StyledMenu = styled(Menu)`
}
}
}
.anticon {
min-width: auto;
}
}
`

View File

@ -1,6 +1,6 @@
import React from 'react'
export type QuickPanelCloseAction = 'enter' | 'click' | 'esc' | 'outsideclick' | string | undefined
export type QuickPanelCloseAction = 'enter' | 'click' | 'esc' | 'outsideclick' | 'enter_empty' | string | undefined
export type QuickPanelCallBackOptions = {
symbol: string
action: QuickPanelCloseAction

View File

@ -2,9 +2,12 @@ import { CheckOutlined, RightOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { classNames } from '@renderer/utils'
import { Flex } from 'antd'
import { theme } from 'antd'
import Color from 'color'
import { t } from 'i18next'
import React, { use, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import * as tinyPinyin from 'tiny-pinyin'
import { QuickPanelContext } from './provider'
import { QuickPanelCallBackOptions, QuickPanelCloseAction, QuickPanelListItem, QuickPanelOpenOptions } from './types'
@ -27,13 +30,19 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
throw new Error('QuickPanel must be used within a QuickPanelProvider')
}
const { token } = theme.useToken()
const colorPrimary = Color(token.colorPrimary || '#008000')
const selectedColor = colorPrimary.alpha(0.15).toString()
const selectedColorHover = colorPrimary.alpha(0.2).toString()
const ASSISTIVE_KEY = isMac ? '⌘' : 'Ctrl'
const [isAssistiveKeyPressed, setIsAssistiveKeyPressed] = useState(false)
// 避免上下翻页时,鼠标干扰
const [isMouseOver, setIsMouseOver] = useState(false)
const [index, setIndex] = useState(ctx.defaultIndex)
const [_index, setIndex] = useState(ctx.defaultIndex)
const index = useDeferredValue(_index)
const [historyPanel, setHistoryPanel] = useState<QuickPanelOpenOptions[]>([])
const bodyRef = useRef<HTMLDivElement>(null)
@ -65,7 +74,21 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
filterText += item.description
}
return filterText.toLowerCase().includes(_searchText.toLowerCase())
const lowerFilterText = filterText.toLowerCase()
const lowerSearchText = _searchText.toLowerCase()
if (lowerFilterText.includes(lowerSearchText)) {
return true
}
if (tinyPinyin.isSupported() && /[\u4e00-\u9fa5]/.test(filterText)) {
const pinyinText = tinyPinyin.convertToPinyin(filterText, '', true)
if (pinyinText.toLowerCase().includes(lowerSearchText)) {
return true
}
}
return false
})
setIndex(newList.length > 0 ? ctx.defaultIndex || 0 : -1)
@ -120,7 +143,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
if (textArea) {
setInputText(textArea.value)
}
} else if (action && !['outsideclick', 'esc'].includes(action)) {
} else if (action && !['outsideclick', 'esc', 'enter_empty'].includes(action)) {
clearSearchText(true)
}
},
@ -175,6 +198,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
}, [searchText])
// 获取当前输入的搜索词
const isComposing = useRef(false)
useEffect(() => {
if (!ctx.isVisible) return
@ -196,11 +220,25 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
}
}
const handleCompositionUpdate = () => {
isComposing.current = true
}
const handleCompositionEnd = () => {
isComposing.current = false
}
textArea.addEventListener('input', handleInput)
textArea.addEventListener('compositionupdate', handleCompositionUpdate)
textArea.addEventListener('compositionend', handleCompositionEnd)
return () => {
textArea.removeEventListener('input', handleInput)
textArea.removeEventListener('compositionupdate', handleCompositionUpdate)
textArea.removeEventListener('compositionend', handleCompositionEnd)
setTimeout(() => {
setSearchText('')
}, 200) // 等待面板关闭动画结束后,再清空搜索词
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ctx.isVisible])
@ -236,7 +274,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
}
}
if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Enter', 'Escape'].includes(e.key)) {
if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Escape'].includes(e.key)) {
e.preventDefault()
e.stopPropagation()
setIsMouseOver(false)
@ -312,8 +350,16 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
break
case 'Enter':
if (isComposing.current) return
if (list?.[index]) {
e.preventDefault()
e.stopPropagation()
setIsMouseOver(false)
handleItemAction(list[index], 'enter')
} else {
handleClose('enter_empty')
}
break
case 'Escape':
@ -366,7 +412,11 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
}, [ctx.isVisible])
return (
<QuickPanelContainer $pageSize={ctx.pageSize} className={ctx.isVisible ? 'visible' : ''}>
<QuickPanelContainer
$pageSize={ctx.pageSize}
$selectedColor={selectedColor}
$selectedColorHover={selectedColorHover}
className={ctx.isVisible ? 'visible' : ''}>
<QuickPanelBody ref={bodyRef} onMouseMove={() => setIsMouseOver(true)}>
<QuickPanelContent ref={contentRef} $pageSize={ctx.pageSize} $isMouseOver={isMouseOver}>
{list.map((item, i) => (
@ -450,9 +500,14 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
)
}
const QuickPanelContainer = styled.div<{ $pageSize: number }>`
const QuickPanelContainer = styled.div<{
$pageSize: number
$selectedColor: string
$selectedColorHover: string
}>`
--focused-color: rgba(0, 0, 0, 0.06);
--selected-color: rgba(0, 0, 0, 0.03);
--selected-color: ${(props) => props.$selectedColor};
--selected-color-dark: ${(props) => props.$selectedColorHover};
max-height: 0;
position: absolute;
top: 1px;
@ -465,27 +520,36 @@ const QuickPanelContainer = styled.div<{ $pageSize: number }>`
transition: max-height 0.2s ease;
overflow: hidden;
pointer-events: none;
&.visible {
pointer-events: auto;
max-height: ${(props) => props.$pageSize * 31 + 100}px;
}
body[theme-mode='dark'] & {
--focused-color: rgba(255, 255, 255, 0.1);
--selected-color: rgba(255, 255, 255, 0.03);
}
`
const QuickPanelBody = styled.div`
background-color: rgba(240, 240, 240, 0.5);
backdrop-filter: blur(35px) saturate(150%);
border-radius: 8px 8px 0 0;
padding: 5px 0;
border-width: 0.5px 0.5px 0 0.5px;
border-style: solid;
border-color: var(--color-border);
position: relative;
&::before {
content: '';
position: absolute;
inset: 0;
background-color: rgba(240, 240, 240, 0.5);
backdrop-filter: blur(35px) saturate(150%);
z-index: -1;
body[theme-mode='dark'] & {
background-color: rgba(40, 40, 40, 0.4);
}
}
`
const QuickPanelFooter = styled.div`
@ -541,6 +605,9 @@ const QuickPanelItem = styled.div`
margin-bottom: 1px;
&.selected {
background-color: var(--selected-color);
&.focused {
background-color: var(--selected-color-dark);
}
}
&.focused {
background-color: var(--focused-color);

View File

@ -1113,6 +1113,7 @@
"messages.input.send_shortcuts": "Send shortcuts",
"messages.input.show_estimated_tokens": "Show estimated tokens",
"messages.input.title": "Input Settings",
"messages.input.enable_quick_triggers": "Enable '/' and '@' triggers",
"messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math engine",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",

View File

@ -1112,6 +1112,7 @@
"messages.input.send_shortcuts": "送信ショートカット",
"messages.input.show_estimated_tokens": "推定トークン数を表示",
"messages.input.title": "入力設定",
"messages.input.enable_quick_triggers": "'/' と '@' を有効にしてクイックメニューを表示します。",
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"messages.math_engine": "数式エンジン",
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",

View File

@ -1112,6 +1112,7 @@
"messages.input.send_shortcuts": "Горячие клавиши для отправки",
"messages.input.show_estimated_tokens": "Показывать затраты токенов",
"messages.input.title": "Настройки ввода",
"messages.input.enable_quick_triggers": "Включите '/' и '@', чтобы вызвать быстрое меню.",
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
"messages.math_engine": "Математический движок",
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",

View File

@ -1113,6 +1113,7 @@
"messages.input.send_shortcuts": "发送快捷键",
"messages.input.show_estimated_tokens": "显示预估 Token 数",
"messages.input.title": "输入设置",
"messages.input.enable_quick_triggers": "启用 '/' 和 '@' 触发快捷菜单",
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
"messages.math_engine": "数学公式引擎",
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",

View File

@ -1112,6 +1112,7 @@
"messages.input.send_shortcuts": "傳送快捷鍵",
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
"messages.input.title": "輸入設定",
"messages.input.enable_quick_triggers": "啟用 '/' 和 '@' 觸發快捷選單",
"messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息",
"messages.math_engine": "Markdown 渲染輸入訊息",
"messages.metrics": "首字延遲 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",

View File

@ -1,7 +1,22 @@
import { FileOutlined } from '@ant-design/icons'
import {
FileExcelFilled,
FileImageFilled,
FileMarkdownFilled,
FilePdfFilled,
FilePptFilled,
FileTextFilled,
FileUnknownFilled,
FileWordFilled,
FileZipFilled,
FolderOpenFilled,
GlobalOutlined,
LinkOutlined
} from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag'
import FileManager from '@renderer/services/FileManager'
import { FileType } from '@renderer/types'
import { ConfigProvider, Image, Tag } from 'antd'
import { formatFileSize } from '@renderer/utils'
import { Flex, Image, Tooltip } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useState } from 'react'
import styled from 'styled-components'
@ -11,39 +26,40 @@ interface Props {
setFiles: (files: FileType[]) => void
}
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
const [visibleId, setVisibleId] = useState('')
const FileNameRender: FC<{ file: FileType }> = ({ file }) => {
const [visible, setVisible] = useState<boolean>(false)
const isImage = (ext: string) => {
return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext)
}
if (isEmpty(files)) {
return null
}
return (
<ContentContainer>
<ConfigProvider
theme={{
components: {
Tag: {
borderRadiusSM: 100
<Tooltip
styles={{
body: {
padding: 5
}
}
}}>
{files.map((file) => (
<Tag
key={file.id}
icon={<FileOutlined />}
bordered={false}
color="cyan"
closable
onClose={() => setFiles(files.filter((f) => f.id !== file.id))}>
}}
fresh
title={
<Flex vertical gap={2} align="center">
{isImage(file.ext) && (
<Image
style={{ width: 80, maxHeight: 200 }}
src={'file://' + FileManager.getSafePath(file)}
preview={{
visible: visible,
src: 'file://' + FileManager.getSafePath(file),
onVisibleChange: setVisible
}}
/>
)}
{formatFileSize(file.size)}
</Flex>
}>
<FileName
onClick={() => {
if (isImage(file.ext)) {
setVisibleId(file.id)
setVisible(true)
return
}
const path = FileManager.getSafePath(file)
@ -52,33 +68,86 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
}
}}>
{FileManager.formatFileName(file)}
{isImage(file.ext) && (
<Image
style={{ display: 'none' }}
src={'file://' + FileManager.getSafePath(file)}
preview={{
visible: visibleId === file.id,
src: 'file://' + FileManager.getSafePath(file),
onVisibleChange: (value) => {
setVisibleId(value ? file.id : '')
}
}}
/>
)}
</FileName>
</Tag>
</Tooltip>
)
}
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
const getFileIcon = (type?: string) => {
if (!type) return <FileUnknownFilled />
const ext = type.toLowerCase()
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
return <FileImageFilled />
}
if (['.doc', '.docx'].includes(ext)) {
return <FileWordFilled />
}
if (['.xls', '.xlsx'].includes(ext)) {
return <FileExcelFilled />
}
if (['.ppt', '.pptx'].includes(ext)) {
return <FilePptFilled />
}
if (ext === '.pdf') {
return <FilePdfFilled />
}
if (['.md', '.markdown'].includes(ext)) {
return <FileMarkdownFilled />
}
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
return <FileZipFilled />
}
if (['.txt', '.json', '.log', '.yml', '.yaml', '.xml', '.csv'].includes(ext)) {
return <FileTextFilled />
}
if (['.url'].includes(ext)) {
return <LinkOutlined />
}
if (['.sitemap'].includes(ext)) {
return <GlobalOutlined />
}
if (['.folder'].includes(ext)) {
return <FolderOpenFilled />
}
return <FileUnknownFilled />
}
if (isEmpty(files)) {
return null
}
return (
<ContentContainer>
{files.map((file) => (
<CustomTag
key={file.id}
icon={getFileIcon(file.ext)}
color="#37a5aa"
closable
onClose={() => setFiles(files.filter((f) => f.id !== file.id))}>
<FileNameRender file={file} />
</CustomTag>
))}
</ConfigProvider>
</ContentContainer>
)
}
const ContentContainer = styled.div`
width: 100%;
padding: 5px 15px 5px 15px;
display: flex;
flex-wrap: wrap;
gap: 4px 0;
padding: 5px 15px 0 10px;
gap: 4px 4px;
`
const FileName = styled.span`

View File

@ -84,7 +84,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
pasteLongTextAsFile,
pasteLongTextThreshold,
showInputEstimatedTokens,
autoTranslateWithSpace
autoTranslateWithSpace,
enableQuickPanelTriggers
} = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
@ -533,7 +534,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const cursorPosition = textArea?.selectionStart ?? 0
const lastSymbol = newText[cursorPosition - 1]
if (!quickPanel.isVisible && lastSymbol === '/') {
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '/') {
quickPanel.open({
title: t('settings.quickPanel.title'),
list: quickPanelMenu,
@ -541,7 +542,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
})
}
if (!quickPanel.isVisible && lastSymbol === '@') {
if (enableQuickPanelTriggers && !quickPanel.isVisible && lastSymbol === '@') {
mentionModelsButtonRef.current?.openQuickPanel()
}
}
@ -881,12 +882,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
id="inputbar"
className={classNames('inputbar-container', inputFocus && 'focus')}
ref={containerRef}>
<AttachmentPreview files={files} setFiles={setFiles} />
{files.length > 0 && <AttachmentPreview files={files} setFiles={setFiles} />}
{selectedKnowledgeBases.length > 0 && (
<KnowledgeBaseInput
selectedKnowledgeBases={selectedKnowledgeBases}
onRemoveKnowledgeBase={handleRemoveKnowledgeBase}
/>
)}
{mentionModels.length > 0 && (
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
)}
<Textarea
value={text}
onChange={onChange}
@ -1049,6 +1054,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
position: relative;
z-index: 2;
`
const InputBarContainer = styled.div`

View File

@ -1,6 +1,6 @@
import { FileSearchOutlined } from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag'
import { KnowledgeBase } from '@renderer/types'
import { ConfigProvider, Flex, Tag } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
@ -9,34 +9,27 @@ const KnowledgeBaseInput: FC<{
onRemoveKnowledgeBase: (knowledgeBase: KnowledgeBase) => void
}> = ({ selectedKnowledgeBases, onRemoveKnowledgeBase }) => {
return (
<Container gap="4px 0" wrap>
<ConfigProvider
theme={{
components: {
Tag: {
borderRadiusSM: 100
}
}
}}>
<Container>
{selectedKnowledgeBases.map((knowledgeBase) => (
<Tag
<CustomTag
icon={<FileSearchOutlined />}
bordered={false}
color="success"
color="#3d9d0f"
key={knowledgeBase.id}
closable
onClose={() => onRemoveKnowledgeBase(knowledgeBase)}>
{knowledgeBase.name}
</Tag>
</CustomTag>
))}
</ConfigProvider>
</Container>
)
}
const Container = styled(Flex)`
const Container = styled.div`
width: 100%;
padding: 5px 15px 0 10px;
padding: 5px 15px 5px 15px;
display: flex;
flex-wrap: wrap;
gap: 4px 4px;
`
export default KnowledgeBaseInput

View File

@ -51,7 +51,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
.reverse()
.map((item) => ({
label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`,
description: <ModelTagsWithLabel model={item.model} showLabel={false} size={10} />,
description: <ModelTagsWithLabel model={item.model} showLabel={false} size={10} style={{ opacity: 0.8 }} />,
icon: (
<Avatar src={getModelLogo(item.model.id)} size={20}>
{first(item.model.name)}

View File

@ -1,7 +1,7 @@
import CustomTag from '@renderer/components/CustomTag'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import { ConfigProvider, Flex, Tag } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -19,38 +19,27 @@ const MentionModelsInput: FC<{
}
return (
<Container gap="4px 0" wrap>
<ConfigProvider
theme={{
components: {
Tag: {
borderRadiusSM: 100
}
}
}}>
<Container>
{selectedModels.map((model) => (
<Tag
<CustomTag
icon={<i className="iconfont icon-at" />}
bordered={false}
color="processing"
color="#1677ff"
key={getModelUniqId(model)}
closable
onClose={() => onRemoveModel(model)}>
{model.name} ({getProviderName(model)})
</Tag>
</CustomTag>
))}
</ConfigProvider>
</Container>
)
}
const Container = styled(Flex)`
const Container = styled.div`
width: 100%;
padding: 5px 15px 10px;
i.iconfont {
font-size: 12px;
margin-inline-end: 7px;
}
padding: 5px 15px 5px 15px;
display: flex;
flex-wrap: wrap;
gap: 4px 4px;
`
export default MentionModelsInput

View File

@ -310,6 +310,7 @@ const Container = styled(Scrollbar)<ContainerProps>`
padding: 10px 0 20px;
overflow-x: hidden;
background-color: var(--color-background);
z-index: 1;
`
export default Messages

View File

@ -27,6 +27,7 @@ import {
setCodeShowLineNumbers,
setCodeStyle,
setCodeWrappable,
setEnableQuickPanelTriggers,
setFontSize,
setMathEngine,
setMessageFont,
@ -88,7 +89,8 @@ const SettingsTab: FC<Props> = (props) => {
pasteLongTextThreshold,
multiModelMessageStyle,
thoughtAutoCollapse,
messageNavigation
messageNavigation,
enableQuickPanelTriggers
} = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
@ -570,6 +572,15 @@ const SettingsTab: FC<Props> = (props) => {
<SettingDivider />
</>
)}
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.enable_quick_triggers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={enableQuickPanelTriggers}
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.input.target_language')}</SettingRowTitleSmall>
<StyledSelect

View File

@ -66,7 +66,6 @@ const TranslatePage: FC = () => {
targetLanguage,
createdAt: new Date().toISOString()
}
console.log('🌟TEO🌟 ~ saveTranslateHistory ~ history:', history)
await db.translate_history.add(history)
}

View File

@ -42,7 +42,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 93,
version: 94,
blacklist: ['runtime', 'messages'],
migrate
},

View File

@ -1184,6 +1184,14 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'94': (state: RootState) => {
try {
state.settings.enableQuickPanelTriggers = false
return state
} catch (error) {
return state
}
}
}

View File

@ -110,6 +110,7 @@ export interface SettingsState {
showOpenedMinappsInSidebar: boolean
// 隐私设置
enableDataCollection: boolean
enableQuickPanelTriggers: boolean
exportMenuOptions: {
image: boolean
markdown: boolean
@ -208,6 +209,7 @@ export const initialState: SettingsState = {
maxKeepAliveMinapps: 3,
showOpenedMinappsInSidebar: true,
enableDataCollection: false,
enableQuickPanelTriggers: false,
exportMenuOptions: {
image: true,
markdown: true,
@ -476,6 +478,9 @@ const settingsSlice = createSlice({
},
setExportMenuOptions: (state, action: PayloadAction<typeof initialState.exportMenuOptions>) => {
state.exportMenuOptions = action.payload
},
setEnableQuickPanelTriggers: (state, action: PayloadAction<boolean>) => {
state.enableQuickPanelTriggers = action.payload
}
}
})
@ -562,6 +567,7 @@ export const {
setMaxKeepAliveMinapps,
setShowOpenedMinappsInSidebar,
setEnableDataCollection,
setEnableQuickPanelTriggers,
setExportMenuOptions
} = settingsSlice.actions

View File

@ -3937,6 +3937,7 @@ __metadata:
axios: "npm:^1.7.3"
babel-plugin-styled-components: "npm:^2.1.4"
browser-image-compression: "npm:^2.0.2"
color: "npm:^5.0.0"
dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8"
dexie-react-hooks: "npm:^1.1.7"
@ -5305,6 +5306,15 @@ __metadata:
languageName: node
linkType: hard
"color-convert@npm:^3.0.1":
version: 3.0.1
resolution: "color-convert@npm:3.0.1"
dependencies:
color-name: "npm:^2.0.0"
checksum: 10c0/1ff3db76f4b247aec9062c079b96050f3bcde4fe2183fabf60652b25933fecb85b191bd92044ca60abece39927ad08a3e6d829d9fda9f505c1a1273d13dbc780
languageName: node
linkType: hard
"color-name@npm:1.1.3":
version: 1.1.3
resolution: "color-name@npm:1.1.3"
@ -5312,6 +5322,13 @@ __metadata:
languageName: node
linkType: hard
"color-name@npm:^2.0.0":
version: 2.0.0
resolution: "color-name@npm:2.0.0"
checksum: 10c0/fc0304606e5c5941f4649a9975c03a2ecd52a22aba3dadb3309b3e4ee61d78c3e13ff245e80b9a930955d38c5f32a9004196a7456c4542822aa1fcfea8e928ed
languageName: node
linkType: hard
"color-name@npm:~1.1.4":
version: 1.1.4
resolution: "color-name@npm:1.1.4"
@ -5319,6 +5336,15 @@ __metadata:
languageName: node
linkType: hard
"color-string@npm:^2.0.0":
version: 2.0.1
resolution: "color-string@npm:2.0.1"
dependencies:
color-name: "npm:^2.0.0"
checksum: 10c0/8547edb171cfcc9b56d54664560fba98afd065deedd6812e9545be6448c9c38f89dff51e38d18249b3670fa11647824cbcb77bfbb0c8bff8e37c53c9c0baecc1
languageName: node
linkType: hard
"color-support@npm:^1.1.3":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
@ -5328,6 +5354,16 @@ __metadata:
languageName: node
linkType: hard
"color@npm:^5.0.0":
version: 5.0.0
resolution: "color@npm:5.0.0"
dependencies:
color-convert: "npm:^3.0.1"
color-string: "npm:^2.0.0"
checksum: 10c0/fa5f2e84add2e1622abe016b917cca739535fc9845305db32043a5bde4b8164033f179fd1807ac3fe52c9ee7888f82d80e5ff90d1e2652454a2341ab3d23d086
languageName: node
linkType: hard
"colorette@npm:^2.0.20":
version: 2.0.20
resolution: "colorette@npm:2.0.20"