@ -4,7 +4,14 @@ import { ClearOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max' ;
import { Button , Checkbox , Divider , Input , Tooltip } from 'antd' ;
import _ from 'lodash' ;
import React , { useCallback , useMemo , useRef , useState } from 'react' ;
import React , {
forwardRef ,
useCallback ,
useImperativeHandle ,
useMemo ,
useRef ,
useState
} from 'react' ;
import { useHotkeys } from 'react-hotkeys-hook' ;
import { Roles } from '../config' ;
import { MessageItem } from '../config/types' ;
@ -64,6 +71,7 @@ const layoutOptions = [
] ;
interface MessageInputProps {
ref? : any ;
handleSubmit : ( params : CurrentMessage ) = > void ;
handleAbortFetch : ( ) = > void ;
updateLayout ? : ( value : { span : number ; count : number } ) = > void ;
@ -89,411 +97,425 @@ interface MessageInputProps {
defaultSize ? : { minRows : number ; maxRows : number } ;
}
const MessageInput : React.FC < MessageInputProps > = ( {
handleSubmit ,
handleAbortFetch ,
presetPrompt ,
clearAll ,
updateLayout ,
addMessage ,
onCheck ,
loading ,
disabled ,
isEmpty ,
submitIcon ,
placeholer ,
tools ,
style ,
checkLabel ,
defaultSize = { minRows : 3 , maxRows : 8 } ,
shouldResetMessage = true ,
actions = [ 'clear' , 'layout' , 'role' , 'upload' , 'add' , 'paste' ]
} ) = > {
const { TextArea } = Input ;
const intl = useIntl ( ) ;
const [ open , setOpen ] = useState ( false ) ;
const [ focused , setFocused ] = useState ( false ) ;
const [ message , setMessage ] = useState < CurrentMessage > ( {
role : Roles.User ,
content : '' ,
imgs : [ ]
} ) ;
const imgCountRef = useRef ( 0 ) ;
const inputRef = useRef < any > ( null ) ;
const isDisabled = useMemo ( ( ) = > {
return disabled
? true
: ! message . content && isEmpty && ! message . imgs ? . length ;
} , [ disabled , message , isEmpty ] ) ;
const resetMessage = ( ) = > {
setMessage ( {
role : message.role ,
const MessageInput : React.FC < MessageInputProps > = forwardRef (
(
{
handleSubmit ,
handleAbortFetch ,
presetPrompt ,
clearAll ,
updateLayout ,
addMessage ,
onCheck ,
loading ,
disabled ,
isEmpty ,
submitIcon ,
placeholer ,
tools ,
style ,
checkLabel ,
defaultSize = { minRows : 3 , maxRows : 8 } ,
shouldResetMessage = true ,
actions = [ 'clear' , 'layout' , 'role' , 'upload' , 'add' , 'paste' ]
} ,
ref
) = > {
const { TextArea } = Input ;
const intl = useIntl ( ) ;
const [ open , setOpen ] = useState ( false ) ;
const [ focused , setFocused ] = useState ( false ) ;
const [ message , setMessage ] = useState < CurrentMessage > ( {
role : Roles.User ,
content : '' ,
imgs : [ ]
} ) ;
} ;
const imgCountRef = useRef ( 0 ) ;
const inputRef = useRef < any > ( null ) ;
const handleInputChange = ( e : any ) = > {
console . log ( 'input change:' , e . target ? . value ) ;
setMessage ( {
. . . message ,
content : e.target?.value
} ) ;
} ;
const handleSendMessage = ( ) = > {
handleSubmit ( { . . . message } ) ;
if ( shouldResetMessage ) {
resetMessage ( ) ;
}
} ;
const onStop = ( ) = > {
handleAbortFetch ( ) ;
} ;
const handleLayoutChange = ( value : { span : number ; count : number } ) = > {
updateLayout ? . ( value ) ;
} ;
const isDisabled = useMemo ( ( ) = > {
return disabled
? true
: ! message . content && isEmpty && ! message . imgs ? . length ;
} , [ disabled , message , isEmpty ] ) ;
const handleToggleRole = ( ) = > {
setMessage ( {
. . . message ,
role : message.role === Roles . User ? Roles.Assistant : Roles.User
} ) ;
} ;
const resetMessage = ( ) = > {
setMessage ( {
role : message.role ,
content : '' ,
imgs : [ ]
} ) ;
} ;
const handleClearAll = ( e : any ) = > {
e . stopPropagation ( ) ;
clearAll ( ) ;
setMessage ( {
role : Roles.User ,
content : '' ,
imgs : [ ]
} ) ;
} ;
const handleInputChange = ( e : any ) = > {
console . log ( 'input change:' , e . target ? . value ) ;
setMessage ( {
. . . message ,
content : e.target?.value
} ) ;
} ;
const handleSendMessage = ( ) = > {
handleSubmit ( { . . . message } ) ;
if ( shouldResetMessage ) {
resetMessage ( ) ;
}
} ;
const onStop = ( ) = > {
handleAbortFetch ( ) ;
} ;
const handleLayoutChange = ( value : { span : number ; count : number } ) = > {
updateLayout ? . ( value ) ;
} ;
const handleAddMessage = ( e? : any ) = > {
e ? . preventDefault ( ) ;
addMessage ? . ( { . . . message } ) ;
resetMessage ( ) ;
setFocused ( true ) ;
setTimeout ( ( ) = > {
inputRef . current ? . focus ? . ( ) ;
} , 100 ) ;
} ;
const handleToggleRole = ( ) = > {
setMessage ( {
. . . message ,
role : message.role === Roles . User ? Roles.Assistant : Roles.User
} ) ;
} ;
const getPasteContent = useCallback (
async ( event : any ) = > {
const clipboardData = event . clipboardData || window . clipboardData ;
const items = clipboardData . items ;
const imgPromises : Promise < string > [ ] = [ ] ;
const handleClearAll = ( e : any ) = > {
e . stopPropagation ( ) ;
clearAll ( ) ;
setMessage ( {
role : Roles.User ,
content : '' ,
imgs : [ ]
} ) ;
} ;
for ( let i = 0 ; i < items . length ; i ++ ) {
let item = items [ i ] ;
const handleAddMessage = ( e? : any ) = > {
e ? . preventDefault ( ) ;
addMessage ? . ( { . . . message } ) ;
resetMessage ( ) ;
setFocused ( true ) ;
setTimeout ( ( ) = > {
inputRef . current ? . focus ? . ( ) ;
} , 100 ) ;
} ;
if ( item . kind === 'file' && item . type . indexOf ( 'image' ) !== - 1 ) {
const file = item . getAsFile ( ) ;
const imgPromise = new Promise < string > ( ( resolve , reject ) = > {
const reader = new FileReader ( ) ;
reader . onload = function ( event ) {
const base64String = event . target ? . result as string ;
if ( base64String ) {
resolve ( base64String ) ;
} else {
reject ( 'Failed to convert image to base64' ) ;
}
} ;
reader . readAsDataURL ( file ) ;
} ) ;
imgPromises . push ( imgPromise ) ;
} else if ( item . kind === 'string' ) {
// string
}
}
const getPasteContent = useCallback (
async ( event : any ) = > {
const clipboardData = event . clipboardData || window . clipboardData ;
const items = clipboardData . items ;
const imgPromises : Promise < string > [ ] = [ ] ;
try {
const imgs = await Promise . all ( imgPromises ) ;
for ( let i = 0 ; i < items . length ; i ++ ) {
let item = items [ i ] ;
if ( imgs . length ) {
const list = _ . map ( imgs , ( img : string ) = > {
imgCountRef . current += 1 ;
return {
uid : imgCountRef.current ,
dataUrl : img
} ;
} ) ;
setMessage ( {
. . . message ,
imgs : [ . . . ( message . imgs || [ ] ) , . . . list ]
} ) ;
if ( item . kind === 'file' && item . type . indexOf ( 'image' ) !== - 1 ) {
const file = item . getAsFile ( ) ;
const imgPromise = new Promise < string > ( ( resolve , reject ) = > {
const reader = new FileReader ( ) ;
reader . onload = function ( event ) {
const base64String = event . target ? . result as string ;
if ( base64String ) {
resolve ( base64String ) ;
} else {
reject ( 'Failed to convert image to base64' ) ;
}
} ;
reader . readAsDataURL ( file ) ;
} ) ;
imgPromises . push ( imgPromise ) ;
} else if ( item . kind === 'string' ) {
// string
}
}
} catch ( error ) {
console . error ( 'Error processing images:' , error ) ;
}
} ,
[ message ]
) ;
// ========== upload image ==========
const handleUpdateImgList = (
list : { uid : number | string ; dataUrl : string } [ ]
) = > {
setMessage ( {
. . . message ,
imgs : [ . . . ( message . imgs || [ ] ) , . . . list ]
} ) ;
} ;
try {
const imgs = await Promise . all ( imgPromises ) ;
const handleDeleteImg = ( uid : number | string ) = > {
const list = _ . filter (
message . imgs ,
( item : MessageItem ) = > item . uid !== uid
if ( imgs . length ) {
const list = _ . map ( imgs , ( img : string ) = > {
imgCountRef . current += 1 ;
return {
uid : imgCountRef.current ,
dataUrl : img
} ;
} ) ;
setMessage ( {
. . . message ,
imgs : [ . . . ( message . imgs || [ ] ) , . . . list ]
} ) ;
}
} catch ( error ) {
console . error ( 'Error processing images:' , error ) ;
}
} ,
[ message ]
) ;
setMessage ( {
. . . message ,
imgs : list
} ) ;
} ;
const handleOnPaste = ( e : any ) = > {
const text = e . clipboardData . getData ( 'text' ) ;
if ( ! text ) {
e . preventDefault ( ) ;
getPasteContent ( e ) ;
}
} ;
// ========== upload image ==========
const handleUpdateImgList = (
list : { uid : number | string ; dataUrl : string } [ ]
) = > {
setMessage ( {
. . . message ,
imgs : [ . . . ( message . imgs || [ ] ) , . . . list ]
} ) ;
} ;
const handleDeleteImg = ( uid : number | string ) = > {
const list = _ . filter (
message . imgs ,
( item : MessageItem ) = > item . uid !== uid
) ;
setMessage ( {
. . . message ,
imgs : list
} ) ;
} ;
const handleDeleteLastImage = useCallback ( ( ) = > {
if ( message . imgs && message . imgs ? . length > 0 ) {
const newImgList = [ . . . ( message . imgs || [ ] ) ] ;
const lastImage = newImgList . pop ( ) ;
if ( lastImage ) {
handleDeleteImg ( lastImage . uid ) ;
const handleOnPaste = ( e : any ) = > {
const text = e . clipboardData . getData ( 'text' ) ;
if ( ! text ) {
e . preventDefault ( ) ;
getPasteContent ( e ) ;
}
}
} , [ message . imgs , handleDeleteImg ] ) ;
} ;
const handleKeyDown = useCallback (
( event : any ) = > {
if (
event . key === 'Backspace' &&
message . content === '' &&
message . imgs &&
message . imgs ? . length > 0
) {
// inputref blur
event . preventDefault ( ) ;
handleDeleteLastImage ( ) ;
const handleDeleteLastImage = useCallback ( ( ) = > {
if ( message . imgs && message . imgs ? . length > 0 ) {
const newImgList = [ . . . ( message . imgs || [ ] ) ] ;
const lastImage = newImgList . pop ( ) ;
if ( lastImage ) {
handleDeleteImg ( lastImage . uid ) ;
}
}
} ,
[ message , handleDeleteLastImage ]
) ;
} , [ message . imgs , handleDeleteImg ] ) ;
const handleSelectPrompt = ( list : CurrentMessage [ ] ) = > {
presetPrompt ? . ( list ) ;
} ;
const handleKeyDown = useCallback (
( event : any ) = > {
if (
event . key === 'Backspace' &&
message . content === '' &&
message . imgs &&
message . imgs ? . length > 0
) {
// inputref blur
event . preventDefault ( ) ;
handleDeleteLastImage ( ) ;
}
} ,
[ message , handleDeleteLastImage ]
) ;
useHotkeys (
HotKeys . SUBMIT ,
( e : any ) = > {
console . log ( 'submit message' , loading ) ;
e . preventDefault ( ) ;
handleSendMessage ( ) ;
} ,
{
enabled : ! loading && ! isDisabled ,
enableOnFormTags : focused ,
preventDefault : true
}
) ;
useHotkeys (
HotKeys . ADD ,
( e : any ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
handleAddMessage ( ) ;
} ,
{
enabled : ! loading ,
enableOnFormTags : focused ,
preventDefault : true
}
) ;
const handleSelectPrompt = ( list : CurrentMessage [ ] ) = > {
presetPrompt ? . ( list ) ;
} ;
useHotkeys (
HotKeys . FOCUS ,
( ) = > {
inputRef . current ? . focus ? . ( {
cursor : 'end'
} ) ;
} ,
{ preventDefault : true }
) ;
useImperativeHandle ( ref , ( ) = > ( {
handleInputChange : handleInputChange
} ) ) ;
useHotkeys (
HotKeys . SUBMIT ,
( e : any ) = > {
console . log ( 'submit message' , loading ) ;
e . preventDefault ( ) ;
handleSendMessage ( ) ;
} ,
{
enabled : ! loading && ! isDisabled ,
enableOnFormTags : focused ,
preventDefault : true
}
) ;
useHotkeys (
HotKeys . ADD ,
( e : any ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
handleAddMessage ( ) ;
} ,
{
enabled : ! loading ,
enableOnFormTags : focused ,
preventDefault : true
}
) ;
useHotkeys (
HotKeys . FOCUS ,
( ) = > {
inputRef . current ? . focus ? . ( {
cursor : 'end'
} ) ;
} ,
{ preventDefault : true }
) ;
return (
< div className = "messageInput" style = { { . . . style } } >
< div className = "tool-bar" >
< div className = "actions" >
{
< >
{ actions . includes ( 'role' ) && (
< >
< Button
type = "text"
return (
< div className = "messageInput" style = { { . . . style } } >
< div className = "tool-bar" >
< div className = "actions" >
{
< >
{ actions . includes ( 'role' ) && (
< >
< Button
type = "text"
size = "middle"
onClick = { handleToggleRole }
icon = { < SwapOutlined rotate = { 90 } / > }
>
{ intl . formatMessage ( { id : ` playground. ${ message . role } ` } ) }
< / Button >
< Divider type = "vertical" style = { { margin : 0 } } / >
< / >
) }
{ actions . includes ( 'upload' ) && message . role === Roles . User && (
< UploadImg
handleUpdateImgList = { handleUpdateImgList }
size = "middle"
onClick = { handleToggleRole }
icon = { < SwapOutlined rotate = { 90 } / > }
>
{ intl . formatMessage ( { id : ` playground. ${ message . role } ` } ) }
< / Button >
< Divider type = "vertical" style = { { margin : 0 } } / >
< / >
) }
{ actions . includes ( 'upload' ) && message . role === Roles . User && (
< UploadImg
handleUpdateImgList = { handleUpdateImgList }
> < / UploadImg >
) }
< / >
}
{ tools }
{ actions . includes ( 'check' ) && (
< Checkbox onChange = { onCheck } defaultChecked = { true } >
{ checkLabel }
< / Checkbox >
) }
{ actions . includes ( 'clear' ) && (
< Tooltip
title = { intl . formatMessage ( { id : 'common.button.clear' } ) }
>
< Button
type = "text"
icon = { < ClearOutlined / > }
size = "middle"
> < / UploadImg >
) }
< / >
}
{ tools }
{ actions . includes ( 'check' ) && (
< Checkbox onChange = { onCheck } defaultChecked = { true } >
{ checkLabel }
< / Checkbox >
) }
{ actions . includes ( 'clear' ) && (
< Tooltip title = { intl . formatMessage ( { id : 'common.button.clear' } ) } >
< Button
type = "text"
icon = { < ClearOutlined / > }
size = "middle"
onClick = { handleClearAll }
> < / Button >
< / Tooltip >
) }
onClick = { handleClearAll }
> < / Button >
< / Tooltip >
) }
{ actions . includes ( 'layout' ) && updateLayout && (
< >
< Divider type = "vertical" style = { { margin : 0 } } / >
{ layoutOptions . map ( ( option ) = > (
< Tooltip
title = { intl . formatMessage ( { id : option.tips } ) }
key = { option . icon }
>
< Button
{ actions . includes ( 'layout' ) && updateLayout && (
< >
< Divider type = "vertical" style = { { margin : 0 } } / >
{ layoutOptions . map ( ( option ) = > (
< Tooltip
title = { intl . formatMessage ( { id : option.tips } ) }
key = { option . icon }
type = "text"
icon = { < IconFont type = { option . icon } > < / IconFont > }
size = "middle"
onClick = { ( ) = > handleLayoutChange ( option . value ) }
> < / Button >
< / Tooltip >
) ) }
< / >
) }
< / div >
< div className = "actions" >
{ actions . includes ( 'add' ) && (
< Tooltip
title = {
< span >
[ { KeyMap . ADD . textKeybinding } ] { ' ' }
>
< Button
key = { option . icon }
type = "text"
icon = { < IconFont type = { option . icon } > < / IconFont > }
size = "middle"
onClick = { ( ) = > handleLayoutChange ( option . value ) }
> < / Button >
< / Tooltip >
) ) }
< / >
) }
< / div >
< div className = "actions" >
{ actions . includes ( 'add' ) && (
< Tooltip
title = {
< span >
[ { KeyMap . ADD . textKeybinding } ] { ' ' }
{ intl . formatMessage ( { id : 'common.button.add' } ) }
< / span >
}
>
< Button type = "default" size = "middle" onClick = { handleAddMessage } >
{ intl . formatMessage ( { id : 'common.button.add' } ) }
< / span >
}
>
< Button type = "default" size = "middle" onClick = { handleAddMessage } >
{ intl . formatMessage ( { id : 'common.button.add' } ) }
< / Button >
< / Tooltip >
) }
{ ! loading ? (
< Tooltip
title = {
< span >
[ { KeyMap . SUBMIT . textKeybinding } ] { ' ' }
{ intl . formatMessage ( { id : 'common.button.submit' } ) }
< / span >
}
>
< / Button >
< / Tooltip >
) }
{ ! loading ? (
< Tooltip
title = {
< span >
[ { KeyMap . SUBMIT . textKeybinding } ] { ' ' }
{ intl . formatMessage ( { id : 'common.button.submit' } ) }
< / span >
}
>
< Button
style = { { width : 46 } }
type = "primary"
onClick = { handleSendMessage }
size = "middle"
disabled = { isDisabled }
>
{ submitIcon ? ? (
< SendOutlined rotate = { 0 } className = "font-size-14" / >
) }
< / Button >
< / Tooltip >
) : (
< Button
style = { { width : 46 } }
type = "primary"
onClick = { handleSendMessage }
onClick = { onStop }
size = "middle"
disabled = { isDisabled }
>
{ submitIcon ? ? (
< SendOutlined rotate = { 0 } className = "font-size-14" / >
) }
< / Button >
< / Tooltip >
icon = {
< IconFont
type = "icon-stop1"
className = "font-size-14"
> < / IconFont >
}
> < / Button >
) }
< / div >
< / div >
< ThumbImg
dataList = { message . imgs || [ ] }
onDelete = { handleDeleteImg }
editable = { true }
> < / ThumbImg >
< div className = "input-box" >
{ actions . includes ( 'paste' ) ? (
< TextArea
ref = { inputRef }
autoSize = { { minRows : 3 , maxRows : 8 } }
onChange = { handleInputChange }
value = { message . content }
size = "large"
variant = "borderless"
onFocus = { ( ) = > setFocused ( true ) }
onBlur = { ( ) = > setFocused ( false ) }
onPaste = { handleOnPaste }
> < / TextArea >
) : (
< Button
style = { { width : 46 } }
type = "primary"
onClick = { onStop }
size = "middle"
icon = {
< IconFont type = "icon-stop1" className = "font-size-14" > < / IconFont >
}
> < / Button >
< TextArea
ref = { inputRef }
autoSize = { {
minRows : defaultSize.minRows ,
maxRows : defaultSize.maxRows
} }
onChange = { handleInputChange }
value = { message . content }
size = "large"
variant = "borderless"
onFocus = { ( ) = > setFocused ( true ) }
onBlur = { ( ) = > setFocused ( false ) }
> < / TextArea >
) }
{ ! message . content && ! focused && (
< span
className = "holder"
dangerouslySetInnerHTML = { {
__html :
placeholer ? ?
intl . formatMessage ( { id : 'playground.input.holder' } )
} }
> < / span >
) }
< / div >
< PromptModal
open = { open }
onCancel = { ( ) = > setOpen ( false ) }
onSelect = { handleSelectPrompt }
> < / PromptModal >
< / div >
< ThumbImg
dataList = { message . imgs || [ ] }
onDelete = { handleDeleteImg }
editable = { true }
> < / ThumbImg >
< div className = "input-box" >
{ actions . includes ( 'paste' ) ? (
< TextArea
ref = { inputRef }
autoSize = { { minRows : 3 , maxRows : 8 } }
onChange = { handleInputChange }
value = { message . content }
size = "large"
variant = "borderless"
onFocus = { ( ) = > setFocused ( true ) }
onBlur = { ( ) = > setFocused ( false ) }
onPaste = { handleOnPaste }
> < / TextArea >
) : (
< TextArea
ref = { inputRef }
autoSize = { {
minRows : defaultSize.minRows ,
maxRows : defaultSize.maxRows
} }
onChange = { handleInputChange }
value = { message . content }
size = "large"
variant = "borderless"
onFocus = { ( ) = > setFocused ( true ) }
onBlur = { ( ) = > setFocused ( false ) }
> < / TextArea >
) }
{ ! message . content && ! focused && (
< span
className = "holder"
dangerouslySetInnerHTML = { {
__html :
placeholer ? ?
intl . formatMessage ( { id : 'playground.input.holder' } )
} }
> < / span >
) }
< / div >
< PromptModal
open = { open }
onCancel = { ( ) = > setOpen ( false ) }
onSelect = { handleSelectPrompt }
> < / PromptModal >
< / div >
) ;
} ;
) ;
}
) ;
export default MessageInput ;