Skip to content

Commit

Permalink
Add live streaming of results using SSE & clear/regenerate buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
JinayJain committed Mar 7, 2023
1 parent e6d157f commit 86d0cd9
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 113 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Use GPT anywhere with just one shortcut.
- [ ] System tray icon
- [ ] Settings window
- [ ] Additional configuration parameters on your search (little downward caret on search bar)
- [ ] Stream response to response window
- [ ] Send notification when response is finished
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@fontsource/manrope": "^4.5.13",
"@microsoft/fetch-event-source": "^2.0.1",
"@tauri-apps/api": "^1.2.0",
"axios": "^1.3.4",
"chakra-ui-markdown-renderer": "^4.1.0",
"framer-motion": "^10.0.2",
"openai": "^3.2.1",
Expand Down
34 changes: 18 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,34 @@ import ResponseBox from "./components/ResponseBox";
import useAI from "./hooks/useAI";
import { useState } from "react";
import { fillerMarkdown } from "./util/consts";
import { chatComplete } from "./util/openai";

function App() {
const ai = useAI(import.meta.env.VITE_OPENAI_API_KEY);

const [response, setResponse] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [lastPrompt, setLastPrompt] = useState("");

const handleGenerate = async (prompt: string, temperature = 1.0) => {
setLastPrompt(prompt);

const handleGenerate = async (prompt: string) => {
if (ai && prompt) {
setIsLoading(true);

try {
const response = await ai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: "You are a concise AI assistant.",
},
{
role: "user",
content: prompt,
},
],
setResponse("");

// setResponse(fillerMarkdown);
await chatComplete({
prompt,
onChunk(chunk) {
setResponse((prev) => prev + chunk);
},
apiParams: {
temperature,
},
});

setResponse(response.data.choices[0].message?.content || "");
console.log(response);
} catch (e) {
console.error(e);
}
Expand All @@ -44,6 +44,8 @@ function App() {
<Box display="flex" flexDirection="column" h="100vh">
<Search onGenerate={handleGenerate} isLoading={isLoading} mb={4} />
<ResponseBox
onClear={() => setResponse("")}
onRegenerate={() => handleGenerate(lastPrompt, 1.5)}
maxH="100%"
overflow="auto"
whiteSpace="pre-wrap"
Expand Down
33 changes: 33 additions & 0 deletions src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button, ButtonProps, Icon } from "@chakra-ui/react";
import { useState } from "react";
import { FiCheck, FiClipboard, FiCopy } from "react-icons/fi";

function CopyButton({
onCopy,
iconOnly = false,
...props
}: {
onCopy: () => void;
iconOnly?: boolean;
} & ButtonProps) {
const [isCopied, setIsCopied] = useState(false);

return (
<Button
colorScheme="green"
onClick={() => {
onCopy();
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1000);
}}
size="sm"
variant={isCopied ? "solid" : "outline"}
{...props}
>
<Icon as={isCopied ? FiCheck : FiCopy} mr={iconOnly ? 0 : 2} />
{iconOnly ? "" : isCopied ? "Copied!" : "Copy"}
</Button>
);
}

export default CopyButton;
95 changes: 95 additions & 0 deletions src/components/ResponseBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
Text,
Box,
BoxProps,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Button,
HStack,
Stack,
IconButton,
Icon,
Flex,
} from "@chakra-ui/react";
import { ReactMarkdown } from "react-markdown/lib/react-markdown";
import renderer from "../util/markdown";
import { writeText } from "@tauri-apps/api/clipboard";
import { FiClipboard, FiCopy, FiRefreshCw } from "react-icons/fi";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import CopyButton from "./CopyButton";

const COPY_MSG_TIMEOUT = 1000;

function Debug({ text }: { text: string }) {
return (
<Accordion allowToggle mt={4}>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Debug
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4} whiteSpace="pre-line">
{text}
</AccordionPanel>
</AccordionItem>
</Accordion>
);
}

function ResponseBox({
responseMarkdown,
onClear,
onRegenerate,
...props
}: {
onClear: () => void;
onRegenerate: () => void;
responseMarkdown: string;
} & BoxProps) {
const onCopy = () => {
writeText(responseMarkdown);
};

return (
<AnimatePresence>
{responseMarkdown && (
<Box
as={motion.div}
bg="blackAlpha.800"
borderRadius="md"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
{...props}
>
{/* <Debug text={responseMarkdown} /> */}
<Box p={4}>
<ReactMarkdown children={responseMarkdown} components={renderer} />
<HStack position="sticky" bottom={4} right={4}>
<Button onClick={onClear} size="sm" ml="auto">
Clear
</Button>
<IconButton
aria-label="Regenerate"
icon={<Icon as={FiRefreshCw} />}
size="sm"
onClick={onRegenerate}
/>
<CopyButton onCopy={onCopy} />
</HStack>
</Box>
</Box>
)}
</AnimatePresence>
);
}

export default ResponseBox;
82 changes: 0 additions & 82 deletions src/components/ResponseBox/index.tsx

This file was deleted.

File renamed without changes.
File renamed without changes.
11 changes: 6 additions & 5 deletions src/util/consts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ const fillerMarkdown = `
Heres some code, \`<div></div>\`, between 2 backticks.
\`\`\`js
\`\`\`rust
// this is multi-line code:
function anotherExample(firstLine, lastLine) {
if (firstLine == '\`\`\`' && lastLine == '\`\`\`') {
return multiLineCode;
fn fib(n: u32) -> u32 {
if n <= 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}
\`\`\`
Expand All @@ -37,7 +38,7 @@ And here. | Okay. | I think we get it.
- With different indentation levels.
- That look like this.
![React Logo w/ Text](https://goo.gl/Umyytc)
![React Logo w/ Text](https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png)
`;

Expand Down
32 changes: 23 additions & 9 deletions src/util/markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import ChakraUIRenderer from "chakra-ui-markdown-renderer";
import { Components } from "react-markdown";
import { Code } from "@chakra-ui/react";
import { Box, Code, IconButton } from "@chakra-ui/react";
import { FiCopy } from "react-icons/fi";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark as style } from "react-syntax-highlighter/dist/esm/styles/prism";
import { writeText } from "@tauri-apps/api/clipboard";
import CopyButton from "../components/CopyButton";

const theme: Components = {
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");

return !inline ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
// @ts-ignore
style={style}
language={match ? match[1].toLowerCase() : "text"}
PreTag="div"
{...props}
/>
<Box position="relative">
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
// @ts-ignore
style={style}
language={match ? match[1].toLowerCase() : "text"}
PreTag="div"
{...props}
/>

<Box position="absolute" top={4} right={4} bottom={4}>
<CopyButton
iconOnly
onCopy={() => writeText(String(children))}
position="sticky"
top={4}
/>
</Box>
</Box>
) : (
<Code className={className} p={1} mx="2px" borderRadius="md" {...props}>
{children}
Expand Down
Loading

0 comments on commit 86d0cd9

Please sign in to comment.