@@ -56,10 +56,26 @@ export const TaskChatInputBox = ({ streamStatus, availableCommands, onSend, send
5656 const fileInputRef = useRef < HTMLInputElement > ( null )
5757 const dragDepthRef = useRef ( 0 )
5858 const isExecuting = ( streamStatus === 'connected' || streamStatus === 'inited' )
59+ const wasExecutingRef = useRef ( isExecuting )
60+ const restoreSubmittedInputOnIdleRef = useRef ( false )
61+ const lastSubmittedInputRef = useRef < { content : string ; uploadedFiles : TaskUploadedFile [ ] } | null > ( null )
5962 const canInput = React . useMemo ( ( ) => {
6063 return ! sending && ! isExecuting && queueSize === 0
6164 } , [ sending , isExecuting , queueSize ] )
6265
66+ React . useEffect ( ( ) => {
67+ if ( wasExecutingRef . current && ! isExecuting && restoreSubmittedInputOnIdleRef . current ) {
68+ const lastSubmittedInput = lastSubmittedInputRef . current
69+ if ( lastSubmittedInput ) {
70+ setContent ( lastSubmittedInput . content )
71+ setUploadedFiles ( lastSubmittedInput . uploadedFiles )
72+ setPreviewFile ( null )
73+ }
74+ restoreSubmittedInputOnIdleRef . current = false
75+ }
76+ wasExecutingRef . current = isExecuting
77+ } , [ isExecuting ] )
78+
6379 const sendCurrentInput = async ( ) => {
6480 if ( content . trim ( ) === '' ) {
6581 return
@@ -77,12 +93,22 @@ export const TaskChatInputBox = ({ streamStatus, availableCommands, onSend, send
7793 return
7894 }
7995
96+ lastSubmittedInputRef . current = {
97+ content,
98+ uploadedFiles,
99+ }
100+ restoreSubmittedInputOnIdleRef . current = false
80101 setContent ( '' )
81102 setUploadedFiles ( [ ] )
82103 setPreviewFile ( null )
83104 setWhiteboardFileIndex ( 1 )
84105 }
85106
107+ const handleCancel = ( ) => {
108+ restoreSubmittedInputOnIdleRef . current = true
109+ onCancel ?.( )
110+ }
111+
86112 const handleSend = ( ) => {
87113 if ( content . trim ( ) === '' ) {
88114 return
@@ -313,6 +339,19 @@ export const TaskChatInputBox = ({ streamStatus, availableCommands, onSend, send
313339 if ( isComposing ) {
314340 return
315341 }
342+ if ( e . key === 'Enter' && e . ctrlKey ) {
343+ e . preventDefault ( )
344+ const textarea = e . currentTarget
345+ const start = textarea . selectionStart
346+ const end = textarea . selectionEnd
347+ const nextContent = `${ content . slice ( 0 , start ) } \n${ content . slice ( end ) } `
348+ setContent ( nextContent )
349+ requestAnimationFrame ( ( ) => {
350+ textarea . selectionStart = start + 1
351+ textarea . selectionEnd = start + 1
352+ } )
353+ return
354+ }
316355 if ( e . key === 'Enter' && ! e . shiftKey ) {
317356 e . preventDefault ( )
318357 handleSend ( )
@@ -360,7 +399,7 @@ export const TaskChatInputBox = ({ streamStatus, availableCommands, onSend, send
360399 { ! isExecuting && (
361400 < InputGroupTextarea
362401 ref = { textareaRef }
363- className = "min-h-8 max-h-48 text-sm break-all"
402+ className = "min-h-8 max-h-36 resize-none overflow-y-auto text-sm break-all [field-sizing:content] "
364403 placeholder = "描述你的需求,Shift+Enter 换行,Enter 发送。"
365404 value = { content }
366405 onChange = { ( e ) => setContent ( e . target . value ) }
@@ -421,57 +460,53 @@ export const TaskChatInputBox = ({ streamStatus, availableCommands, onSend, send
421460 ) }
422461 </ DropdownMenuContent >
423462 </ DropdownMenu >
424- { ! isExecuting && (
425- < >
426- { canUploadMoreFiles && (
427- < Tooltip >
428- < TooltipTrigger asChild >
429- < Button
430- type = "button"
431- variant = "outline"
432- size = "icon-sm"
433- className = "rounded-full"
434- disabled = { controlsDisabled }
435- aria-label = "上传附件"
436- onClick = { handleSelectFile }
437- >
438- < IconUpload />
439- </ Button >
440- </ TooltipTrigger >
441- < TooltipContent > 上传附件</ TooltipContent >
442- </ Tooltip >
443- ) }
444- < Tooltip >
445- < TooltipTrigger asChild >
446- < Button
447- type = "button"
448- variant = "outline"
449- size = "icon-sm"
450- className = "rounded-full"
451- disabled = { controlsDisabled }
452- aria-label = "画板"
453- onClick = { ( ) => setWhiteboardDialogOpen ( true ) }
454- >
455- < IconPalette />
456- </ Button >
457- </ TooltipTrigger >
458- < TooltipContent > 画板</ TooltipContent >
459- </ Tooltip >
460- { uploadedFiles . map ( ( uploadedFile ) => (
461- < TaskUploadedFileItem
462- key = { uploadedFile . accessUrl }
463- file = { uploadedFile }
464- onPreview = { ( ) => setPreviewFile ( uploadedFile ) }
465- onRemove = { ( ) => {
466- if ( previewFile ?. accessUrl === uploadedFile . accessUrl ) {
467- setPreviewFile ( null )
468- }
469- setUploadedFiles ( ( prev ) => prev . filter ( ( file ) => file . accessUrl !== uploadedFile . accessUrl ) )
470- } }
471- />
472- ) ) }
473- </ >
463+ { canUploadMoreFiles && (
464+ < Tooltip >
465+ < TooltipTrigger asChild >
466+ < Button
467+ type = "button"
468+ variant = "outline"
469+ size = "icon-sm"
470+ className = "rounded-full"
471+ disabled = { controlsDisabled }
472+ aria-label = "上传附件"
473+ onClick = { handleSelectFile }
474+ >
475+ < IconUpload />
476+ </ Button >
477+ </ TooltipTrigger >
478+ < TooltipContent > 上传附件</ TooltipContent >
479+ </ Tooltip >
474480 ) }
481+ < Tooltip >
482+ < TooltipTrigger asChild >
483+ < Button
484+ type = "button"
485+ variant = "outline"
486+ size = "icon-sm"
487+ className = "rounded-full"
488+ disabled = { controlsDisabled }
489+ aria-label = "画板"
490+ onClick = { ( ) => setWhiteboardDialogOpen ( true ) }
491+ >
492+ < IconPalette />
493+ </ Button >
494+ </ TooltipTrigger >
495+ < TooltipContent > 画板</ TooltipContent >
496+ </ Tooltip >
497+ { uploadedFiles . map ( ( uploadedFile ) => (
498+ < TaskUploadedFileItem
499+ key = { uploadedFile . accessUrl }
500+ file = { uploadedFile }
501+ onPreview = { ( ) => setPreviewFile ( uploadedFile ) }
502+ onRemove = { ( ) => {
503+ if ( previewFile ?. accessUrl === uploadedFile . accessUrl ) {
504+ setPreviewFile ( null )
505+ }
506+ setUploadedFiles ( ( prev ) => prev . filter ( ( file ) => file . accessUrl !== uploadedFile . accessUrl ) )
507+ } }
508+ />
509+ ) ) }
475510 </ div >
476511 < div className = "flex flex-row gap-2 items-center min-w-0" >
477512 { isExecuting && (
@@ -490,7 +525,7 @@ export const TaskChatInputBox = ({ streamStatus, availableCommands, onSend, send
490525 className = { cn ( "flex flex-row gap-2 items-center" , isExecuting && "rounded-full" ) }
491526 variant = { isExecuting ? "destructive" : "default" }
492527 size = { isExecuting ? "icon-sm" : "sm" }
493- onClick = { isExecuting ? onCancel : handleSend }
528+ onClick = { isExecuting ? handleCancel : handleSend }
494529 disabled = { isExecuting ? ! onCancel : ! canSend || inputDisabled }
495530 >
496531 { isExecuting ? < IconPlayerStopFilled /> : < IconSend /> }
0 commit comments