feat: add resize handle to input textarea with drag interaction (#2174)
* feat: add resize handle to input textarea with drag interaction * handle auto size inputbar * optimize auto resize function and add i18n * fix: expand button bug in inputbar and rebase to latest main * rebase to main
This commit is contained in:
parent
4d9476e99b
commit
309b66e4df
@ -104,6 +104,7 @@
|
|||||||
"input.web_search.button.ok": "Go to Settings",
|
"input.web_search.button.ok": "Go to Settings",
|
||||||
"input.web_search.enable": "Enable web search",
|
"input.web_search.enable": "Enable web search",
|
||||||
"input.web_search.enable_content": "Enable web search in Settings",
|
"input.web_search.enable_content": "Enable web search in Settings",
|
||||||
|
"input.auto_resize": "Auto resize height",
|
||||||
"message.new.branch": "New Branch",
|
"message.new.branch": "New Branch",
|
||||||
"message.new.branch.created": "New Branch Created",
|
"message.new.branch.created": "New Branch Created",
|
||||||
"message.new.context": "New Context",
|
"message.new.context": "New Context",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.web_search.button.ok": "設定に移動",
|
"input.web_search.button.ok": "設定に移動",
|
||||||
"input.web_search.enable": "ウェブ検索を有効にする",
|
"input.web_search.enable": "ウェブ検索を有効にする",
|
||||||
"input.web_search.enable_content": "ウェブ検索を有効にするには、設定でウェブ検索を有効にする必要があります",
|
"input.web_search.enable_content": "ウェブ検索を有効にするには、設定でウェブ検索を有効にする必要があります",
|
||||||
|
"input.auto_resize": "高さを自動調整",
|
||||||
"message.new.branch": "新しいブランチ",
|
"message.new.branch": "新しいブランチ",
|
||||||
"message.new.branch.created": "新しいブランチが作成されました",
|
"message.new.branch.created": "新しいブランチが作成されました",
|
||||||
"message.new.context": "新しいコンテキスト",
|
"message.new.context": "新しいコンテキスト",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.web_search.button.ok": "Перейти в Настройки",
|
"input.web_search.button.ok": "Перейти в Настройки",
|
||||||
"input.web_search.enable": "Включить веб-поиск",
|
"input.web_search.enable": "Включить веб-поиск",
|
||||||
"input.web_search.enable_content": "Необходимо включить веб-поиск в Настройки",
|
"input.web_search.enable_content": "Необходимо включить веб-поиск в Настройки",
|
||||||
|
"input.auto_resize": "Автоматическая высота",
|
||||||
"message.new.branch": "Новая ветка",
|
"message.new.branch": "Новая ветка",
|
||||||
"message.new.branch.created": "Новая ветка создана",
|
"message.new.branch.created": "Новая ветка создана",
|
||||||
"message.new.context": "Новый контекст",
|
"message.new.context": "Новый контекст",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.web_search.button.ok": "去设置",
|
"input.web_search.button.ok": "去设置",
|
||||||
"input.web_search.enable": "开启网络搜索",
|
"input.web_search.enable": "开启网络搜索",
|
||||||
"input.web_search.enable_content": "需要先在设置中开启网络搜索",
|
"input.web_search.enable_content": "需要先在设置中开启网络搜索",
|
||||||
|
"input.auto_resize": "自动调整高度",
|
||||||
"message.new.branch": "分支",
|
"message.new.branch": "分支",
|
||||||
"message.new.branch.created": "新分支已创建",
|
"message.new.branch.created": "新分支已创建",
|
||||||
"message.new.context": "清除上下文",
|
"message.new.context": "清除上下文",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.web_search.button.ok": "去設定",
|
"input.web_search.button.ok": "去設定",
|
||||||
"input.web_search.enable": "開啟網路搜索",
|
"input.web_search.enable": "開啟網路搜索",
|
||||||
"input.web_search.enable_content": "需要先在設定中開啟網路搜索",
|
"input.web_search.enable_content": "需要先在設定中開啟網路搜索",
|
||||||
|
"input.auto_resize": "自動調整高度",
|
||||||
"message.new.branch": "分支",
|
"message.new.branch": "分支",
|
||||||
"message.new.branch.created": "新分支已建立",
|
"message.new.branch.created": "新分支已建立",
|
||||||
"message.new.context": "新上下文",
|
"message.new.context": "新上下文",
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
|
ColumnHeightOutlined,
|
||||||
FormOutlined,
|
FormOutlined,
|
||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
|
HolderOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
PicCenterOutlined,
|
PicCenterOutlined,
|
||||||
QuestionCircleOutlined
|
QuestionCircleOutlined
|
||||||
@ -87,6 +89,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||||
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
|
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const [textareaHeight, setTextareaHeight] = useState<number>()
|
||||||
|
const startDragY = useRef<number>(0)
|
||||||
|
const startHeight = useRef<number>(0)
|
||||||
const currentMessageId = useRef<string>()
|
const currentMessageId = useRef<string>()
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
@ -305,6 +311,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
const resizeTextArea = () => {
|
const resizeTextArea = () => {
|
||||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
if (textArea) {
|
if (textArea) {
|
||||||
|
// 如果已经手动设置了高度,则不自动调整
|
||||||
|
if (textareaHeight) {
|
||||||
|
return
|
||||||
|
}
|
||||||
textArea.style.height = 'auto'
|
textArea.style.height = 'auto'
|
||||||
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
|
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
|
||||||
}
|
}
|
||||||
@ -319,7 +329,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
if (isExpended) {
|
if (isExpended) {
|
||||||
textArea.style.height = '70vh'
|
textArea.style.height = '70vh'
|
||||||
} else {
|
} else {
|
||||||
resizeTextArea()
|
resetHeight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,6 +438,50 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
setTimeout(() => resizeTextArea(), 0)
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDragStart = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDragging(true)
|
||||||
|
startDragY.current = e.clientY
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
startHeight.current = textArea.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrag = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (!isDragging) return
|
||||||
|
|
||||||
|
const delta = startDragY.current - e.clientY // 改变计算方向
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
const maxHeightInPixels = viewportHeight * 0.7
|
||||||
|
|
||||||
|
const newHeight = Math.min(maxHeightInPixels, Math.max(startHeight.current + delta, 30))
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.style.height = `${newHeight}px`
|
||||||
|
setExpend(newHeight == maxHeightInPixels)
|
||||||
|
setTextareaHeight(newHeight)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDragging]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
setIsDragging(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.addEventListener('mousemove', handleDrag)
|
||||||
|
document.addEventListener('mouseup', handleDragEnd)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleDrag)
|
||||||
|
document.removeEventListener('mouseup', handleDragEnd)
|
||||||
|
}
|
||||||
|
}, [isDragging, handleDrag, handleDragEnd])
|
||||||
|
|
||||||
useShortcut('new_topic', () => {
|
useShortcut('new_topic', () => {
|
||||||
if (!generating) {
|
if (!generating) {
|
||||||
addNewTopic()
|
addNewTopic()
|
||||||
@ -552,6 +606,21 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
}
|
}
|
||||||
}, [assistant, model, updateAssistant])
|
}, [assistant, model, updateAssistant])
|
||||||
|
|
||||||
|
const resetHeight = () => {
|
||||||
|
if (expended) {
|
||||||
|
setExpend(false)
|
||||||
|
}
|
||||||
|
setTextareaHeight(undefined)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.style.height = 'auto'
|
||||||
|
const contentHeight = textArea.scrollHeight
|
||||||
|
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
|
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
|
||||||
<NarrowLayout style={{ width: '100%' }}>
|
<NarrowLayout style={{ width: '100%' }}>
|
||||||
@ -572,7 +641,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
rows={textareaRows}
|
rows={textareaRows}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
style={{ fontSize }}
|
style={{
|
||||||
|
fontSize,
|
||||||
|
height: textareaHeight ? `${textareaHeight}px` : undefined
|
||||||
|
}}
|
||||||
styles={{ textarea: TextareaStyle }}
|
styles={{ textarea: TextareaStyle }}
|
||||||
onFocus={(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
onFocus={(e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
setInputFocus(true)
|
setInputFocus(true)
|
||||||
@ -588,6 +660,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||||
onClick={() => searching && dispatch(setSearching(false))}
|
onClick={() => searching && dispatch(setSearching(false))}
|
||||||
/>
|
/>
|
||||||
|
<DragHandle onMouseDown={handleDragStart}>
|
||||||
|
<HolderOutlined />
|
||||||
|
</DragHandle>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
|
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
|
||||||
@ -639,6 +714,13 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{textareaHeight && (
|
||||||
|
<Tooltip placement="top" title={t('chat.input.auto_resize')} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={resetHeight}>
|
||||||
|
<ColumnHeightOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<TokenCount
|
<TokenCount
|
||||||
estimateTokenCount={estimateTokenCount}
|
estimateTokenCount={estimateTokenCount}
|
||||||
inputTokenCount={inputTokenCount}
|
inputTokenCount={inputTokenCount}
|
||||||
@ -665,6 +747,32 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add these styled components at the bottom
|
||||||
|
const DragHandle = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: row-resize;
|
||||||
|
color: var(--color-icon);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -677,12 +785,13 @@ const InputBarContainer = styled.div`
|
|||||||
margin: 14px 20px;
|
margin: 14px 20px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
padding-top: 6px; // 为拖动手柄留出空间
|
||||||
background-color: var(--color-background-opacity);
|
background-color: var(--color-background-opacity);
|
||||||
`
|
`
|
||||||
|
|
||||||
const TextareaStyle: CSSProperties = {
|
const TextareaStyle: CSSProperties = {
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
padding: '10px 15px 8px'
|
padding: '4px 15px 8px' // 减小顶部padding
|
||||||
}
|
}
|
||||||
|
|
||||||
const Textarea = styled(TextArea)`
|
const Textarea = styled(TextArea)`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user