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", "@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"color": "^5.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"docx": "^9.0.2", "docx": "^9.0.2",
"electron-log": "^5.1.5", "electron-log": "^5.1.5",

View File

@ -1,3 +1,4 @@
import { CloseOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -8,13 +9,16 @@ interface CustomTagProps {
color: string color: string
size?: number size?: number
tooltip?: string 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 ( return (
<Tooltip title={tooltip} placement="top"> <Tooltip title={tooltip} placement="top">
<Tag $color={color} $size={size}> <Tag $color={color} $size={size} $closable={closable}>
{icon && icon} {children} {icon && icon} {children}
{closable && <CloseIcon $size={size} $color={color} onClick={onClose} />}
</Tag> </Tag>
</Tooltip> </Tooltip>
) )
@ -22,19 +26,42 @@ const CustomTag: FC<CustomTagProps> = ({ children, icon, color, size = 12, toolt
export default CustomTag export default CustomTag
const Tag = styled.div<{ $color: string; $size: number }>` const Tag = styled.div<{ $color: string; $size: number; $closable: boolean }>`
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
padding: ${({ $size }) => $size / 3}px ${({ $size }) => $size * 0.8}px; 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; border-radius: 99px;
color: ${({ $color }) => $color}; color: ${({ $color }) => $color};
background-color: ${({ $color }) => $color + '20'}; background-color: ${({ $color }) => $color + '20'};
font-size: ${({ $size }) => $size}px; font-size: ${({ $size }) => $size}px;
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
position: relative;
.iconfont { .iconfont {
font-size: ${({ $size }) => $size}px; font-size: ${({ $size }) => $size}px;
color: ${({ $color }) => $color}; 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 showToolsCalling?: boolean
size?: number size?: number
showLabel?: boolean showLabel?: boolean
style?: React.CSSProperties
} }
const ModelTagsWithLabel: FC<ModelTagsProps> = ({ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
@ -31,7 +32,8 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
showReasoning = true, showReasoning = true,
showToolsCalling = true, showToolsCalling = true,
size = 12, size = 12,
showLabel = true showLabel = true,
style
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [_showLabel, _setShowLabel] = useState(showLabel) const [_showLabel, _setShowLabel] = useState(showLabel)
@ -64,7 +66,7 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
}, [showLabel]) }, [showLabel])
return ( return (
<Container ref={containerRef}> <Container ref={containerRef} style={style}>
{isVisionModel(model) && ( {isVisionModel(model) && (
<CustomTag <CustomTag
size={size} size={size}

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react' 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 = { export type QuickPanelCallBackOptions = {
symbol: string symbol: string
action: QuickPanelCloseAction action: QuickPanelCloseAction

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1112,6 +1112,7 @@
"messages.input.send_shortcuts": "傳送快捷鍵", "messages.input.send_shortcuts": "傳送快捷鍵",
"messages.input.show_estimated_tokens": "顯示預估 Token 數", "messages.input.show_estimated_tokens": "顯示預估 Token 數",
"messages.input.title": "輸入設定", "messages.input.title": "輸入設定",
"messages.input.enable_quick_triggers": "啟用 '/' 和 '@' 觸發快捷選單",
"messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息", "messages.markdown_rendering_input_message": "Markdown 渲染輸入訊息",
"messages.math_engine": "Markdown 渲染輸入訊息", "messages.math_engine": "Markdown 渲染輸入訊息",
"messages.metrics": "首字延遲 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens", "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 FileManager from '@renderer/services/FileManager'
import { FileType } from '@renderer/types' 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 { isEmpty } from 'lodash'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -11,74 +26,128 @@ interface Props {
setFiles: (files: FileType[]) => void setFiles: (files: FileType[]) => void
} }
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => { const FileNameRender: FC<{ file: FileType }> = ({ file }) => {
const [visibleId, setVisibleId] = useState('') const [visible, setVisible] = useState<boolean>(false)
const isImage = (ext: string) => { const isImage = (ext: string) => {
return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext) return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext)
} }
return (
<Tooltip
styles={{
body: {
padding: 5
}
}}
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)) {
setVisible(true)
return
}
const path = FileManager.getSafePath(file)
if (path) {
window.api.file.openPath(path)
}
}}>
{FileManager.formatFileName(file)}
</FileName>
</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)) { if (isEmpty(files)) {
return null return null
} }
return ( return (
<ContentContainer> <ContentContainer>
<ConfigProvider {files.map((file) => (
theme={{ <CustomTag
components: { key={file.id}
Tag: { icon={getFileIcon(file.ext)}
borderRadiusSM: 100 color="#37a5aa"
} closable
} onClose={() => setFiles(files.filter((f) => f.id !== file.id))}>
}}> <FileNameRender file={file} />
{files.map((file) => ( </CustomTag>
<Tag ))}
key={file.id}
icon={<FileOutlined />}
bordered={false}
color="cyan"
closable
onClose={() => setFiles(files.filter((f) => f.id !== file.id))}>
<FileName
onClick={() => {
if (isImage(file.ext)) {
setVisibleId(file.id)
return
}
const path = FileManager.getSafePath(file)
if (path) {
window.api.file.openPath(path)
}
}}>
{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>
))}
</ConfigProvider>
</ContentContainer> </ContentContainer>
) )
} }
const ContentContainer = styled.div` const ContentContainer = styled.div`
width: 100%; width: 100%;
padding: 5px 15px 5px 15px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px 0; gap: 4px 4px;
padding: 5px 15px 0 10px;
` `
const FileName = styled.span` const FileName = styled.span`

View File

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

View File

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

View File

@ -51,7 +51,7 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
.reverse() .reverse()
.map((item) => ({ .map((item) => ({
label: `${item.provider.isSystem ? t(`provider.${item.provider.id}`) : item.provider.name} | ${item.model.name}`, 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: ( icon: (
<Avatar src={getModelLogo(item.model.id)} size={20}> <Avatar src={getModelLogo(item.model.id)} size={20}>
{first(item.model.name)} {first(item.model.name)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3937,6 +3937,7 @@ __metadata:
axios: "npm:^1.7.3" axios: "npm:^1.7.3"
babel-plugin-styled-components: "npm:^2.1.4" babel-plugin-styled-components: "npm:^2.1.4"
browser-image-compression: "npm:^2.0.2" browser-image-compression: "npm:^2.0.2"
color: "npm:^5.0.0"
dayjs: "npm:^1.11.11" dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8" dexie: "npm:^4.0.8"
dexie-react-hooks: "npm:^1.1.7" dexie-react-hooks: "npm:^1.1.7"
@ -5305,6 +5306,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "color-name@npm:1.1.3":
version: 1.1.3 version: 1.1.3
resolution: "color-name@npm:1.1.3" resolution: "color-name@npm:1.1.3"
@ -5312,6 +5322,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "color-name@npm:~1.1.4":
version: 1.1.4 version: 1.1.4
resolution: "color-name@npm:1.1.4" resolution: "color-name@npm:1.1.4"
@ -5319,6 +5336,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "color-support@npm:^1.1.3":
version: 1.1.3 version: 1.1.3
resolution: "color-support@npm:1.1.3" resolution: "color-support@npm:1.1.3"
@ -5328,6 +5354,16 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "colorette@npm:^2.0.20":
version: 2.0.20 version: 2.0.20
resolution: "colorette@npm:2.0.20" resolution: "colorette@npm:2.0.20"