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:
parent
f7f7d2bde8
commit
a0be911dc9
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
setSearchText('')
|
||||
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,26 +520,35 @@ 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);
|
||||
body[theme-mode='dark'] & {
|
||||
background-color: rgba(40, 40, 40, 0.4);
|
||||
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);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,74 +26,128 @@ 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)
|
||||
}
|
||||
|
||||
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)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Tag: {
|
||||
borderRadiusSM: 100
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{files.map((file) => (
|
||||
<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>
|
||||
{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>
|
||||
))}
|
||||
</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`
|
||||
|
||||
@ -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} />
|
||||
<KnowledgeBaseInput
|
||||
selectedKnowledgeBases={selectedKnowledgeBases}
|
||||
onRemoveKnowledgeBase={handleRemoveKnowledgeBase}
|
||||
/>
|
||||
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
|
||||
{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`
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{selectedKnowledgeBases.map((knowledgeBase) => (
|
||||
<Tag
|
||||
icon={<FileSearchOutlined />}
|
||||
bordered={false}
|
||||
color="success"
|
||||
key={knowledgeBase.id}
|
||||
closable
|
||||
onClose={() => onRemoveKnowledgeBase(knowledgeBase)}>
|
||||
{knowledgeBase.name}
|
||||
</Tag>
|
||||
))}
|
||||
</ConfigProvider>
|
||||
<Container>
|
||||
{selectedKnowledgeBases.map((knowledgeBase) => (
|
||||
<CustomTag
|
||||
icon={<FileSearchOutlined />}
|
||||
color="#3d9d0f"
|
||||
key={knowledgeBase.id}
|
||||
closable
|
||||
onClose={() => onRemoveKnowledgeBase(knowledgeBase)}>
|
||||
{knowledgeBase.name}
|
||||
</CustomTag>
|
||||
))}
|
||||
</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
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{selectedModels.map((model) => (
|
||||
<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>
|
||||
{selectedModels.map((model) => (
|
||||
<CustomTag
|
||||
icon={<i className="iconfont icon-at" />}
|
||||
color="#1677ff"
|
||||
key={getModelUniqId(model)}
|
||||
closable
|
||||
onClose={() => onRemoveModel(model)}>
|
||||
{model.name} ({getProviderName(model)})
|
||||
</CustomTag>
|
||||
))}
|
||||
</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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -66,7 +66,6 @@ const TranslatePage: FC = () => {
|
||||
targetLanguage,
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
console.log('🌟TEO🌟 ~ saveTranslateHistory ~ history:', history)
|
||||
await db.translate_history.add(history)
|
||||
}
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 93,
|
||||
version: 94,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -1184,6 +1184,14 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'94': (state: RootState) => {
|
||||
try {
|
||||
state.settings.enableQuickPanelTriggers = false
|
||||
return state
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user