2023-03-06 05:30:58 -08:00
import styled from '@emotion/styled' ;
2023-03-09 01:38:59 -08:00
import { Button , CopyButton , Loader , Textarea } from '@mantine/core' ;
2023-03-06 05:30:58 -08:00
import { Message } from "../types" ;
import { share } from '../utils' ;
2023-03-10 14:00:37 -08:00
import { ElevenLabsReaderButton } from '../tts/elevenlabs' ;
2023-03-08 13:30:11 -08:00
import { Markdown } from './markdown' ;
2023-03-09 01:38:59 -08: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 05:30:58 -08:00
// hide for everyone but screen readers
const SROnly = styled . span `
position: fixed;
left: -9999px;
top: -9999px;
` ;
const Container = styled . div `
&.by-user {
2023-03-14 11:00:40 +00:00
background: #22232b;
2023-03-06 05:30:58 -08:00
}
&.by-assistant {
2023-03-14 11:00:40 +00:00
background: #292933;
2023-03-06 05:30:58 -08: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 05:30:58 -08: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 1rem;
}
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.5rem;
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.618rem - 0.5rem);
left: 50%;
width: 0.5rem;
height: 0.5rem;
margin-left: -0.25rem;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
` ;
2023-03-09 01:38:59 -08: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 05:30:58 -08: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 01:38:59 -08: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 01:38:59 -08:00
2023-03-10 14:00:37 -08:00
const elem = useMemo (() => {
if ( props . message . role === 'system' ) {
return null ;
}
2023-03-06 05:30:58 -08:00
2023-03-10 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08: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 14:00:37 -08:00
return elem ;
2023-03-06 05:30:58 -08:00
}