Spaces:
Sleeping
Sleeping
Praneeth Yerrapragada commited on
Commit ·
88be6fd
0
Parent(s):
feat: repo setup
Browse files- .env.example +3 -0
- .eslintrc.json +7 -0
- .gitignore +36 -0
- Dockerfile +16 -0
- README.md +71 -0
- app/components/chat-section.tsx +44 -0
- app/components/header.tsx +28 -0
- app/components/ui/README.md +1 -0
- app/components/ui/button.tsx +56 -0
- app/components/ui/chat/chat-actions.tsx +28 -0
- app/components/ui/chat/chat-avatar.tsx +25 -0
- app/components/ui/chat/chat-events.tsx +48 -0
- app/components/ui/chat/chat-image.tsx +17 -0
- app/components/ui/chat/chat-input.tsx +84 -0
- app/components/ui/chat/chat-message.tsx +127 -0
- app/components/ui/chat/chat-messages.tsx +69 -0
- app/components/ui/chat/chat-sources.tsx +148 -0
- app/components/ui/chat/chat-tools.tsx +26 -0
- app/components/ui/chat/chat.interface.ts +18 -0
- app/components/ui/chat/codeblock.tsx +139 -0
- app/components/ui/chat/index.ts +54 -0
- app/components/ui/chat/markdown.tsx +77 -0
- app/components/ui/chat/use-copy-to-clipboard.tsx +33 -0
- app/components/ui/chat/widgets/PdfDialog.tsx +56 -0
- app/components/ui/chat/widgets/WeatherCard.tsx +213 -0
- app/components/ui/collapsible.tsx +11 -0
- app/components/ui/drawer.tsx +118 -0
- app/components/ui/file-uploader.tsx +105 -0
- app/components/ui/hover-card.tsx +29 -0
- app/components/ui/input.tsx +25 -0
- app/components/ui/lib/url.ts +11 -0
- app/components/ui/lib/utils.ts +6 -0
- app/components/ui/upload-image-preview.tsx +32 -0
- app/favicon.ico +0 -0
- app/globals.css +94 -0
- app/layout.tsx +23 -0
- app/markdown.css +23 -0
- app/observability/index.ts +12 -0
- app/page.tsx +11 -0
- next.config.json +17 -0
- next.config.mjs +8 -0
- package-lock.json +0 -0
- package.json +59 -0
- postcss.config.js +6 -0
- prettier.config.js +3 -0
- public/llama.png +0 -0
- tailwind.config.ts +78 -0
- tsconfig.json +28 -0
- 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
|
| 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 |
+
}
|