diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts index 1e128bbd..2dbb2d0c 100644 --- a/apps/cf-ai-backend/src/index.ts +++ b/apps/cf-ai-backend/src/index.ts @@ -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; } diff --git a/apps/cf-ai-backend/src/prompts/prompt1.ts b/apps/cf-ai-backend/src/prompts/prompt1.ts index aa7694d3..d2ee988c 100644 --- a/apps/cf-ai-backend/src/prompts/prompt1.ts +++ b/apps/cf-ai-backend/src/prompts/prompt1.ts @@ -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 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 tags and your final answer between tags, formatting both in markdown. - +- provide your justification in the end, in a 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 }) => { diff --git a/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts b/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts index 3514f579..be5839b1 100644 --- a/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts +++ b/apps/cf-ai-backend/src/utils/OpenAIEmbedder.ts @@ -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; } } diff --git a/apps/web/app/(dash)/actions.ts b/apps/web/app/(dash)/actions.ts deleted file mode 100644 index 70c2a567..00000000 --- a/apps/web/app/(dash)/actions.ts +++ /dev/null @@ -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; -} diff --git a/apps/web/app/(dash)/chat/CodeBlock.tsx b/apps/web/app/(dash)/chat/CodeBlock.tsx new file mode 100644 index 00000000..0bb6a19d --- /dev/null +++ b/apps/web/app/(dash)/chat/CodeBlock.tsx @@ -0,0 +1,90 @@ +import React, { useRef, useState } from "react"; + +const CodeBlock = ({ + lang, + codeChildren, +}: { + lang: string; + codeChildren: React.ReactNode & React.ReactNode[]; +}) => { + const codeRef = useRef(null); + + return ( +
+ +
+ + {codeChildren} + +
+
+ ); +}; + +const CodeBar = React.memo( + ({ + lang, + codeRef, + }: { + lang: string; + codeRef: React.RefObject; + }) => { + const [isCopied, setIsCopied] = useState(false); + return ( +
+ {lang} + +
+ ); + }, +); +export default CodeBlock; diff --git a/apps/web/app/(dash)/chat/actions.ts b/apps/web/app/(dash)/chat/actions.ts index 908fe79e..e69de29b 100644 --- a/apps/web/app/(dash)/chat/actions.ts +++ b/apps/web/app/(dash)/chat/actions.ts @@ -1 +0,0 @@ -"use server"; diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx index 43c337ee..b631c835 100644 --- a/apps/web/app/(dash)/chat/chatWindow.tsx +++ b/apps/web/app/(dash)/chat/chatWindow.tsx @@ -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([ + { + 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 ( -
+
{layout === "initial" ? (
@@ -36,16 +99,111 @@ function ChatWindow({ q }: { q: string }) { ) : (
-

- {q} -

+ {chatHistory.map((chat, idx) => ( +
+

+ {chat.question} +

+ +
+
0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`} + > + + + + Related Memories + + {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */} + + {/* Loading state */} + {chat.answer.sources.length > 0 || + (chat.answer.parts.length === 0 && ( + <> + {[1, 2, 3, 4].map((_, idx) => ( +
+
+
+
+ ))} + + ))} + {chat.answer.sources.map((source, idx) => ( +
+
+ {source.type} +
+
{source.title}
+
+ ))} +
+
+
+
+ + {/* Summary */} +
+
Summary
+
+ {chat.answer.parts.length === 0 && ( +
+
+
+
+
+
+ )} + + {chat.answer.parts.map((part) => part.text).join("")} + +
+
+
+
+ ))}
)} diff --git a/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx b/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx new file mode 100644 index 00000000..747d4fca --- /dev/null +++ b/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx @@ -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 ; +}); + +export const p = memo( + ( + props?: Omit< + DetailedHTMLProps< + HTMLAttributes, + HTMLParagraphElement + >, + "ref" + >, + ) => { + return

{props?.children}

; + }, +); diff --git a/apps/web/app/(dash)/chat/page.tsx b/apps/web/app/(dash)/chat/page.tsx index 9e28fda7..fd4de826 100644 --- a/apps/web/app/(dash)/chat/page.tsx +++ b/apps/web/app/(dash)/chat/page.tsx @@ -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 ; + return ; } export default Page; diff --git a/apps/web/app/(dash)/dynamicisland.tsx b/apps/web/app/(dash)/dynamicisland.tsx index b703d55a..31f76fda 100644 --- a/apps/web/app/(dash)/dynamicisland.tsx +++ b/apps/web/app/(dash)/dynamicisland.tsx @@ -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 ? (
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" >
Add icon @@ -99,7 +110,25 @@ function DynamicIslandContent() { const fakeitems = ["spaces", "page", "note"]; function ToolBar({ cancelfn }: { cancelfn: () => void }) { + const [spaces, setSpaces] = useState([]); + 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 ( void }) { }} className="flex flex-col items-center" > -
+
void }) { {index === 0 ? ( ) : index === 1 ? ( - + ) : ( - + )} @@ -182,7 +211,10 @@ export const HoverEffect = ({ function SpaceForm({ cancelfn }: { cancelfn: () => void }) { return ( -
+
pull from store -
cancel -
+
*/} +
-
+ ); } -function PageForm({ cancelfn }: { cancelfn: () => void }) { +function PageForm({ + cancelfn, + spaces, +}: { + cancelfn: () => void; + spaces: Space[]; +}) { return ( -
+
-