diff options
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/api/ask/route.ts | 48 | ||||
| -rw-r--r-- | apps/web/src/components/QueryAI.tsx | 134 | ||||
| -rw-r--r-- | apps/web/src/components/SearchResults.tsx | 38 | ||||
| -rw-r--r-- | apps/web/src/components/ui/dropdown-menu.tsx | 200 | ||||
| -rw-r--r-- | apps/web/src/components/ui/label.tsx | 26 |
5 files changed, 446 insertions, 0 deletions
diff --git a/apps/web/src/app/api/ask/route.ts b/apps/web/src/app/api/ask/route.ts new file mode 100644 index 00000000..cad7a671 --- /dev/null +++ b/apps/web/src/app/api/ask/route.ts @@ -0,0 +1,48 @@ +import { db } from "@/server/db"; +import { eq } from "drizzle-orm"; +import { sessions, users } from "@/server/db/schema"; +import { type NextRequest, NextResponse } from "next/server"; +import { env } from "@/env"; + +export const runtime = "edge"; + +export async function POST(req: NextRequest) { + const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", ""); + + const sessionData = await db.select().from(sessions).where(eq(sessions.sessionToken, token!)) + + if (!sessionData || sessionData.length === 0) { + return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 }); + } + + const user = await db.select().from(users).where(eq(users.id, sessionData[0].userId)).limit(1) + + if (!user || user.length === 0) { + return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 }); + } + + const body = await req.json() as { + query: string; + } + + const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/ask`, { + headers: { + "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, + }, + method: "POST", + body: JSON.stringify({ + query: body.query, + }), + }) + + if (resp.status !== 200 || !resp.ok) { + const errorData = await resp.json(); + return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status }); + } + + // Stream the response back to the client + const { readable, writable } = new TransformStream(); + resp && resp.body!.pipeTo(writable); + + return new Response(readable, { status: 200 }); +}
\ No newline at end of file diff --git a/apps/web/src/components/QueryAI.tsx b/apps/web/src/components/QueryAI.tsx new file mode 100644 index 00000000..811dd899 --- /dev/null +++ b/apps/web/src/components/QueryAI.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { Label } from './ui/label'; +import React, { useEffect, useState } from 'react'; +import { Input } from './ui/input'; +import { Button } from './ui/button'; +import SearchResults from './SearchResults'; + +function QueryAI() { + const [searchResults, setSearchResults] = useState<string[]>([]); + const [isAiLoading, setIsAiLoading] = useState(false); + + const [aiResponse, setAIResponse] = useState(''); + const [input, setInput] = useState(''); + const [toBeParsed, setToBeParsed] = useState(''); + + const handleStreamData = (newChunk: string) => { + // Append the new chunk to the existing data to be parsed + setToBeParsed((prev) => prev + newChunk); + }; + + useEffect(() => { + // Define a function to try parsing the accumulated data + const tryParseAccumulatedData = () => { + // Attempt to parse the "toBeParsed" state as JSON + try { + // Split the accumulated data by the known delimiter "\n\n" + const parts = toBeParsed.split('\n\n'); + let remainingData = ''; + + // Process each part to extract JSON objects + parts.forEach((part, index) => { + try { + const parsedPart = JSON.parse(part.replace('data: ', '')); // Try to parse the part as JSON + + // If the part is the last one and couldn't be parsed, keep it to accumulate more data + if (index === parts.length - 1 && !parsedPart) { + remainingData = part; + } else if (parsedPart && parsedPart.response) { + // If the part is parsable and has the "response" field, update the AI response state + setAIResponse((prev) => prev + parsedPart.response); + } + } catch (error) { + // If parsing fails and it's not the last part, it's a malformed JSON + if (index !== parts.length - 1) { + console.error('Malformed JSON part: ', part); + } else { + // If it's the last part, it may be incomplete, so keep it + remainingData = part; + } + } + }); + + // Update the toBeParsed state to only contain the unparsed remainder + if (remainingData !== toBeParsed) { + setToBeParsed(remainingData); + } + } catch (error) { + console.error('Error parsing accumulated data: ', error); + } + }; + + // Call the parsing function if there's data to be parsed + if (toBeParsed) { + tryParseAccumulatedData(); + } + }, [toBeParsed]); + + const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + setIsAiLoading(true); + + const sourcesResponse = await fetch( + `/api/query?sourcesOnly=true&q=${input}`, + ); + + const sourcesInJson = (await sourcesResponse.json()) as { + ids: string[]; + }; + + setSearchResults(sourcesInJson.ids); + + const response = await fetch(`/api/query?q=${input}`); + + if (response.body) { + let reader = response.body.getReader(); + let decoder = new TextDecoder('utf-8'); + let result = ''; + + // @ts-ignore + reader.read().then(function processText({ done, value }) { + if (done) { + // setSearchResults(JSON.parse(result.replace('data: ', ''))); + // setIsAiLoading(false); + return; + } + + handleStreamData(decoder.decode(value)); + + return reader.read().then(processText); + }); + } + }; + + return ( + <div className="w-full max-w-2xl mx-auto"> + <form onSubmit={async (e) => await getSearchResults(e)} className="mt-8"> + <Label htmlFor="searchInput">Ask your SuperMemory</Label> + <div className="flex flex-col md:flex-row md:w-full md:items-center space-y-2 md:space-y-0 md:space-x-2"> + <Input + value={input} + onChange={(e) => setInput(e.target.value)} + placeholder="Search using AI... ✨" + id="searchInput" + /> + <Button + disabled={isAiLoading} + className="max-w-min md:w-full" + type="submit" + variant="default" + > + Ask AI + </Button> + </div> + </form> + + {searchResults && ( + <SearchResults aiResponse={aiResponse} sources={searchResults} /> + )} + </div> + ); +} + +export default QueryAI; diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx new file mode 100644 index 00000000..0445d0b4 --- /dev/null +++ b/apps/web/src/components/SearchResults.tsx @@ -0,0 +1,38 @@ +'use client' + +import React from 'react'; +import { Card, CardContent } from './ui/card'; +import Markdown from 'react-markdown'; +import remarkGfm from 'remark-gfm' + +function SearchResults({ + aiResponse, + sources, +}: { + aiResponse: string; + sources: string[]; +}) { + return ( + <div + style={{ + backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`, + }} + className="w-full max-w-2xl mx-auto px-4 py-6 space-y-6 border mt-4 rounded-xl" + > + <div className="text-start"> + <div className="text-xl text-black"> + <Markdown remarkPlugins={[remarkGfm]}>{aiResponse.replace('</s>', '')}</Markdown> + </div> + </div> + <div className="grid gap-6"> + {sources.map((value, index) => ( + <Card key={index}> + <CardContent className="space-y-2">{value}</CardContent> + </Card> + ))} + </div> + </div> + ); +} + +export default SearchResults; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..4243e7f2 --- /dev/null +++ b/apps/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + <DropdownMenuPrimitive.SubTrigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-800 dark:data-[state=open]:bg-gray-800", + inset && "pl-8", + className + )} + {...props} + > + {children} + <ChevronRight className="ml-auto h-4 w-4" /> + </DropdownMenuPrimitive.SubTrigger> +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> +>(({ className, ...props }, ref) => ( + <DropdownMenuPrimitive.SubContent + ref={ref} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-lg 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 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50", + className + )} + {...props} + /> +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> +>(({ className, sideOffset = 4, ...props }, ref) => ( + <DropdownMenuPrimitive.Portal> + <DropdownMenuPrimitive.Content + ref={ref} + sideOffset={sideOffset} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 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 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50", + className + )} + {...props} + /> + </DropdownMenuPrimitive.Portal> +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + <DropdownMenuPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> +>(({ className, children, checked, ...props }, ref) => ( + <DropdownMenuPrimitive.CheckboxItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", + className + )} + checked={checked} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Check className="h-4 w-4" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.CheckboxItem> +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> +>(({ className, children, ...props }, ref) => ( + <DropdownMenuPrimitive.RadioItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", + className + )} + {...props} + > + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Circle className="h-2 w-2 fill-current" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.RadioItem> +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + <DropdownMenuPrimitive.Label + ref={ref} + className={cn( + "px-2 py-1.5 text-sm font-semibold", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef<typeof DropdownMenuPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <DropdownMenuPrimitive.Separator + ref={ref} + className={cn("-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-800", className)} + {...props} + /> +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn("ml-auto text-xs tracking-widest opacity-60", className)} + {...props} + /> + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx new file mode 100644 index 00000000..53418217 --- /dev/null +++ b/apps/web/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef<typeof LabelPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & + VariantProps<typeof labelVariants> +>(({ className, ...props }, ref) => ( + <LabelPrimitive.Root + ref={ref} + className={cn(labelVariants(), className)} + {...props} + /> +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } |