feat(QuickPanel): Optimize QuickPanel (#4404)
feat(QuickPanel): Add footer resizing and improve item action handling
This commit is contained in:
parent
c76f274562
commit
bc02727633
@ -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,16 +395,17 @@ export const QuickPanelView: React.FC<{
|
||||
</QuickPanelItem>
|
||||
))}
|
||||
</QuickPanelContent>
|
||||
<QuickPanelFooter>
|
||||
<QuickPanelFooterTips>
|
||||
<QuickPanelTitle>{ctx.title || ''}</QuickPanelTitle>
|
||||
<Flex align="center" gap={16}>
|
||||
<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>
|
||||
|
||||
{footerWidth >= 500 && (
|
||||
<>
|
||||
<Flex align="center" gap={4}>
|
||||
<span style={{ color: isAssistiveKeyPressed ? 'var(--color-primary)' : 'var(--color-text-3)' }}>
|
||||
{ASSISTIVE_KEY}
|
||||
@ -402,6 +421,8 @@ export const QuickPanelView: React.FC<{
|
||||
+ ◀︎▶︎ {t('settings.quickPanel.back')}/{t('settings.quickPanel.forward')}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Flex align="center" gap={4}>
|
||||
↩︎ {t('settings.quickPanel.confirm')}
|
||||
@ -415,7 +436,6 @@ export const QuickPanelView: React.FC<{
|
||||
+ ↩︎ {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 }>`
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user