v0.1.0
This commit is contained in:
267
src/components/message.tsx
Normal file
267
src/components/message.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
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>
|
||||
}
|
Reference in New Issue
Block a user