feat(QuickPanel): Optimize QuickPanel (#4404)

feat(QuickPanel): Add footer resizing and improve item action handling
This commit is contained in:
Teo 2025-04-05 20:19:43 +08:00 committed by GitHub
parent c76f274562
commit bc02727633
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 141 additions and 65 deletions

View File

@ -34,6 +34,7 @@ export const QuickPanelView: React.FC<{
const bodyRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const footerRef = useRef<HTMLDivElement>(null)
const scrollBlock = useRef<ScrollLogicalPosition>('nearest')
@ -343,6 +344,20 @@ export const QuickPanelView: React.FC<{
}
}, [index, isAssistiveKeyPressed, historyPanel, ctx, list, handleItemAction, handleClose, clearSearchText])
const [footerWidth, setFooterWidth] = useState(0)
useEffect(() => {
if (!footerRef.current || !ctx.isVisible) return
const footerWidth = footerRef.current.clientWidth
setFooterWidth(footerWidth)
const handleResize = () => {
const footerWidth = footerRef.current!.clientWidth
setFooterWidth(footerWidth)
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [ctx.isVisible])
return (
<QuickPanelContainer $pageSize={ctx.pageSize} className={ctx.isVisible ? 'visible' : ''}>
<QuickPanelBody ref={bodyRef} onMouseMove={() => setIsMouseOver(true)}>
@ -355,7 +370,10 @@ export const QuickPanelView: React.FC<{
disabled: item.disabled
})}
key={i}
onClick={() => handleItemAction(item, 'click')}
onClick={(e) => {
e.stopPropagation()
handleItemAction(item, 'click')
}}
onMouseEnter={() => setIndex(i)}>
<QuickPanelItemLeft>
<QuickPanelItemIcon>{item.icon}</QuickPanelItemIcon>
@ -377,45 +395,47 @@ export const QuickPanelView: React.FC<{
</QuickPanelItem>
))}
</QuickPanelContent>
<QuickPanelFooter>
<QuickPanelFooterTips>
<QuickPanelTitle>{ctx.title || ''}</QuickPanelTitle>
<Flex align="center" gap={16}>
<span>ESC {t('settings.quickPanel.close')}</span>
<QuickPanelFooter ref={footerRef}>
<QuickPanelFooterTitle>{ctx.title || ''}</QuickPanelFooterTitle>
<QuickPanelFooterTips $footerWidth={footerWidth}>
<span>ESC {t('settings.quickPanel.close')}</span>
<Flex align="center" gap={4}>
{t('settings.quickPanel.select')}
</Flex>
<Flex align="center" gap={4}>
{t('settings.quickPanel.select')}
</Flex>
{footerWidth >= 500 && (
<>
<Flex align="center" gap={4}>
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
{ASSISTIVE_KEY}
</span>
+ {t('settings.quickPanel.page')}
</Flex>
{canForwardAndBackward && (
<Flex align="center" gap={4}>
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
{ASSISTIVE_KEY}
</span>
+ {t('settings.quickPanel.back')}/{t('settings.quickPanel.forward')}
</Flex>
)}
</>
)}
<Flex align="center" gap={4}>
{t('settings.quickPanel.confirm')}
</Flex>
{ctx.multiple && (
<Flex align="center" gap={4}>
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
{ASSISTIVE_KEY}
</span>
+ {t('settings.quickPanel.page')}
+ {t('settings.quickPanel.multiple')}
</Flex>
{canForwardAndBackward && (
<Flex align="center" gap={4}>
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
{ASSISTIVE_KEY}
</span>
+ {t('settings.quickPanel.back')}/{t('settings.quickPanel.forward')}
</Flex>
)}
<Flex align="center" gap={4}>
{t('settings.quickPanel.confirm')}
</Flex>
{ctx.multiple && (
<Flex align="center" gap={4}>
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
{ASSISTIVE_KEY}
</span>
+ {t('settings.quickPanel.multiple')}
</Flex>
)}
</Flex>
)}
</QuickPanelFooterTips>
</QuickPanelFooter>
</QuickPanelBody>
@ -462,22 +482,30 @@ const QuickPanelBody = styled.div`
`
const QuickPanelFooter = styled.div`
width: 100%;
`
const QuickPanelFooterTips = styled.div`
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
gap: 8px;
font-size: 10px;
color: var(--color-text-3);
align-items: center;
gap: 16px;
padding: 8px 12px 5px;
`
const QuickPanelTitle = styled.div`
const QuickPanelFooterTips = styled.div<{ $footerWidth: number }>`
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
gap: 16px;
font-size: 10px;
color: var(--color-text-3);
`
const QuickPanelFooterTitle = styled.div`
font-size: 11px;
color: var(--color-text-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const QuickPanelContent = styled.div<{ $pageSize: number; $isMouseOver: boolean }>`

View File

@ -54,8 +54,10 @@ const AttachmentButton: FC<Props> = ({ ref, model, files, setFiles, ToolbarButto
placement="top"
title={isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document')}
arrow>
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile} disabled={disabled}>
<PaperClipOutlined style={{ fontSize: 17 }} />
<ToolbarButton type="text" onClick={onSelectFile} disabled={disabled}>
<PaperClipOutlined
style={{ fontSize: 17, color: files.length ? 'var(--color-primary)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
)

View File

@ -1,8 +1,9 @@
import { FileOutlined } from '@ant-design/icons'
import FileManager from '@renderer/services/FileManager'
import { FileType } from '@renderer/types'
import { Upload as AntdUpload, UploadFile } from 'antd'
import { ConfigProvider, Image, Tag } from 'antd'
import { isEmpty } from 'lodash'
import { FC } from 'react'
import { FC, useState } from 'react'
import styled from 'styled-components'
interface Props {
@ -11,39 +12,79 @@ interface Props {
}
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
const [visibleId, setVisibleId] = useState('')
const isImage = (ext: string) => {
return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'].includes(ext)
}
if (isEmpty(files)) {
return null
}
return (
<ContentContainer>
<Upload
listType={files.length > 20 ? 'text' : 'picture-card'}
fileList={files.map(
(file) =>
({
uid: file.id,
url: 'file://' + FileManager.getSafePath(file),
status: 'done',
name: file.origin_name || file.name
}) as UploadFile
)}
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
/>
<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)
}
}}>
{file.origin_name || file.name}
{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>
)
}
const ContentContainer = styled.div`
max-height: 40vh;
overflow-y: auto;
width: 100%;
padding: 10px 15px 0;
display: flex;
flex-wrap: wrap;
gap: 4px 0;
padding: 5px 15px 0;
`
const Upload = styled(AntdUpload)`
.ant-upload-list-item {
background-color: var(--color-background);
const FileName = styled.span`
cursor: pointer;
&:hover {
text-decoration: underline;
}
`

View File

@ -479,6 +479,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
return newSelectedKnowledgeBases
})
return event.preventDefault()
if (event.key === 'Backspace' && text.trim() === '' && files.length > 0) {
setFiles((prev) => prev.slice(0, -1))
return event.preventDefault()
}
}
}