Merge branch 'codetorso' of https://github.com/Dhravya/supermemory into codetorso

This commit is contained in:
Kartik 2024-06-15 20:36:41 +05:30
commit 8ecfc654c7
36 changed files with 1266 additions and 317 deletions

View file

@ -88,8 +88,8 @@ app.post(
"query",
z.object({
query: z.string(),
topK: z.number().optional().default(10),
user: z.string(),
topK: z.number().optional().default(10),
spaces: z.string().optional(),
sourcesOnly: z.string().optional().default("false"),
model: z.string().optional().default("gpt-4o"),
@ -100,28 +100,25 @@ app.post(
const query = c.req.valid("query");
const body = c.req.valid("json");
if (body.chatHistory) {
body.chatHistory = body.chatHistory.map((i) => ({
...i,
content: i.parts.length > 0 ? i.parts.join(" ") : i.content,
}));
}
const sourcesOnly = query.sourcesOnly === "true";
const spaces = query.spaces?.split(",") || [undefined];
const spaces = query.spaces?.split(",") ?? [""];
// Get the AI model maker and vector store
const { model, store } = await initQuery(c, query.model);
const filter: VectorizeVectorMetadataFilter = { user: query.user };
console.log("Spaces", spaces);
// Converting the query to a vector so that we can search for similar vectors
const queryAsVector = await store.embeddings.embedQuery(query.query);
const responses: VectorizeMatches = { matches: [], count: 0 };
console.log("hello world", spaces);
// SLICED to 5 to avoid too many queries
for (const space of spaces.slice(0, 5)) {
if (space !== undefined) {
console.log("space", space);
if (space !== "") {
// it's possible for space list to be [undefined] so we only add space filter conditionally
filter.space = space;
}

View file

@ -6,15 +6,12 @@ To generate your answer:
- Carefully analyze the question and identify the key information needed to address it
- Locate the specific parts of each context that contain this key information
- Compare the relevance scores of the provided contexts
- In the <justification> tags, provide a brief justification for which context(s) are more relevant to answering the question based on the scores
- Concisely summarize the relevant information from the higher-scoring context(s) in your own words
- Provide a direct answer to the question
- Use markdown formatting in your answer, including bold, italics, and bullet points as appropriate to improve readability and highlight key points
- Give detailed and accurate responses for things like 'write a blog' or long-form questions.
- The normalisedScore is a value in which the scores are 'balanced' to give a better representation of the relevance of the context, between 1 and 100, out of the top 10 results
Provide your justification between <justification> tags and your final answer between <answer> tags, formatting both in markdown.
- provide your justification in the end, in a <justification> </justification> tag
If no context is provided, introduce yourself and explain that the user can save content which will allow you to answer questions about that content in the future. Do not provide an answer if no context is provided.`;
export const template = ({ contexts, question }) => {

View file

@ -1,3 +1,5 @@
import { z } from "zod";
interface OpenAIEmbeddingsParams {
apiKey: string;
modelName: string;
@ -32,12 +34,22 @@ export class OpenAIEmbeddings {
}),
});
const data = (await response.json()) as {
data: {
embedding: number[];
}[];
};
const data = await response.json();
return data.data[0].embedding;
const zodTypeExpected = z.object({
data: z.array(
z.object({
embedding: z.array(z.number()),
}),
),
});
const json = zodTypeExpected.safeParse(data);
if (!json.success) {
throw new Error("Invalid response from OpenAI: " + json.error.message);
}
return json.data.data[0].embedding;
}
}

View file

@ -1,48 +0,0 @@
"use server";
import { cookies, headers } from "next/headers";
import { db } from "../helpers/server/db";
import { sessions, users, space } from "../helpers/server/db/schema";
import { eq } from "drizzle-orm";
import { redirect } from "next/navigation";
export async function ensureAuth() {
const token =
cookies().get("next-auth.session-token")?.value ??
cookies().get("__Secure-authjs.session-token")?.value ??
cookies().get("authjs.session-token")?.value ??
headers().get("Authorization")?.replace("Bearer ", "");
if (!token) {
return undefined;
}
const sessionData = await db
.select()
.from(sessions)
.innerJoin(users, eq(users.id, sessions.userId))
.where(eq(sessions.sessionToken, token));
if (!sessionData || sessionData.length < 0) {
return undefined;
}
return {
user: sessionData[0]!.user,
session: sessionData[0]!,
};
}
export async function getSpaces() {
const data = await ensureAuth();
if (!data) {
redirect("/signin");
}
const sp = await db
.select()
.from(space)
.where(eq(space.user, data.user.email));
return sp;
}

View file

@ -0,0 +1,90 @@
import React, { useRef, useState } from "react";
const CodeBlock = ({
lang,
codeChildren,
}: {
lang: string;
codeChildren: React.ReactNode & React.ReactNode[];
}) => {
const codeRef = useRef<HTMLElement>(null);
return (
<div className="bg-black rounded-md">
<CodeBar lang={lang} codeRef={codeRef} />
<div className="p-4 overflow-y-auto">
<code ref={codeRef} className={`!whitespace-pre hljs language-${lang}`}>
{codeChildren}
</code>
</div>
</div>
);
};
const CodeBar = React.memo(
({
lang,
codeRef,
}: {
lang: string;
codeRef: React.RefObject<HTMLElement>;
}) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
return (
<div className="flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans">
<span className="">{lang}</span>
<button
className="flex ml-auto gap-2"
aria-label="copy codeblock"
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString)
navigator.clipboard.writeText(codeString).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 3000);
});
}}
>
{isCopied ? (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75"
/>
</svg>
Copied!
</>
) : (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
/>
</svg>
Copy code
</>
)}
</button>
</div>
);
},
);
export default CodeBlock;

View file

@ -1 +0,0 @@
"use server";

View file

@ -6,29 +6,92 @@ import QueryInput from "../home/queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { ChatHistory } from "@repo/shared-types";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@repo/ui/shadcn/accordion";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeHighlight from "rehype-highlight";
import { code, p } from "./markdownRenderHelpers";
import { codeLanguageSubset } from "@/app/helpers/constants";
function ChatWindow({ q }: { q: string }) {
function ChatWindow({
q,
spaces,
}: {
q: string;
spaces: { id: string; name: string }[];
}) {
const [layout, setLayout] = useState<"chat" | "initial">("initial");
const [chatHistory, setChatHistory] = useState<ChatHistory[]>([
{
question: q,
answer: {
parts: [
// {
// text: `It seems like there might be a typo in your question. Could you please clarify or provide more context? If you meant "interesting," please let me know what specific information or topic you find interesting, and I can help you with that.`,
// },
],
sources: [],
},
},
]);
const router = useRouter();
const getAnswer = async (query: string, spaces: string[]) => {
const resp = await fetch(`/api/chat?q=${query}&spaces=${spaces}`, {
method: "POST",
body: JSON.stringify({ chatHistory }),
});
const reader = resp.body?.getReader();
let done = false;
let result = "";
while (!done && reader) {
const { value, done: d } = await reader.read();
done = d;
setChatHistory((prevChatHistory) => {
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) return prevChatHistory;
lastAnswer.answer.parts.push({ text: new TextDecoder().decode(value) });
return newChatHistory;
});
}
console.log(result);
};
useEffect(() => {
if (q !== "") {
if (q.trim().length > 0) {
getAnswer(
q,
spaces.map((s) => s.id),
);
setTimeout(() => {
setLayout("chat");
}, 300);
} else {
router.push("/home");
}
}, [q]);
}, []);
return (
<div>
<div className="h-full">
<AnimatePresence mode="popLayout">
{layout === "initial" ? (
<motion.div
exit={{ opacity: 0 }}
key="initial"
className="max-w-3xl flex mx-auto w-full flex-col"
className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col"
>
<div className="w-full h-96">
<QueryInput initialQuery={q} initialSpaces={[]} disabled />
@ -36,16 +99,111 @@ function ChatWindow({ q }: { q: string }) {
</motion.div>
) : (
<div
className="max-w-3xl flex mx-auto w-full flex-col mt-8"
className="max-w-3xl flex mx-auto w-full flex-col mt-24"
key="chat"
>
<h2
className={cn(
"transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl",
)}
>
{q}
</h2>
{chatHistory.map((chat, idx) => (
<div
key={idx}
className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b" : ""}`}
>
<h2
className={cn(
"text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl",
)}
>
{chat.question}
</h2>
<div className="flex flex-col gap-2 mt-2">
<div
className={`${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
>
<Accordion
defaultValue={
idx === chatHistory.length - 1 ? "memories" : ""
}
type="single"
collapsible
>
<AccordionItem value="memories">
<AccordionTrigger className="text-foreground-menu">
Related Memories
</AccordionTrigger>
{/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
<AccordionContent
className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
defaultChecked
>
{/* Loading state */}
{chat.answer.sources.length > 0 ||
(chat.answer.parts.length === 0 && (
<>
{[1, 2, 3, 4].map((_, idx) => (
<div
key={`loadingState-${idx}`}
className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72 animate-pulse"
>
<div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
<div className="bg-slate-700 h-2 rounded-full w-full"></div>
</div>
))}
</>
))}
{chat.answer.sources.map((source, idx) => (
<div
key={idx}
className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72"
>
<div className="text-foreground-menu">
{source.type}
</div>
<div>{source.title}</div>
</div>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
{/* Summary */}
<div>
<div className="text-foreground-menu py-2">Summary</div>
<div className="text-base">
{chat.answer.parts.length === 0 && (
<div className="animate-pulse flex space-x-4">
<div className="flex-1 space-y-3 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
)}
<Markdown
remarkPlugins={[remarkGfm, [remarkMath]]}
rehypePlugins={[
rehypeKatex,
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: codeLanguageSubset,
},
],
]}
components={{
code: code as any,
p: p as any,
}}
className="flex flex-col gap-2"
>
{chat.answer.parts.map((part) => part.text).join("")}
</Markdown>
</div>
</div>
</div>
</div>
))}
</div>
)}
</AnimatePresence>

View file

@ -0,0 +1,25 @@
import { DetailedHTMLProps, HTMLAttributes, memo } from "react";
import { ExtraProps } from "react-markdown";
import CodeBlock from "./CodeBlock";
export const code = memo((props: JSX.IntrinsicElements["code"]) => {
const { className, children } = props;
const match = /language-(\w+)/.exec(className || "");
const lang = match && match[1];
return <CodeBlock lang={lang || "text"} codeChildren={children as any} />;
});
export const p = memo(
(
props?: Omit<
DetailedHTMLProps<
HTMLAttributes<HTMLParagraphElement>,
HTMLParagraphElement
>,
"ref"
>,
) => {
return <p className="whitespace-pre-wrap">{props?.children}</p>;
},
);

View file

@ -1,5 +1,7 @@
import ChatWindow from "./chatWindow";
import { chatSearchParamsCache } from "../../helpers/lib/searchParams";
// @ts-expect-error
await import("katex/dist/katex.min.css");
function Page({
searchParams,
@ -10,7 +12,7 @@ function Page({
console.log(spaces);
return <ChatWindow q={q} />;
return <ChatWindow q={q} spaces={[]} />;
}
export default Page;

View file

@ -9,6 +9,17 @@ import { motion } from "framer-motion";
import { Label } from "@repo/ui/shadcn/label";
import { Input } from "@repo/ui/shadcn/input";
import { Textarea } from "@repo/ui/shadcn/textarea";
import { createSpace } from "../actions/doers";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/ui/shadcn/select";
import { Space } from "../actions/types";
import { getSpaces } from "../actions/fetchers";
import { toast } from "sonner";
export function DynamicIsland() {
const { scrollYProgress } = useScroll();
@ -80,7 +91,7 @@ function DynamicIslandContent() {
{show ? (
<div
onClick={() => setshow(!show)}
className="bg-[#1F2428] px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer"
className="bg-secondary px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer"
>
<div className="flex gap-4 items-center">
<Image src={AddIcon} alt="Add icon" />
@ -99,7 +110,25 @@ function DynamicIslandContent() {
const fakeitems = ["spaces", "page", "note"];
function ToolBar({ cancelfn }: { cancelfn: () => void }) {
const [spaces, setSpaces] = useState<Space[]>([]);
const [index, setIndex] = useState(0);
useEffect(() => {
(async () => {
let spaces = await getSpaces();
if (!spaces.success || !spaces.data) {
toast.warning("Unable to get spaces", {
richColors: true,
});
setSpaces([]);
return;
}
setSpaces(spaces.data);
})();
}, []);
return (
<AnimatePresence mode="wait">
<motion.div
@ -120,7 +149,7 @@ function ToolBar({ cancelfn }: { cancelfn: () => void }) {
}}
className="flex flex-col items-center"
>
<div className="bg-[#1F2428] py-[.35rem] px-[.6rem] rounded-2xl">
<div className="bg-secondary py-[.35rem] px-[.6rem] rounded-2xl">
<HoverEffect
items={fakeitems}
index={index}
@ -130,9 +159,9 @@ function ToolBar({ cancelfn }: { cancelfn: () => void }) {
{index === 0 ? (
<SpaceForm cancelfn={cancelfn} />
) : index === 1 ? (
<PageForm cancelfn={cancelfn} />
<PageForm cancelfn={cancelfn} spaces={spaces} />
) : (
<NoteForm cancelfn={cancelfn} />
<NoteForm cancelfn={cancelfn} spaces={spaces} />
)}
</motion.div>
</AnimatePresence>
@ -182,7 +211,10 @@ export const HoverEffect = ({
function SpaceForm({ cancelfn }: { cancelfn: () => void }) {
return (
<div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
<form
action={createSpace}
className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"
>
<div>
<Label className="text-[#858B92]" htmlFor="name">
Name
@ -190,34 +222,55 @@ function SpaceForm({ cancelfn }: { cancelfn: () => void }) {
<Input
className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0"
id="name"
name="name"
/>
</div>
<div className="flex justify-between">
<a className="text-blue-500" href="">
pull from store
</a>
<div
{/* <div
onClick={cancelfn}
className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer"
>
cancel
</div>
</div> */}
<button
type="submit"
className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer"
>
Submit
</button>
</div>
</div>
</form>
);
}
function PageForm({ cancelfn }: { cancelfn: () => void }) {
function PageForm({
cancelfn,
spaces,
}: {
cancelfn: () => void;
spaces: Space[];
}) {
return (
<div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
<div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
<div>
<Label className="text-[#858B92]" htmlFor="name">
<Label className="text-[#858B92]" htmlFor="space">
Space
</Label>
<Input
className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0"
id="name"
/>
<Select>
<SelectTrigger>
<SelectValue placeholder="Space" />
</SelectTrigger>
<SelectContent className="bg-secondary text-white">
{spaces.map((space) => (
<SelectItem key={space.id} value={space.id.toString()}>
{space.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[#858B92]" htmlFor="name">
@ -240,17 +293,31 @@ function PageForm({ cancelfn }: { cancelfn: () => void }) {
);
}
function NoteForm({ cancelfn }: { cancelfn: () => void }) {
function NoteForm({
cancelfn,
spaces,
}: {
cancelfn: () => void;
spaces: Space[];
}) {
return (
<div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
<div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
<div>
<Label className="text-[#858B92]" htmlFor="name">
Space
</Label>
<Input
className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0"
id="name"
/>
<Select>
<SelectTrigger>
<SelectValue placeholder="Space" />
</SelectTrigger>
<SelectContent className="bg-secondary text-white">
{spaces.map((space) => (
<SelectItem key={space.id} value={space.id.toString()}>
{space.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[#858B92]" htmlFor="name">

View file

@ -9,7 +9,7 @@ import DynamicIsland from "./dynamicisland";
function Header() {
return (
<div>
<div className="absolute left-0 w-full flex items-center justify-between z-10">
<div className="fixed left-0 w-full flex items-center justify-between z-10">
<Link className="px-5" href="/home">
<Image
src={Logo}

View file

@ -3,7 +3,7 @@ import Menu from "../menu";
import Header from "../header";
import QueryInput from "./queryinput";
import { homeSearchParamsCache } from "@/app/helpers/lib/searchParams";
import { getSpaces } from "../actions";
import { getSpaces } from "@/app/actions/fetchers";
async function Page({
searchParams,
@ -13,7 +13,12 @@ async function Page({
// TODO: use this to show a welcome page/modal
const { firstTime } = homeSearchParamsCache.parse(searchParams);
const spaces = await getSpaces();
let spaces = await getSpaces();
if (!spaces.success) {
// TODO: handle this error properly.
spaces.data = [];
}
return (
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col">
@ -21,7 +26,7 @@ async function Page({
{/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
<div className="w-full h-96">
<QueryInput initialSpaces={spaces} />
<QueryInput initialSpaces={spaces.data} />
</div>
</div>
);

View file

@ -2,10 +2,11 @@
import { ArrowRightIcon } from "@repo/ui/icons";
import Image from "next/image";
import React, { useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Divider from "@repo/ui/shadcn/divider";
import { MultipleSelector, Option } from "@repo/ui/shadcn/combobox";
import { useRouter } from "next/navigation";
import { getSpaces } from "@/app/actions/fetchers";
function QueryInput({
initialQuery = "",
@ -13,7 +14,10 @@ function QueryInput({
disabled = false,
}: {
initialQuery?: string;
initialSpaces?: { user: string | null; id: number; name: string }[];
initialSpaces?: {
id: number;
name: string;
}[];
disabled?: boolean;
}) {
const [q, setQ] = useState(initialQuery);
@ -41,10 +45,14 @@ function QueryInput({
return newQ;
};
const options = initialSpaces.map((x) => ({
label: x.name,
value: x.id.toString(),
}));
const options = useMemo(
() =>
initialSpaces.map((x) => ({
label: x.name,
value: x.id.toString(),
})),
[initialSpaces],
);
return (
<div>
@ -82,6 +90,7 @@ function QueryInput({
{/* selected sources */}
<div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]">
<MultipleSelector
key={options.length}
disabled={disabled}
defaultOptions={options}
onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))}

View file

@ -1,10 +1,11 @@
import Header from "./header";
import Menu from "./menu";
import { ensureAuth } from "./actions";
import { redirect } from "next/navigation";
import { auth } from "../helpers/server/auth";
import { Toaster } from "@repo/ui/shadcn/sonner";
async function Layout({ children }: { children: React.ReactNode }) {
const info = await ensureAuth();
const info = await auth();
if (!info) {
return redirect("/signin");
@ -17,6 +18,8 @@ async function Layout({ children }: { children: React.ReactNode }) {
<Menu />
{children}
<Toaster />
</main>
);
}

View file

@ -23,7 +23,7 @@ function Menu() {
];
return (
<div className="absolute h-screen pb-[25vh] w-full p-4 flex items-end justify-end lg:justify-start lg:items-center top-0 left-0 pointer-events-none">
<div className="fixed h-screen pb-[25vh] w-full p-4 flex items-end justify-end lg:justify-start lg:items-center top-0 left-0 pointer-events-none">
<div className="">
<div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] bg-secondary px-3 py-4 duration-200 hover:w-40">
{menuItems.map((item) => (

View file

@ -1,5 +0,0 @@
{
"scripts": {
"vercel-build": "next build"
}
}

View file

@ -16,7 +16,7 @@ export default async function Home() {
console.log(user);
if (user) {
// await redirect("/home")
await redirect("/home");
}
return (

View file

@ -0,0 +1,43 @@
"use server";
import { revalidatePath } from "next/cache";
import { db } from "../helpers/server/db";
import { space } from "../helpers/server/db/schema";
import { ServerActionReturnType } from "./types";
import { auth } from "../helpers/server/auth";
export const createSpace = async (
input: string | FormData,
): ServerActionReturnType<number> => {
const data = await auth();
if (!data || !data.user) {
return { error: "Not authenticated", success: false };
}
if (typeof input === "object") {
input = (input as FormData).get("name") as string;
}
try {
const resp = await db
.insert(space)
.values({ name: input, user: data.user.id });
revalidatePath("/home");
return { success: true, data: 1 };
} catch (e: unknown) {
const error = e as Error;
if (
error.message.includes("D1_ERROR: UNIQUE constraint failed: space.name")
) {
return { success: false, data: 0, error: "Space already exists" };
} else {
return {
success: false,
data: 0,
error: "Failed to create space with error: " + error.message,
};
}
}
};

View file

@ -0,0 +1,25 @@
"use server";
import { eq } from "drizzle-orm";
import { db } from "../helpers/server/db";
import { users } from "../helpers/server/db/schema";
import { ServerActionReturnType, Space } from "./types";
import { auth } from "../helpers/server/auth";
export const getSpaces = async (): ServerActionReturnType<Space[]> => {
const data = await auth();
if (!data || !data.user) {
return { error: "Not authenticated", success: false };
}
const spaces = await db.query.space.findMany({
where: eq(users, data.user.id),
});
const spacesWithoutUser = spaces.map((space) => {
return { ...space, user: undefined };
});
return { success: true, data: spacesWithoutUser };
};

View file

@ -0,0 +1,10 @@
export type Space = {
id: number;
name: string;
};
export type ServerActionReturnType<T> = Promise<{
error?: string;
success: boolean;
data?: T;
}>;

View file

@ -1,6 +1,11 @@
import { type NextRequest } from "next/server";
import { ChatHistory } from "@repo/shared-types";
import {
ChatHistory,
ChatHistoryZod,
convertChatHistoryList,
} from "@repo/shared-types";
import { ensureAuth } from "../ensureAuth";
import { z } from "zod";
export const runtime = "edge";
@ -15,59 +20,69 @@ export async function POST(req: NextRequest) {
return new Response("Missing BACKEND_SECURITY_KEY", { status: 500 });
}
const query = new URL(req.url).searchParams.get("q");
const spaces = new URL(req.url).searchParams.get("spaces");
const url = new URL(req.url);
const sourcesOnly =
new URL(req.url).searchParams.get("sourcesOnly") ?? "false";
const query = url.searchParams.get("q");
const spaces = url.searchParams.get("spaces");
const chatHistory = (await req.json()) as {
chatHistory: ChatHistory[];
};
const sourcesOnly = url.searchParams.get("sourcesOnly") ?? "false";
console.log("CHathistory", chatHistory);
const chatHistory = await req.json();
if (!query) {
if (!query || query.trim.length < 0) {
return new Response(JSON.stringify({ message: "Invalid query" }), {
status: 400,
});
}
try {
const resp = await fetch(
`https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
{
headers: {
"X-Custom-Auth-Key": process.env.BACKEND_SECURITY_KEY!,
},
method: "POST",
body: JSON.stringify({
chatHistory: chatHistory.chatHistory ?? [],
}),
},
const validated = z
.object({ chatHistory: z.array(ChatHistoryZod) })
.safeParse(chatHistory ?? []);
if (!validated.success) {
return new Response(
JSON.stringify({
message: "Invalid chat history",
error: validated.error,
}),
{ status: 400 },
);
}
console.log("sourcesOnly", sourcesOnly);
const modelCompatible = await convertChatHistoryList(
validated.data.chatHistory,
);
if (sourcesOnly == "true") {
const data = await resp.json();
console.log("data", data);
return new Response(JSON.stringify(data), { status: 200 });
}
const resp = await fetch(
`https://new-cf-ai-backend.dhravya.workers.dev/api/chat?query=${query}&user=${session.user.email}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_SECURITY_KEY}`,
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
chatHistory: modelCompatible,
}),
},
);
if (resp.status !== 200 || !resp.ok) {
const errorData = await resp.json();
console.log(errorData);
return new Response(
JSON.stringify({ message: "Error in CF function", error: errorData }),
{ status: resp.status },
);
}
console.log("sourcesOnly", sourcesOnly);
// Stream the response back to the client
const { readable, writable } = new TransformStream();
resp && resp.body!.pipeTo(writable);
if (sourcesOnly == "true") {
const data = await resp.json();
console.log("data", data);
return new Response(JSON.stringify(data), { status: 200 });
}
return new Response(readable, { status: 200 });
} catch {}
if (resp.status !== 200 || !resp.ok) {
const errorData = await resp.text();
console.log(errorData);
return new Response(
JSON.stringify({ message: "Error in CF function", error: errorData }),
{ status: resp.status },
);
}
return new Response(resp.body, { status: 200 });
}

View file

@ -0,0 +1,37 @@
export const codeLanguageSubset = [
"python",
"javascript",
"java",
"go",
"bash",
"c",
"cpp",
"csharp",
"css",
"diff",
"graphql",
"json",
"kotlin",
"less",
"lua",
"makefile",
"markdown",
"objectivec",
"perl",
"php",
"php-template",
"plaintext",
"python-repl",
"r",
"ruby",
"rust",
"scss",
"shell",
"sql",
"swift",
"typescript",
"vbnet",
"wasm",
"xml",
"yaml",
];

View file

@ -2,6 +2,7 @@ import NextAuth, { NextAuthResult } from "next-auth";
import Google from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "./db";
import { accounts, sessions, users, verificationTokens } from "./db/schema";
export const {
handlers: { GET, POST },
@ -10,16 +11,20 @@ export const {
auth,
} = NextAuth({
secret: process.env.BACKEND_SECURITY_KEY,
callbacks: {
session: ({ session, token, user }) => ({
...session,
user: {
...session.user,
id: user.id,
},
}),
},
adapter: DrizzleAdapter(db),
// callbacks: {
// session: ({ session, token, user }) => ({
// ...session,
// user: {
// ...session.user,
// },
// }),
// },
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens,
}),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,

View file

@ -7,75 +7,88 @@ import {
text,
integer,
} from "drizzle-orm/sqlite-core";
import type { AdapterAccountType } from "next-auth/adapters";
export const createTable = sqliteTableCreator((name) => `${name}`);
export const users = createTable("user", {
id: text("id", { length: 255 }).notNull().primaryKey(),
name: text("name", { length: 255 }),
email: text("email", { length: 255 }).notNull(),
emailVerified: int("emailVerified", { mode: "timestamp" }).default(
sql`CURRENT_TIMESTAMP`,
),
image: text("image", { length: 255 }),
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").notNull(),
emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
image: text("image"),
});
export type User = typeof users.$inferSelect;
export const usersRelations = relations(users, ({ many }) => ({
accounts: many(accounts),
sessions: many(sessions),
}));
export const accounts = createTable(
"account",
{
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
userId: text("userId", { length: 255 })
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type", { length: 255 }).notNull(),
provider: text("provider", { length: 255 }).notNull(),
providerAccountId: text("providerAccountId", { length: 255 }).notNull(),
type: text("type").$type<AdapterAccountType>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("providerAccountId").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: int("expires_at"),
token_type: text("token_type", { length: 255 }),
scope: text("scope", { length: 255 }),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state", { length: 255 }),
oauth_token_secret: text("oauth_token_secret"),
oauth_token: text("oauth_token"),
session_state: text("session_state"),
},
(account) => ({
userIdIdx: index("account_userId_idx").on(account.userId),
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
}),
);
export const sessions = createTable(
"session",
{
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
sessionToken: text("sessionToken", { length: 255 }).notNull(),
userId: text("userId", { length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: int("expires", { mode: "timestamp" }).notNull(),
},
(session) => ({
userIdIdx: index("session_userId_idx").on(session.userId),
}),
);
export const sessions = createTable("session", {
sessionToken: text("sessionToken").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
});
export const verificationTokens = createTable(
"verificationToken",
{
identifier: text("identifier", { length: 255 }).notNull(),
token: text("token", { length: 255 }).notNull(),
expires: int("expires", { mode: "timestamp" }).notNull(),
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
(verificationToken) => ({
compositePk: primaryKey({
columns: [verificationToken.identifier, verificationToken.token],
}),
}),
);
export const authenticators = createTable(
"authenticator",
{
credentialID: text("credentialID").notNull().unique(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
providerAccountId: text("providerAccountId").notNull(),
credentialPublicKey: text("credentialPublicKey").notNull(),
counter: integer("counter").notNull(),
credentialDeviceType: text("credentialDeviceType").notNull(),
credentialBackedUp: integer("credentialBackedUp", {
mode: "boolean",
}).notNull(),
transports: text("transports"),
},
(authenticator) => ({
compositePK: primaryKey({
columns: [authenticator.userId, authenticator.credentialID],
}),
}),
);
@ -94,7 +107,7 @@ export const storedContent = createTable(
"page",
),
image: text("image", { length: 255 }),
user: text("user", { length: 255 }).references(() => users.id, {
userId: int("user").references(() => users.id, {
onDelete: "cascade",
}),
},
@ -102,7 +115,7 @@ export const storedContent = createTable(
urlIdx: index("storedContent_url_idx").on(sc.url),
savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt),
titleInx: index("storedContent_title_idx").on(sc.title),
userIdx: index("storedContent_user_idx").on(sc.user),
userIdx: index("storedContent_user_idx").on(sc.userId),
}),
);

View file

@ -1,18 +1,29 @@
CREATE TABLE `account` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`userId` text(255) NOT NULL,
`type` text(255) NOT NULL,
`provider` text(255) NOT NULL,
`providerAccountId` text(255) NOT NULL,
`userId` text NOT NULL,
`type` text NOT NULL,
`provider` text NOT NULL,
`providerAccountId` text NOT NULL,
`refresh_token` text,
`access_token` text,
`expires_at` integer,
`token_type` text(255),
`scope` text(255),
`token_type` text,
`scope` text,
`id_token` text,
`session_state` text(255),
`oauth_token_secret` text,
`oauth_token` text,
`session_state` text,
PRIMARY KEY(`provider`, `providerAccountId`),
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `authenticator` (
`credentialID` text NOT NULL,
`userId` text NOT NULL,
`providerAccountId` text NOT NULL,
`credentialPublicKey` text NOT NULL,
`counter` integer NOT NULL,
`credentialDeviceType` text NOT NULL,
`credentialBackedUp` integer NOT NULL,
`transports` text,
PRIMARY KEY(`credentialID`, `userId`),
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
@ -25,9 +36,8 @@ CREATE TABLE `contentToSpace` (
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`sessionToken` text(255) NOT NULL,
`userId` text(255) NOT NULL,
`sessionToken` text PRIMARY KEY NOT NULL,
`userId` text NOT NULL,
`expires` integer NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
@ -50,27 +60,26 @@ CREATE TABLE `storedContent` (
`ogImage` text(255),
`type` text DEFAULT 'page',
`image` text(255),
`user` text(255),
`user` integer,
FOREIGN KEY (`user`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` text(255) PRIMARY KEY NOT NULL,
`name` text(255),
`email` text(255) NOT NULL,
`emailVerified` integer DEFAULT CURRENT_TIMESTAMP,
`image` text(255)
`id` text PRIMARY KEY NOT NULL,
`name` text,
`email` text NOT NULL,
`emailVerified` integer,
`image` text
);
--> statement-breakpoint
CREATE TABLE `verificationToken` (
`identifier` text(255) NOT NULL,
`token` text(255) NOT NULL,
`identifier` text NOT NULL,
`token` text NOT NULL,
`expires` integer NOT NULL,
PRIMARY KEY(`identifier`, `token`)
);
--> statement-breakpoint
CREATE INDEX `account_userId_idx` ON `account` (`userId`);--> statement-breakpoint
CREATE INDEX `session_userId_idx` ON `session` (`userId`);--> statement-breakpoint
CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);--> statement-breakpoint
CREATE UNIQUE INDEX `space_name_unique` ON `space` (`name`);--> statement-breakpoint
CREATE INDEX `spaces_name_idx` ON `space` (`name`);--> statement-breakpoint
CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint

View file

@ -1,43 +1,36 @@
{
"version": "6",
"dialect": "sqlite",
"id": "409cec60-0c4b-4cda-8751-3e70768bbb6c",
"id": "4a568d9b-a0e6-44ed-946b-694e34b063f3",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
@ -65,14 +58,14 @@
},
"token_type": {
"name": "token_type",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
@ -86,33 +79,13 @@
},
"session_state": {
"name": "session_state",
"type": "text(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"oauth_token_secret": {
"name": "oauth_token_secret",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"oauth_token": {
"name": "oauth_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": ["userId"],
"isUnique": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
@ -124,7 +97,98 @@
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"compositePrimaryKeys": {
"account_provider_providerAccountId_pk": {
"columns": ["provider", "providerAccountId"],
"name": "account_provider_providerAccountId_pk"
}
},
"uniqueConstraints": {}
},
"authenticator": {
"name": "authenticator",
"columns": {
"credentialID": {
"name": "credentialID",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentialPublicKey": {
"name": "credentialPublicKey",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentialDeviceType": {
"name": "credentialDeviceType",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentialBackedUp": {
"name": "credentialBackedUp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"transports": {
"name": "transports",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"authenticator_credentialID_unique": {
"name": "authenticator_credentialID_unique",
"columns": ["credentialID"],
"isUnique": true
}
},
"foreignKeys": {
"authenticator_userId_user_id_fk": {
"name": "authenticator_userId_user_id_fk",
"tableFrom": "authenticator",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"authenticator_userId_credentialID_pk": {
"columns": ["credentialID", "userId"],
"name": "authenticator_userId_credentialID_pk"
}
},
"uniqueConstraints": {}
},
"contentToSpace": {
@ -177,23 +241,16 @@
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"sessionToken": {
"name": "sessionToken",
"type": "text(255)",
"primaryKey": false,
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
@ -206,13 +263,7 @@
"autoincrement": false
}
},
"indexes": {
"session_userId_idx": {
"name": "session_userId_idx",
"columns": ["userId"],
"isUnique": false
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
@ -360,7 +411,7 @@
},
"user": {
"name": "user",
"type": "text(255)",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
@ -407,21 +458,21 @@
"columns": {
"id": {
"name": "id",
"type": "text(255)",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
@ -431,12 +482,11 @@
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
"autoincrement": false
},
"image": {
"name": "image",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
@ -452,14 +502,14 @@
"columns": {
"identifier": {
"name": "identifier",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text(255)",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false

View file

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1716677954608,
"tag": "0000_calm_monster_badoon",
"when": 1718412145023,
"tag": "0000_absurd_pandemic",
"breakpoints": true
}
]

View file

@ -4,6 +4,7 @@ import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev";
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@repo/ui"],
reactStrictMode: false,
};
export default MillionLint.next({
rsc: true,

View file

@ -11,7 +11,8 @@
"pages:build": "bunx @cloudflare/next-on-pages",
"preview": "bun pages:build && wrangler pages dev",
"deploy": "bun pages:build && wrangler pages deploy",
"schema-update": "bunx drizzle-orm"
"schema-update": "bunx drizzle-kit generate sqlite",
"update-local-db": "bunx wrangler d1 execute dev-d1-anycontext --local"
},
"dependencies": {
"@million/lint": "^1.0.0-rc.11",

View file

@ -27,7 +27,7 @@
"readline-sync": "^1.4.10",
"tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.7",
"turbo": "latest",
"turbo": "2.0.3",
"vercel": "^34.2.0"
},
"engines": {
@ -52,10 +52,12 @@
"@iarna/toml": "^2.2.5",
"@langchain/cloudflare": "^0.0.6",
"@million/lint": "^1.0.0-rc.18",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
@ -67,13 +69,21 @@
"compromise": "^14.13.0",
"drizzle-orm": "^0.30.10",
"framer-motion": "^11.2.6",
"katex": "^0.16.10",
"lucide-react": "^0.379.0",
"next-app-theme": "^0.1.10",
"next-auth": "^5.0.0-beta.18",
"next-themes": "^0.3.0",
"random-js": "^2.1.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.51.5",
"sonner": "^1.4.41",
"react-markdown": "^9.0.1",
"rehype-highlight": "^7.0.0",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"sonner": "^1.5.0",
"tailwind-scrollbar": "^3.1.0",
"tldraw": "^2.1.4",
"uploadthing": "^6.10.4",
"zod": "^3.23.8"

View file

@ -1,7 +1,54 @@
export type ChatHistory = {
question: string;
answer: {
parts: { text: string }[];
sources: { isNote: boolean; source: string }[];
};
};
import { z } from "zod";
export const ChatHistoryZod = z.object({
question: z.string(),
answer: z.object({
parts: z.array(z.object({ text: z.string() })),
sources: z.array(
z.object({
type: z.enum(["note", "page", "tweet"]),
source: z.string(),
title: z.string(),
content: z.string(),
}),
),
}),
});
export type ChatHistory = z.infer<typeof ChatHistoryZod>;
export const ModelCompatibleChatHistoryZod = z.array(
z.object({
role: z.union([
z.literal("user"),
z.literal("assistant"),
z.literal("system"),
]),
content: z.string(),
}),
);
export type ModelCompatibleChatHistory = z.infer<
typeof ModelCompatibleChatHistoryZod
>;
export function convertChatHistoryList(
chatHistoryList: ChatHistory[],
): ModelCompatibleChatHistory {
let convertedChats: ModelCompatibleChatHistory = [];
chatHistoryList.forEach((chat) => {
convertedChats.push(
{
role: "user",
content: chat.question,
},
{
role: "assistant",
content: chat.answer.parts.map((part) => part.text).join(" "),
},
);
});
return convertedChats;
}

View file

@ -49,6 +49,42 @@ body {
align-items: center;
justify-content: center;
}
.markdown table {
--tw-border-spacing-x: 0px;
--tw-border-spacing-y: 0px;
border-collapse: separate;
border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y);
width: 100%;
}
.markdown th {
background-color: rgba(236, 236, 241, 0.2);
border-bottom-width: 1px;
border-left-width: 1px;
border-top-width: 1px;
padding: 0.25rem 0.75rem;
}
.markdown th:first-child {
border-top-left-radius: 0.375rem;
}
.markdown th:last-child {
border-right-width: 1px;
border-top-right-radius: 0.375rem;
}
.markdown td {
border-bottom-width: 1px;
border-left-width: 1px;
padding: 0.25rem 0.75rem;
}
.markdown td:last-child {
border-right-width: 1px;
}
.markdown tbody tr:last-child td:first-child {
border-bottom-left-radius: 0.375rem;
}
.markdown tbody tr:last-child td:last-child {
border-bottom-right-radius: 0.375rem;
}
}
@layer utilities {
@ -57,6 +93,34 @@ body {
}
}
@layer components {
.markdown ol,
.markdown ul {
display: flex;
flex-direction: column;
padding-left: 1rem;
}
.markdown ol li,
.markdown ol li > p,
.markdown ol ol,
.markdown ol ul,
.markdown ul li,
.markdown ul li > p,
.markdown ul ol,
.markdown ul ul {
margin: 0;
}
.markdown ul li:before {
content: "•";
font-size: 0.875rem;
line-height: 1.25rem;
margin-left: -1rem;
position: absolute;
}
}
.gradient-background {
background: linear-gradient(
150deg,
@ -84,3 +148,62 @@ body {
::-webkit-scrollbar-thumb:hover {
background: #22303d;
}
.no-scrollbar {
/* For WebKit (Safari, Chrome, etc.) */
&::-webkit-scrollbar {
display: none;
}
/* For Firefox */
scrollbar-width: none;
/* For IE and Edge */
-ms-overflow-style: none;
}
:not(pre) > code.hljs,
:not(pre) > code[class*="language-"] {
border-radius: 0.3em;
white-space: normal;
}
.hljs-comment {
color: hsla(0, 0%, 100%, 0.5);
}
.hljs-meta {
color: hsla(0, 0%, 100%, 0.6);
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #e9950c;
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword,
.hljs-literal {
color: #2e95d3;
}
.hljs-addition,
.hljs-attribute,
.hljs-meta-string,
.hljs-regexp,
.hljs-string {
color: #00a67d;
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #df3079;
}
.hljs-bullet,
.hljs-link,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #f22c3d;
}

View file

@ -71,7 +71,11 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
require("tailwind-scrollbar"),
],
} satisfies Config;
export default config;

View file

@ -0,0 +1,54 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@repo/ui/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn(className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center gap-2 py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View file

@ -0,0 +1,160 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@repo/ui/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-foreground-menu data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};

View file

@ -0,0 +1,31 @@
"use client";
import { useTheme } from "next-themes";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster };