thomfoolery's picture
fullstack solution chatface
ddaa426
import { useEffect, useRef, useState } from "react";
import { FileUpload } from "./FileUpload";
function Message({ message, role }: Message) {
return (
<pre
className={`p-4 mb-4 rounded-3xl shadow-md whitespace-pre-wrap ${
role === "user"
? "bg-blue-200 text-blue-800 ml-20 self-end"
: "bg-green-200 text-green-800 mr-20 self-start"
}`}
>
{message}
</pre>
);
}
interface Message {
message: string;
role: "user" | "assistant";
}
function App() {
const scrollUpRef = useRef(false);
const [result, setResult] = useState("");
const [error, setError] = useState("");
const [isStreaming, setIsStreaming] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [isFileUploaded, setIsFileUploaded] = useState(false);
useEffect(() => {
function handleWheel(event: WheelEvent) {
if (isStreaming && event.deltaY < 0) {
scrollUpRef.current = true;
}
}
document.body.addEventListener("wheel", handleWheel);
return () => {
document.body.removeEventListener("wheel", handleWheel);
};
}, [isStreaming]);
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
e.currentTarget.form?.requestSubmit();
}
};
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
const msg = formData.get("msg") as string;
const scrollingElement = document.scrollingElement;
if (msg.trim() === "") return;
scrollUpRef.current = false;
scrollingElement?.scrollTo({
behavior: "smooth",
top: scrollingElement?.scrollHeight,
});
setIsStreaming(true);
setMessages((prev) => [
...prev,
{
message: msg,
role: "user",
},
]);
try {
const session_id = sessionStorage.getItem("session_id");
const response = await fetch(form.action, {
method: "POST",
headers: session_id
? {
"X-Session-ID": session_id,
}
: undefined,
body: msg,
});
let result = "";
const reader = response.body?.getReader();
const decoder = new TextDecoder();
form.msg.value = "";
await reader?.read().then(function processText(msg): Promise<void> {
const { value, done } = msg;
const chunk = decoder.decode(value, { stream: true });
const scrollingElement = document.scrollingElement;
if (done) {
setResult("");
setMessages((prev) => [
...prev,
{
message: result,
role: "assistant",
},
]);
if (scrollingElement) {
scrollingElement.scrollTop = scrollingElement?.scrollHeight;
}
return Promise.resolve();
}
result += chunk;
setResult(result);
if (!scrollUpRef.current && scrollingElement) {
scrollingElement.scrollTop = scrollingElement.scrollHeight;
}
// Read some more, and call this function again
return reader.read().then(processText);
});
} catch (error) {
setError("Error submitting message");
console.error("Error submitting message", error);
} finally {
setIsStreaming(false);
}
}
return (
<div className="max-w-prose flex flex-col items-center justify-end min-h-screen pb-4 mx-auto">
{!isFileUploaded && (
<div className="w-full min-h-screen flex flex-col items-center justify-center">
<h1 className="text-5xl font-bold mb-2">DocTalk</h1>
<p className="text-xl text-gray-500 mb-10 text-center">
Talk to your documents.
</p>
<FileUpload
isFileUploaded={isFileUploaded}
setIsFileUploaded={setIsFileUploaded}
/>
<div className="text-xs text-center text-gray-500 mt-4">
Built with{" "}
<a
target="_blank"
href="https://openai.com/"
rel="noopener noreferrer"
className="underline text-blue-500"
>
OpenAI
</a>
,{" "}
<a
target="_blank"
href="https://fastapi.tiangolo.com/"
rel="noopener noreferrer"
className="underline text-blue-500"
>
FastAPI
</a>
,{" "}
<a
target="_blank"
href="https://qdrant.tech/"
rel="noopener noreferrer"
className="underline text-blue-500"
>
Qdrant
</a>
,{" "}
<a
target="_blank"
href="https://react.dev/"
rel="noopener noreferrer"
className="underline text-blue-500"
>
React
</a>
,{" "}
<a
target="_blank"
href="https://vitejs.dev/"
rel="noopener noreferrer"
className="underline text-blue-500"
>
Vite
</a>
, and{" "}
<a
target="_blank"
href="https://tailwindcss.com/"
rel="noopener noreferrer"
className="underline text-blue-500"
>
TailwindCSS
</a>
</div>
</div>
)}
<div className="w-full flex flex-col justify-end">
{messages.map(({ message, role }) => (
<Message message={message} role={role} key={`${message}-${role}`} />
))}
{result && <Message message={result} role="assistant" />}
{error && <p className="text-red-500 text-center mb-4">{error}</p>}
</div>
{isFileUploaded && (
<form
method="post"
action="/api/chat"
onSubmit={onSubmit}
className="relative w-full"
>
<textarea
id="msg"
name="msg"
rows={3}
onKeyDown={onKeyDown}
disabled={isStreaming}
placeholder="Ask me anything about the document's contents..."
className="block bg-white w-full p-4 pr-20 m-0 rounded-3xl shadow-md text-xl disabled:opacity-50 disabled:cursor-not-allowed resize-none"
></textarea>
<button
type="submit"
disabled={isStreaming}
aria-label="Send message"
className="absolute right-0 bottom-0 bg-blue-500 text-white p-2 m-2 rounded-full shadow-md hover:bg-blue-600 focus:bg-blue-600 cursor-pointer transition-colors duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-6 h-6"
>
<path d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z" />
</svg>
</button>
</form>
)}
</div>
);
}
export default App;