Skip to content

Commit

Permalink
feat: basic implementation of StarSearch (#3162)
Browse files Browse the repository at this point in the history
  • Loading branch information
zeucapua committed Apr 11, 2024
1 parent 788a4b1 commit 0086134
Showing 1 changed file with 132 additions and 23 deletions.
155 changes: 132 additions & 23 deletions pages/starsearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { GetServerSidePropsContext } from "next";
import { createPagesServerClient } from "@supabase/auth-helpers-nextjs";
import { MdOutlineSubdirectoryArrowRight } from "react-icons/md";
import { useState } from "react";
import Image from "next/image";
import { getAllFeatureFlags } from "lib/utils/server/feature-flags";
import Card from "components/atoms/Card/card";
import ProfileLayout from "layouts/profile";
Expand All @@ -24,28 +25,107 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return { notFound: true };
}

return { props: { userId } };
return { props: { userId, bearerToken: session?.access_token } };
}

type StarSearchPageProps = {
userId: number;
bearerToken: string;
};

export default function StarSearchPage({ userId }: StarSearchPageProps) {
type StarSearchChat = {
author: "You" | "StarSearch";
content: string;
};

export default function StarSearchPage({ userId, bearerToken }: StarSearchPageProps) {
const [starSearchState, setStarSearchState] = useState<"initial" | "chat">("initial");
const [chat, setChat] = useState<StarSearchChat[]>([]);
const [isRunning, setIsRunning] = useState(false);

const submitPrompt = async (prompt: string) => {
if (isRunning) {
return;
}
if (starSearchState === "initial") {
setStarSearchState("chat");
}
setIsRunning(true); // disables input

// add user prompt to history
setChat((history) => {
const temp = history;
temp.push({ author: "You", content: prompt });
return temp;
});

// get ReadableStream from API
const baseUrl = new URL(process.env.NEXT_PUBLIC_API_URL!);
const response = await fetch(`${baseUrl}/star-search/stream`, {
method: "POST",
body: JSON.stringify({
query_text: prompt,
}),
headers: {
Accept: "*/*",
"Content-Type": "application/json",
Authorization: `Bearer ${bearerToken}`,
},
});

if (response.status !== 200) {
setChat((history) => {
const temp = history;
temp.push({ author: "StarSearch", content: "There's been an error. Try again." });
return temp;
});
setIsRunning(false); // enables input
return;
}

setChat((history) => {
const temp = history;
temp.push({ author: "StarSearch", content: "" });
return temp;
});

const decoder = new TextDecoderStream();
const reader = response.body?.pipeThrough(decoder).getReader();
while (true) {
const { done, value } = await reader!.read();
if (done) {
setIsRunning(false); // enables input
return;
}

const values = value.split("\n");
values
.filter((v) => v.startsWith("data:"))
.forEach((v) => {
let { data } = v.match(/data:(?<data>.*)/ms)?.groups || { data: "" };
const result = /(\s{1}[!"#$%&'()*+,\-./:;<=>?@[\]^_`{|}~])(\w*)/g.test(data) ? data.trimStart() : data;
const temp = [...chat];
const changed = temp.at(temp.length - 1);
changed!.content += result;
setChat(temp);
});
}
};

const renderState = () => {
switch (starSearchState) {
case "initial":
return <Header />;
case "chat":
return <ChatHistory userId={userId} />;
return <ChatHistory userId={userId} chat={chat} />;
}
};

return (
<ProfileLayout>
<div className="relative -mt-1.5 flex flex-col p-4 lg:p-8 justify-between items-center w-full h-full grow bg-slate-50">
{renderState()}
<StarSearchInput onSubmit={() => setStarSearchState(starSearchState === "initial" ? "chat" : "initial")} />
<StarSearchInput isRunning={isRunning} onSubmitPrompt={submitPrompt} />
<div className="absolute inset-x-0 top-0 z-0 h-[125px] w-full translate-y-[-100%] lg:translate-y-[-50%] rounded-full bg-gradient-to-r from-light-red-10 via-sauced-orange to-amber-400 opacity-40 blur-[40px]"></div>
</div>
</ProfileLayout>
Expand All @@ -56,7 +136,7 @@ function Header() {
return (
<section className="flex flex-col text-center items-center gap-4 lg:pt-24">
<div className="flex gap-4 items-center">
<img src="/assets/star-search-logo.svg" alt="Star Search Logo" className="w-10 h-10 mb-1" />
<Image src="/assets/star-search-logo.svg" alt="Star Search Logo" width={40} height={40} />
<h1 className="text-3xl lg:text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-sauced-orange to-amber-400">
StarSearch
</h1>
Expand Down Expand Up @@ -100,27 +180,39 @@ function SuggestionBoxes() {
);
}

function ChatHistory({ userId }: { userId: number }) {
function ChatHistory({ userId, chat }: { userId: number; chat: StarSearchChat[] }) {
return (
<ScrollArea className="grow items-center w-full p-4 lg:p-8">
<Chatbox author="You" userId={userId} content="Who are the biggest contributors for the open-sauced/app repo?" />
<Chatbox
author="StarSearch"
content="The top 3 biggest contributors for open-sauced/app nickytonline, brandonroberts, and jpmcb."
/>
<ScrollArea className="grow items-center w-full p-4 lg:p-8 flex flex-col">
{chat.map((message, i) => (
<Chatbox key={i} userId={userId} author={message.author} content={message.content} />
))}
</ScrollArea>
);
}

function Chatbox({ author, content, userId }: { author: "You" | "StarSearch"; content: string; userId?: number }) {
function Chatbox({ author, content, userId }: StarSearchChat & { userId?: number }) {
const renderAvatar = () => {
switch (author) {
case "You":
return <img src={getAvatarById(`${userId}`)} className="w-8 h-8 lg:w-10 lg:h-10 rounded-full" />;
return (
<Image
src={getAvatarById(`${userId}`)}
alt="Your profile picture"
width={32}
height={32}
className="w-8 h-8 lg:w-10 lg:h-10 rounded-full"
/>
);
case "StarSearch":
return (
<div className="bg-gradient-to-br from-sauced-orange to-amber-400 px-3 py-1 lg:p-2 rounded-full">
<img src="/assets/star-search-logo-white.svg" className="w-6 h-6" />
<div className="bg-gradient-to-br from-sauced-orange to-amber-400 px-1.5 py-1 lg:p-2 rounded-full">
<Image
src="/assets/star-search-logo-white.svg"
alt="StarSearch logo"
width={24}
height={24}
className="w-6 h-6"
/>
</div>
);
}
Expand All @@ -129,28 +221,45 @@ function Chatbox({ author, content, userId }: { author: "You" | "StarSearch"; co
return (
<li className="flex gap-2 items-start my-4">
{renderAvatar()}
<Card className="flex flex-col grow bg-white z-10 p-2 lg:p-4">
<Card className="flex flex-col grow bg-white z-10 p-2 lg:p-4 w-full max-w-xl lg:max-w-5xl">
<h3 className="font-semibold text-sauced-orange">{author}</h3>
<p>{content}</p>
</Card>
</li>
);
}

function StarSearchInput({ onSubmit }: { onSubmit: () => void }) {
function StarSearchInput({
isRunning,
onSubmitPrompt,
}: {
isRunning: boolean;
onSubmitPrompt: (prompt: string) => void;
}) {
return (
<section className="w-full h-fit max-w-4xl px-1 py-[3px] rounded-xl bg-gradient-to-r from-sauced-orange via-amber-400 to-sauced-orange">
<div className="w-full h-fit flex justify-between rounded-lg">
<form
onSubmit={(event) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
onSubmitPrompt(formData.get("prompt") as string);
form.reset();
}}
className="w-full bg-white h-fit flex justify-between rounded-lg"
>
<input
required
type="text"
name="prompt"
disabled={isRunning}
placeholder="Ask a question"
className="p-4 border focus:outline-none grow rounded-l-lg border-none"
required
className="p-4 border bg-white focus:outline-none grow rounded-l-lg border-none"
/>
<button className="bg-white p-2 rounded-r-lg" onClick={onSubmit}>
<button type="submit" disabled={isRunning} className="bg-white p-2 rounded-r-lg">
<MdOutlineSubdirectoryArrowRight className="rounded-lg w-10 h-10 p-2 bg-light-orange-3 text-light-orange-10" />
</button>
</div>
</form>
</section>
);
}

0 comments on commit 0086134

Please sign in to comment.