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 bodyRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null) const contentRef = useRef<HTMLDivElement>(null)
const footerRef = useRef<HTMLDivElement>(null)
const scrollBlock = useRef<ScrollLogicalPosition>('nearest') const scrollBlock = useRef<ScrollLogicalPosition>('nearest')
@ -343,6 +344,20 @@ export const QuickPanelView: React.FC<{
} }
}, [index, isAssistiveKeyPressed, historyPanel, ctx, list, handleItemAction, handleClose, clearSearchText]) }, [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 ( return (
<QuickPanelContainer $pageSize={ctx.pageSize} className={ctx.isVisible ? 'visible' : ''}> <QuickPanelContainer $pageSize={ctx.pageSize} className={ctx.isVisible ? 'visible' : ''}>
<QuickPanelBody ref={bodyRef} onMouseMove={() => setIsMouseOver(true)}> <QuickPanelBody ref={bodyRef} onMouseMove={() => setIsMouseOver(true)}>
@ -355,7 +370,10 @@ export const QuickPanelView: React.FC<{
disabled: item.disabled disabled: item.disabled
})} })}
key={i} key={i}
onClick={() => handleItemAction(item, 'click')} onClick={(e) => {
e.stopPropagation()
handleItemAction(item, 'click')
}}
onMouseEnter={() => setIndex(i)}> onMouseEnter={() => setIndex(i)}>
<QuickPanelItemLeft> <QuickPanelItemLeft>
<QuickPanelItemIcon>{item.icon}</QuickPanelItemIcon> <QuickPanelItemIcon>{item.icon}</QuickPanelItemIcon>
@ -377,16 +395,17 @@ export const QuickPanelView: React.FC<{
</QuickPanelItem> </QuickPanelItem>
))} ))}
</QuickPanelContent> </QuickPanelContent>
<QuickPanelFooter> <QuickPanelFooter ref={footerRef}>
<QuickPanelFooterTips> <QuickPanelFooterTitle>{ctx.title || ''}</QuickPanelFooterTitle>
<QuickPanelTitle>{ctx.title || ''}</QuickPanelTitle> <QuickPanelFooterTips $footerWidth={footerWidth}>
<Flex align="center" gap={16}>
<span>ESC {t('settings.quickPanel.close')}</span> <span>ESC {t('settings.quickPanel.close')}</span>
<Flex align="center" gap={4}> <Flex align="center" gap={4}>
{t('settings.quickPanel.select')} {t('settings.quickPanel.select')}
</Flex> </Flex>
{footerWidth >= 500 && (
<>
<Flex align="center" gap={4}> <Flex align="center" gap={4}>
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}> <span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
{ASSISTIVE_KEY} {ASSISTIVE_KEY}
@ -402,6 +421,8 @@ export const QuickPanelView: React.FC<{
+ {t('settings.quickPanel.back')}/{t('settings.quickPanel.forward')} + {t('settings.quickPanel.back')}/{t('settings.quickPanel.forward')}
</Flex> </Flex>
)} )}
</>
)}
<Flex align="center" gap={4}> <Flex align="center" gap={4}>
{t('settings.quickPanel.confirm')} {t('settings.quickPanel.confirm')}
@ -415,7 +436,6 @@ export const QuickPanelView: React.FC<{
+ {t('settings.quickPanel.multiple')} + {t('settings.quickPanel.multiple')}
</Flex> </Flex>
)} )}
</Flex>
</QuickPanelFooterTips> </QuickPanelFooterTips>
</QuickPanelFooter> </QuickPanelFooter>
</QuickPanelBody> </QuickPanelBody>
@ -462,22 +482,30 @@ const QuickPanelBody = styled.div`
` `
const QuickPanelFooter = styled.div` const QuickPanelFooter = styled.div`
width: 100%;
`
const QuickPanelFooterTips = styled.div`
display: flex; display: flex;
align-items: center; width: 100%;
justify-content: space-between; justify-content: space-between;
gap: 8px; align-items: center;
font-size: 10px; gap: 16px;
color: var(--color-text-3);
padding: 8px 12px 5px; 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; font-size: 11px;
color: var(--color-text-3); color: var(--color-text-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
` `
const QuickPanelContent = styled.div<{ $pageSize: number; $isMouseOver: boolean }>` 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" placement="top"
title={isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document')} title={isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document')}
arrow> arrow>
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile} disabled={disabled}> <ToolbarButton type="text" onClick={onSelectFile} disabled={disabled}>
<PaperClipOutlined style={{ fontSize: 17 }} /> <PaperClipOutlined
style={{ fontSize: 17, color: files.length ? 'var(--color-primary)' : 'var(--color-icon)' }}
/>
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
) )

View File

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

View File

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