mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
chat UI and markdown renderer ⚡
This commit is contained in:
parent
f59cedec7e
commit
5569aa278c
15 changed files with 574 additions and 25834 deletions
|
|
@ -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 }) => {
|
||||
|
|
|
|||
90
apps/web/app/(dash)/chat/CodeBlock.tsx
Normal file
90
apps/web/app/(dash)/chat/CodeBlock.tsx
Normal 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-6"
|
||||
>
|
||||
<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-6"
|
||||
>
|
||||
<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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
"use server";
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
25
apps/web/app/(dash)/chat/markdownRenderHelpers.tsx
Normal file
25
apps/web/app/(dash)/chat/markdownRenderHelpers.tsx
Normal 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>;
|
||||
},
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
37
apps/web/app/helpers/constants.ts
Normal file
37
apps/web/app/helpers/constants.ts
Normal 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",
|
||||
];
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
GOOGLE_CLIENT_ID="-"
|
||||
GOOGLE_CLIENT_SECRET="-"
|
||||
NEXTAUTH_SECRET='nextauthsecret'
|
||||
DATABASE_URL='database.sqlite'
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
||||
BACKEND_SECURITY_KEY='veryrandomsecuritykey'
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
25760
package-lock.json
generated
25760
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -53,6 +53,7 @@
|
|||
"@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",
|
||||
|
|
@ -68,13 +69,20 @@
|
|||
"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",
|
||||
"random-js": "^2.1.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"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.4.41",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tldraw": "^2.1.4",
|
||||
"uploadthing": "^6.10.4",
|
||||
"zod": "^3.23.8"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue