267 lines
7.6 KiB
TypeScript
267 lines
7.6 KiB
TypeScript
![]() |
import styled from '@emotion/styled';
|
||
|
import ReactMarkdown from 'react-markdown';
|
||
|
import { Button, CopyButton, Loader } from '@mantine/core';
|
||
|
|
||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||
|
import remarkGfm from 'remark-gfm';
|
||
|
import remarkMath from 'remark-math'
|
||
|
import rehypeKatex from 'rehype-katex'
|
||
|
|
||
|
import { Message } from "../types";
|
||
|
import { share } from '../utils';
|
||
|
import { ElevenLabsReaderButton } from '../elevenlabs';
|
||
|
|
||
|
// hide for everyone but screen readers
|
||
|
const SROnly = styled.span`
|
||
|
position: fixed;
|
||
|
left: -9999px;
|
||
|
top: -9999px;
|
||
|
`;
|
||
|
|
||
|
const Container = styled.div`
|
||
|
&.by-user {
|
||
|
}
|
||
|
|
||
|
&.by-assistant {
|
||
|
background: rgba(255, 255, 255, 0.02);
|
||
|
}
|
||
|
|
||
|
&.by-assistant + &.by-assistant, &.by-user + &.by-user {
|
||
|
border-top: 0.2rem dotted rgba(0, 0, 0, 0.1);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
`;
|
||
|
|
||
|
function getRoleName(role: string, share = false) {
|
||
|
switch (role) {
|
||
|
case 'user':
|
||
|
return !share ? 'You' : 'User';
|
||
|
case 'assistant':
|
||
|
return 'ChatGPT';
|
||
|
case 'system':
|
||
|
return 'System';
|
||
|
default:
|
||
|
return role;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 }) {
|
||
|
if (props.message.role === 'system') {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
return <Container className={"message by-" + props.message.role}>
|
||
|
<div className="inner">
|
||
|
<div className="metadata">
|
||
|
<span>
|
||
|
<strong>
|
||
|
{getRoleName(props.message.role, props.share)}<SROnly>:</SROnly>
|
||
|
</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" />
|
||
|
<span>{copied ? 'Copied' : 'Copy'}</span>
|
||
|
</Button>
|
||
|
)}
|
||
|
</CopyButton>
|
||
|
{typeof navigator.share !== 'undefined' && (
|
||
|
<Button variant="subtle" size="sm" compact onClick={() => share(props.message.content)}>
|
||
|
<i className="fa fa-share" />
|
||
|
<span>Share</span>
|
||
|
</Button>
|
||
|
)}
|
||
|
</div>
|
||
|
<div className={"prose dark:prose-invert content content-" + props.message.id}>
|
||
|
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]}
|
||
|
rehypePlugins={[rehypeKatex]}
|
||
|
components={{
|
||
|
code({ node, inline, className, children, ...props }) {
|
||
|
const match = /language-(\w+)/.exec(className || '')
|
||
|
return !inline ? (
|
||
|
<div>
|
||
|
<CopyButton value={String(children)}>
|
||
|
{({ copy, copied }) => (
|
||
|
<Button variant="subtle" size="sm" compact onClick={copy}>
|
||
|
<i className="fa fa-clipboard" />
|
||
|
<span>{copied ? 'Copied' : 'Copy'}</span>
|
||
|
</Button>
|
||
|
)}
|
||
|
</CopyButton>
|
||
|
<SyntaxHighlighter
|
||
|
children={String(children).replace(/\n$/, '')}
|
||
|
style={vscDarkPlus as any}
|
||
|
language={match?.[1] || 'text'}
|
||
|
PreTag="div"
|
||
|
{...props}
|
||
|
/>
|
||
|
</div>
|
||
|
) : (
|
||
|
<code className={className} {...props}>
|
||
|
{children}
|
||
|
</code>
|
||
|
)
|
||
|
}
|
||
|
}}>{props.message.content}</ReactMarkdown>
|
||
|
</div>
|
||
|
</div>
|
||
|
{props.last && <EndOfChatMarker />}
|
||
|
</Container>
|
||
|
}
|