11import React , { useState , useRef , useCallback , useEffect , useMemo } from 'react' ;
22import { Button , Tooltip , App , theme , Dropdown , Tag , Popover , Checkbox , Badge } from 'antd' ;
33import type { MenuProps } from 'antd' ;
4- import { Paperclip , Trash2 , Mic , Eraser , Scissors , Globe , Brain , BrainCog , Plug , SlidersHorizontal , ArrowUp , Square , Check , Zap , ZapOff , Gauge , Shrink , Upload , LayoutGrid , X , BookOpen } from 'lucide-react' ;
4+ import { Paperclip , Trash2 , Mic , Eraser , Scissors , Globe , Brain , BrainCog , Plug , SlidersHorizontal , ArrowUp , Square , Check , Zap , ZapOff , Gauge , Shrink , Upload , LayoutGrid , X , BookOpen , GripHorizontal } from 'lucide-react' ;
55import { useTranslation } from 'react-i18next' ;
66import { useConversationStore , useProviderStore , useSettingsStore , useSearchStore , useMcpStore , useMemoryStore , useKnowledgeStore } from '@/stores' ;
77import { useUIStore } from '@/stores/uiStore' ;
@@ -58,6 +58,15 @@ export function InputArea() {
5858 useConversationStore . getState ( ) . activeConversationId ?? null
5959 ) ;
6060
61+ // Drag-to-resize state: userMinHeight controls the minimum visible height of the textarea
62+ const INITIAL_MIN_HEIGHT = 44 ;
63+ const ABSOLUTE_MAX_HEIGHT = 600 ;
64+ const [ userMinHeight , setUserMinHeight ] = useState ( INITIAL_MIN_HEIGHT ) ;
65+ const userMinHeightRef = useRef ( userMinHeight ) ;
66+ userMinHeightRef . current = userMinHeight ;
67+ const dragStateRef = useRef < { startY : number ; startH : number } | null > ( null ) ;
68+ const containerRef = useRef < HTMLDivElement > ( null ) ;
69+
6170 // Multi-model companion state
6271 const [ companionModels , setCompanionModels ] = useState < Array < { providerId : string ; modelId : string } > > ( [ ] ) ;
6372 const [ multiModelOpen , setMultiModelOpen ] = useState ( false ) ;
@@ -583,7 +592,8 @@ export function InputArea() {
583592 const textarea = textareaRef . current ;
584593 if ( textarea ) {
585594 textarea . style . height = 'auto' ;
586- textarea . style . height = `${ Math . min ( textarea . scrollHeight , 200 ) } px` ;
595+ const desired = Math . max ( textarea . scrollHeight , userMinHeightRef . current ) ;
596+ textarea . style . height = Math . min ( desired , ABSOLUTE_MAX_HEIGHT ) + 'px' ;
587597 }
588598 } ) ;
589599 }
@@ -601,7 +611,8 @@ export function InputArea() {
601611 if ( ! textarea ) return ;
602612 textarea . focus ( ) ;
603613 textarea . style . height = 'auto' ;
604- textarea . style . height = `${ Math . min ( textarea . scrollHeight , 200 ) } px` ;
614+ const desired = Math . max ( textarea . scrollHeight , userMinHeightRef . current ) ;
615+ textarea . style . height = Math . min ( desired , ABSOLUTE_MAX_HEIGHT ) + 'px' ;
605616 } ) ;
606617 } , [ messages , streaming ] ) ;
607618
@@ -712,12 +723,47 @@ export function InputArea() {
712723 [ handleSend ] ,
713724 ) ;
714725
715- // Auto-resize textarea
726+ // Auto-resize textarea: height = max(userMinHeight, contentHeight), capped at ABSOLUTE_MAX
727+ const autoResizeTextarea = useCallback ( ( el : HTMLTextAreaElement ) => {
728+ el . style . height = 'auto' ;
729+ const desired = Math . max ( el . scrollHeight , userMinHeightRef . current ) ;
730+ el . style . height = Math . min ( desired , ABSOLUTE_MAX_HEIGHT ) + 'px' ;
731+ } , [ ] ) ;
732+
716733 const handleInput = useCallback ( ( e : React . ChangeEvent < HTMLTextAreaElement > ) => {
717734 setValue ( e . target . value ) ;
718- const el = e . target ;
719- el . style . height = 'auto' ;
720- el . style . height = Math . min ( el . scrollHeight , 200 ) + 'px' ;
735+ autoResizeTextarea ( e . target ) ;
736+ } , [ autoResizeTextarea ] ) ;
737+
738+ // Drag-to-resize: changes userMinHeight so the textarea grows even with short content
739+ const handleResizeMouseDown = useCallback ( ( e : React . MouseEvent ) => {
740+ e . preventDefault ( ) ;
741+ const textarea = textareaRef . current ;
742+ const startHeight = textarea ? textarea . offsetHeight : userMinHeightRef . current ;
743+ dragStateRef . current = { startY : e . clientY , startH : startHeight } ;
744+ const onMouseMove = ( ev : MouseEvent ) => {
745+ if ( ! dragStateRef . current ) return ;
746+ const delta = dragStateRef . current . startY - ev . clientY ;
747+ const newH = Math . max ( INITIAL_MIN_HEIGHT , Math . min ( ABSOLUTE_MAX_HEIGHT , dragStateRef . current . startH + delta ) ) ;
748+ setUserMinHeight ( newH ) ;
749+ userMinHeightRef . current = newH ;
750+ if ( textarea ) {
751+ textarea . style . height = 'auto' ;
752+ const desired = Math . max ( textarea . scrollHeight , newH ) ;
753+ textarea . style . height = Math . min ( desired , ABSOLUTE_MAX_HEIGHT ) + 'px' ;
754+ }
755+ } ;
756+ const onMouseUp = ( ) => {
757+ dragStateRef . current = null ;
758+ document . removeEventListener ( 'mousemove' , onMouseMove ) ;
759+ document . removeEventListener ( 'mouseup' , onMouseUp ) ;
760+ document . body . style . cursor = '' ;
761+ document . body . style . userSelect = '' ;
762+ } ;
763+ document . addEventListener ( 'mousemove' , onMouseMove ) ;
764+ document . addEventListener ( 'mouseup' , onMouseUp ) ;
765+ document . body . style . cursor = 'ns-resize' ;
766+ document . body . style . userSelect = 'none' ;
721767 } , [ ] ) ;
722768
723769 // Listen for Escape to close voice overlay
@@ -778,7 +824,8 @@ export function InputArea() {
778824 if ( ! textarea ) return ;
779825 textarea . focus ( ) ;
780826 textarea . style . height = 'auto' ;
781- textarea . style . height = `${ Math . min ( textarea . scrollHeight , 200 ) } px` ;
827+ const desired = Math . max ( textarea . scrollHeight , userMinHeightRef . current ) ;
828+ textarea . style . height = Math . min ( desired , ABSOLUTE_MAX_HEIGHT ) + 'px' ;
782829 } ) ;
783830 } ;
784831 window . addEventListener ( 'aqbot:fill-input' , onFillInput ) ;
@@ -821,13 +868,28 @@ export function InputArea() {
821868
822869 { /* Main input container */ }
823870 < div
871+ ref = { containerRef }
824872 style = { {
825873 border : '1px solid var(--border-color)' ,
826874 borderRadius : 16 ,
827875 backgroundColor : token . colorBgContainer ,
828876 overflow : 'hidden' ,
829877 } }
830878 >
879+ { /* Drag-to-resize handle */ }
880+ < div
881+ onMouseDown = { handleResizeMouseDown }
882+ style = { {
883+ height : 10 ,
884+ cursor : 'ns-resize' ,
885+ display : 'flex' ,
886+ alignItems : 'center' ,
887+ justifyContent : 'center' ,
888+ flexShrink : 0 ,
889+ } }
890+ >
891+ < GripHorizontal size = { 14 } style = { { color : token . colorTextQuaternary , opacity : 0.5 } } />
892+ </ div >
831893 { /* Companion model tags */ }
832894 { companionModels . length > 0 && (
833895 < div className = "flex flex-wrap gap-1.5 px-3 pt-3 pb-1" >
@@ -893,14 +955,15 @@ export function InputArea() {
893955 border : 'none' ,
894956 outline : 'none' ,
895957 resize : 'none' ,
896- padding : '14px 16px 8px' ,
958+ padding : '4px 16px 8px' ,
897959 fontSize : token . fontSize ,
898960 lineHeight : 1.6 ,
899961 backgroundColor : 'transparent' ,
900962 color : token . colorText ,
901963 fontFamily : 'inherit' ,
902- minHeight : 44 ,
903- maxHeight : 200 ,
964+ minHeight : userMinHeight ,
965+ maxHeight : ABSOLUTE_MAX_HEIGHT ,
966+ overflowY : 'auto' ,
904967 } }
905968 />
906969
0 commit comments