Praneeth Yerrapragada commited on
Commit
88be6fd
·
0 Parent(s):

feat: repo setup

Browse files
Files changed (49) hide show
  1. .env.example +3 -0
  2. .eslintrc.json +7 -0
  3. .gitignore +36 -0
  4. Dockerfile +16 -0
  5. README.md +71 -0
  6. app/components/chat-section.tsx +44 -0
  7. app/components/header.tsx +28 -0
  8. app/components/ui/README.md +1 -0
  9. app/components/ui/button.tsx +56 -0
  10. app/components/ui/chat/chat-actions.tsx +28 -0
  11. app/components/ui/chat/chat-avatar.tsx +25 -0
  12. app/components/ui/chat/chat-events.tsx +48 -0
  13. app/components/ui/chat/chat-image.tsx +17 -0
  14. app/components/ui/chat/chat-input.tsx +84 -0
  15. app/components/ui/chat/chat-message.tsx +127 -0
  16. app/components/ui/chat/chat-messages.tsx +69 -0
  17. app/components/ui/chat/chat-sources.tsx +148 -0
  18. app/components/ui/chat/chat-tools.tsx +26 -0
  19. app/components/ui/chat/chat.interface.ts +18 -0
  20. app/components/ui/chat/codeblock.tsx +139 -0
  21. app/components/ui/chat/index.ts +54 -0
  22. app/components/ui/chat/markdown.tsx +77 -0
  23. app/components/ui/chat/use-copy-to-clipboard.tsx +33 -0
  24. app/components/ui/chat/widgets/PdfDialog.tsx +56 -0
  25. app/components/ui/chat/widgets/WeatherCard.tsx +213 -0
  26. app/components/ui/collapsible.tsx +11 -0
  27. app/components/ui/drawer.tsx +118 -0
  28. app/components/ui/file-uploader.tsx +105 -0
  29. app/components/ui/hover-card.tsx +29 -0
  30. app/components/ui/input.tsx +25 -0
  31. app/components/ui/lib/url.ts +11 -0
  32. app/components/ui/lib/utils.ts +6 -0
  33. app/components/ui/upload-image-preview.tsx +32 -0
  34. app/favicon.ico +0 -0
  35. app/globals.css +94 -0
  36. app/layout.tsx +23 -0
  37. app/markdown.css +23 -0
  38. app/observability/index.ts +12 -0
  39. app/page.tsx +11 -0
  40. next.config.json +17 -0
  41. next.config.mjs +8 -0
  42. package-lock.json +0 -0
  43. package.json +59 -0
  44. postcss.config.js +6 -0
  45. prettier.config.js +3 -0
  46. public/llama.png +0 -0
  47. tailwind.config.ts +78 -0
  48. tsconfig.json +28 -0
  49. webpack.config.mjs +13 -0
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # The backend API for chat endpoint.
2
+ NEXT_PUBLIC_CHAT_API=http://localhost:8000/api/chat
3
+
.eslintrc.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": ["next/core-web-vitals", "prettier"],
3
+ "rules": {
4
+ "max-params": ["error", 4],
5
+ "prefer-const": "error"
6
+ }
7
+ }
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ .env
29
+ .env*.local
30
+
31
+ # vercel
32
+ .vercel
33
+
34
+ # typescript
35
+ *.tsbuildinfo
36
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine as build
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY package.json package-lock.* ./
7
+ RUN npm install
8
+
9
+ # Build the application
10
+ COPY . .
11
+ RUN npm run build
12
+
13
+ # ====================================
14
+ FROM build as release
15
+
16
+ CMD ["npm", "run", "start"]
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
2
+
3
+ ## Getting Started
4
+
5
+ First, install the dependencies:
6
+
7
+ ```
8
+ npm install
9
+ ```
10
+
11
+ Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step):
12
+
13
+ ```
14
+ npm run generate
15
+ ```
16
+
17
+ Third, run the development server:
18
+
19
+ ```
20
+ npm run dev
21
+ ```
22
+
23
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
24
+
25
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
26
+
27
+ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
28
+
29
+ ## Using Docker
30
+
31
+ 1. Build an image for the Next.js app:
32
+
33
+ ```
34
+ docker build -t <your_app_image_name> .
35
+ ```
36
+
37
+ 2. Generate embeddings:
38
+
39
+ Parse the data and generate the vector embeddings if the `./data` folder exists - otherwise, skip this step:
40
+
41
+ ```
42
+ docker run \
43
+ --rm \
44
+ -v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
45
+ -v $(pwd)/config:/app/config \
46
+ -v $(pwd)/data:/app/data \
47
+ -v $(pwd)/cache:/app/cache \ # Use your file system to store the vector database
48
+ <your_app_image_name> \
49
+ npm run generate
50
+ ```
51
+
52
+ 3. Start the app:
53
+
54
+ ```
55
+ docker run \
56
+ --rm \
57
+ -v $(pwd)/.env:/app/.env \ # Use ENV variables and configuration from your file-system
58
+ -v $(pwd)/config:/app/config \
59
+ -v $(pwd)/cache:/app/cache \ # Use your file system to store gea vector database
60
+ -p 3000:3000 \
61
+ <your_app_image_name>
62
+ ```
63
+
64
+ ## Learn More
65
+
66
+ To learn more about LlamaIndex, take a look at the following resources:
67
+
68
+ - [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
69
+ - [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
70
+
71
+ You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
app/components/chat-section.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useChat } from "ai/react";
4
+ import { ChatInput, ChatMessages } from "./ui/chat";
5
+
6
+ export default function ChatSection() {
7
+ const {
8
+ messages,
9
+ input,
10
+ isLoading,
11
+ handleSubmit,
12
+ handleInputChange,
13
+ reload,
14
+ stop,
15
+ } = useChat({
16
+ api: process.env.NEXT_PUBLIC_CHAT_API,
17
+ headers: {
18
+ "Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
19
+ },
20
+ onError: (error: unknown) => {
21
+ if (!(error instanceof Error)) throw error;
22
+ const message = JSON.parse(error.message);
23
+ alert(message.detail);
24
+ },
25
+ });
26
+
27
+ return (
28
+ <div className="space-y-4 max-w-5xl w-full">
29
+ <ChatMessages
30
+ messages={messages}
31
+ isLoading={isLoading}
32
+ reload={reload}
33
+ stop={stop}
34
+ />
35
+ <ChatInput
36
+ input={input}
37
+ handleSubmit={handleSubmit}
38
+ handleInputChange={handleInputChange}
39
+ isLoading={isLoading}
40
+ multiModal={true}
41
+ />
42
+ </div>
43
+ );
44
+ }
app/components/header.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from "next/image";
2
+
3
+ export default function Header() {
4
+ return (
5
+ <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
6
+ <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
7
+ Get started by editing&nbsp;
8
+ <code className="font-mono font-bold">app/page.tsx</code>
9
+ </p>
10
+ <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
11
+ <a
12
+ href="https://www.llamaindex.ai/"
13
+ className="flex items-center justify-center font-nunito text-lg font-bold gap-2"
14
+ >
15
+ <span>Built by LlamaIndex</span>
16
+ <Image
17
+ className="rounded-xl"
18
+ src="/llama.png"
19
+ alt="Llama Logo"
20
+ width={40}
21
+ height={40}
22
+ priority
23
+ />
24
+ </a>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
app/components/ui/README.md ADDED
@@ -0,0 +1 @@
 
 
1
+ Using the chat component from https://github.com/marcusschiesser/ui (based on https://ui.shadcn.com/)
app/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "./lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
app/components/ui/chat/chat-actions.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PauseCircle, RefreshCw } from "lucide-react";
2
+
3
+ import { Button } from "../button";
4
+ import { ChatHandler } from "./chat.interface";
5
+
6
+ export default function ChatActions(
7
+ props: Pick<ChatHandler, "stop" | "reload"> & {
8
+ showReload?: boolean;
9
+ showStop?: boolean;
10
+ },
11
+ ) {
12
+ return (
13
+ <div className="space-x-4">
14
+ {props.showStop && (
15
+ <Button variant="outline" size="sm" onClick={props.stop}>
16
+ <PauseCircle className="mr-2 h-4 w-4" />
17
+ Stop generating
18
+ </Button>
19
+ )}
20
+ {props.showReload && (
21
+ <Button variant="outline" size="sm" onClick={props.reload}>
22
+ <RefreshCw className="mr-2 h-4 w-4" />
23
+ Regenerate
24
+ </Button>
25
+ )}
26
+ </div>
27
+ );
28
+ }
app/components/ui/chat/chat-avatar.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { User2 } from "lucide-react";
2
+ import Image from "next/image";
3
+
4
+ export default function ChatAvatar({ role }: { role: string }) {
5
+ if (role === "user") {
6
+ return (
7
+ <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-background shadow">
8
+ <User2 className="h-4 w-4" />
9
+ </div>
10
+ );
11
+ }
12
+
13
+ return (
14
+ <div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border bg-black text-white shadow">
15
+ <Image
16
+ className="rounded-md"
17
+ src="/llama.png"
18
+ alt="Llama Logo"
19
+ width={24}
20
+ height={24}
21
+ priority
22
+ />
23
+ </div>
24
+ );
25
+ }
app/components/ui/chat/chat-events.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { Button } from "../button";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "../collapsible";
9
+ import { EventData } from "./index";
10
+
11
+ export function ChatEvents({
12
+ data,
13
+ isLoading,
14
+ }: {
15
+ data: EventData[];
16
+ isLoading: boolean;
17
+ }) {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+
20
+ const buttonLabel = isOpen ? "Hide events" : "Show events";
21
+
22
+ const EventIcon = isOpen ? (
23
+ <ChevronDown className="h-4 w-4" />
24
+ ) : (
25
+ <ChevronRight className="h-4 w-4" />
26
+ );
27
+
28
+ return (
29
+ <div className="border-l-2 border-indigo-400 pl-2">
30
+ <Collapsible open={isOpen} onOpenChange={setIsOpen}>
31
+ <CollapsibleTrigger asChild>
32
+ <Button variant="secondary" className="space-x-2">
33
+ {isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
34
+ <span>{buttonLabel}</span>
35
+ {EventIcon}
36
+ </Button>
37
+ </CollapsibleTrigger>
38
+ <CollapsibleContent asChild>
39
+ <div className="mt-4 text-sm space-y-2">
40
+ {data.map((eventItem, index) => (
41
+ <div key={index}>{eventItem.title}</div>
42
+ ))}
43
+ </div>
44
+ </CollapsibleContent>
45
+ </Collapsible>
46
+ </div>
47
+ );
48
+ }
app/components/ui/chat/chat-image.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Image from "next/image";
2
+ import { type ImageData } from "./index";
3
+
4
+ export function ChatImage({ data }: { data: ImageData }) {
5
+ return (
6
+ <div className="rounded-md max-w-[200px] shadow-md">
7
+ <Image
8
+ src={data.url}
9
+ width={0}
10
+ height={0}
11
+ sizes="100vw"
12
+ style={{ width: "100%", height: "auto" }}
13
+ alt=""
14
+ />
15
+ </div>
16
+ );
17
+ }
app/components/ui/chat/chat-input.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Button } from "../button";
3
+ import FileUploader from "../file-uploader";
4
+ import { Input } from "../input";
5
+ import UploadImagePreview from "../upload-image-preview";
6
+ import { ChatHandler } from "./chat.interface";
7
+
8
+ export default function ChatInput(
9
+ props: Pick<
10
+ ChatHandler,
11
+ | "isLoading"
12
+ | "input"
13
+ | "onFileUpload"
14
+ | "onFileError"
15
+ | "handleSubmit"
16
+ | "handleInputChange"
17
+ > & {
18
+ multiModal?: boolean;
19
+ },
20
+ ) {
21
+ const [imageUrl, setImageUrl] = useState<string | null>(null);
22
+
23
+ const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
24
+ if (imageUrl) {
25
+ props.handleSubmit(e, {
26
+ data: { imageUrl: imageUrl },
27
+ });
28
+ setImageUrl(null);
29
+ return;
30
+ }
31
+ props.handleSubmit(e);
32
+ };
33
+
34
+ const onRemovePreviewImage = () => setImageUrl(null);
35
+
36
+ const handleUploadImageFile = async (file: File) => {
37
+ const base64 = await new Promise<string>((resolve, reject) => {
38
+ const reader = new FileReader();
39
+ reader.readAsDataURL(file);
40
+ reader.onload = () => resolve(reader.result as string);
41
+ reader.onerror = (error) => reject(error);
42
+ });
43
+ setImageUrl(base64);
44
+ };
45
+
46
+ const handleUploadFile = async (file: File) => {
47
+ try {
48
+ if (props.multiModal && file.type.startsWith("image/")) {
49
+ return await handleUploadImageFile(file);
50
+ }
51
+ props.onFileUpload?.(file);
52
+ } catch (error: any) {
53
+ props.onFileError?.(error.message);
54
+ }
55
+ };
56
+
57
+ return (
58
+ <form
59
+ onSubmit={onSubmit}
60
+ className="rounded-xl bg-white p-4 shadow-xl space-y-4"
61
+ >
62
+ {imageUrl && (
63
+ <UploadImagePreview url={imageUrl} onRemove={onRemovePreviewImage} />
64
+ )}
65
+ <div className="flex w-full items-start justify-between gap-4 ">
66
+ <Input
67
+ autoFocus
68
+ name="message"
69
+ placeholder="Type a message"
70
+ className="flex-1"
71
+ value={props.input}
72
+ onChange={props.handleInputChange}
73
+ />
74
+ <FileUploader
75
+ onFileUpload={handleUploadFile}
76
+ onFileError={props.onFileError}
77
+ />
78
+ <Button type="submit" disabled={props.isLoading}>
79
+ Send message
80
+ </Button>
81
+ </div>
82
+ </form>
83
+ );
84
+ }
app/components/ui/chat/chat-message.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Check, Copy } from "lucide-react";
2
+
3
+ import { Message } from "ai";
4
+ import { Fragment } from "react";
5
+ import { Button } from "../button";
6
+ import ChatAvatar from "./chat-avatar";
7
+ import { ChatEvents } from "./chat-events";
8
+ import { ChatImage } from "./chat-image";
9
+ import { ChatSources } from "./chat-sources";
10
+ import ChatTools from "./chat-tools";
11
+ import {
12
+ AnnotationData,
13
+ EventData,
14
+ ImageData,
15
+ MessageAnnotation,
16
+ MessageAnnotationType,
17
+ SourceData,
18
+ ToolData,
19
+ } from "./index";
20
+ import Markdown from "./markdown";
21
+ import { useCopyToClipboard } from "./use-copy-to-clipboard";
22
+
23
+ type ContentDisplayConfig = {
24
+ order: number;
25
+ component: JSX.Element | null;
26
+ };
27
+
28
+ function getAnnotationData<T extends AnnotationData>(
29
+ annotations: MessageAnnotation[],
30
+ type: MessageAnnotationType,
31
+ ): T[] {
32
+ return annotations.filter((a) => a.type === type).map((a) => a.data as T);
33
+ }
34
+
35
+ function ChatMessageContent({
36
+ message,
37
+ isLoading,
38
+ }: {
39
+ message: Message;
40
+ isLoading: boolean;
41
+ }) {
42
+ const annotations = message.annotations as MessageAnnotation[] | undefined;
43
+ if (!annotations?.length) return <Markdown content={message.content} />;
44
+
45
+ const imageData = getAnnotationData<ImageData>(
46
+ annotations,
47
+ MessageAnnotationType.IMAGE,
48
+ );
49
+ const eventData = getAnnotationData<EventData>(
50
+ annotations,
51
+ MessageAnnotationType.EVENTS,
52
+ );
53
+ const sourceData = getAnnotationData<SourceData>(
54
+ annotations,
55
+ MessageAnnotationType.SOURCES,
56
+ );
57
+ const toolData = getAnnotationData<ToolData>(
58
+ annotations,
59
+ MessageAnnotationType.TOOLS,
60
+ );
61
+
62
+ const contents: ContentDisplayConfig[] = [
63
+ {
64
+ order: -3,
65
+ component: imageData[0] ? <ChatImage data={imageData[0]} /> : null,
66
+ },
67
+ {
68
+ order: -2,
69
+ component:
70
+ eventData.length > 0 ? (
71
+ <ChatEvents isLoading={isLoading} data={eventData} />
72
+ ) : null,
73
+ },
74
+ {
75
+ order: -1,
76
+ component: toolData[0] ? <ChatTools data={toolData[0]} /> : null,
77
+ },
78
+ {
79
+ order: 0,
80
+ component: <Markdown content={message.content} />,
81
+ },
82
+ {
83
+ order: 1,
84
+ component: sourceData[0] ? <ChatSources data={sourceData[0]} /> : null,
85
+ },
86
+ ];
87
+
88
+ return (
89
+ <div className="flex-1 gap-4 flex flex-col">
90
+ {contents
91
+ .sort((a, b) => a.order - b.order)
92
+ .map((content, index) => (
93
+ <Fragment key={index}>{content.component}</Fragment>
94
+ ))}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ export default function ChatMessage({
100
+ chatMessage,
101
+ isLoading,
102
+ }: {
103
+ chatMessage: Message;
104
+ isLoading: boolean;
105
+ }) {
106
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
107
+ return (
108
+ <div className="flex items-start gap-4 pr-5 pt-5">
109
+ <ChatAvatar role={chatMessage.role} />
110
+ <div className="group flex flex-1 justify-between gap-2">
111
+ <ChatMessageContent message={chatMessage} isLoading={isLoading} />
112
+ <Button
113
+ onClick={() => copyToClipboard(chatMessage.content)}
114
+ size="icon"
115
+ variant="ghost"
116
+ className="h-8 w-8 opacity-0 group-hover:opacity-100"
117
+ >
118
+ {isCopied ? (
119
+ <Check className="h-4 w-4" />
120
+ ) : (
121
+ <Copy className="h-4 w-4" />
122
+ )}
123
+ </Button>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
app/components/ui/chat/chat-messages.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Loader2 } from "lucide-react";
2
+ import { useEffect, useRef } from "react";
3
+
4
+ import ChatActions from "./chat-actions";
5
+ import ChatMessage from "./chat-message";
6
+ import { ChatHandler } from "./chat.interface";
7
+
8
+ export default function ChatMessages(
9
+ props: Pick<ChatHandler, "messages" | "isLoading" | "reload" | "stop">,
10
+ ) {
11
+ const scrollableChatContainerRef = useRef<HTMLDivElement>(null);
12
+ const messageLength = props.messages.length;
13
+ const lastMessage = props.messages[messageLength - 1];
14
+
15
+ const scrollToBottom = () => {
16
+ if (scrollableChatContainerRef.current) {
17
+ scrollableChatContainerRef.current.scrollTop =
18
+ scrollableChatContainerRef.current.scrollHeight;
19
+ }
20
+ };
21
+
22
+ const isLastMessageFromAssistant =
23
+ messageLength > 0 && lastMessage?.role !== "user";
24
+ const showReload =
25
+ props.reload && !props.isLoading && isLastMessageFromAssistant;
26
+ const showStop = props.stop && props.isLoading;
27
+
28
+ // `isPending` indicate
29
+ // that stream response is not yet received from the server,
30
+ // so we show a loading indicator to give a better UX.
31
+ const isPending = props.isLoading && !isLastMessageFromAssistant;
32
+
33
+ useEffect(() => {
34
+ scrollToBottom();
35
+ }, [messageLength, lastMessage]);
36
+
37
+ return (
38
+ <div className="w-full rounded-xl bg-white p-4 shadow-xl pb-0">
39
+ <div
40
+ className="flex h-[50vh] flex-col gap-5 divide-y overflow-y-auto pb-4"
41
+ ref={scrollableChatContainerRef}
42
+ >
43
+ {props.messages.map((m, i) => {
44
+ const isLoadingMessage = i === messageLength - 1 && props.isLoading;
45
+ return (
46
+ <ChatMessage
47
+ key={m.id}
48
+ chatMessage={m}
49
+ isLoading={isLoadingMessage}
50
+ />
51
+ );
52
+ })}
53
+ {isPending && (
54
+ <div className="flex justify-center items-center pt-10">
55
+ <Loader2 className="h-4 w-4 animate-spin" />
56
+ </div>
57
+ )}
58
+ </div>
59
+ <div className="flex justify-end py-4">
60
+ <ChatActions
61
+ reload={props.reload}
62
+ stop={props.stop}
63
+ showReload={showReload}
64
+ showStop={showStop}
65
+ />
66
+ </div>
67
+ </div>
68
+ );
69
+ }
app/components/ui/chat/chat-sources.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Check, Copy } from "lucide-react";
2
+ import { useMemo } from "react";
3
+ import { Button } from "../button";
4
+ import { HoverCard, HoverCardContent, HoverCardTrigger } from "../hover-card";
5
+ import { getStaticFileDataUrl } from "../lib/url";
6
+ import { SourceData, SourceNode } from "./index";
7
+ import { useCopyToClipboard } from "./use-copy-to-clipboard";
8
+ import PdfDialog from "./widgets/PdfDialog";
9
+
10
+ const SCORE_THRESHOLD = 0.3;
11
+
12
+ function SourceNumberButton({ index }: { index: number }) {
13
+ return (
14
+ <div className="text-xs w-5 h-5 rounded-full bg-gray-100 mb-2 flex items-center justify-center hover:text-white hover:bg-primary hover:cursor-pointer">
15
+ {index + 1}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ enum NODE_TYPE {
21
+ URL,
22
+ FILE,
23
+ UNKNOWN,
24
+ }
25
+
26
+ type NodeInfo = {
27
+ id: string;
28
+ type: NODE_TYPE;
29
+ path?: string;
30
+ url?: string;
31
+ };
32
+
33
+ function getNodeInfo(node: SourceNode): NodeInfo {
34
+ if (typeof node.metadata["URL"] === "string") {
35
+ const url = node.metadata["URL"];
36
+ return {
37
+ id: node.id,
38
+ type: NODE_TYPE.URL,
39
+ path: url,
40
+ url,
41
+ };
42
+ }
43
+ if (typeof node.metadata["file_path"] === "string") {
44
+ const fileName = node.metadata["file_name"] as string;
45
+ return {
46
+ id: node.id,
47
+ type: NODE_TYPE.FILE,
48
+ path: node.metadata["file_path"],
49
+ url: getStaticFileDataUrl(fileName),
50
+ };
51
+ }
52
+
53
+ return {
54
+ id: node.id,
55
+ type: NODE_TYPE.UNKNOWN,
56
+ };
57
+ }
58
+
59
+ export function ChatSources({ data }: { data: SourceData }) {
60
+ const sources: NodeInfo[] = useMemo(() => {
61
+ // aggregate nodes by url or file_path (get the highest one by score)
62
+ const nodesByPath: { [path: string]: NodeInfo } = {};
63
+
64
+ data.nodes
65
+ .filter((node) => (node.score ?? 1) > SCORE_THRESHOLD)
66
+ .sort((a, b) => (b.score ?? 1) - (a.score ?? 1))
67
+ .forEach((node) => {
68
+ const nodeInfo = getNodeInfo(node);
69
+ const key = nodeInfo.path ?? nodeInfo.id; // use id as key for UNKNOWN type
70
+ if (!nodesByPath[key]) {
71
+ nodesByPath[key] = nodeInfo;
72
+ }
73
+ });
74
+
75
+ return Object.values(nodesByPath);
76
+ }, [data.nodes]);
77
+
78
+ if (sources.length === 0) return null;
79
+
80
+ return (
81
+ <div className="space-x-2 text-sm">
82
+ <span className="font-semibold">Sources:</span>
83
+ <div className="inline-flex gap-1 items-center">
84
+ {sources.map((nodeInfo: NodeInfo, index: number) => {
85
+ if (nodeInfo.path?.endsWith(".pdf")) {
86
+ return (
87
+ <PdfDialog
88
+ key={nodeInfo.id}
89
+ documentId={nodeInfo.id}
90
+ url={nodeInfo.url!}
91
+ path={nodeInfo.path}
92
+ trigger={<SourceNumberButton index={index} />}
93
+ />
94
+ );
95
+ }
96
+ return (
97
+ <div key={nodeInfo.id}>
98
+ <HoverCard>
99
+ <HoverCardTrigger>
100
+ <SourceNumberButton index={index} />
101
+ </HoverCardTrigger>
102
+ <HoverCardContent className="w-[320px]">
103
+ <NodeInfo nodeInfo={nodeInfo} />
104
+ </HoverCardContent>
105
+ </HoverCard>
106
+ </div>
107
+ );
108
+ })}
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ function NodeInfo({ nodeInfo }: { nodeInfo: NodeInfo }) {
115
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 1000 });
116
+
117
+ if (nodeInfo.type !== NODE_TYPE.UNKNOWN) {
118
+ // this is a node generated by the web loader or file loader,
119
+ // add a link to view its URL and a button to copy the URL to the clipboard
120
+ return (
121
+ <div className="flex items-center my-2">
122
+ <a className="hover:text-blue-900" href={nodeInfo.url} target="_blank">
123
+ <span>{nodeInfo.path}</span>
124
+ </a>
125
+ <Button
126
+ onClick={() => copyToClipboard(nodeInfo.path!)}
127
+ size="icon"
128
+ variant="ghost"
129
+ className="h-12 w-12 shrink-0"
130
+ >
131
+ {isCopied ? (
132
+ <Check className="h-4 w-4" />
133
+ ) : (
134
+ <Copy className="h-4 w-4" />
135
+ )}
136
+ </Button>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ // node generated by unknown loader, implement renderer by analyzing logged out metadata
142
+ return (
143
+ <p>
144
+ Sorry, unknown node type. Please add a new renderer in the NodeInfo
145
+ component.
146
+ </p>
147
+ );
148
+ }
app/components/ui/chat/chat-tools.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ToolData } from "./index";
2
+ import { WeatherCard, WeatherData } from "./widgets/WeatherCard";
3
+
4
+ // TODO: If needed, add displaying more tool outputs here
5
+ export default function ChatTools({ data }: { data: ToolData }) {
6
+ if (!data) return null;
7
+ const { toolCall, toolOutput } = data;
8
+
9
+ if (toolOutput.isError) {
10
+ return (
11
+ <div className="border-l-2 border-red-400 pl-2">
12
+ There was an error when calling the tool {toolCall.name} with input:{" "}
13
+ <br />
14
+ {JSON.stringify(toolCall.input)}
15
+ </div>
16
+ );
17
+ }
18
+
19
+ switch (toolCall.name) {
20
+ case "get_weather_information":
21
+ const weatherData = toolOutput.output as unknown as WeatherData;
22
+ return <WeatherCard data={weatherData} />;
23
+ default:
24
+ return null;
25
+ }
26
+ }
app/components/ui/chat/chat.interface.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Message } from "ai";
2
+
3
+ export interface ChatHandler {
4
+ messages: Message[];
5
+ input: string;
6
+ isLoading: boolean;
7
+ handleSubmit: (
8
+ e: React.FormEvent<HTMLFormElement>,
9
+ ops?: {
10
+ data?: any;
11
+ },
12
+ ) => void;
13
+ handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
14
+ reload?: () => void;
15
+ stop?: () => void;
16
+ onFileUpload?: (file: File) => Promise<void>;
17
+ onFileError?: (errMsg: string) => void;
18
+ }
app/components/ui/chat/codeblock.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Check, Copy, Download } from "lucide-react";
4
+ import { FC, memo } from "react";
5
+ import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter";
6
+ import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
7
+
8
+ import { Button } from "../button";
9
+ import { useCopyToClipboard } from "./use-copy-to-clipboard";
10
+
11
+ // TODO: Remove this when @type/react-syntax-highlighter is updated
12
+ const SyntaxHighlighter = Prism as unknown as FC<SyntaxHighlighterProps>;
13
+
14
+ interface Props {
15
+ language: string;
16
+ value: string;
17
+ }
18
+
19
+ interface languageMap {
20
+ [key: string]: string | undefined;
21
+ }
22
+
23
+ export const programmingLanguages: languageMap = {
24
+ javascript: ".js",
25
+ python: ".py",
26
+ java: ".java",
27
+ c: ".c",
28
+ cpp: ".cpp",
29
+ "c++": ".cpp",
30
+ "c#": ".cs",
31
+ ruby: ".rb",
32
+ php: ".php",
33
+ swift: ".swift",
34
+ "objective-c": ".m",
35
+ kotlin: ".kt",
36
+ typescript: ".ts",
37
+ go: ".go",
38
+ perl: ".pl",
39
+ rust: ".rs",
40
+ scala: ".scala",
41
+ haskell: ".hs",
42
+ lua: ".lua",
43
+ shell: ".sh",
44
+ sql: ".sql",
45
+ html: ".html",
46
+ css: ".css",
47
+ // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48
+ };
49
+
50
+ export const generateRandomString = (length: number, lowercase = false) => {
51
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
52
+ let result = "";
53
+ for (let i = 0; i < length; i++) {
54
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
55
+ }
56
+ return lowercase ? result.toLowerCase() : result;
57
+ };
58
+
59
+ const CodeBlock: FC<Props> = memo(({ language, value }) => {
60
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
61
+
62
+ const downloadAsFile = () => {
63
+ if (typeof window === "undefined") {
64
+ return;
65
+ }
66
+ const fileExtension = programmingLanguages[language] || ".file";
67
+ const suggestedFileName = `file-${generateRandomString(
68
+ 3,
69
+ true,
70
+ )}${fileExtension}`;
71
+ const fileName = window.prompt("Enter file name" || "", suggestedFileName);
72
+
73
+ if (!fileName) {
74
+ // User pressed cancel on prompt.
75
+ return;
76
+ }
77
+
78
+ const blob = new Blob([value], { type: "text/plain" });
79
+ const url = URL.createObjectURL(blob);
80
+ const link = document.createElement("a");
81
+ link.download = fileName;
82
+ link.href = url;
83
+ link.style.display = "none";
84
+ document.body.appendChild(link);
85
+ link.click();
86
+ document.body.removeChild(link);
87
+ URL.revokeObjectURL(url);
88
+ };
89
+
90
+ const onCopy = () => {
91
+ if (isCopied) return;
92
+ copyToClipboard(value);
93
+ };
94
+
95
+ return (
96
+ <div className="codeblock relative w-full bg-zinc-950 font-sans">
97
+ <div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
98
+ <span className="text-xs lowercase">{language}</span>
99
+ <div className="flex items-center space-x-1">
100
+ <Button variant="ghost" onClick={downloadAsFile} size="icon">
101
+ <Download />
102
+ <span className="sr-only">Download</span>
103
+ </Button>
104
+ <Button variant="ghost" size="icon" onClick={onCopy}>
105
+ {isCopied ? (
106
+ <Check className="h-4 w-4" />
107
+ ) : (
108
+ <Copy className="h-4 w-4" />
109
+ )}
110
+ <span className="sr-only">Copy code</span>
111
+ </Button>
112
+ </div>
113
+ </div>
114
+ <SyntaxHighlighter
115
+ language={language}
116
+ style={coldarkDark}
117
+ PreTag="div"
118
+ showLineNumbers
119
+ customStyle={{
120
+ width: "100%",
121
+ background: "transparent",
122
+ padding: "1.5rem 1rem",
123
+ borderRadius: "0.5rem",
124
+ }}
125
+ codeTagProps={{
126
+ style: {
127
+ fontSize: "0.9rem",
128
+ fontFamily: "var(--font-mono)",
129
+ },
130
+ }}
131
+ >
132
+ {value}
133
+ </SyntaxHighlighter>
134
+ </div>
135
+ );
136
+ });
137
+ CodeBlock.displayName = "CodeBlock";
138
+
139
+ export { CodeBlock };
app/components/ui/chat/index.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { JSONValue } from "ai";
2
+ import ChatInput from "./chat-input";
3
+ import ChatMessages from "./chat-messages";
4
+
5
+ export { type ChatHandler } from "./chat.interface";
6
+ export { ChatInput, ChatMessages };
7
+
8
+ export enum MessageAnnotationType {
9
+ IMAGE = "image",
10
+ SOURCES = "sources",
11
+ EVENTS = "events",
12
+ TOOLS = "tools",
13
+ }
14
+
15
+ export type ImageData = {
16
+ url: string;
17
+ };
18
+
19
+ export type SourceNode = {
20
+ id: string;
21
+ metadata: Record<string, unknown>;
22
+ score?: number;
23
+ text: string;
24
+ };
25
+
26
+ export type SourceData = {
27
+ nodes: SourceNode[];
28
+ };
29
+
30
+ export type EventData = {
31
+ title: string;
32
+ isCollapsed: boolean;
33
+ };
34
+
35
+ export type ToolData = {
36
+ toolCall: {
37
+ id: string;
38
+ name: string;
39
+ input: {
40
+ [key: string]: JSONValue;
41
+ };
42
+ };
43
+ toolOutput: {
44
+ output: JSONValue;
45
+ isError: boolean;
46
+ };
47
+ };
48
+
49
+ export type AnnotationData = ImageData | SourceData | EventData | ToolData;
50
+
51
+ export type MessageAnnotation = {
52
+ type: MessageAnnotationType;
53
+ data: AnnotationData;
54
+ };
app/components/ui/chat/markdown.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "katex/dist/katex.min.css";
2
+ import { FC, memo } from "react";
3
+ import ReactMarkdown, { Options } from "react-markdown";
4
+ import rehypeKatex from "rehype-katex";
5
+ import remarkGfm from "remark-gfm";
6
+ import remarkMath from "remark-math";
7
+
8
+ import { CodeBlock } from "./codeblock";
9
+
10
+ const MemoizedReactMarkdown: FC<Options> = memo(
11
+ ReactMarkdown,
12
+ (prevProps, nextProps) =>
13
+ prevProps.children === nextProps.children &&
14
+ prevProps.className === nextProps.className,
15
+ );
16
+
17
+ const preprocessLaTeX = (content: string) => {
18
+ // Replace block-level LaTeX delimiters \[ \] with $$ $$
19
+ const blockProcessedContent = content.replace(
20
+ /\\\[(.*?)\\\]/gs,
21
+ (_, equation) => `$$${equation}$$`,
22
+ );
23
+ // Replace inline LaTeX delimiters \( \) with $ $
24
+ const inlineProcessedContent = blockProcessedContent.replace(
25
+ /\\\((.*?)\\\)/gs,
26
+ (_, equation) => `$${equation}$`,
27
+ );
28
+ return inlineProcessedContent;
29
+ };
30
+
31
+ export default function Markdown({ content }: { content: string }) {
32
+ const processedContent = preprocessLaTeX(content);
33
+ return (
34
+ <MemoizedReactMarkdown
35
+ className="prose dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words custom-markdown"
36
+ remarkPlugins={[remarkGfm, remarkMath]}
37
+ rehypePlugins={[rehypeKatex as any]}
38
+ components={{
39
+ p({ children }) {
40
+ return <p className="mb-2 last:mb-0">{children}</p>;
41
+ },
42
+ code({ node, inline, className, children, ...props }) {
43
+ if (children.length) {
44
+ if (children[0] == "▍") {
45
+ return (
46
+ <span className="mt-1 animate-pulse cursor-default">▍</span>
47
+ );
48
+ }
49
+
50
+ children[0] = (children[0] as string).replace("`▍`", "▍");
51
+ }
52
+
53
+ const match = /language-(\w+)/.exec(className || "");
54
+
55
+ if (inline) {
56
+ return (
57
+ <code className={className} {...props}>
58
+ {children}
59
+ </code>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <CodeBlock
65
+ key={Math.random()}
66
+ language={(match && match[1]) || ""}
67
+ value={String(children).replace(/\n$/, "")}
68
+ {...props}
69
+ />
70
+ );
71
+ },
72
+ }}
73
+ >
74
+ {processedContent}
75
+ </MemoizedReactMarkdown>
76
+ );
77
+ }
app/components/ui/chat/use-copy-to-clipboard.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ export interface useCopyToClipboardProps {
6
+ timeout?: number;
7
+ }
8
+
9
+ export function useCopyToClipboard({
10
+ timeout = 2000,
11
+ }: useCopyToClipboardProps) {
12
+ const [isCopied, setIsCopied] = React.useState<Boolean>(false);
13
+
14
+ const copyToClipboard = (value: string) => {
15
+ if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
16
+ return;
17
+ }
18
+
19
+ if (!value) {
20
+ return;
21
+ }
22
+
23
+ navigator.clipboard.writeText(value).then(() => {
24
+ setIsCopied(true);
25
+
26
+ setTimeout(() => {
27
+ setIsCopied(false);
28
+ }, timeout);
29
+ });
30
+ };
31
+
32
+ return { isCopied, copyToClipboard };
33
+ }
app/components/ui/chat/widgets/PdfDialog.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PDFViewer, PdfFocusProvider } from "@llamaindex/pdf-viewer";
2
+ import { Button } from "../../button";
3
+ import {
4
+ Drawer,
5
+ DrawerClose,
6
+ DrawerContent,
7
+ DrawerDescription,
8
+ DrawerHeader,
9
+ DrawerTitle,
10
+ DrawerTrigger,
11
+ } from "../../drawer";
12
+
13
+ export interface PdfDialogProps {
14
+ documentId: string;
15
+ path: string;
16
+ url: string;
17
+ trigger: React.ReactNode;
18
+ }
19
+
20
+ export default function PdfDialog(props: PdfDialogProps) {
21
+ return (
22
+ <Drawer direction="left">
23
+ <DrawerTrigger>{props.trigger}</DrawerTrigger>
24
+ <DrawerContent className="w-3/5 mt-24 h-full max-h-[96%] ">
25
+ <DrawerHeader className="flex justify-between">
26
+ <div className="space-y-2">
27
+ <DrawerTitle>PDF Content</DrawerTitle>
28
+ <DrawerDescription>
29
+ File path:{" "}
30
+ <a
31
+ className="hover:text-blue-900"
32
+ href={props.url}
33
+ target="_blank"
34
+ >
35
+ {props.path}
36
+ </a>
37
+ </DrawerDescription>
38
+ </div>
39
+ <DrawerClose asChild>
40
+ <Button variant="outline">Close</Button>
41
+ </DrawerClose>
42
+ </DrawerHeader>
43
+ <div className="m-4">
44
+ <PdfFocusProvider>
45
+ <PDFViewer
46
+ file={{
47
+ id: props.documentId,
48
+ url: props.url,
49
+ }}
50
+ />
51
+ </PdfFocusProvider>
52
+ </div>
53
+ </DrawerContent>
54
+ </Drawer>
55
+ );
56
+ }
app/components/ui/chat/widgets/WeatherCard.tsx ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface WeatherData {
2
+ latitude: number;
3
+ longitude: number;
4
+ generationtime_ms: number;
5
+ utc_offset_seconds: number;
6
+ timezone: string;
7
+ timezone_abbreviation: string;
8
+ elevation: number;
9
+ current_units: {
10
+ time: string;
11
+ interval: string;
12
+ temperature_2m: string;
13
+ weather_code: string;
14
+ };
15
+ current: {
16
+ time: string;
17
+ interval: number;
18
+ temperature_2m: number;
19
+ weather_code: number;
20
+ };
21
+ hourly_units: {
22
+ time: string;
23
+ temperature_2m: string;
24
+ weather_code: string;
25
+ };
26
+ hourly: {
27
+ time: string[];
28
+ temperature_2m: number[];
29
+ weather_code: number[];
30
+ };
31
+ daily_units: {
32
+ time: string;
33
+ weather_code: string;
34
+ };
35
+ daily: {
36
+ time: string[];
37
+ weather_code: number[];
38
+ };
39
+ }
40
+
41
+ // Follow WMO Weather interpretation codes (WW)
42
+ const weatherCodeDisplayMap: Record<
43
+ string,
44
+ {
45
+ icon: JSX.Element;
46
+ status: string;
47
+ }
48
+ > = {
49
+ "0": {
50
+ icon: <span>☀️</span>,
51
+ status: "Clear sky",
52
+ },
53
+ "1": {
54
+ icon: <span>🌤️</span>,
55
+ status: "Mainly clear",
56
+ },
57
+ "2": {
58
+ icon: <span>☁️</span>,
59
+ status: "Partly cloudy",
60
+ },
61
+ "3": {
62
+ icon: <span>☁️</span>,
63
+ status: "Overcast",
64
+ },
65
+ "45": {
66
+ icon: <span>🌫️</span>,
67
+ status: "Fog",
68
+ },
69
+ "48": {
70
+ icon: <span>🌫️</span>,
71
+ status: "Depositing rime fog",
72
+ },
73
+ "51": {
74
+ icon: <span>🌧️</span>,
75
+ status: "Drizzle",
76
+ },
77
+ "53": {
78
+ icon: <span>🌧️</span>,
79
+ status: "Drizzle",
80
+ },
81
+ "55": {
82
+ icon: <span>🌧️</span>,
83
+ status: "Drizzle",
84
+ },
85
+ "56": {
86
+ icon: <span>🌧️</span>,
87
+ status: "Freezing Drizzle",
88
+ },
89
+ "57": {
90
+ icon: <span>🌧️</span>,
91
+ status: "Freezing Drizzle",
92
+ },
93
+ "61": {
94
+ icon: <span>🌧️</span>,
95
+ status: "Rain",
96
+ },
97
+ "63": {
98
+ icon: <span>🌧️</span>,
99
+ status: "Rain",
100
+ },
101
+ "65": {
102
+ icon: <span>🌧️</span>,
103
+ status: "Rain",
104
+ },
105
+ "66": {
106
+ icon: <span>🌧️</span>,
107
+ status: "Freezing Rain",
108
+ },
109
+ "67": {
110
+ icon: <span>🌧️</span>,
111
+ status: "Freezing Rain",
112
+ },
113
+ "71": {
114
+ icon: <span>❄️</span>,
115
+ status: "Snow fall",
116
+ },
117
+ "73": {
118
+ icon: <span>❄️</span>,
119
+ status: "Snow fall",
120
+ },
121
+ "75": {
122
+ icon: <span>❄️</span>,
123
+ status: "Snow fall",
124
+ },
125
+ "77": {
126
+ icon: <span>❄️</span>,
127
+ status: "Snow grains",
128
+ },
129
+ "80": {
130
+ icon: <span>🌧️</span>,
131
+ status: "Rain showers",
132
+ },
133
+ "81": {
134
+ icon: <span>🌧️</span>,
135
+ status: "Rain showers",
136
+ },
137
+ "82": {
138
+ icon: <span>🌧️</span>,
139
+ status: "Rain showers",
140
+ },
141
+ "85": {
142
+ icon: <span>❄️</span>,
143
+ status: "Snow showers",
144
+ },
145
+ "86": {
146
+ icon: <span>❄️</span>,
147
+ status: "Snow showers",
148
+ },
149
+ "95": {
150
+ icon: <span>⛈️</span>,
151
+ status: "Thunderstorm",
152
+ },
153
+ "96": {
154
+ icon: <span>⛈️</span>,
155
+ status: "Thunderstorm",
156
+ },
157
+ "99": {
158
+ icon: <span>⛈️</span>,
159
+ status: "Thunderstorm",
160
+ },
161
+ };
162
+
163
+ const displayDay = (time: string) => {
164
+ return new Date(time).toLocaleDateString("en-US", {
165
+ weekday: "long",
166
+ });
167
+ };
168
+
169
+ export function WeatherCard({ data }: { data: WeatherData }) {
170
+ const currentDayString = new Date(data.current.time).toLocaleDateString(
171
+ "en-US",
172
+ {
173
+ weekday: "long",
174
+ month: "long",
175
+ day: "numeric",
176
+ },
177
+ );
178
+
179
+ return (
180
+ <div className="bg-[#61B9F2] rounded-2xl shadow-xl p-5 space-y-4 text-white w-fit">
181
+ <div className="flex justify-between">
182
+ <div className="space-y-2">
183
+ <div className="text-xl">{currentDayString}</div>
184
+ <div className="text-5xl font-semibold flex gap-4">
185
+ <span>
186
+ {data.current.temperature_2m} {data.current_units.temperature_2m}
187
+ </span>
188
+ {weatherCodeDisplayMap[data.current.weather_code].icon}
189
+ </div>
190
+ </div>
191
+ <span className="text-xl">
192
+ {weatherCodeDisplayMap[data.current.weather_code].status}
193
+ </span>
194
+ </div>
195
+ <div className="gap-2 grid grid-cols-6">
196
+ {data.daily.time.map((time, index) => {
197
+ if (index === 0) return null; // skip the current day
198
+ return (
199
+ <div key={time} className="flex flex-col items-center gap-4">
200
+ <span>{displayDay(time)}</span>
201
+ <div className="text-4xl">
202
+ {weatherCodeDisplayMap[data.daily.weather_code[index]].icon}
203
+ </div>
204
+ <span className="text-sm">
205
+ {weatherCodeDisplayMap[data.daily.weather_code[index]].status}
206
+ </span>
207
+ </div>
208
+ );
209
+ })}
210
+ </div>
211
+ </div>
212
+ );
213
+ }
app/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root;
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10
+
11
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger };
app/components/ui/drawer.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Drawer as DrawerPrimitive } from "vaul";
5
+
6
+ import { cn } from "./lib/utils";
7
+
8
+ const Drawer = ({
9
+ shouldScaleBackground = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
12
+ <DrawerPrimitive.Root
13
+ shouldScaleBackground={shouldScaleBackground}
14
+ {...props}
15
+ />
16
+ );
17
+ Drawer.displayName = "Drawer";
18
+
19
+ const DrawerTrigger = DrawerPrimitive.Trigger;
20
+
21
+ const DrawerPortal = DrawerPrimitive.Portal;
22
+
23
+ const DrawerClose = DrawerPrimitive.Close;
24
+
25
+ const DrawerOverlay = React.forwardRef<
26
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
27
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
28
+ >(({ className, ...props }, ref) => (
29
+ <DrawerPrimitive.Overlay
30
+ ref={ref}
31
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
32
+ {...props}
33
+ />
34
+ ));
35
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36
+
37
+ const DrawerContent = React.forwardRef<
38
+ React.ElementRef<typeof DrawerPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <DrawerPortal>
42
+ <DrawerOverlay />
43
+ <DrawerPrimitive.Content
44
+ ref={ref}
45
+ className={cn(
46
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
47
+ className,
48
+ )}
49
+ {...props}
50
+ >
51
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
52
+ {children}
53
+ </DrawerPrimitive.Content>
54
+ </DrawerPortal>
55
+ ));
56
+ DrawerContent.displayName = "DrawerContent";
57
+
58
+ const DrawerHeader = ({
59
+ className,
60
+ ...props
61
+ }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div
63
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
64
+ {...props}
65
+ />
66
+ );
67
+ DrawerHeader.displayName = "DrawerHeader";
68
+
69
+ const DrawerFooter = ({
70
+ className,
71
+ ...props
72
+ }: React.HTMLAttributes<HTMLDivElement>) => (
73
+ <div
74
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
75
+ {...props}
76
+ />
77
+ );
78
+ DrawerFooter.displayName = "DrawerFooter";
79
+
80
+ const DrawerTitle = React.forwardRef<
81
+ React.ElementRef<typeof DrawerPrimitive.Title>,
82
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
83
+ >(({ className, ...props }, ref) => (
84
+ <DrawerPrimitive.Title
85
+ ref={ref}
86
+ className={cn(
87
+ "text-lg font-semibold leading-none tracking-tight",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ ));
93
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94
+
95
+ const DrawerDescription = React.forwardRef<
96
+ React.ElementRef<typeof DrawerPrimitive.Description>,
97
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
98
+ >(({ className, ...props }, ref) => (
99
+ <DrawerPrimitive.Description
100
+ ref={ref}
101
+ className={cn("text-sm text-muted-foreground", className)}
102
+ {...props}
103
+ />
104
+ ));
105
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106
+
107
+ export {
108
+ Drawer,
109
+ DrawerClose,
110
+ DrawerContent,
111
+ DrawerDescription,
112
+ DrawerFooter,
113
+ DrawerHeader,
114
+ DrawerOverlay,
115
+ DrawerPortal,
116
+ DrawerTitle,
117
+ DrawerTrigger,
118
+ };
app/components/ui/file-uploader.tsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Loader2, Paperclip } from "lucide-react";
4
+ import { ChangeEvent, useState } from "react";
5
+ import { buttonVariants } from "./button";
6
+ import { cn } from "./lib/utils";
7
+
8
+ export interface FileUploaderProps {
9
+ config?: {
10
+ inputId?: string;
11
+ fileSizeLimit?: number;
12
+ allowedExtensions?: string[];
13
+ checkExtension?: (extension: string) => string | null;
14
+ disabled: boolean;
15
+ };
16
+ onFileUpload: (file: File) => Promise<void>;
17
+ onFileError?: (errMsg: string) => void;
18
+ }
19
+
20
+ const DEFAULT_INPUT_ID = "fileInput";
21
+ const DEFAULT_FILE_SIZE_LIMIT = 1024 * 1024 * 50; // 50 MB
22
+
23
+ export default function FileUploader({
24
+ config,
25
+ onFileUpload,
26
+ onFileError,
27
+ }: FileUploaderProps) {
28
+ const [uploading, setUploading] = useState(false);
29
+
30
+ const inputId = config?.inputId || DEFAULT_INPUT_ID;
31
+ const fileSizeLimit = config?.fileSizeLimit || DEFAULT_FILE_SIZE_LIMIT;
32
+ const allowedExtensions = config?.allowedExtensions;
33
+ const defaultCheckExtension = (extension: string) => {
34
+ if (allowedExtensions && !allowedExtensions.includes(extension)) {
35
+ return `Invalid file type. Please select a file with one of these formats: ${allowedExtensions!.join(
36
+ ",",
37
+ )}`;
38
+ }
39
+ return null;
40
+ };
41
+ const checkExtension = config?.checkExtension ?? defaultCheckExtension;
42
+
43
+ const isFileSizeExceeded = (file: File) => {
44
+ return file.size > fileSizeLimit;
45
+ };
46
+
47
+ const resetInput = () => {
48
+ const fileInput = document.getElementById(inputId) as HTMLInputElement;
49
+ fileInput.value = "";
50
+ };
51
+
52
+ const onFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
53
+ const file = e.target.files?.[0];
54
+ if (!file) return;
55
+
56
+ setUploading(true);
57
+ await handleUpload(file);
58
+ resetInput();
59
+ setUploading(false);
60
+ };
61
+
62
+ const handleUpload = async (file: File) => {
63
+ const onFileUploadError = onFileError || window.alert;
64
+ const fileExtension = file.name.split(".").pop() || "";
65
+ const extensionFileError = checkExtension(fileExtension);
66
+ if (extensionFileError) {
67
+ return onFileUploadError(extensionFileError);
68
+ }
69
+
70
+ if (isFileSizeExceeded(file)) {
71
+ return onFileUploadError(
72
+ `File size exceeded. Limit is ${fileSizeLimit / 1024 / 1024} MB`,
73
+ );
74
+ }
75
+
76
+ await onFileUpload(file);
77
+ };
78
+
79
+ return (
80
+ <div className="self-stretch">
81
+ <input
82
+ type="file"
83
+ id={inputId}
84
+ style={{ display: "none" }}
85
+ onChange={onFileChange}
86
+ accept={allowedExtensions?.join(",")}
87
+ disabled={config?.disabled || uploading}
88
+ />
89
+ <label
90
+ htmlFor={inputId}
91
+ className={cn(
92
+ buttonVariants({ variant: "secondary", size: "icon" }),
93
+ "cursor-pointer",
94
+ uploading && "opacity-50",
95
+ )}
96
+ >
97
+ {uploading ? (
98
+ <Loader2 className="h-4 w-4 animate-spin" />
99
+ ) : (
100
+ <Paperclip className="-rotate-45 w-4 h-4" />
101
+ )}
102
+ </label>
103
+ </div>
104
+ );
105
+ }
app/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "./lib/utils";
7
+
8
+ const HoverCard = HoverCardPrimitive.Root;
9
+
10
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
11
+
12
+ const HoverCardContent = React.forwardRef<
13
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
15
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16
+ <HoverCardPrimitive.Content
17
+ ref={ref}
18
+ align={align}
19
+ sideOffset={sideOffset}
20
+ className={cn(
21
+ "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
22
+ className,
23
+ )}
24
+ {...props}
25
+ />
26
+ ));
27
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28
+
29
+ export { HoverCard, HoverCardContent, HoverCardTrigger };
app/components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./lib/utils";
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className,
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ );
21
+ },
22
+ );
23
+ Input.displayName = "Input";
24
+
25
+ export { Input };
app/components/ui/lib/url.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const STORAGE_FOLDER = "data";
2
+
3
+ export const getStaticFileDataUrl = (filename: string) => {
4
+ const isUsingBackend = !!process.env.NEXT_PUBLIC_CHAT_API;
5
+ const fileUrl = `/api/${STORAGE_FOLDER}/${filename}`;
6
+ if (isUsingBackend) {
7
+ const backendOrigin = new URL(process.env.NEXT_PUBLIC_CHAT_API!).origin;
8
+ return `${backendOrigin}/${fileUrl}`;
9
+ }
10
+ return fileUrl;
11
+ };
app/components/ui/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
app/components/ui/upload-image-preview.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { XCircleIcon } from "lucide-react";
2
+ import Image from "next/image";
3
+ import { cn } from "./lib/utils";
4
+
5
+ export default function UploadImagePreview({
6
+ url,
7
+ onRemove,
8
+ }: {
9
+ url: string;
10
+ onRemove: () => void;
11
+ }) {
12
+ return (
13
+ <div className="relative w-20 h-20 group">
14
+ <Image
15
+ src={url}
16
+ alt="Uploaded image"
17
+ fill
18
+ className="object-cover w-full h-full rounded-xl hover:brightness-75"
19
+ />
20
+ <div
21
+ className={cn(
22
+ "absolute -top-2 -right-2 w-6 h-6 z-10 bg-gray-500 text-white rounded-full hidden group-hover:block",
23
+ )}
24
+ >
25
+ <XCircleIcon
26
+ className="w-6 h-6 bg-gray-500 text-white rounded-full"
27
+ onClick={onRemove}
28
+ />
29
+ </div>
30
+ </div>
31
+ );
32
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 47.4% 11.2%;
9
+
10
+ --muted: 210 40% 96.1%;
11
+ --muted-foreground: 215.4 16.3% 46.9%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 222.2 47.4% 11.2%;
15
+
16
+ --border: 214.3 31.8% 91.4%;
17
+ --input: 214.3 31.8% 91.4%;
18
+
19
+ --card: 0 0% 100%;
20
+ --card-foreground: 222.2 47.4% 11.2%;
21
+
22
+ --primary: 222.2 47.4% 11.2%;
23
+ --primary-foreground: 210 40% 98%;
24
+
25
+ --secondary: 210 40% 96.1%;
26
+ --secondary-foreground: 222.2 47.4% 11.2%;
27
+
28
+ --accent: 210 40% 96.1%;
29
+ --accent-foreground: 222.2 47.4% 11.2%;
30
+
31
+ --destructive: 0 100% 50%;
32
+ --destructive-foreground: 210 40% 98%;
33
+
34
+ --ring: 215 20.2% 65.1%;
35
+
36
+ --radius: 0.5rem;
37
+ }
38
+
39
+ .dark {
40
+ --background: 224 71% 4%;
41
+ --foreground: 213 31% 91%;
42
+
43
+ --muted: 223 47% 11%;
44
+ --muted-foreground: 215.4 16.3% 56.9%;
45
+
46
+ --accent: 216 34% 17%;
47
+ --accent-foreground: 210 40% 98%;
48
+
49
+ --popover: 224 71% 4%;
50
+ --popover-foreground: 215 20.2% 65.1%;
51
+
52
+ --border: 216 34% 17%;
53
+ --input: 216 34% 17%;
54
+
55
+ --card: 224 71% 4%;
56
+ --card-foreground: 213 31% 91%;
57
+
58
+ --primary: 210 40% 98%;
59
+ --primary-foreground: 222.2 47.4% 1.2%;
60
+
61
+ --secondary: 222.2 47.4% 11.2%;
62
+ --secondary-foreground: 210 40% 98%;
63
+
64
+ --destructive: 0 63% 31%;
65
+ --destructive-foreground: 210 40% 98%;
66
+
67
+ --ring: 216 34% 17%;
68
+
69
+ --radius: 0.5rem;
70
+ }
71
+ }
72
+
73
+ @layer base {
74
+ * {
75
+ @apply border-border;
76
+ }
77
+ body {
78
+ @apply bg-background text-foreground;
79
+ font-feature-settings:
80
+ "rlig" 1,
81
+ "calt" 1;
82
+ }
83
+ .background-gradient {
84
+ background-color: #fff;
85
+ background-image: radial-gradient(
86
+ at 21% 11%,
87
+ rgba(186, 186, 233, 0.53) 0,
88
+ transparent 50%
89
+ ),
90
+ radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%),
91
+ radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%),
92
+ radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%);
93
+ }
94
+ }
app/layout.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+ import "./markdown.css";
5
+
6
+ const inter = Inter({ subsets: ["latin"] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: "Create Llama App",
10
+ description: "Generated by create-llama",
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={inter.className}>{children}</body>
21
+ </html>
22
+ );
23
+ }
app/markdown.css ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom CSS for chat message markdown */
2
+ .custom-markdown ul {
3
+ list-style-type: disc;
4
+ margin-left: 20px;
5
+ }
6
+
7
+ .custom-markdown ol {
8
+ list-style-type: decimal;
9
+ margin-left: 20px;
10
+ }
11
+
12
+ .custom-markdown li {
13
+ margin-bottom: 5px;
14
+ }
15
+
16
+ .custom-markdown ol ol {
17
+ list-style: lower-alpha;
18
+ }
19
+
20
+ .custom-markdown ul ul,
21
+ .custom-markdown ol ol {
22
+ margin-left: 20px;
23
+ }
app/observability/index.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as traceloop from "@traceloop/node-server-sdk";
2
+ import * as LlamaIndex from "llamaindex";
3
+
4
+ export const initObservability = () => {
5
+ traceloop.initialize({
6
+ appName: "llama-app",
7
+ disableBatch: true,
8
+ instrumentModules: {
9
+ llamaIndex: LlamaIndex,
10
+ },
11
+ });
12
+ };
app/page.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Header from "@/app/components/header";
2
+ import ChatSection from "./components/chat-section";
3
+
4
+ export default function Home() {
5
+ return (
6
+ <main className="flex min-h-screen flex-col items-center gap-10 p-24 background-gradient">
7
+ <Header />
8
+ <ChatSection />
9
+ </main>
10
+ );
11
+ }
next.config.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "experimental": {
3
+ "outputFileTracingIncludes": {
4
+ "/*": [
5
+ "./cache/**/*"
6
+ ]
7
+ },
8
+ "serverComponentsExternalPackages": [
9
+ "sharp",
10
+ "onnxruntime-node"
11
+ ]
12
+ },
13
+ "output": "export",
14
+ "images": {
15
+ "unoptimized": true
16
+ }
17
+ }
next.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ import fs from "fs";
3
+ import webpack from "./webpack.config.mjs";
4
+
5
+ const nextConfig = JSON.parse(fs.readFileSync("./next.config.json", "utf-8"));
6
+ nextConfig.webpack = webpack;
7
+
8
+ export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "codepath-project",
3
+ "version": "0.1.0",
4
+ "scripts": {
5
+ "format": "prettier --ignore-unknown --cache --check .",
6
+ "format:write": "prettier --ignore-unknown --write .",
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint",
11
+ "generate": "tsx app/api/chat/engine/generate.ts"
12
+ },
13
+ "dependencies": {
14
+ "@radix-ui/react-collapsible": "^1.0.3",
15
+ "@radix-ui/react-hover-card": "^1.0.7",
16
+ "@radix-ui/react-slot": "^1.0.2",
17
+ "ai": "^3.0.21",
18
+ "ajv": "^8.12.0",
19
+ "class-variance-authority": "^0.7.0",
20
+ "clsx": "^2.1.1",
21
+ "dotenv": "^16.3.1",
22
+ "llamaindex": "0.3.13",
23
+ "lucide-react": "^0.294.0",
24
+ "next": "^14.0.3",
25
+ "pdf2json": "3.0.5",
26
+ "react": "^18.2.0",
27
+ "react-dom": "^18.2.0",
28
+ "react-markdown": "^8.0.7",
29
+ "react-syntax-highlighter": "^15.5.0",
30
+ "remark": "^14.0.3",
31
+ "remark-code-import": "^1.2.0",
32
+ "remark-gfm": "^3.0.1",
33
+ "remark-math": "^5.1.1",
34
+ "rehype-katex": "^7.0.0",
35
+ "supports-color": "^8.1.1",
36
+ "tailwind-merge": "^2.1.0",
37
+ "vaul": "^0.9.1",
38
+ "@llamaindex/pdf-viewer": "^1.1.1",
39
+ "@traceloop/node-server-sdk": "^0.5.19"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.10.3",
43
+ "@types/react": "^18.2.42",
44
+ "@types/react-dom": "^18.2.17",
45
+ "@types/react-syntax-highlighter": "^15.5.11",
46
+ "autoprefixer": "^10.4.16",
47
+ "cross-env": "^7.0.3",
48
+ "eslint": "^8.55.0",
49
+ "eslint-config-next": "^14.0.3",
50
+ "eslint-config-prettier": "^8.10.0",
51
+ "postcss": "^8.4.32",
52
+ "prettier": "^3.2.5",
53
+ "prettier-plugin-organize-imports": "^3.2.4",
54
+ "tailwindcss": "^3.3.6",
55
+ "tsx": "^4.7.2",
56
+ "typescript": "^5.3.2",
57
+ "node-loader": "^2.0.0"
58
+ }
59
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
prettier.config.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ module.exports = {
2
+ plugins: ["prettier-plugin-organize-imports"],
3
+ };
public/llama.png ADDED
tailwind.config.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from "tailwindcss";
2
+ import { fontFamily } from "tailwindcss/defaultTheme";
3
+
4
+ const config: Config = {
5
+ darkMode: ["class"],
6
+ content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
7
+ theme: {
8
+ container: {
9
+ center: true,
10
+ padding: "2rem",
11
+ screens: {
12
+ "2xl": "1400px",
13
+ },
14
+ },
15
+ extend: {
16
+ colors: {
17
+ border: "hsl(var(--border))",
18
+ input: "hsl(var(--input))",
19
+ ring: "hsl(var(--ring))",
20
+ background: "hsl(var(--background))",
21
+ foreground: "hsl(var(--foreground))",
22
+ primary: {
23
+ DEFAULT: "hsl(var(--primary))",
24
+ foreground: "hsl(var(--primary-foreground))",
25
+ },
26
+ secondary: {
27
+ DEFAULT: "hsl(var(--secondary))",
28
+ foreground: "hsl(var(--secondary-foreground))",
29
+ },
30
+ destructive: {
31
+ DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
32
+ foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
33
+ },
34
+ muted: {
35
+ DEFAULT: "hsl(var(--muted))",
36
+ foreground: "hsl(var(--muted-foreground))",
37
+ },
38
+ accent: {
39
+ DEFAULT: "hsl(var(--accent))",
40
+ foreground: "hsl(var(--accent-foreground))",
41
+ },
42
+ popover: {
43
+ DEFAULT: "hsl(var(--popover))",
44
+ foreground: "hsl(var(--popover-foreground))",
45
+ },
46
+ card: {
47
+ DEFAULT: "hsl(var(--card))",
48
+ foreground: "hsl(var(--card-foreground))",
49
+ },
50
+ },
51
+ borderRadius: {
52
+ xl: `calc(var(--radius) + 4px)`,
53
+ lg: `var(--radius)`,
54
+ md: `calc(var(--radius) - 2px)`,
55
+ sm: "calc(var(--radius) - 4px)",
56
+ },
57
+ fontFamily: {
58
+ sans: ["var(--font-sans)", ...fontFamily.sans],
59
+ },
60
+ keyframes: {
61
+ "accordion-down": {
62
+ from: { height: "0" },
63
+ to: { height: "var(--radix-accordion-content-height)" },
64
+ },
65
+ "accordion-up": {
66
+ from: { height: "var(--radix-accordion-content-height)" },
67
+ to: { height: "0" },
68
+ },
69
+ },
70
+ animation: {
71
+ "accordion-down": "accordion-down 0.2s ease-out",
72
+ "accordion-up": "accordion-up 0.2s ease-out",
73
+ },
74
+ },
75
+ },
76
+ plugins: [],
77
+ };
78
+ export default config;
tsconfig.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ },
24
+ "forceConsistentCasingInFileNames": true
25
+ },
26
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27
+ "exclude": ["node_modules"]
28
+ }
webpack.config.mjs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function webpack(config, isServer) {
2
+ config.resolve.fallback = {
3
+ aws4: false,
4
+ };
5
+ config.module.rules.push({
6
+ test: /\.node$/,
7
+ loader: "node-loader",
8
+ });
9
+ if (isServer) {
10
+ config.ignoreWarnings = [{ module: /opentelemetry/ }];
11
+ }
12
+ return config;
13
+ }