2023-03-06 13:30:58 +00:00
import styled from '@emotion/styled' ;
2023-03-09 09:38:59 +00:00
import { Button , CopyButton , Loader , Textarea } from '@mantine/core' ;
2023-03-06 13:30:58 +00:00
import { Message } from "../types" ;
import { share } from '../utils' ;
2023-03-10 22:00:37 +00:00
import { ElevenLabsReaderButton } from '../tts/elevenlabs' ;
2023-03-08 21:30:11 +00:00
import { Markdown } from './markdown' ;
2023-03-09 09:38:59 +00:00
import { useAppContext } from '../context' ;
2023-03-14 11:00:40 +00:00
import { useCallback , useMemo , useState } from 'react' ;
import { FormattedMessage , useIntl } from 'react-intl' ;
2023-03-06 13:30:58 +00:00
// hide for everyone but screen readers
const SROnly = styled . span `
position : fixed ;
left : - 9999 px ;
top : - 9999 px ;
` ;
const Container = styled . div `
& . by - user {
2023-03-14 11:00:40 +00:00
background : # 22232 b ;
2023-03-06 13:30:58 +00:00
}
& . by - assistant {
2023-03-14 11:00:40 +00:00
background : # 292933 ;
2023-03-06 13:30:58 +00:00
}
& . by - assistant + & . by - assistant , & . by - user + & . by - user {
border - top : 0.2rem dotted rgba ( 0 , 0 , 0 , 0.1 ) ;
}
2023-03-14 11:00:40 +00:00
& . by - assistant {
border - bottom : 0.2rem solid rgba ( 0 , 0 , 0 , 0.1 ) ;
}
2023-03-06 13:30:58 +00:00
position : relative ;
padding : 1.618rem ;
@media ( max - width : 40em ) {
padding : 1rem ;
}
. inner {
margin : auto ;
}
. content {
font - family : "Open Sans" , sans - serif ;
margin - top : 0rem ;
max - width : 100 % ;
* {
color : white ;
}
p , ol , ul , li , h1 , h2 , h3 , h4 , h5 , h6 , img , blockquote , & > pre {
max - width : 50rem ;
margin - left : auto ;
margin - right : auto ;
}
img {
display : block ;
max - width : 50rem ;
@media ( max - width : 50rem ) {
max - width : 100 % ;
}
}
ol {
counter - reset : list - item ;
li {
counter - increment : list - item ;
}
}
em , i {
font - style : italic ;
}
code {
& , * {
font - family : "Fira Code" , monospace ! important ;
}
vertical - align : bottom ;
}
/* Tables */
table {
margin - top : 1.618rem ;
border - spacing : 0px ;
border - collapse : collapse ;
border : thin solid rgba ( 255 , 255 , 255 , 0.1 ) ;
width : 100 % ;
max - width : 55rem ;
margin - left : auto ;
margin - right : auto ;
}
td + td , th + th {
border - left : thin solid rgba ( 255 , 255 , 255 , 0.1 ) ;
}
tr {
border - top : thin solid rgba ( 255 , 255 , 255 , 0.1 ) ;
}
table td ,
table th {
padding : 0.618rem 1 rem ;
}
th {
font - weight : 600 ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
}
}
. metadata {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
font - family : "Work Sans" , sans - serif ;
font - size : 0.8rem ;
font - weight : 400 ;
opacity : 0.6 ;
max - width : 50rem ;
margin - bottom : 0.0rem ;
margin - right : - 0.5 rem ;
margin - left : auto ;
margin - right : auto ;
span + span {
margin - left : 1em ;
}
. fa {
font - size : 85 % ;
}
. fa + span {
margin - left : 0.2em ;
}
. mantine - Button - root {
color : # ccc ;
font - size : 0.8rem ;
font - weight : 400 ;
. mantine - Button - label {
display : flex ;
align - items : center ;
}
}
}
. fa {
margin - right : 0.5em ;
font - size : 85 % ;
}
. buttons {
text - align : right ;
}
strong {
font - weight : bold ;
}
` ;
const EndOfChatMarker = styled . div `
position : absolute ;
bottom : calc ( - 1.618 rem - 0.5 rem ) ;
left : 50 % ;
width : 0.5rem ;
height : 0.5rem ;
margin - left : - 0.25 rem ;
border - radius : 50 % ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
` ;
2023-03-09 09:38:59 +00:00
const Editor = styled . div `
max - width : 50rem ;
margin - left : auto ;
margin - right : auto ;
margin - top : 0.5rem ;
. mantine - Button - root {
margin - top : 1rem ;
}
` ;
2023-03-06 13:30:58 +00:00
function InlineLoader() {
return (
< Loader variant = "dots" size = "xs" style = { {
marginLeft : '1rem' ,
position : 'relative' ,
top : '-0.2rem' ,
} } / >
) ;
}
export default function MessageComponent ( props : { message : Message , last : boolean , share? : boolean } ) {
2023-03-09 09:38:59 +00:00
const context = useAppContext ( ) ;
const [ editing , setEditing ] = useState ( false ) ;
const [ content , setContent ] = useState ( '' ) ;
2023-03-14 11:00:40 +00:00
const intl = useIntl ( ) ;
const getRoleName = useCallback ( ( role : string , share = false ) = > {
switch ( role ) {
case 'user' :
if ( share ) {
2023-03-16 20:05:45 +00:00
return intl . formatMessage ( { id : 'role-user-formal' , defaultMessage : 'User' , description : "Label that is shown above messages written by the user (as opposed to the AI) for publicly shared conversation (third person, formal)." } ) ;
2023-03-14 11:00:40 +00:00
} else {
2023-03-16 20:05:45 +00:00
return intl . formatMessage ( { id : 'role-user' , defaultMessage : 'You' , description : "Label that is shown above messages written by the user (as opposed to the AI) in the user's own chat sessions (first person)." } ) ;
2023-03-14 11:00:40 +00:00
}
break ;
case 'assistant' :
2023-03-16 20:05:45 +00:00
return intl . formatMessage ( { id : 'role-chatgpt' , defaultMessage : 'ChatGPT' , description : "Label that is shown above messages written by the AI (as opposed to the user)" } ) ;
2023-03-14 11:00:40 +00:00
case 'system' :
2023-03-16 20:05:45 +00:00
return intl . formatMessage ( { id : 'role-system' , defaultMessage : 'System' , description : "Label that is shown above messages inserted into the conversation automatically by the system (as opposed to either the user or AI)" } ) ;
2023-03-14 11:00:40 +00:00
default :
return role ;
}
} , [ intl ] ) ;
2023-03-09 09:38:59 +00:00
2023-03-10 22:00:37 +00:00
const elem = useMemo ( ( ) = > {
if ( props . message . role === 'system' ) {
return null ;
}
2023-03-06 13:30:58 +00:00
2023-03-10 22:00:37 +00:00
return (
< Container className = { "message by-" + props . message . role } >
< div className = "inner" >
< div className = "metadata" >
< span >
< strong >
2023-03-15 22:18:53 +00:00
{ getRoleName ( props . message . role , props . share ) } { props . message . model === 'gpt-4' && ' (GPT 4)' } < SROnly > : < / SROnly >
2023-03-10 22:00:37 +00:00
< / strong >
{ props . message . role === 'assistant' && props . last && ! props . message . done && < InlineLoader / > }
< / span >
{ props . message . done && < ElevenLabsReaderButton selector = { '.content-' + props . message . id } / > }
< div style = { { flexGrow : 1 } } / >
< CopyButton value = { props . message . content } >
{ ( { copy , copied } ) = > (
< Button variant = "subtle" size = "sm" compact onClick = { copy } style = { { marginLeft : '1rem' } } >
< i className = "fa fa-clipboard" / >
2023-03-16 20:05:45 +00:00
{ copied ? < FormattedMessage defaultMessage = "Copied" description = "Label for copy-to-clipboard button after a successful copy" / >
: < FormattedMessage defaultMessage = "Copy" description = "Label for copy-to-clipboard button" / > }
2023-03-10 22:00:37 +00:00
< / Button >
) }
< / CopyButton >
{ typeof navigator . share !== 'undefined' && (
< Button variant = "subtle" size = "sm" compact onClick = { ( ) = > share ( props . message . content ) } >
< i className = "fa fa-share" / >
2023-03-14 11:00:40 +00:00
< span >
2023-03-16 20:05:45 +00:00
< FormattedMessage defaultMessage = "Share" description = "Label for a button which shares the text of a chat message using the user device's share functionality" / >
2023-03-14 11:00:40 +00:00
< / span >
2023-03-10 22:00:37 +00:00
< / Button >
) }
{ ! context . isShare && props . message . role === 'user' && (
< Button variant = "subtle" size = "sm" compact onClick = { ( ) = > {
setContent ( props . message . content ) ;
2023-03-14 11:00:40 +00:00
setEditing ( v = > ! v ) ;
2023-03-10 22:00:37 +00:00
} } >
< i className = "fa fa-edit" / >
2023-03-16 20:05:45 +00:00
< span >
{ editing ? < FormattedMessage defaultMessage = "Cancel" description = "Label for a button that appears when the user is editing the text of one of their messages, to cancel without saving changes" / >
: < FormattedMessage defaultMessage = "Edit" description = "Label for the button the user can click to edit the text of one of their messages" / > }
< / span >
2023-03-10 22:00:37 +00:00
< / Button >
) }
{ ! context . isShare && props . message . role === 'assistant' && (
< Button variant = "subtle" size = "sm" compact onClick = { ( ) = > context . regenerateMessage ( props . message ) } >
< i className = "fa fa-refresh" / >
2023-03-14 11:00:40 +00:00
< span >
2023-03-16 20:05:45 +00:00
< FormattedMessage defaultMessage = "Regenerate" description = "Label for the button used to ask the AI to regenerate one of its messages. Since message generations are stochastic, the resulting message will be different." / >
2023-03-14 11:00:40 +00:00
< / span >
2023-03-10 22:00:37 +00:00
< / Button >
) }
< / div >
{ ! editing && < Markdown content = { props . message . content } className = { "content content-" + props . message . id } / > }
{ editing && ( < Editor >
< Textarea value = { content }
onChange = { e = > setContent ( e . currentTarget . value ) }
autosize = { true } / >
2023-03-14 11:00:40 +00:00
< Button variant = "light" onClick = { ( ) = > context . editMessage ( props . message , content ) } >
2023-03-16 20:05:45 +00:00
< FormattedMessage defaultMessage = "Save changes" description = "Label for a button that appears when the user is editing the text of one of their messages, to save the changes" / >
2023-03-14 11:00:40 +00:00
< / Button >
< Button variant = "subtle" onClick = { ( ) = > setEditing ( false ) } >
2023-03-16 20:05:45 +00:00
< FormattedMessage defaultMessage = "Cancel" description = "Label for a button that appears when the user is editing the text of one of their messages, to cancel without saving changes" / >
2023-03-14 11:00:40 +00:00
< / Button >
2023-03-10 22:00:37 +00:00
< / Editor > ) }
< / div >
{ props . last && < EndOfChatMarker / > }
< / Container >
)
2023-03-14 11:00:40 +00:00
} , [ props . last , props . share , editing , content , context , props . message , props . message . content ] ) ;
2023-03-10 22:00:37 +00:00
return elem ;
2023-03-06 13:30:58 +00:00
}