chore: reorganize client files for docker

This commit is contained in:
Danny Avila 2023-03-06 10:47:06 -05:00
parent affbaaf1a5
commit f5e079742a
93 changed files with 178726 additions and 1 deletions

View file

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import Clipboard from '../svg/Clipboard';
import CheckMark from '../svg/CheckMark';
export default function Embed({ children, language = '', code, matched }) {
const [buttonText, setButtonText] = useState('Copy code');
const isClicked = buttonText === 'Copy code';
const clickHandler = () => {
navigator.clipboard.writeText(code.trim());
setButtonText('Copied!');
setTimeout(() => {
setButtonText('Copy code');
}, 3000);
};
return (
<pre>
<div className="mb-4 rounded-md bg-black">
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-800 px-4 py-2 font-sans text-xs text-gray-200">
<span className="">{language === 'javascript' && !matched ? '' : language}</span>
<button
className="ml-auto flex gap-2"
onClick={clickHandler}
disabled={!isClicked}
>
{isClicked ? <Clipboard /> : <CheckMark />}
{buttonText}
</button>
</div>
<div className="overflow-y-auto p-4">{children}</div>
</div>
</pre>
);
}

View file

@ -0,0 +1,16 @@
import React, { useState, useEffect } from 'react';
import hljs from 'highlight.js';
export default function Highlight({language, code}) {
const [highlightedCode, setHighlightedCode] = useState(code);
useEffect(() => {
setHighlightedCode(hljs.highlight(code, { language }).value);
}, [code, language]);
return (
<pre>
<code className={`language-${language}`} dangerouslySetInnerHTML={{__html: highlightedCode}}/>
</pre>
);
}

View file

@ -0,0 +1,19 @@
import React from 'react';
// import Clipboard from '../svg/Clipboard';
import EditIcon from '../svg/EditIcon';
export default function HoverButtons({ user }) {
return (
<div className="visible mt-2 flex justify-center gap-3 self-end text-gray-400 md:gap-4 lg:absolute lg:top-0 lg:right-0 lg:mt-0 lg:translate-x-full lg:gap-1 lg:self-center lg:pl-2">
{user && (
<button className="rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400">
<EditIcon />
</button>
)}
{/* <button className="rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400">
<Clipboard />
</button> */}
</div>
);
}

View file

