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 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,45 +395,47 @@ 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}>
|
||||||
|
<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}>
|
<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}
|
||||||
</span>
|
</span>
|
||||||
+ ▲▼ {t('settings.quickPanel.page')}
|
+ ↩︎ {t('settings.quickPanel.multiple')}
|
||||||
</Flex>
|
</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>
|
</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 }>`
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user