@@ -15,7 +15,7 @@ import { FILE_INPUT_ACCEPT, isSupportedFile, isWithinSizeLimit } from "@/lib/fil
1515import { attachedToMessageFormat } from "@/lib/attachFiles"
1616import { usePreparedAttachments } from "@/hooks/usePreparedAttachments"
1717import { useAuth } from "@/context/AuthContext"
18- import ContentTypePicker , { ContentTypeChips } from "@/components/ui/ContentTypePicker"
18+ import ContentTypePicker , { InlineContentChips , type ContentTypePickerKeyHandler } from "@/components/ui/ContentTypePicker"
1919import { readStoredModel , writeStoredModel } from "@/lib/models"
2020import { readStoredContentTypes , writeStoredContentTypes , buildContentTypeAutoPrompt } from "@/lib/contentTypes"
2121
@@ -73,6 +73,7 @@ const ChatInput = memo(function ChatInput() {
7373 const [ micHover , setMicHover ] = useState ( false )
7474 const [ sendHover , setSendHover ] = useState ( false )
7575 const fileInputRef = useRef < HTMLInputElement > ( null )
76+ const pickerKeyRef = useRef < ContentTypePickerKeyHandler | null > ( null )
7677 const localSendInFlightRef = useRef ( false )
7778 const {
7879 attachments,
@@ -277,95 +278,96 @@ const ChatInput = memo(function ChatInput() {
277278 </ div >
278279 ) }
279280
280- { /* Content type chips */ }
281- < ContentTypeChips
282- selected = { inputFeatures . requestedContentTypes ?? [ ] }
283- onRemove = { ( id ) => {
284- const next = ( inputFeatures . requestedContentTypes ?? [ ] ) . filter ( ( t ) => t !== id )
285- setInputFeatures ( { requestedContentTypes : next } )
286- writeStoredContentTypes ( next )
287- } }
288- isDark = { isDark }
289- />
290-
291- { /* Row 1: Input area + typewriter placeholder — fixed width, text wraps within box */ }
292- < div style = { { position : "relative" , height : `${ inputHeight } px` , minHeight : `${ ROW_HEIGHT } px` , width : "100%" , minWidth : 0 , overflow : "hidden" } } >
293- < textarea
294- ref = { textareaRef }
295- value = { inputValue }
296- onChange = { handleChange }
297- onPaste = { handlePaste }
298- onKeyDown = { ( e ) => {
299- if ( e . key === "Enter" && ! e . shiftKey ) {
300- e . preventDefault ( )
301- void handleSend ( )
302- }
303- } }
304- onFocus = { ( ) => setInputFocused ( true ) }
305- onBlur = { ( ) => setInputFocused ( false ) }
306- rows = { 1 }
307- aria-label = "Message input"
308- style = { {
309- width : "100%" ,
310- maxWidth : "100%" ,
311- boxSizing : "border-box" ,
312- background : "transparent" ,
313- border : "none" ,
314- outline : "none" ,
315- resize : "none" ,
316- fontFamily : FONTS . sans ,
317- fontSize : "16px" ,
318- fontWeight : 400 ,
319- color : COLORS . textPrimary ,
320- lineHeight : `${ ROW_HEIGHT } px` ,
321- padding : 0 ,
322- margin : 0 ,
323- height : `${ inputHeight } px` ,
324- maxHeight : `${ ROW_HEIGHT * 2 } px` ,
325- overflowX : "hidden" ,
326- overflowY : "hidden" ,
327- overflowWrap : "break-word" ,
328- wordBreak : "break-word" ,
329- whiteSpace : "pre-wrap" ,
330- position : "relative" ,
331- zIndex : 2 ,
281+ { /* Row 1: inline content chips + input area + typewriter placeholder */ }
282+ < div style = { { display : "flex" , flexWrap : "wrap" , alignItems : "center" , gap : "6px" , minHeight : `${ ROW_HEIGHT } px` , width : "100%" , minWidth : 0 } } >
283+ < InlineContentChips
284+ selected = { inputFeatures . requestedContentTypes ?? [ ] }
285+ onRemove = { ( id ) => {
286+ const next = ( inputFeatures . requestedContentTypes ?? [ ] ) . filter ( ( t ) => t !== id )
287+ setInputFeatures ( { requestedContentTypes : next } )
288+ writeStoredContentTypes ( next )
332289 } }
290+ isDark = { isDark }
333291 />
334-
335- { /* Typewriter placeholder */ }
336- { ! inputValue && ! inputFocused && (
337- < span
338- aria-hidden = "true"
292+ < div style = { { position : "relative" , flex : 1 , minWidth : "100px" , height : `${ inputHeight } px` , minHeight : `${ ROW_HEIGHT } px` , overflow : "hidden" } } >
293+ < textarea
294+ ref = { textareaRef }
295+ value = { inputValue }
296+ onChange = { handleChange }
297+ onPaste = { handlePaste }
298+ onKeyDown = { ( e ) => {
299+ if ( atMention && pickerKeyRef . current ?. handleKeyDown ( e . key ) ) { e . preventDefault ( ) ; return }
300+ if ( e . key === "Enter" && ! e . shiftKey ) {
301+ e . preventDefault ( )
302+ void handleSend ( )
303+ }
304+ } }
305+ onFocus = { ( ) => setInputFocused ( true ) }
306+ onBlur = { ( ) => setInputFocused ( false ) }
307+ rows = { 1 }
308+ aria-label = "Message input"
339309 style = { {
340- position : "absolute" ,
341- left : 0 ,
342- top : 0 ,
310+ width : "100%" ,
311+ maxWidth : "100%" ,
312+ boxSizing : "border-box" ,
313+ background : "transparent" ,
314+ border : "none" ,
315+ outline : "none" ,
316+ resize : "none" ,
343317 fontFamily : FONTS . sans ,
344318 fontSize : "16px" ,
345319 fontWeight : 400 ,
346- color : COLORS . textDimmed ,
320+ color : COLORS . textPrimary ,
347321 lineHeight : `${ ROW_HEIGHT } px` ,
348- height : `${ ROW_HEIGHT } px` ,
349- pointerEvents : "none" ,
350- zIndex : 1 ,
351- whiteSpace : "nowrap" ,
352- display : "flex" ,
353- alignItems : "center" ,
322+ padding : 0 ,
323+ margin : 0 ,
324+ height : `${ inputHeight } px` ,
325+ maxHeight : `${ ROW_HEIGHT * 2 } px` ,
326+ overflowX : "hidden" ,
327+ overflowY : "hidden" ,
328+ overflowWrap : "break-word" ,
329+ wordBreak : "break-word" ,
330+ whiteSpace : "pre-wrap" ,
331+ position : "relative" ,
332+ zIndex : 2 ,
354333 } }
355- >
356- { typedText }
334+ />
335+
336+ { /* Typewriter placeholder */ }
337+ { ! inputValue && ! inputFocused && ( inputFeatures . requestedContentTypes ?? [ ] ) . length === 0 && (
357338 < span
339+ aria-hidden = "true"
358340 style = { {
359- display : "inline-block" ,
360- width : "1.5px" ,
361- height : "22px" ,
362- background : COLORS . textDimmed ,
363- marginLeft : "1px" ,
364- animation : "cursorBlink 0.8s step-end infinite" ,
341+ position : "absolute" ,
342+ left : 0 ,
343+ top : 0 ,
344+ fontFamily : FONTS . sans ,
345+ fontSize : "16px" ,
346+ fontWeight : 400 ,
347+ color : COLORS . textDimmed ,
348+ lineHeight : `${ ROW_HEIGHT } px` ,
349+ height : `${ ROW_HEIGHT } px` ,
350+ pointerEvents : "none" ,
351+ zIndex : 1 ,
352+ whiteSpace : "nowrap" ,
353+ display : "flex" ,
354+ alignItems : "center" ,
365355 } }
366- />
367- </ span >
368- ) }
356+ >
357+ { typedText }
358+ < span
359+ style = { {
360+ display : "inline-block" ,
361+ width : "1.5px" ,
362+ height : "22px" ,
363+ background : COLORS . textDimmed ,
364+ marginLeft : "1px" ,
365+ animation : "cursorBlink 0.8s step-end infinite" ,
366+ } }
367+ />
368+ </ span >
369+ ) }
370+ </ div >
369371 </ div >
370372
371373 { /* Row 2: Action buttons — preventDefault keeps focus on textarea when tapping icons */ }
@@ -401,6 +403,7 @@ const ChatInput = memo(function ChatInput() {
401403 COLORS = { COLORS }
402404 triggerSearch = { atMention ?. search }
403405 onTriggerConsumed = { handleTriggerConsumed }
406+ keyHandlerRef = { pickerKeyRef }
404407 />
405408 { /* Web search toggle */ }
406409 < button
0 commit comments