@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
import TextWrapper from './TextWrapper';
import { useSelector } from 'react-redux';
import GPTIcon from '../svg/GPTIcon';
import BingIcon from '../svg/BingIcon';
import HoverButtons from './HoverButtons';
export default function Message({
sender,
text,
last = false,
error = false,
scrollToBottom
}) {
const { isSubmitting } = useSelector((state) => state.submit);
const [abortScroll, setAbort] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const notUser = sender.toLowerCase() !== 'user';
const blinker = isSubmitting && last && notUser;
useEffect(() => {
if (blinker && !abortScroll) {
scrollToBottom();
}
}, [isSubmitting, text, blinker, scrollToBottom, abortScroll]);
const handleWheel = () => {
if (blinker) {
setAbort(true);
} else {
setAbort(false);
}
};
const handleMouseOver = () => {
setIsHovering(true);
};
const handleMouseOut = () => {
setIsHovering(false);
};
const props = {
className:
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
};
const bgColors = {
chatgpt: 'rgb(16, 163, 127)',
chatgptBrowser: 'rgb(25, 207, 207)',
bingai: ''
};
let icon = `${sender}:`;
let backgroundColor = bgColors[sender];
if (notUser) {
props.className =
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
}
if (notUser && backgroundColor || sender === 'bingai') {
icon = (
<div
style={{ backgroundColor }}
className="relative flex h-[30px] w-[30px] items-center justify-center rounded-sm p-1 text-white"
>
{sender === 'bingai' ? <BingIcon /> : <GPTIcon />}
</div>
);
}
const wrapText = (text) => <TextWrapper text={text} />;
return (
<div
{...props}
onWheel={handleWheel}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<strong className="relative flex w-[30px] flex-col items-end text-right">
{icon}
</strong>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
<div className="flex flex-grow flex-col gap-3">
{error ? (
<div className="flex flex min-h-[20px] flex-row flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500">
<div className="rounded-md border border-red-500 bg-red-500/10 py-2 px-3 text-sm text-gray-600 dark:text-gray-100">
{text}
</div>
</div>
) : (
<div className="flex min-h-[20px] flex-col items-start gap-4 whitespace-pre-wrap">
{/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */}
<div className="markdown prose dark:prose-invert light w-full break-words">
{notUser ? wrapText(text) : text}
{blinker && <span className="result-streaming"></span>}
</div>
</div>
)}
</div>
<div className="flex justify-between">
{isHovering && <HoverButtons user={!notUser} />}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,32 @@
import React from 'react';
export default function ScrollToBottom({ scrollHandler}) {
return (
<button
onClick={scrollHandler}
className="absolute right-6 bottom-[124px] z-10 cursor-pointer rounded-full border border-gray-200 bg-gray-50 text-gray-600 dark:border-white/10 dark:bg-white/10 dark:text-gray-200 md:bottom-[120px]"
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="m-1 h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
y1="5"
x2="12"
y2="19"
/>
<polyline points="19 12 12 19 5 12" />
</svg>
</button>
);
}

View file

@ -0,0 +1,137 @@
import React from 'react';
import Markdown from 'markdown-to-jsx';
import Embed from './Embed';
import Highlight from './Highlight';
import regexSplit from '~/utils/regexSplit';
import { wrapperRegex } from '~/utils';
const { codeRegex, inLineRegex, markupRegex, languageMatch, newLineMatch } = wrapperRegex;
const mdOptions = { wrapper: React.Fragment, forceWrapper: true };
const inLineWrap = (parts) => {
let previousElement = null;
return parts.map((part, i) => {
if (part.match(markupRegex)) {
const codeElement = <code key={i}>{part.slice(1, -1)}</code>;
if (previousElement && typeof previousElement !== 'string') {
// Append code element as a child to previous non-code element
previousElement = (
<Markdown
options={mdOptions}
key={i}
>
{previousElement}
{codeElement}
</Markdown>
);
return previousElement;
} else {
return codeElement;
}
} else {
previousElement = part;
return previousElement;
}
});
};
export default function TextWrapper({ text }) {
let embedTest = false;
// to match unenclosed code blocks
if (text.match(/```/g)?.length === 1) {
embedTest = true;
}
// match enclosed code blocks
if (text.match(codeRegex)) {
const parts = regexSplit(text);
// console.log(parts);
const codeParts = parts.map((part, i) => {
if (part.match(codeRegex)) {
let language = 'javascript';
let matched = false;
if (part.match(languageMatch)) {
language = part.match(languageMatch)[1].toLowerCase();
part = part.replace(languageMatch, '```');
matched = true;
// highlight.js language validation
// const validLanguage = languages.some((lang) => language === lang);
// part = validLanguage ? part.replace(languageMatch, '```') : part;
// language = validLanguage ? language : 'javascript';
}
part = part.replace(newLineMatch, '```');
return (
<Embed
key={i}
language={language}
code={part.slice(3, -3)}
matched={matched}
>
<Highlight
language={language}
code={part.slice(3, -3)}
/>
</Embed>
);
} else if (part.match(inLineRegex)) {
const innerParts = part.split(inLineRegex);
return inLineWrap(innerParts);
} else {
return (
<Markdown
options={mdOptions}
key={i}
>
{part}
</Markdown>
);
}
});
return <>{codeParts}</>; // return the wrapped text
} else if (embedTest) {
const language = text.match(/```(\w+)/)?.[1].toLowerCase() || 'javascript';
const parts = text.split(text.match(/```(\w+)/)?.[0] || '```');
const codeParts = parts.map((part, i) => {
if (i === 1) {
part = part.replace(/^\n+/, '');
return (
<Embed
key={i}
language={language}
>
<Highlight
code={part}
language={language}
/>
</Embed>
);
} else if (part.match(inLineRegex)) {
const innerParts = part.split(inLineRegex);
return inLineWrap(innerParts);
} else {
return (
<Markdown
options={mdOptions}
key={i}
>
{part}
</Markdown>
);
}
});
return <>{codeParts}</>; // return the wrapped text
} else if (text.match(markupRegex)) {
// map over the parts and wrap any text between tildes with <code> tags
const parts = text.split(markupRegex);
const codeParts = inLineWrap(parts);
return <>{codeParts}</>; // return the wrapped text
} else {
return <Markdown options={mdOptions}>{text}</Markdown>;
}
}

View file

@ -0,0 +1,91 @@
import React, { useEffect, useState, useRef } from 'react';
import { CSSTransition } from 'react-transition-group';
import ScrollToBottom from './ScrollToBottom';
import Message from './Message';
const Messages = ({ messages }) => {
const [showScrollButton, setShowScrollButton] = useState(false);
const scrollableRef = useRef(null);
const messagesEndRef = useRef(null);
useEffect(() => {
const timeoutId = setTimeout(() => {
const scrollable = scrollableRef.current;
const hasScrollbar = scrollable.scrollHeight > scrollable.clientHeight;
setShowScrollButton(hasScrollbar);
}, 650);
return () => {
clearTimeout(timeoutId);
};
}, [messages]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowScrollButton(false);
};
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
const diff = Math.abs(scrollHeight - scrollTop);
const bottom =
diff === clientHeight || (diff <= clientHeight + 25 && diff >= clientHeight - 25);
if (bottom) {
setShowScrollButton(false);
} else {
setShowScrollButton(true);
}
};
let timeoutId = null;
const debouncedHandleScroll = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(handleScroll, 100);
};
const scrollHandler = (e) => {
e.preventDefault();
scrollToBottom();
};
return (
<div
className="flex-1 overflow-y-auto "
ref={scrollableRef}
onScroll={debouncedHandleScroll}
>
{/* <div className="flex-1 overflow-hidden"> */}
<div className="h-full dark:gpt-dark-gray">
<div className="flex h-full flex-col items-center text-sm dark:gpt-dark-gray">
{messages.map((message, i) => (
<Message
key={i}
sender={message.sender}
text={message.text}
last={i === messages.length - 1}
error={message.error ? true : false}
scrollToBottom={i === messages.length - 1 ? scrollToBottom : null}
/>
))}
<CSSTransition
in={showScrollButton}
timeout={400}
classNames="scroll-down"
unmountOnExit={false}
// appear
>
{() => showScrollButton && <ScrollToBottom scrollHandler={scrollHandler} />}
</CSSTransition>
<div
className="group h-32 w-full flex-shrink-0 dark:border-gray-900/50 dark:gpt-dark-gray md:h-48"
ref={messagesEndRef}
/>
</div>
</div>
{/* </div> */}
</div>
);
};
export default Messages;