├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── dependabot.yml
├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc.json
├── .prettierrc.js
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── api
│ ├── chat
│ │ ├── engine
│ │ │ ├── chat.ts
│ │ │ ├── generate.ts
│ │ │ └── index.ts
│ │ ├── llamaindex
│ │ │ └── streaming
│ │ │ │ └── events.ts
│ │ ├── route.ts
│ │ └── upload
│ │ │ └── route.ts
│ └── share
│ │ └── route.ts
├── b
│ └── [botId]
│ │ └── page.tsx
├── components
│ ├── bot
│ │ ├── bot-item.tsx
│ │ ├── bot-list.tsx
│ │ ├── bot-options
│ │ │ ├── delete-bot-dialog.tsx
│ │ │ ├── edit-bot-dialog.tsx
│ │ │ ├── index.tsx
│ │ │ └── share-bot-dialog.tsx
│ │ ├── bot-settings
│ │ │ ├── bot-config.tsx
│ │ │ ├── config-item.tsx
│ │ │ ├── context-prompt.tsx
│ │ │ ├── index.tsx
│ │ │ └── model-config.tsx
│ │ └── use-bot.tsx
│ ├── chat
│ │ ├── chat-header.tsx
│ │ ├── chat-session.tsx
│ │ ├── index.tsx
│ │ └── useChatSession.ts
│ ├── home.tsx
│ ├── layout
│ │ ├── error.tsx
│ │ ├── sidebar.tsx
│ │ ├── theme-provider.tsx
│ │ └── theme-toggle.tsx
│ ├── settings.tsx
│ └── ui
│ │ ├── alert-dialog.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── emoji.tsx
│ │ ├── hover-card.tsx
│ │ ├── image-preview.tsx
│ │ ├── input.tsx
│ │ ├── loading.tsx
│ │ ├── markdown.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── tooltip.tsx
│ │ ├── typography.tsx
│ │ └── use-toast.ts
├── constant.ts
├── layout.tsx
├── lib
│ └── utils.ts
├── locales
│ ├── en.ts
│ └── index.ts
├── page.tsx
├── store
│ ├── bot.data.ts
│ └── bot.ts
├── styles
│ ├── globals.css
│ └── lib
│ │ ├── highlight.css
│ │ └── markdown.css
└── utils
│ ├── clipboard.ts
│ ├── download.ts
│ └── mobile.ts
├── components.json
├── datasources
└── .gitignore
├── docker-compose.yml
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-2048x2048.png
├── favicon-32x32.png
├── favicon.ico
├── llama.png
├── robots.txt
├── screenshot.png
├── serviceWorker.js
├── serviceWorkerRegister.js
└── site.webmanifest
├── scripts
├── create-llama.sh
├── generate-demo.sh
└── get-demo.sh
├── sentry.client.config.ts
├── sentry.edge.config.ts
├── sentry.server.config.ts
├── tailwind.config.ts
├── test
└── data
│ └── .gitignore
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | .github
2 | .env
3 | .env.template
4 | .env.development.local
5 | .env.*
6 | Dockerfile
7 | docker-compose.yml
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/serviceWorker.js
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "no-unused-vars": ["warn", { "args": "none" }],
6 | "@next/next/no-img-element": "off"
7 | },
8 | "ignorePatterns": ["**/*.css"]
9 | }
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[Bug] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Deployment**
27 | - [ ] Docker
28 | - [ ] Vercel
29 | - [ ] Server
30 |
31 | **Desktop (please complete the following information):**
32 | - OS: [e.g. iOS]
33 | - Browser [e.g. chrome, safari]
34 | - Version [e.g. 22]
35 |
36 | **Smartphone (please complete the following information):**
37 | - Device: [e.g. iPhone6]
38 | - OS: [e.g. iOS8.1]
39 | - Browser [e.g. stock browser, safari]
40 | - Version [e.g. 22]
41 |
42 | **Additional Logs**
43 | Add any logs about the problem here.
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[Feature] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | target-branch: "develop"
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | dev
38 |
39 | .vscode
40 | .idea
41 |
42 | # docker-compose env files
43 | .env
44 |
45 | *.key
46 | *.key.pub
47 | # Sentry Config File
48 | .sentryclirc
49 |
50 | # create-llama copies
51 | app/api/chat/config/
52 | app/api/files/
53 | cl/
54 |
55 | # uploaded files
56 | output/
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
3 | "eslint --fix",
4 | "prettier --write"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | tabWidth: 2,
4 | useTabs: false,
5 | semi: true,
6 | singleQuote: false,
7 | trailingComma: 'all',
8 | bracketSpacing: true,
9 | arrowParens: 'always',
10 | };
11 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # ---- Build Stage ----
2 | FROM node:18-bookworm-slim AS build
3 |
4 | # Install ca-certificates. Issue: #89
5 | RUN apt-get update
6 | RUN apt-get install -y ca-certificates
7 |
8 | # Install Python, g++, and make for building native dependencies
9 | # Issue: https://github.com/docker/getting-started/issues/124
10 | RUN apt-get install -y python3 g++ make && \
11 | apt-get clean
12 |
13 | # Set the working directory
14 | WORKDIR /usr/src/app
15 |
16 | # Copy the application's package.json and pnpm-lock.yaml to the container
17 | COPY package.json pnpm-lock.yaml ./
18 |
19 | # Install pnpm and application dependencies
20 | RUN npm install -g pnpm && \
21 | pnpm install
22 |
23 | # Copy the rest of the application to the container
24 | COPY . .
25 |
26 | # Build the application for production
27 | RUN pnpm build
28 |
29 | # ---- Production Stage ----
30 | FROM node:18-bookworm-slim AS runtime
31 |
32 | # Use a non-root user
33 | USER node
34 |
35 | # Set the working directory
36 | WORKDIR /usr/src/app
37 |
38 | # Copy the build artifacts from the build stage
39 | COPY --from=build /usr/src/app/.next ./.next
40 | COPY --from=build /usr/src/app/public ./public
41 | COPY --from=build /usr/src/app/package.json ./package.json
42 | COPY --from=build /usr/src/app/node_modules ./node_modules
43 |
44 | # Expose port 3000 to be accessed outside the container
45 | EXPOSE 3000
46 |
47 | # Start the application in production mode
48 | CMD ["npx", "pnpm", "start"]
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 LlamaIndex
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | LlamaIndex Chat
8 | Create chat bots that know your data
9 |
10 |
11 |
16 |
17 |
18 | Welcome to [LlamaIndex Chat](https://github.com/run-llama/chat-llamaindex). You can create and share LLM chatbots that know your data (PDF or text documents).
19 |
20 | Getting started with LlamaIndex Chat is a breeze. Visit https://chat.llamaindex.ai - a hosted version of LlamaIndex Chat with no user authentication that provides an immediate start.
21 |
22 | ## 🚀 Features
23 |
24 | LlamaIndex Chat is an example chatbot application for [LlamaIndexTS](https://github.com/run-llama/LlamaIndexTS) featuring [LlamaCloud](https://cloud.llamaindex.ai/).
25 |
26 | You can:
27 |
28 | - Create bots using prompt engineering and share them with other users.
29 | - Modify the demo bots by using the UI or directly editing the [./app/bots/bot.data.ts](./app/store/bot.data.ts) file.
30 | - Integrate your data by uploading documents or generating new [data sources](#📀-data-sources).
31 |
32 | ## ⚡️ Quick start
33 |
34 | ### Local Development
35 |
36 | Requirement: [NodeJS](https://nodejs.org) 18
37 |
38 | - Clone the repository
39 |
40 | ```bash
41 | git clone https://github.com/run-llama/chat-llamaindex
42 | cd chat-llamaindex
43 | ```
44 |
45 | - Prepare the project
46 |
47 | ```bash
48 | pnpm install
49 | pnpm run create-llama
50 | ```
51 |
52 | > **Note**: The last step copies the chat UI component and file server route from the [create-llama](https://github.com/run-llama/create-llama) project, see [./create-llama.sh](./create-llama.sh).
53 |
54 | - Set the environment variables
55 |
56 | Edit environment variables in `.env.development.local`. Especially check your `OPENAI_API_KEY` and `LLAMA_CLOUD_API_KEY` and the LlamaCloud project to use (`LLAMA_CLOUD_PROJECT_NAME`).
57 |
58 | - Download the demo datasources
59 |
60 | ```bash
61 | pnpm run get-demo
62 | ```
63 |
64 | - Upload the demo datasources to your LlamaCloud account
65 |
66 | ```bash
67 | pnpm run generate-demo
68 | ```
69 |
70 | - Run the dev server
71 |
72 | ```bash
73 | pnpm dev
74 | ```
75 |
76 | ### 🐳 Docker
77 |
78 | Note: This sections has not been used for a while and might be outdated.
79 |
80 | You can use Docker for development and deployment of LlamaIndex Chat.
81 |
82 | #### Building the Docker Image
83 |
84 | ```bash
85 | docker build -t chat-llamaindex .
86 | ```
87 |
88 | #### Running in a Docker Container
89 |
90 | ```bash
91 | docker run -p 3000:3000 --env-file .env.development.local chat-llamaindex
92 | ```
93 |
94 | #### Docker Compose
95 |
96 | For those preferring Docker Compose, we've included a docker-compose.yml file. To run using Docker Compose:
97 |
98 | ```bash
99 | docker compose up
100 | ```
101 |
102 | Go to http://localhost:3000 in your web browser.
103 |
104 | **Note**: By default, the Docker Compose setup maps the `cache` and `datasources` directories from your host machine to the Docker container, ensuring data persistence and accessibility between container restarts.
105 |
106 | ### Vercel Deployment
107 |
108 | Deploying to Vercel is simple; click the button below and follow the instructions:
109 |
110 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Frun-llama%2Fchat-llamaindex&env=OPENAI_API_KEY)
111 |
112 | If you're deploying to a [Vercel Hobby](https://vercel.com/docs/accounts/plans#hobby) account, [change the running time](./app/api/llm/route.ts#L196) to 10 seconds, as this is the limit for the free plan.
113 |
114 | If you want to use the [sharing](#🔄-sharing) functionality, then you need to create a Vercel KV store and connect it to your project.
115 | Just follow [this step from the quickstart](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database). No further configuration is necessary as the app automatically uses a connected KV store.
116 |
117 | ## 🔄 Sharing
118 |
119 | LlamaIndex Chat supports the sharing of bots via URLs. Demo bots are read-only and can't be shared. But you can create new bots (or clone and modify a demo bot) and call the share functionality in the context menu. It will create a unique URL that you can share with others. Opening the URL, users can directly use the shared bot.
120 |
121 | ## 📀 Data Sources
122 |
123 | The app is using a [`ChatEngine`](https://ts.llamaindex.ai/modules/chat_engine) for each bot with a [`LlamaCloudIndex`](https://ts.llamaindex.ai/modules/llamacloud) attached.
124 |
125 | To set which `LlamaCloudIndex` is used for a bot, change the `datasource` attribute in the [bot's data](./app/store/bot.data.ts).
126 |
127 | > **Note**: To use the changed bots, you have to clear your local storage. Otherwise, the old bots are still used. You can clear your local storage by opening the developer tools and running `localStorage.clear()` in the console and reloading the page.
128 |
129 | ### Generate Data Sources
130 |
131 | To generate a new data source, create a new subfolder in the `datasources` directory and add the data files (e.g., PDFs).
132 |
133 | Then, run the following command to create as an index in the `Default` project on LlamaCloud
134 |
135 | ```bash
136 | pnpm run generate
137 | ```
138 |
139 | Where `` is the name of the subfolder with your data files.
140 |
141 | > **Note**: On Windows, use `pnpm run generate:win ` instead.
142 |
143 | ## 🙏 Thanks
144 |
145 | Thanks go to @Yidadaa for his [ChatGPT-Next-Web](https://github.com/Yidadaa/ChatGPT-Next-Web) project, which was used as a starter template for this project.
146 |
--------------------------------------------------------------------------------
/app/api/chat/engine/chat.ts:
--------------------------------------------------------------------------------
1 | import { ContextChatEngine, Settings } from "llamaindex";
2 | import { getDataSource, LlamaCloudDataSourceParams } from "./index";
3 | import { generateFilters } from "@/cl/app/api/chat/engine/queryFilter";
4 |
5 | interface ChatEngineOptions {
6 | datasource: LlamaCloudDataSourceParams;
7 | documentIds?: string[];
8 | }
9 |
10 | export async function createChatEngine({
11 | datasource,
12 | documentIds,
13 | }: ChatEngineOptions) {
14 | const index = await getDataSource(datasource);
15 | if (!index) {
16 | throw new Error(
17 | `StorageContext is empty - call 'pnpm run generate ${datasource}' to generate the storage first`,
18 | );
19 | }
20 | const retriever = index.asRetriever({
21 | similarityTopK: process.env.TOP_K ? parseInt(process.env.TOP_K) : 3,
22 | filters: generateFilters(documentIds || []) as any,
23 | });
24 | return new ContextChatEngine({
25 | chatModel: Settings.llm,
26 | retriever,
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/app/api/chat/engine/generate.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv";
2 | import * as fs from "fs/promises";
3 | import * as path from "path";
4 | import { getDataSource } from ".";
5 | import { FilesService, PipelinesService } from "@llamaindex/cloud/api";
6 | import { initService } from "llamaindex/cloud/utils";
7 |
8 | const DATA_DIR = "./datasources";
9 |
10 | // Load environment variables from local .env.development.local file
11 | dotenv.config({ path: ".env.development.local" });
12 |
13 | async function getRuntime(func: any) {
14 | const start = Date.now();
15 | await func();
16 | const end = Date.now();
17 | return end - start;
18 | }
19 |
20 | async function* walk(dir: string): AsyncGenerator {
21 | const directory = await fs.opendir(dir);
22 |
23 | for await (const dirent of directory) {
24 | const entryPath = path.join(dir, dirent.name);
25 |
26 | if (dirent.isDirectory()) {
27 | yield* walk(entryPath); // Recursively walk through directories
28 | } else if (dirent.isFile()) {
29 | yield entryPath; // Yield file paths
30 | }
31 | }
32 | }
33 |
34 | // TODO: should be moved to LlamaCloudFileService of LlamaIndexTS
35 | async function addFileToPipeline(
36 | projectId: string,
37 | pipelineId: string,
38 | uploadFile: File | Blob,
39 | customMetadata: Record = {},
40 | ) {
41 | const file = await FilesService.uploadFileApiV1FilesPost({
42 | projectId,
43 | formData: {
44 | upload_file: uploadFile,
45 | },
46 | });
47 | const files = [
48 | {
49 | file_id: file.id,
50 | custom_metadata: { file_id: file.id, ...customMetadata },
51 | },
52 | ];
53 | await PipelinesService.addFilesToPipelineApiV1PipelinesPipelineIdFilesPut({
54 | pipelineId,
55 | requestBody: files,
56 | });
57 | }
58 |
59 | async function generateDatasource() {
60 | const datasource = process.argv[2];
61 | if (!datasource) {
62 | console.error("Please provide a datasource as an argument.");
63 | process.exit(1);
64 | }
65 |
66 | console.log(`Generating storage context for datasource '${datasource}'...`);
67 |
68 | const ms = await getRuntime(async () => {
69 | const index = await getDataSource({
70 | pipeline: datasource,
71 | ensureIndex: true,
72 | });
73 | const projectId = await index.getProjectId();
74 | const pipelineId = await index.getPipelineId();
75 |
76 | // walk through the data directory and upload each file to LlamaCloud
77 | for await (const filePath of walk(path.join(DATA_DIR, datasource))) {
78 | const buffer = await fs.readFile(filePath);
79 | const filename = path.basename(filePath);
80 | const file = new File([buffer], filename);
81 | await addFileToPipeline(projectId, pipelineId, file, {
82 | private: "false",
83 | });
84 | }
85 | });
86 | console.log(
87 | `Successfully uploaded documents to LlamaCloud in ${ms / 1000}s.`,
88 | );
89 | }
90 |
91 | (async () => {
92 | initService();
93 | await generateDatasource();
94 | console.log("Finished generating storage.");
95 | })();
96 |
--------------------------------------------------------------------------------
/app/api/chat/engine/index.ts:
--------------------------------------------------------------------------------
1 | import { LlamaCloudIndex } from "llamaindex/cloud/LlamaCloudIndex";
2 | import type { CloudConstructorParams } from "llamaindex/cloud/constants";
3 |
4 | export type LlamaCloudDataSourceParams = {
5 | project?: string;
6 | pipeline?: string;
7 | ensureIndex?: boolean;
8 | };
9 |
10 | export function parseDataSource(
11 | datasource: string,
12 | ): LlamaCloudDataSourceParams {
13 | try {
14 | return JSON.parse(datasource) as LlamaCloudDataSourceParams;
15 | } catch (e) {
16 | return {};
17 | }
18 | }
19 |
20 | export async function getDataSource(params: LlamaCloudDataSourceParams) {
21 | checkEnvVars();
22 | if (params.ensureIndex) {
23 | // ensure that the index exists
24 | try {
25 | await LlamaCloudIndex.fromDocuments({
26 | ...createParams(params),
27 | documents: [],
28 | });
29 | } catch (e) {
30 | if ((e as any).status === 400) {
31 | // ignore 400 error, it's caused by calling fromDocuments with empty documents
32 | // TODO: fix in LLamaIndexTS
33 | } else {
34 | throw e;
35 | }
36 | }
37 | }
38 | return new LlamaCloudIndex(createParams(params));
39 | }
40 |
41 | function createParams({
42 | project,
43 | pipeline,
44 | }: LlamaCloudDataSourceParams): CloudConstructorParams {
45 | if (!pipeline) {
46 | throw new Error("Set pipeline in the params.");
47 | }
48 | const params = {
49 | organizationId: process.env.LLAMA_CLOUD_ORGANIZATION_ID,
50 | name: pipeline,
51 | projectName: project ?? process.env.LLAMA_CLOUD_PROJECT_NAME!,
52 | apiKey: process.env.LLAMA_CLOUD_API_KEY,
53 | baseUrl: process.env.LLAMA_CLOUD_BASE_URL,
54 | };
55 | return params;
56 | }
57 |
58 | function checkEnvVars() {
59 | if (
60 | !process.env.LLAMA_CLOUD_PROJECT_NAME ||
61 | !process.env.LLAMA_CLOUD_API_KEY
62 | ) {
63 | throw new Error(
64 | "LLAMA_CLOUD_PROJECT_NAME and LLAMA_CLOUD_API_KEY environment variables must be set.",
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/api/chat/llamaindex/streaming/events.ts:
--------------------------------------------------------------------------------
1 | import { StreamData } from "ai";
2 | import {
3 | CallbackManager,
4 | LLamaCloudFileService,
5 | Metadata,
6 | MetadataMode,
7 | NodeWithScore,
8 | ToolCall,
9 | ToolOutput,
10 | } from "llamaindex";
11 |
12 | export async function appendSourceData(
13 | data: StreamData,
14 | sourceNodes?: NodeWithScore[],
15 | ) {
16 | if (!sourceNodes?.length) return;
17 | try {
18 | const nodes = await Promise.all(
19 | sourceNodes.map(async (node) => ({
20 | metadata: node.node.metadata,
21 | id: node.node.id_,
22 | score: node.score ?? null,
23 | url: await getNodeUrl(node.node.metadata),
24 | text: node.node.getContent(MetadataMode.NONE),
25 | })),
26 | );
27 | data.appendMessageAnnotation({
28 | type: "sources",
29 | data: {
30 | nodes,
31 | },
32 | });
33 | } catch (error) {
34 | console.error("Error appending source data:", error);
35 | }
36 | }
37 |
38 | export function appendEventData(data: StreamData, title?: string) {
39 | if (!title) return;
40 | data.appendMessageAnnotation({
41 | type: "events",
42 | data: {
43 | title,
44 | },
45 | });
46 | }
47 |
48 | export function appendToolData(
49 | data: StreamData,
50 | toolCall: ToolCall,
51 | toolOutput: ToolOutput,
52 | ) {
53 | data.appendMessageAnnotation({
54 | type: "tools",
55 | data: {
56 | toolCall: {
57 | id: toolCall.id,
58 | name: toolCall.name,
59 | input: toolCall.input,
60 | },
61 | toolOutput: {
62 | output: toolOutput.output,
63 | isError: toolOutput.isError,
64 | },
65 | },
66 | });
67 | }
68 |
69 | export function createStreamTimeout(stream: StreamData) {
70 | const timeout = Number(process.env.STREAM_TIMEOUT ?? 1000 * 60 * 5); // default to 5 minutes
71 | const t = setTimeout(() => {
72 | appendEventData(stream, `Stream timed out after ${timeout / 1000} seconds`);
73 | stream.close();
74 | }, timeout);
75 | return t;
76 | }
77 |
78 | export function createCallbackManager(stream: StreamData) {
79 | const callbackManager = new CallbackManager();
80 |
81 | callbackManager.on("retrieve-end", (data: any) => {
82 | const { nodes, query } = data.detail;
83 | appendSourceData(stream, nodes);
84 | appendEventData(stream, `Retrieving context for query: '${query}'`);
85 | appendEventData(
86 | stream,
87 | `Retrieved ${nodes.length} sources to use as context for the query`,
88 | );
89 | });
90 |
91 | callbackManager.on("llm-tool-call", (event: any) => {
92 | const { name, input } = event.detail.toolCall;
93 | const inputString = Object.entries(input)
94 | .map(([key, value]) => `${key}: ${value}`)
95 | .join(", ");
96 | appendEventData(
97 | stream,
98 | `Using tool: '${name}' with inputs: '${inputString}'`,
99 | );
100 | });
101 |
102 | callbackManager.on("llm-tool-result", (event: any) => {
103 | const { toolCall, toolResult } = event.detail;
104 | appendToolData(stream, toolCall, toolResult);
105 | });
106 |
107 | return callbackManager;
108 | }
109 |
110 | async function getNodeUrl(metadata: Metadata) {
111 | try {
112 | const fileName = metadata["file_name"];
113 | const pipelineId = metadata["pipeline_id"];
114 | if (fileName && pipelineId) {
115 | // file has been uploaded to LlamaCloud, so we can get the URL from there
116 | const downloadUrl = await LLamaCloudFileService.getFileUrl(
117 | pipelineId,
118 | fileName,
119 | );
120 | if (downloadUrl) {
121 | console.log(`Retrieved documents URL from LlamaCloud: ${downloadUrl}`);
122 | return downloadUrl;
123 | }
124 | }
125 | } catch (error) {
126 | console.error("Error retrieving document URL:", error);
127 | }
128 | console.warn(
129 | "Couldn't retrieve document URL from LlamaCloud for node with metadata",
130 | metadata,
131 | );
132 | return null;
133 | }
134 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { JSONValue, Message, StreamData, StreamingTextResponse } from "ai";
2 | import {
3 | ChatMessage,
4 | OpenAI,
5 | Settings,
6 | SimpleChatHistory,
7 | SummaryChatHistory,
8 | } from "llamaindex";
9 | import { NextRequest, NextResponse } from "next/server";
10 | import { createChatEngine } from "./engine/chat";
11 | import { LlamaIndexStream } from "@/cl/app/api/chat/llamaindex/streaming/stream";
12 | import {
13 | convertMessageContent,
14 | retrieveDocumentIds,
15 | } from "@/cl/app/api/chat/llamaindex/streaming/annotations";
16 | import {
17 | createCallbackManager,
18 | createStreamTimeout,
19 | } from "./llamaindex/streaming/events";
20 | import { LLMConfig } from "@/app/store/bot";
21 | import { parseDataSource } from "./engine";
22 |
23 | export const runtime = "nodejs";
24 | export const dynamic = "force-dynamic";
25 |
26 | interface ChatRequestBody {
27 | messages: Message[];
28 | context: Message[];
29 | modelConfig: LLMConfig;
30 | datasource?: string;
31 | }
32 |
33 | export async function POST(request: NextRequest) {
34 | // Init Vercel AI StreamData and timeout
35 | const vercelStreamData = new StreamData();
36 | const streamTimeout = createStreamTimeout(vercelStreamData);
37 |
38 | try {
39 | const body = await request.json();
40 | const { messages, context, modelConfig, datasource } =
41 | body as ChatRequestBody;
42 | const userMessage = messages.pop();
43 | if (
44 | !messages ||
45 | !userMessage ||
46 | userMessage.role !== "user" ||
47 | !datasource
48 | ) {
49 | return NextResponse.json(
50 | {
51 | detail:
52 | "datasource and messages are required in the request body and the last message must be from the user",
53 | },
54 | { status: 400 },
55 | );
56 | }
57 |
58 | let annotations = userMessage.annotations;
59 | if (!annotations) {
60 | // the user didn't send any new annotations with the last message
61 | // so use the annotations from the last user message that has annotations
62 | // REASON: GPT4 doesn't consider MessageContentDetail from previous messages, only strings
63 | annotations = messages
64 | .slice()
65 | .reverse()
66 | .find(
67 | (message) => message.role === "user" && message.annotations,
68 | )?.annotations;
69 | }
70 |
71 | // retrieve document Ids from the annotations of all messages (if any) and create chat engine with index
72 | const allAnnotations: JSONValue[] = [...messages, userMessage].flatMap(
73 | (message) => {
74 | return message.annotations ?? [];
75 | },
76 | );
77 |
78 | const ids = retrieveDocumentIds(allAnnotations);
79 |
80 | // Create chat engine instance with llm config from request
81 | const llm = new OpenAI(modelConfig);
82 | const chatEngine = await Settings.withLLM(llm, async () => {
83 | return await createChatEngine({
84 | datasource: parseDataSource(datasource),
85 | documentIds: ids,
86 | });
87 | });
88 |
89 | // Convert message content from Vercel/AI format to LlamaIndex/OpenAI format
90 | const userMessageContent = convertMessageContent(
91 | userMessage.content,
92 | annotations,
93 | );
94 |
95 | // Setup callbacks
96 | const callbackManager = createCallbackManager(vercelStreamData);
97 |
98 | // Append context messages to the top of the chat history
99 | const chatMessages = context.concat(messages) as ChatMessage[];
100 | const chatHistory = modelConfig.sendMemory
101 | ? new SummaryChatHistory({ messages: chatMessages, llm })
102 | : new SimpleChatHistory({ messages: chatMessages });
103 |
104 | // Calling LlamaIndex's ChatEngine to get a streamed response
105 | const response = await Settings.withCallbackManager(callbackManager, () => {
106 | return chatEngine.chat({
107 | message: userMessageContent,
108 | chatHistory,
109 | stream: true,
110 | });
111 | });
112 |
113 | // Transform LlamaIndex stream to Vercel/AI format
114 | const stream = LlamaIndexStream(response, vercelStreamData, chatMessages);
115 |
116 | // Return a StreamingTextResponse, which can be consumed by the Vercel/AI client
117 | return new StreamingTextResponse(stream, {}, vercelStreamData);
118 | } catch (error) {
119 | console.error("[LlamaIndex]", error);
120 | return NextResponse.json(
121 | {
122 | detail: (error as Error).message,
123 | },
124 | {
125 | status: 500,
126 | },
127 | );
128 | } finally {
129 | clearTimeout(streamTimeout);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/app/api/chat/upload/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { uploadDocument } from "@/cl/app/api/chat/llamaindex/documents/upload";
3 | import { getDataSource, parseDataSource } from "../engine";
4 |
5 | export const runtime = "nodejs";
6 | export const dynamic = "force-dynamic";
7 |
8 | // Custom upload API to use datasource from request body
9 | export async function POST(request: NextRequest) {
10 | try {
11 | const {
12 | filename,
13 | base64,
14 | datasource,
15 | }: { filename: string; base64: string; datasource: string } =
16 | await request.json();
17 | if (!base64 || !datasource) {
18 | return NextResponse.json(
19 | { error: "base64 and datasource is required in the request body" },
20 | { status: 400 },
21 | );
22 | }
23 | const index = await getDataSource(parseDataSource(datasource));
24 | if (!index) {
25 | throw new Error(
26 | `StorageContext is empty - call 'pnpm run generate ${datasource}' to generate the storage first`,
27 | );
28 | }
29 | return NextResponse.json(await uploadDocument(index, filename, base64));
30 | } catch (error) {
31 | console.error("[Upload API]", error);
32 | return NextResponse.json(
33 | { error: (error as Error).message },
34 | { status: 500 },
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/api/share/route.ts:
--------------------------------------------------------------------------------
1 | import { kv } from "@vercel/kv";
2 | import { NextRequest, NextResponse } from "next/server";
3 | import { nanoid } from "nanoid";
4 | import { Bot } from "@/app/store/bot";
5 |
6 | const DAYS_TO_LIVE = 30;
7 | const TTL = 60 * 60 * 24 * DAYS_TO_LIVE;
8 | const MAX_KEY_GENERATION_RETRY = 100;
9 |
10 | export interface ShareResponse {
11 | key: string;
12 | url: string;
13 | }
14 |
15 | async function getKey() {
16 | let key;
17 | let counter = 0;
18 |
19 | do {
20 | key = nanoid();
21 | counter++;
22 | } while ((await kv.exists(key)) && counter < MAX_KEY_GENERATION_RETRY);
23 |
24 | if (counter === MAX_KEY_GENERATION_RETRY) {
25 | // Handle the case when a unique key was not found within the maximum allowed iterations
26 | throw new Error("Failed to generate a unique key");
27 | }
28 | return key;
29 | }
30 |
31 | export async function POST(req: NextRequest) {
32 | try {
33 | const body: { bot: Bot } = await req.json();
34 |
35 | const key = await getKey();
36 | body.bot.share = { ...body.bot.share, id: key };
37 | const data = await kv.set<{ bot: Bot }>(key, body, {
38 | ex: TTL,
39 | });
40 | if (!data) {
41 | throw new Error(`Can't store bot with key ${key}`);
42 | }
43 |
44 | const protocol = req.headers.get("x-forwarded-proto") || "http";
45 | const url = `${protocol}://${req.headers.get("host")}/b/${key}`;
46 |
47 | console.log(`[Share] shared bot '${body.bot.name}' created at ${url}`);
48 |
49 | return NextResponse.json({
50 | key: key,
51 | url: url,
52 | data: data,
53 | days: DAYS_TO_LIVE,
54 | } as ShareResponse);
55 | } catch (error) {
56 | console.error("[Share] error while sharing bot", error);
57 | return NextResponse.json(
58 | {
59 | error: true,
60 | msg: (error as Error).message,
61 | },
62 | {
63 | status: 500,
64 | },
65 | );
66 | }
67 | }
68 |
69 | export const runtime = "edge";
70 |
--------------------------------------------------------------------------------
/app/b/[botId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Home } from "@/app/components/home";
2 | import { Bot } from "@/app/store/bot";
3 | import { Analytics } from "@vercel/analytics/react";
4 | import { kv } from "@vercel/kv";
5 |
6 | export default async function App({ params }: { params: { botId: string } }) {
7 | console.log(`[Share] try loading bot with key ${params.botId}`);
8 | let bot: Bot | null = null;
9 | try {
10 | const res: { bot: Bot } | null = await kv.get(params.botId);
11 | bot = res?.bot || null;
12 | } catch (e) {
13 | console.error(`[Share] failed to load bot with key ${params.botId}`, e);
14 | }
15 |
16 | if (!bot) {
17 | console.log(`[Share] requested unknown bot with id ${params.botId}`);
18 | return (
19 | <>
20 | Sorry, there is no bot at this URL. Try
21 | creating your own bot.
22 | >
23 | );
24 | }
25 |
26 | console.debug("[Share] bot loaded", bot);
27 |
28 | return (
29 | <>
30 |
31 |
32 | >
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/components/bot/bot-item.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/app/lib/utils";
2 | import { Bot } from "../../store/bot";
3 | import BotOptions from "./bot-options";
4 | import { BotItemContextProvider, useBot } from "./use-bot";
5 | import { BotAvatar } from "@/app/components/ui/emoji";
6 |
7 | function BotItemUI() {
8 | const { bot, isActive, ensureSession } = useBot();
9 | return (
10 |
16 |
20 |
21 |
22 |
23 |
{bot.name}
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default function BotItem(props: { bot: Bot }) {
33 | return (
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/app/components/bot/bot-list.tsx:
--------------------------------------------------------------------------------
1 | import EditBotDialogContent from "@/app/components/bot/bot-options/edit-bot-dialog";
2 | import { BotItemContextProvider } from "@/app/components/bot/use-bot";
3 | import { Dialog, DialogTrigger } from "@/app/components/ui/dialog";
4 | import { PlusCircle } from "lucide-react";
5 | import { useState } from "react";
6 | import { useNavigate } from "react-router-dom";
7 | import { Path } from "../../constant";
8 | import Locale from "../../locales";
9 | import { useBotStore } from "../../store/bot";
10 | import { Button } from "../ui/button";
11 | import { Input } from "../ui/input";
12 | import { ScrollArea } from "../ui/scroll-area";
13 | import BotItem from "./bot-item";
14 |
15 | export default function BotList() {
16 | const botStore = useBotStore();
17 | const navigate = useNavigate();
18 | const [searchText, setSearchText] = useState("");
19 | const [editBotId, setEditBotId] = useState(undefined);
20 |
21 | const onClickContainer = (e: React.MouseEvent) => {
22 | if (e.target === e.currentTarget) {
23 | navigate(Path.Home);
24 | }
25 | };
26 |
27 | const onClickCreate = () => {
28 | const newBot = botStore.create();
29 | botStore.selectBot(newBot.id);
30 | setEditBotId(newBot.id);
31 | };
32 |
33 | const allBots = botStore.getAll();
34 | const filteredBots = allBots.filter((b) =>
35 | b.name.toLowerCase().includes(searchText.toLowerCase()),
36 | );
37 | const botList = searchText.length > 0 ? filteredBots : allBots;
38 | const editBot = editBotId ? botStore.get(editBotId) : undefined;
39 |
40 | return (
41 |
42 |
43 |
55 |
setSearchText(e.currentTarget.value)}
60 | />
61 |
62 |
63 | {botList.map((b) => (
64 |
65 | ))}
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/app/components/bot/bot-options/delete-bot-dialog.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/app/lib/utils";
2 | import Locale from "../../../locales";
3 | import {
4 | AlertDialogAction,
5 | AlertDialogCancel,
6 | AlertDialogContent,
7 | AlertDialogDescription,
8 | AlertDialogFooter,
9 | AlertDialogHeader,
10 | AlertDialogTitle,
11 | } from "../../ui/alert-dialog";
12 | import { useBot } from "../use-bot";
13 | import { buttonVariants } from "@/app/components/ui/button";
14 |
15 | export default function DeleteBotDialogContent() {
16 | const { deleteBot } = useBot();
17 | return (
18 |
19 |
20 | Are you absolutely sure?
21 |
22 | {Locale.Bot.Item.DeleteConfirm}
23 |
24 |
25 |
26 | Cancel
27 |
31 | Continue
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/components/bot/bot-options/edit-bot-dialog.tsx:
--------------------------------------------------------------------------------
1 | import Locale from "../../../locales";
2 | import { DialogContent, DialogHeader, DialogTitle } from "../../ui/dialog";
3 | import { ScrollArea } from "../../ui/scroll-area";
4 | import { Separator } from "../../ui/separator";
5 | import BotSettings from "../bot-settings";
6 |
7 | export default function EditBotDialogContent() {
8 | return (
9 |
10 |
11 | {Locale.Bot.EditModal.Title}
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/app/components/bot/bot-options/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ClipboardEdit,
3 | Copy,
4 | MoreHorizontal,
5 | Share2,
6 | XCircle,
7 | } from "lucide-react";
8 | import { useState } from "react";
9 | import Locale from "../../../locales";
10 | import { AlertDialog, AlertDialogTrigger } from "../../ui/alert-dialog";
11 | import { Button } from "../../ui/button";
12 | import { Dialog, DialogTrigger } from "../../ui/dialog";
13 | import {
14 | DropdownMenu,
15 | DropdownMenuContent,
16 | DropdownMenuItem,
17 | DropdownMenuLabel,
18 | DropdownMenuSeparator,
19 | DropdownMenuTrigger,
20 | } from "../../ui/dropdown-menu";
21 | import { useBot } from "../use-bot";
22 | import DeleteBotDialogContent from "./delete-bot-dialog";
23 | import EditBotDialogContent from "./edit-bot-dialog";
24 | import ShareBotDialogContent from "./share-bot-dialog";
25 |
26 | export default function BotOptions() {
27 | const { isReadOnly, isShareble, cloneBot } = useBot();
28 | const [dialogContent, setDialogContent] = useState(null);
29 |
30 | return (
31 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/app/components/bot/bot-options/share-bot-dialog.tsx:
--------------------------------------------------------------------------------
1 | import { ShareResponse } from "@/app/api/share/route";
2 | import { Card, CardContent } from "@/app/components/ui/card";
3 | import { Input } from "@/app/components/ui/input";
4 | import { Loading } from "@/app/components/ui/loading";
5 | import { useToast } from "@/app/components/ui/use-toast";
6 | import { Bot } from "@/app/store/bot";
7 | import { copyToClipboard } from "@/app/utils/clipboard";
8 | import { Copy } from "lucide-react";
9 | import { useEffect } from "react";
10 | import { useMutation } from "react-query";
11 | import Locale from "../../../locales";
12 | import { Button } from "../../ui/button";
13 | import {
14 | DialogContent,
15 | DialogFooter,
16 | DialogHeader,
17 | DialogTitle,
18 | } from "../../ui/dialog";
19 | import { useBot } from "../use-bot";
20 |
21 | async function share(bot: Bot): Promise {
22 | const res = await fetch("/api/share", {
23 | method: "POST",
24 | body: JSON.stringify({ bot: bot }),
25 | });
26 | const json = await res.json();
27 | console.log("[Share]", json);
28 | if (!res.ok) {
29 | throw new Error(json.msg);
30 | }
31 | return json;
32 | }
33 |
34 | export default function ShareBotDialogContent() {
35 | const { toast } = useToast();
36 | const { bot, updateBot } = useBot();
37 |
38 | const shareMutation = useMutation(share, {
39 | onSuccess: (data) => {
40 | updateBot((bot) => {
41 | bot.share = { ...bot.share, id: data.key };
42 | });
43 | },
44 | });
45 |
46 | // FIXME: check dependency warning
47 | useEffect(() => {
48 | shareMutation.mutate(bot);
49 | }, []);
50 |
51 | return (
52 |
53 |
54 | {Locale.Share.Title}
55 |
56 |
57 | {!shareMutation.error && (
58 |
59 |
60 |
61 |
{Locale.Share.Url.Title}
62 | {shareMutation.data ? (
63 |
64 |
70 |
79 |
80 | ) : (
81 |
82 |
83 | Loading...
84 |
85 | )}
86 |
87 |
88 |
89 | )}
90 |
91 |
92 |
93 | {shareMutation.error ? (
94 |
{Locale.Share.Url.Error}
95 | ) : (
96 |
{Locale.Share.Url.Hint}
97 | )}
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/app/components/bot/bot-settings/bot-config.tsx:
--------------------------------------------------------------------------------
1 | import { useBot } from "@/app/components/bot/use-bot";
2 | import { LlamaCloudSelector } from "@/cl/app/components/ui/chat/widgets/LlamaCloudSelector";
3 | import Locale from "../../../locales";
4 | import { Card, CardContent } from "../../ui/card";
5 | import { Input } from "../../ui/input";
6 | import ConfigItem from "./config-item";
7 |
8 | export default function BotConfig() {
9 | const { bot, updateBot } = useBot();
10 | return (
11 | <>
12 | {Locale.Bot.Config.Title}
13 |
14 |
15 |
16 |
20 | updateBot((bot) => {
21 | bot.name = e.currentTarget.value;
22 | })
23 | }
24 | />
25 |
26 |
27 | {
33 | if (pipeline) {
34 | updateBot((bot) => {
35 | bot.datasource = JSON.stringify(pipeline); // stringify configs as datasource
36 | });
37 | }
38 | }}
39 | />
40 |
41 |
42 |
43 | >
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/app/components/bot/bot-settings/config-item.tsx:
--------------------------------------------------------------------------------
1 | export default function ConfigItem(props: {
2 | title: string;
3 | subTitle?: string;
4 | children: JSX.Element;
5 | }) {
6 | return (
7 |
8 |
9 |
{props.title}
10 |
{props.subTitle}
11 |
12 |
{props.children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/app/components/bot/bot-settings/context-prompt.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/app/components/ui/button";
2 | import {
3 | Select,
4 | SelectContent,
5 | SelectItem,
6 | SelectTrigger,
7 | SelectValue,
8 | } from "@/app/components/ui/select";
9 | import { Textarea } from "@/app/components/ui/textarea";
10 | import { ArrowDownLeftSquare, PlusCircle, XCircle } from "lucide-react";
11 | import Locale from "../../../locales";
12 | import { Message as ChatMessage } from "ai";
13 | import { v4 as uuidv4 } from "uuid";
14 | import { MESSAGE_ROLES } from "@/app/store/bot";
15 |
16 | function ContextPromptItem(props: {
17 | index: number;
18 | prompt: ChatMessage;
19 | update: (prompt: ChatMessage) => void;
20 | remove: () => void;
21 | insert: () => void;
22 | }) {
23 | const handleUpdatePrompt = async (input: string) => {
24 | props.update({
25 | ...props.prompt,
26 | content: input,
27 | });
28 | };
29 |
30 | return (
31 | <>
32 |
33 |
34 |
54 |
55 |
56 |
88 | >
89 | );
90 | }
91 |
92 | export function ContextPrompts(props: {
93 | context: ChatMessage[];
94 | updateContext: (updater: (context: ChatMessage[]) => void) => void;
95 | }) {
96 | const context = props.context;
97 |
98 | const addContextPrompt = (prompt: ChatMessage, i: number) => {
99 | props.updateContext((context) => context.splice(i, 0, prompt));
100 | };
101 |
102 | const createNewPrompt = (index = props.context.length) => {
103 | addContextPrompt(
104 | {
105 | role: "user",
106 | content: "",
107 | id: uuidv4(),
108 | },
109 | index,
110 | );
111 | };
112 |
113 | const removeContextPrompt = (i: number) => {
114 | props.updateContext((context) => context.splice(i, 1));
115 | };
116 |
117 | const updateContextPrompt = (i: number, prompt: ChatMessage) => {
118 | props.updateContext((context) => (context[i] = prompt));
119 | };
120 |
121 | return (
122 | <>
123 |
124 |
125 |
{Locale.Context.Title}
126 |
129 |
130 | {context.map((c, i) => (
131 |
132 | updateContextPrompt(i, prompt)}
136 | remove={() => removeContextPrompt(i)}
137 | insert={() => createNewPrompt(i + 1)}
138 | />
139 |
140 | ))}
141 |
142 | >
143 | );
144 | }
145 |
--------------------------------------------------------------------------------
/app/components/bot/bot-settings/index.tsx:
--------------------------------------------------------------------------------
1 | import { ContextPrompts } from "@/app/components/bot/bot-settings/context-prompt";
2 | import { useBot } from "@/app/components/bot/use-bot";
3 | import BotConfig from "./bot-config";
4 | import { ModelConfigList } from "./model-config";
5 | import { Separator } from "@/app/components/ui/separator";
6 | import { LLMConfig } from "@/app/store/bot";
7 |
8 | export default function BotSettings(props: { extraConfigs?: JSX.Element }) {
9 | const { bot, updateBot } = useBot();
10 | const updateConfig = (updater: (config: LLMConfig) => void) => {
11 | if (bot.readOnly) return;
12 | const config = { ...bot.modelConfig };
13 | updater(config);
14 | updateBot((bot) => {
15 | bot.modelConfig = config;
16 | });
17 | };
18 | return (
19 |
20 | {
23 | const context = bot.context.slice();
24 | updater(context);
25 | updateBot((bot) => (bot.context = context));
26 | }}
27 | />
28 |
29 |
30 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/components/bot/bot-settings/model-config.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from "@/app/components/ui/checkbox";
2 | import { Input, InputRange } from "@/app/components/ui/input";
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | SelectValue,
9 | } from "@/app/components/ui/select";
10 | import Locale from "../../../locales";
11 | import { Card, CardContent } from "../../ui/card";
12 | import ConfigItem from "./config-item";
13 | import { ALL_MODELS, ModelType, LLMConfig } from "@/app/store/bot";
14 |
15 | function limitNumber(
16 | x: number,
17 | min: number,
18 | max: number,
19 | defaultValue: number,
20 | ) {
21 | if (typeof x !== "number" || isNaN(x)) {
22 | return defaultValue;
23 | }
24 |
25 | return Math.min(max, Math.max(min, x));
26 | }
27 |
28 | const ModalConfigValidator = {
29 | model(x: string) {
30 | return x as ModelType;
31 | },
32 | maxTokens(x: number) {
33 | return limitNumber(x, 0, 4096, 2000);
34 | },
35 | temperature(x: number) {
36 | return limitNumber(x, 0, 1, 1);
37 | },
38 | topP(x: number) {
39 | return limitNumber(x, 0, 1, 1);
40 | },
41 | };
42 |
43 | export function ModelConfigList(props: {
44 | modelConfig: LLMConfig;
45 | updateConfig: (updater: (config: LLMConfig) => void) => void;
46 | }) {
47 | return (
48 |
49 |
50 |
51 |
70 |
71 |
72 |
76 | {
82 | props.updateConfig(
83 | (config) =>
84 | (config.temperature = ModalConfigValidator.temperature(
85 | e.currentTarget.valueAsNumber,
86 | )),
87 | );
88 | }}
89 | >
90 |
91 |
95 | {
101 | props.updateConfig(
102 | (config) =>
103 | (config.topP = ModalConfigValidator.topP(
104 | e.currentTarget.valueAsNumber,
105 | )),
106 | );
107 | }}
108 | >
109 |
110 |
114 |
120 | props.updateConfig(
121 | (config) =>
122 | (config.maxTokens = ModalConfigValidator.maxTokens(
123 | e.currentTarget.valueAsNumber,
124 | )),
125 | )
126 | }
127 | />
128 |
129 |
130 |
131 |
134 | props.updateConfig(
135 | (config) => (config.sendMemory = Boolean(checked)),
136 | )
137 | }
138 | />
139 |
140 |
141 |
142 | );
143 | }
144 |
--------------------------------------------------------------------------------
/app/components/bot/use-bot.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { Path } from "../../constant";
4 | import { Bot, useBotStore } from "../../store/bot";
5 | import { useSidebarContext } from "../home";
6 |
7 | type Updater = (updater: (value: T) => void) => void;
8 |
9 | const BotItemContext = createContext<{
10 | bot: Bot;
11 | isActive: boolean;
12 | isReadOnly: boolean;
13 | isShareble: boolean;
14 | ensureSession: () => void;
15 | cloneBot: () => void;
16 | deleteBot: () => void;
17 | updateBot: Updater;
18 | }>({} as any);
19 |
20 | export const BotItemContextProvider = (props: {
21 | bot: Bot;
22 | children: JSX.Element;
23 | }) => {
24 | const bot = props.bot;
25 | const botStore = useBotStore();
26 | const navigate = useNavigate();
27 | const { setShowSidebar } = useSidebarContext();
28 |
29 | const cloneBot = () => {
30 | const newBot = botStore.create(bot, {
31 | reset: true,
32 | });
33 | newBot.name = `My ${bot.name}`;
34 | };
35 |
36 | const isReadOnly = bot.readOnly;
37 | const isShareble = !!bot.share;
38 |
39 | const ensureSession = () => {
40 | navigate(Path.Home);
41 | botStore.selectBot(bot.id);
42 | setShowSidebar(false);
43 | };
44 |
45 | const deleteBot = () => {
46 | botStore.delete(bot.id);
47 | };
48 |
49 | const updateBot: Updater = (updater) => {
50 | botStore.update(bot.id, updater);
51 | };
52 |
53 | const isActive = botStore.currentBotId === props.bot.id;
54 |
55 | return (
56 |
68 | {props.children}
69 |
70 | );
71 | };
72 |
73 | export const useBot = () => useContext(BotItemContext);
74 |
--------------------------------------------------------------------------------
/app/components/chat/chat-header.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/app/components/ui/button";
2 | import { useBotStore } from "@/app/store/bot";
3 | import { Undo2 } from "lucide-react";
4 | import Locale from "../../locales";
5 | import { useMobileScreen } from "../../utils/mobile";
6 | import { useSidebarContext } from "../home";
7 | import { Separator } from "../ui/separator";
8 | import Typography from "../ui/typography";
9 |
10 | export default function ChatHeader() {
11 | const isMobileScreen = useMobileScreen();
12 | const { setShowSidebar } = useSidebarContext();
13 | const botStore = useBotStore();
14 | const bot = botStore.currentBot();
15 | const session = botStore.currentSession();
16 | const numberOfMessages = session.messages.length;
17 | return (
18 |
19 |
20 | {isMobileScreen && (
21 |
29 | )}
30 |
31 |
32 |
{bot.name}
33 |
34 | {Locale.Chat.SubTitle(numberOfMessages)}
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/chat/chat-session.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useBotStore } from "@/app/store/bot";
4 | import { useChatSession } from "./useChatSession";
5 | import { ChatMessages, ChatInput } from "@/cl/app/components/ui/chat";
6 |
7 | // Custom ChatSection for ChatLlamaindex
8 | export default function ChatSection() {
9 | const {
10 | messages,
11 | input,
12 | isLoading,
13 | handleSubmit,
14 | handleInputChange,
15 | reload,
16 | stop,
17 | append,
18 | setInput,
19 | } = useChatSession();
20 | const botStore = useBotStore();
21 | const bot = botStore.currentBot();
22 | return (
23 |
24 |
31 | alert(errMsg)}
41 | />
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/app/components/chat/index.tsx:
--------------------------------------------------------------------------------
1 | import ChatSection from "./chat-session";
2 | import ChatHeader from "./chat-header";
3 |
4 | export default function ChatPage() {
5 | return (
6 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/components/chat/useChatSession.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useBotStore } from "@/app/store/bot";
4 | import { useChat } from "ai/react";
5 | import { useCallback, useEffect, useState } from "react";
6 | import { useClientConfig } from "@/cl/app/components/ui/chat/hooks/use-config";
7 |
8 | // Combine useChat and useBotStore to manage chat session
9 | export function useChatSession() {
10 | const botStore = useBotStore();
11 | const bot = botStore.currentBot();
12 | const session = botStore.currentSession();
13 | const { updateBotSession } = botStore;
14 |
15 | const [isFinished, setIsFinished] = useState(false);
16 | const { backend } = useClientConfig();
17 | const {
18 | messages,
19 | setMessages,
20 | input,
21 | isLoading,
22 | handleSubmit,
23 | handleInputChange,
24 | reload,
25 | stop,
26 | append,
27 | setInput,
28 | } = useChat({
29 | api: `${backend}/api/chat`,
30 | headers: {
31 | "Content-Type": "application/json", // using JSON because of vercel/ai 2.2.26
32 | },
33 | body: {
34 | context: bot.context,
35 | modelConfig: bot.modelConfig,
36 | datasource: bot.datasource,
37 | },
38 | onError: (error: unknown) => {
39 | if (!(error instanceof Error)) throw error;
40 | const message = JSON.parse(error.message);
41 | alert(message.detail);
42 | },
43 | onFinish: () => setIsFinished(true),
44 | });
45 |
46 | // load chat history from session when component mounts
47 | const loadChatHistory = useCallback(() => {
48 | setMessages(session.messages);
49 | }, [session, setMessages]);
50 |
51 | // sync chat history with bot session when finishing streaming
52 | const syncChatHistory = useCallback(() => {
53 | if (messages.length === 0) return;
54 | updateBotSession((session) => (session.messages = messages), bot.id);
55 | }, [messages, updateBotSession, bot.id]);
56 |
57 | useEffect(() => {
58 | loadChatHistory();
59 | }, [loadChatHistory]);
60 |
61 | useEffect(() => {
62 | if (isFinished) {
63 | syncChatHistory();
64 | setIsFinished(false);
65 | }
66 | }, [isFinished, setIsFinished, syncChatHistory]);
67 |
68 | return {
69 | messages,
70 | input,
71 | isLoading,
72 | handleSubmit,
73 | handleInputChange,
74 | reload,
75 | stop,
76 | append,
77 | setInput,
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/app/components/home.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useContext, useEffect, useState } from "react";
4 |
5 | import { QueryClient, QueryClientProvider } from "react-query";
6 | import { useMobileScreen } from "../utils/mobile";
7 |
8 | import dynamic from "next/dynamic";
9 | import { Path } from "../constant";
10 | import { ErrorBoundary } from "./layout/error";
11 |
12 | import {
13 | Route,
14 | HashRouter as Router,
15 | Routes,
16 | useNavigate,
17 | } from "react-router-dom";
18 | import { Bot, useBotStore } from "../store/bot";
19 | import { SideBar } from "./layout/sidebar";
20 | import { LoadingPage } from "@/app/components/ui/loading";
21 |
22 | const SettingsPage = dynamic(
23 | async () => (await import("./settings")).Settings,
24 | {
25 | loading: () => ,
26 | },
27 | );
28 |
29 | const ChatPage = dynamic(async () => (await import("./chat/index")).default, {
30 | loading: () => ,
31 | });
32 |
33 | const useHasHydrated = () => {
34 | const [hasHydrated, setHasHydrated] = useState(false);
35 |
36 | useEffect(() => {
37 | setHasHydrated(true);
38 | }, []);
39 |
40 | return hasHydrated;
41 | };
42 |
43 | const loadAsyncGoogleFont = () => {
44 | const linkEl = document.createElement("link");
45 | const googleFontUrl = "https://fonts.googleapis.com";
46 | linkEl.rel = "stylesheet";
47 | linkEl.href =
48 | googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap";
49 | document.head.appendChild(linkEl);
50 | };
51 |
52 | // if a bot is passed this HOC ensures that the bot is added to the store
53 | // and that the user can directly have a chat session with it
54 | function withBot(Component: React.FunctionComponent, bot?: Bot) {
55 | return function WithBotComponent() {
56 | const [botInitialized, setBotInitialized] = useState(false);
57 | const navigate = useNavigate();
58 | const botStore = useBotStore();
59 | if (bot && !botInitialized) {
60 | if (!bot.share?.id) {
61 | throw new Error("bot must have a shared id");
62 | }
63 | // ensure that bot for the same share id is not created a 2nd time
64 | let sharedBot = botStore.getByShareId(bot.share?.id);
65 | if (!sharedBot) {
66 | sharedBot = botStore.create(bot, { readOnly: true });
67 | }
68 | // let the user directly chat with the bot
69 | botStore.selectBot(sharedBot.id);
70 | setTimeout(() => {
71 | // redirect to chat - use history API to clear URL
72 | history.pushState({}, "", "/");
73 | navigate(Path.Chat);
74 | }, 1);
75 | setBotInitialized(true);
76 | return ;
77 | }
78 |
79 | return ;
80 | };
81 | }
82 |
83 | const SidebarContext = React.createContext<{
84 | showSidebar: boolean;
85 | setShowSidebar: (show: boolean) => void;
86 | } | null>(null);
87 |
88 | function SidebarContextProvider(props: { children: React.ReactNode }) {
89 | const [showSidebar, setShowSidebar] = useState(true);
90 | return (
91 |
92 | {props.children}
93 |
94 | );
95 | }
96 |
97 | export const useSidebarContext = () => {
98 | const context = useContext(SidebarContext);
99 | if (!context) {
100 | throw new Error(
101 | "useSidebarContext must be used within an SidebarContextProvider",
102 | );
103 | }
104 | return context;
105 | };
106 |
107 | function Screen() {
108 | const isMobileScreen = useMobileScreen();
109 | const { showSidebar } = useSidebarContext();
110 |
111 | const showSidebarOnMobile = showSidebar || !isMobileScreen;
112 |
113 | useEffect(() => {
114 | loadAsyncGoogleFont();
115 | }, []);
116 |
117 | return (
118 |
119 | <>
120 | {showSidebarOnMobile && }
121 |
122 |
123 | } />
124 | } />
125 |
126 |
127 | >
128 |
129 | );
130 | }
131 |
132 | export function Home({ bot }: { bot?: Bot }) {
133 | if (!useHasHydrated()) {
134 | return ;
135 | }
136 |
137 | const BotScreen = withBot(Screen, bot);
138 | const queryClient = new QueryClient();
139 |
140 | return (
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/app/components/layout/error.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { GITHUB_URL } from "../../constant";
3 | import Locale from "../../locales";
4 | import { downloadAs } from "../../utils/download";
5 | import {
6 | AlertDialog,
7 | AlertDialogAction,
8 | AlertDialogCancel,
9 | AlertDialogContent,
10 | AlertDialogFooter,
11 | AlertDialogHeader,
12 | AlertDialogTitle,
13 | AlertDialogTrigger,
14 | } from "@/app/components/ui/alert-dialog";
15 | import { cn } from "@/app/lib/utils";
16 | import { Button, buttonVariants } from "@/app/components/ui/button";
17 | import { Github, RefreshCcw } from "lucide-react";
18 | import {
19 | Card,
20 | CardContent,
21 | CardHeader,
22 | CardTitle,
23 | } from "@/app/components/ui/card";
24 |
25 | interface IErrorBoundaryState {
26 | hasError: boolean;
27 | error: Error | null;
28 | info: React.ErrorInfo | null;
29 | }
30 |
31 | export class ErrorBoundary extends React.Component {
32 | constructor(props: any) {
33 | super(props);
34 | this.state = { hasError: false, error: null, info: null };
35 | }
36 |
37 | componentDidCatch(error: Error, info: React.ErrorInfo) {
38 | // Update state with error details
39 | this.setState({ hasError: true, error, info });
40 | }
41 |
42 | clearAndSaveData() {
43 | try {
44 | downloadAs(JSON.stringify(localStorage), "chat-llamaindex-snapshot.json");
45 | } finally {
46 | localStorage.clear();
47 | location.reload();
48 | }
49 | }
50 |
51 | render() {
52 | if (this.state.hasError) {
53 | // Render error message
54 | return (
55 |
56 |
57 |
58 | Oops, something went wrong!
59 |
60 |
61 |
62 | {this.state.error?.toString()}
63 | {this.state.info?.componentStack}
64 |
65 |
66 |
74 |
75 |
76 |
80 |
81 |
82 |
83 |
84 | {Locale.Settings.Danger.Clear.Confirm}
85 |
86 |
87 |
88 | Cancel
89 | {
94 | this.clearAndSaveData();
95 | }}
96 | >
97 | Continue
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 | // if no error occurred, render children
109 | return this.props.children;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/app/components/layout/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeToggle } from "@/app/components/layout/theme-toggle";
2 | import { Github, Settings } from "lucide-react";
3 | import dynamic from "next/dynamic";
4 | import { useNavigate } from "react-router-dom";
5 | import { GITHUB_URL, Path } from "../../constant";
6 | import Locale from "../../locales";
7 | import { Button } from "../ui/button";
8 | import Typography from "../ui/typography";
9 | import { useSidebarContext } from "@/app/components/home";
10 |
11 | const BotList = dynamic(async () => (await import("../bot/bot-list")).default, {
12 | loading: () => null,
13 | });
14 |
15 | export function SideBar(props: { className?: string }) {
16 | const navigate = useNavigate();
17 | const { setShowSidebar } = useSidebarContext();
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
{Locale.Welcome.Title}
26 |
27 | {Locale.Welcome.SubTitle}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
46 |
47 |
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/app/components/layout/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/app/components/layout/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/app/components/ui/button";
8 |
9 | export function ThemeToggle() {
10 | const { setTheme, theme } = useTheme();
11 |
12 | return (
13 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/components/settings.tsx:
--------------------------------------------------------------------------------
1 | import ConfigItem from "@/app/components/bot/bot-settings/config-item";
2 | import { useSidebarContext } from "@/app/components/home";
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogFooter,
9 | AlertDialogHeader,
10 | AlertDialogTitle,
11 | AlertDialogTrigger,
12 | } from "@/app/components/ui/alert-dialog";
13 | import { Button, buttonVariants } from "@/app/components/ui/button";
14 | import { Card, CardContent } from "@/app/components/ui/card";
15 | import { ScrollArea } from "@/app/components/ui/scroll-area";
16 | import { Separator } from "@/app/components/ui/separator";
17 | import Typography from "@/app/components/ui/typography";
18 | import { useToast } from "@/app/components/ui/use-toast";
19 | import { cn } from "@/app/lib/utils";
20 | import { ArchiveRestore, HardDriveDownload, X } from "lucide-react";
21 | import { useEffect } from "react";
22 | import { useNavigate } from "react-router-dom";
23 | import { FileName, Path } from "../constant";
24 | import Locale from "../locales";
25 | import { useBotStore } from "../store/bot";
26 | import { downloadAs, readFromFile } from "../utils/download";
27 | import { useMobileScreen } from "../utils/mobile";
28 | import { ErrorBoundary } from "./layout/error";
29 |
30 | function SettingHeader() {
31 | const navigate = useNavigate();
32 | const { setShowSidebar } = useSidebarContext();
33 | const isMobileScreen = useMobileScreen();
34 | return (
35 |
36 |
37 |
{Locale.Settings.Title}
38 |
39 | {Locale.Settings.SubTitle}
40 |
41 |
42 |
52 |
53 | );
54 | }
55 |
56 | function DangerItems() {
57 | const botStore = useBotStore();
58 | return (
59 |
60 |
61 |
65 |
66 |
67 |
70 |
71 |
72 |
73 |
74 | {Locale.Settings.Danger.Clear.Confirm}
75 |
76 |
77 |
78 | Cancel
79 | {
82 | botStore.clearAllData();
83 | }}
84 | >
85 | Continue
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | function BackupItems() {
97 | const botStore = useBotStore();
98 | const { toast } = useToast();
99 |
100 | const backupBots = () => {
101 | downloadAs(JSON.stringify(botStore.backup()), FileName.Bots);
102 | };
103 |
104 | const restoreBots = async () => {
105 | try {
106 | const content = await readFromFile();
107 | const importBots = JSON.parse(content);
108 | botStore.restore(importBots);
109 | toast({
110 | title: Locale.Settings.Backup.Upload.Success,
111 | variant: "success",
112 | });
113 | } catch (err) {
114 | console.error("[Restore] ", err);
115 | toast({
116 | title: Locale.Settings.Backup.Upload.Failed((err as Error).message),
117 | variant: "destructive",
118 | });
119 | }
120 | };
121 |
122 | return (
123 |
124 |
125 |
129 |
132 |
133 |
137 |
140 |
141 |
142 |
143 | );
144 | }
145 |
146 | export function Settings() {
147 | const navigate = useNavigate();
148 | const { setShowSidebar } = useSidebarContext();
149 | const isMobileScreen = useMobileScreen();
150 | useEffect(() => {
151 | const keydownEvent = (e: KeyboardEvent) => {
152 | if (e.key === "Escape") {
153 | navigate(Path.Home);
154 | if (isMobileScreen) setShowSidebar(true);
155 | }
156 | };
157 | document.addEventListener("keydown", keydownEvent);
158 | return () => {
159 | document.removeEventListener("keydown", keydownEvent);
160 | };
161 | // eslint-disable-next-line react-hooks/exhaustive-deps
162 | }, []);
163 | return (
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | );
173 | }
174 |
--------------------------------------------------------------------------------
/app/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5 |
6 | import { cn } from "@/app/lib/utils";
7 | import { buttonVariants } from "@/app/components/ui/button";
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root;
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
12 |
13 | const AlertDialogPortal = ({
14 | className,
15 | ...props
16 | }: AlertDialogPrimitive.AlertDialogPortalProps) => (
17 |
18 | );
19 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;
20 |
21 | const AlertDialogOverlay = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
33 | ));
34 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
35 |
36 | const AlertDialogContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, ...props }, ref) => (
40 |
41 |
42 |
50 |
51 | ));
52 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
53 |
54 | const AlertDialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | );
66 | AlertDialogHeader.displayName = "AlertDialogHeader";
67 |
68 | const AlertDialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | );
80 | AlertDialogFooter.displayName = "AlertDialogFooter";
81 |
82 | const AlertDialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
91 | ));
92 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
93 |
94 | const AlertDialogDescription = React.forwardRef<
95 | React.ElementRef,
96 | React.ComponentPropsWithoutRef
97 | >(({ className, ...props }, ref) => (
98 |
103 | ));
104 | AlertDialogDescription.displayName =
105 | AlertDialogPrimitive.Description.displayName;
106 |
107 | const AlertDialogAction = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ));
117 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
118 |
119 | const AlertDialogCancel = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, ...props }, ref) => (
123 |
132 | ));
133 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
134 |
135 | export {
136 | AlertDialog,
137 | AlertDialogTrigger,
138 | AlertDialogContent,
139 | AlertDialogHeader,
140 | AlertDialogFooter,
141 | AlertDialogTitle,
142 | AlertDialogDescription,
143 | AlertDialogAction,
144 | AlertDialogCancel,
145 | };
146 |
--------------------------------------------------------------------------------
/app/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/app/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | },
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/app/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center 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,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | },
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/app/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/app/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = "CardTitle";
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = "CardDescription";
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = "CardContent";
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = "CardFooter";
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/app/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5 | import { Check } from "lucide-react";
6 |
7 | import { cn } from "@/app/lib/utils";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/app/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "@/app/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({
14 | className,
15 | ...props
16 | }: DialogPrimitive.DialogPortalProps) => (
17 |
18 | );
19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
20 |
21 | const DialogOverlay = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
33 | ));
34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
35 |
36 | const DialogContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, children, ...props }, ref) => (
40 |
41 |
42 |
50 | {children}
51 |
52 |
53 | Close
54 |
55 |
56 |
57 | ));
58 | DialogContent.displayName = DialogPrimitive.Content.displayName;
59 |
60 | const DialogHeader = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | );
72 | DialogHeader.displayName = "DialogHeader";
73 |
74 | const DialogFooter = ({
75 | className,
76 | ...props
77 | }: React.HTMLAttributes) => (
78 |
85 | );
86 | DialogFooter.displayName = "DialogFooter";
87 |
88 | const DialogTitle = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
100 | ));
101 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
102 |
103 | const DialogDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ));
113 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
114 |
115 | export {
116 | Dialog,
117 | DialogTrigger,
118 | DialogContent,
119 | DialogHeader,
120 | DialogFooter,
121 | DialogTitle,
122 | DialogDescription,
123 | };
124 |
--------------------------------------------------------------------------------
/app/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import { Check, ChevronRight, Circle } from "lucide-react";
6 |
7 | import { cn } from "@/app/lib/utils";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | );
181 | };
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | };
201 |
--------------------------------------------------------------------------------
/app/components/ui/emoji.tsx:
--------------------------------------------------------------------------------
1 | import EmojiPicker, {
2 | Emoji,
3 | EmojiStyle,
4 | Theme as EmojiTheme,
5 | } from "emoji-picker-react";
6 |
7 | export function getEmojiUrl(unified: string, style: EmojiStyle) {
8 | return `https://cdnjs.cloudflare.com/ajax/libs/emoji-datasource-apple/15.0.1/img/${style}/64/${unified}.png`;
9 | }
10 |
11 | export function EmojiAvatarPicker(props: {
12 | onEmojiClick: (emojiId: string) => void;
13 | }) {
14 | return (
15 | {
20 | props.onEmojiClick(e.unified);
21 | }}
22 | />
23 | );
24 | }
25 |
26 | export function EmojiAvatar(props: { avatar: string; size?: number }) {
27 | return (
28 |
33 | );
34 | }
35 |
36 | export function BotAvatar(props: { avatar: string }) {
37 | const { avatar } = props;
38 | return ;
39 | }
40 |
--------------------------------------------------------------------------------
/app/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
--------------------------------------------------------------------------------
/app/components/ui/image-preview.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/app/lib/utils";
2 | import { Loader2Icon, XCircleIcon } from "lucide-react";
3 | import Image from "next/image";
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipProvider,
8 | TooltipTrigger,
9 | } from "./tooltip";
10 |
11 | export default function ImagePreview({
12 | url,
13 | uploading,
14 | onRemove,
15 | }: {
16 | url: string;
17 | uploading: boolean;
18 | onRemove: () => void;
19 | }) {
20 | return (
21 |
22 |
28 |
34 |
35 |
36 |
37 | {uploading ? (
38 |
39 | ) : (
40 |
44 | )}
45 |
46 |
47 | {uploading ? "Uploading file..." : "Remove file"}
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/app/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | export interface InputRangeProps {
9 | onChange: React.ChangeEventHandler;
10 | title?: string;
11 | value: number | string;
12 | className?: string;
13 | min: string;
14 | max: string;
15 | step: string;
16 | }
17 |
18 | const Input = React.forwardRef(
19 | ({ className, type, ...props }, ref) => {
20 | return (
21 |
30 | );
31 | },
32 | );
33 | Input.displayName = "Input";
34 |
35 | function InputRange(props: InputRangeProps) {
36 | const { className, title, value, ...rest } = props;
37 | return (
38 |
44 | {title || value}
45 |
52 |
53 | );
54 | }
55 |
56 | export { Input, InputRange };
57 |
--------------------------------------------------------------------------------
/app/components/ui/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 |
3 | export function Loading() {
4 | return ;
5 | }
6 |
7 | export function LoadingPage() {
8 | return (
9 |
10 |
11 | Loading...
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/components/ui/markdown.tsx:
--------------------------------------------------------------------------------
1 | import "katex/dist/katex.min.css";
2 | import mermaid from "mermaid";
3 | import { RefObject, useEffect, useRef, useState } from "react";
4 | import ReactMarkdown from "react-markdown";
5 | import RehypeHighlight from "rehype-highlight";
6 | import RehypeKatex from "rehype-katex";
7 | import RemarkBreaks from "remark-breaks";
8 | import RemarkGfm from "remark-gfm";
9 | import RemarkMath from "remark-math";
10 | import Locale from "../../locales";
11 | import { copyToClipboard } from "@/app/utils/clipboard";
12 |
13 | import {
14 | Dialog,
15 | DialogContent,
16 | DialogHeader,
17 | DialogTitle,
18 | DialogTrigger,
19 | } from "@/app/components/ui/dialog";
20 | import { Separator } from "@/app/components/ui/separator";
21 | import { useToast } from "@/app/components/ui/use-toast";
22 | import React from "react";
23 | import { useDebouncedCallback } from "use-debounce";
24 | import { Loading } from "@/app/components/ui/loading";
25 |
26 | export function Mermaid(props: { code: string }) {
27 | const ref = useRef(null);
28 | const [hasError, setHasError] = useState(false);
29 | const [imageUrl, setImageUrl] = useState(undefined);
30 |
31 | useEffect(() => {
32 | if (props.code && ref.current) {
33 | mermaid
34 | .run({
35 | nodes: [ref.current],
36 | suppressErrors: true,
37 | })
38 | .catch((e) => {
39 | setHasError(true);
40 | console.error("[Mermaid] ", e.message);
41 | });
42 | }
43 | // eslint-disable-next-line react-hooks/exhaustive-deps
44 | }, [props.code]);
45 |
46 | function viewSvgInNewWindow() {
47 | const svg = ref.current?.querySelector("svg");
48 | if (!svg) return;
49 | const text = new XMLSerializer().serializeToString(svg);
50 | const blob = new Blob([text], { type: "image/svg+xml" });
51 | setImageUrl(URL.createObjectURL(blob));
52 | }
53 |
54 | if (hasError) {
55 | return null;
56 | }
57 |
58 | return (
59 |
83 | );
84 | }
85 |
86 | export function PreCode(props: { children: any }) {
87 | const { toast } = useToast();
88 | const ref = useRef(null);
89 | const refText = ref.current?.innerText;
90 | const [mermaidCode, setMermaidCode] = useState("");
91 |
92 | const renderMermaid = useDebouncedCallback(() => {
93 | if (!ref.current) return;
94 | const mermaidDom = ref.current.querySelector("code.language-mermaid");
95 | if (mermaidDom) {
96 | setMermaidCode((mermaidDom as HTMLElement).innerText);
97 | }
98 | }, 600);
99 |
100 | useEffect(() => {
101 | setTimeout(renderMermaid, 1);
102 | // eslint-disable-next-line react-hooks/exhaustive-deps
103 | }, [refText]);
104 |
105 | return (
106 | <>
107 | {mermaidCode.length > 0 && (
108 |
109 | )}
110 |
111 | {
114 | if (ref.current) {
115 | const code = ref.current.innerText;
116 | copyToClipboard(code, toast);
117 | }
118 | }}
119 | >
120 | {props.children}
121 |
122 | >
123 | );
124 | }
125 |
126 | function _MarkDownContent(props: { content: string }) {
127 | return (
128 | ,
143 | a: (aProps) => {
144 | const href = aProps.href || "";
145 | const isInternal = /^\/#/i.test(href);
146 | const target = isInternal ? "_self" : aProps.target ?? "_blank";
147 | return ;
148 | },
149 | }}
150 | >
151 | {props.content}
152 |
153 | );
154 | }
155 |
156 | export const MarkdownContent = React.memo(_MarkDownContent);
157 |
158 | export function Markdown(
159 | props: {
160 | content: string;
161 | loading?: boolean;
162 | fontSize?: number;
163 | parentRef?: RefObject;
164 | defaultShow?: boolean;
165 | } & React.DOMAttributes,
166 | ) {
167 | const mdRef = useRef(null);
168 |
169 | return (
170 |
179 | {props.loading ? (
180 |
181 | ) : (
182 |
183 | )}
184 |
185 | );
186 | }
187 |
--------------------------------------------------------------------------------
/app/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/app/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ProgressPrimitive from "@radix-ui/react-progress";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
--------------------------------------------------------------------------------
/app/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
16 |
20 | {children}
21 |
22 |
23 |
24 |
25 | ));
26 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
27 |
28 | const ScrollBar = React.forwardRef<
29 | React.ElementRef,
30 | React.ComponentPropsWithoutRef
31 | >(({ className, orientation = "vertical", ...props }, ref) => (
32 |
45 |
46 |
47 | ));
48 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
49 |
50 | export { ScrollArea, ScrollBar };
51 |
--------------------------------------------------------------------------------
/app/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { Check, ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "@/app/lib/utils";
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ));
63 | SelectContent.displayName = SelectPrimitive.Content.displayName;
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ));
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ));
98 | SelectItem.displayName = SelectPrimitive.Item.displayName;
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ));
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator,
121 | };
122 |
--------------------------------------------------------------------------------
/app/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref,
15 | ) => (
16 |
27 | ),
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/app/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/app/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/app/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as ToastPrimitives from "@radix-ui/react-toast";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { X } from "lucide-react";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const ToastProvider = ToastPrimitives.Provider;
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | success: "text-primary bg-background group border-primary",
32 | destructive:
33 | "destructive group border-destructive bg-destructive text-destructive-foreground",
34 | },
35 | },
36 | defaultVariants: {
37 | variant: "default",
38 | },
39 | },
40 | );
41 |
42 | const Toast = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef &
45 | VariantProps
46 | >(({ className, variant, ...props }, ref) => {
47 | return (
48 |
53 | );
54 | });
55 | Toast.displayName = ToastPrimitives.Root.displayName;
56 |
57 | const ToastAction = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 | ));
70 | ToastAction.displayName = ToastPrimitives.Action.displayName;
71 |
72 | const ToastClose = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >(({ className, ...props }, ref) => (
76 |
85 |
86 |
87 | ));
88 | ToastClose.displayName = ToastPrimitives.Close.displayName;
89 |
90 | const ToastTitle = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ));
100 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
101 |
102 | const ToastDescription = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
113 |
114 | type ToastProps = React.ComponentPropsWithoutRef;
115 |
116 | type ToastActionElement = React.ReactElement;
117 |
118 | export {
119 | type ToastProps,
120 | type ToastActionElement,
121 | ToastProvider,
122 | ToastViewport,
123 | Toast,
124 | ToastTitle,
125 | ToastDescription,
126 | ToastClose,
127 | ToastAction,
128 | };
129 |
--------------------------------------------------------------------------------
/app/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/app/components/ui/toast";
11 | import { useToast } from "@/app/components/ui/use-toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/app/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/app/components/ui/typography.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/app/lib/utils";
2 |
3 | type HeadingProps = React.DetailedHTMLProps<
4 | React.HTMLAttributes,
5 | HTMLHeadingElement
6 | >;
7 |
8 | type ParagraphProps = React.DetailedHTMLProps<
9 | React.HTMLAttributes,
10 | HTMLDivElement
11 | >;
12 |
13 | type LinkProps = React.DetailedHTMLProps<
14 | React.AnchorHTMLAttributes,
15 | HTMLAnchorElement
16 | >;
17 |
18 | function H1(props: HeadingProps) {
19 | const { className, children, ...rest } = props;
20 | return (
21 |
28 | {children}
29 |
30 | );
31 | }
32 |
33 | function H2(props: HeadingProps) {
34 | const { className, children, ...rest } = props;
35 | return (
36 |
43 | {children}
44 |
45 | );
46 | }
47 |
48 | function H3(props: HeadingProps) {
49 | const { className, children, ...rest } = props;
50 | return (
51 |
58 | {children}
59 |
60 | );
61 | }
62 |
63 | function H4(props: HeadingProps) {
64 | const { className, children, ...rest } = props;
65 | return (
66 |
73 | {children}
74 |
75 | );
76 | }
77 |
78 | function P(props: ParagraphProps) {
79 | const { className, children, ...rest } = props;
80 | return (
81 |
82 | {children}
83 |
84 | );
85 | }
86 |
87 | function Link(props: LinkProps) {
88 | const { className, children, ...rest } = props;
89 | return (
90 |
97 | {children}
98 |
99 | );
100 | }
101 |
102 | const Typography = {
103 | H1,
104 | H2,
105 | H3,
106 | H4,
107 | P,
108 | Link,
109 | };
110 |
111 | export default Typography;
112 |
--------------------------------------------------------------------------------
/app/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react";
3 |
4 | import type { ToastActionElement, ToastProps } from "@/app/components/ui/toast";
5 |
6 | const TOAST_LIMIT = 1;
7 | const TOAST_REMOVE_DELAY = 1000000;
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string;
11 | title?: React.ReactNode;
12 | description?: React.ReactNode;
13 | action?: ToastActionElement;
14 | };
15 |
16 | const actionTypes = {
17 | ADD_TOAST: "ADD_TOAST",
18 | UPDATE_TOAST: "UPDATE_TOAST",
19 | DISMISS_TOAST: "DISMISS_TOAST",
20 | REMOVE_TOAST: "REMOVE_TOAST",
21 | } as const;
22 |
23 | let count = 0;
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE;
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | type: ActionType["ADD_TOAST"];
35 | toast: ToasterToast;
36 | }
37 | | {
38 | type: ActionType["UPDATE_TOAST"];
39 | toast: Partial;
40 | }
41 | | {
42 | type: ActionType["DISMISS_TOAST"];
43 | toastId?: ToasterToast["id"];
44 | }
45 | | {
46 | type: ActionType["REMOVE_TOAST"];
47 | toastId?: ToasterToast["id"];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t,
85 | ),
86 | };
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action;
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId);
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id);
98 | });
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t,
110 | ),
111 | };
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | };
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: Array<(state: State) => void> = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | });
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss();
158 | },
159 | },
160 | });
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | };
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState);
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState);
174 | return () => {
175 | const index = listeners.indexOf(setState);
176 | if (index > -1) {
177 | listeners.splice(index, 1);
178 | }
179 | };
180 | }, [state]);
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | };
187 | }
188 |
189 | export { useToast, toast };
190 |
--------------------------------------------------------------------------------
/app/constant.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | export const GITHUB_URL = "https://github.com/run-llama/chat-llamaindex";
3 |
4 | export enum Path {
5 | Home = "/",
6 | Chat = "/",
7 | Settings = "/settings",
8 | Bots = "/",
9 | }
10 |
11 | export enum FileName {
12 | Bots = "bots.json",
13 | }
14 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./styles/globals.css";
2 | import "./styles/lib/markdown.css";
3 | import "./styles/lib/highlight.css";
4 |
5 | import Locale from "./locales";
6 | import { Viewport, type Metadata } from "next";
7 | import { Toaster } from "@/app/components/ui/toaster";
8 | import { ThemeProvider } from "@/app/components/layout/theme-provider";
9 |
10 | export const metadata: Metadata = {
11 | title: Locale.Welcome.Title,
12 | description: Locale.Welcome.SubTitle,
13 | appleWebApp: {
14 | title: Locale.Welcome.Title,
15 | statusBarStyle: "default",
16 | },
17 | };
18 |
19 | export const viewport: Viewport = {
20 | themeColor: [
21 | { media: "(prefers-color-scheme: light)", color: "white" },
22 | { media: "(prefers-color-scheme: dark)", color: "black" },
23 | ],
24 | width: "device-width",
25 | initialScale: 1,
26 | maximumScale: 1,
27 | };
28 |
29 | export default function RootLayout({
30 | children,
31 | }: {
32 | children: React.ReactNode;
33 | }) {
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {children}
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/app/locales/en.ts:
--------------------------------------------------------------------------------
1 | const en = {
2 | Chat: {
3 | SubTitle: (count: number) => `${count} messages`,
4 | Actions: {
5 | ChatList: "Go To Chat List",
6 | Copy: "Copy",
7 | Delete: "Delete",
8 | },
9 | InputActions: {
10 | Stop: "Stop generating",
11 | Clear: "Clear Context",
12 | },
13 | Thinking: "Thinking…",
14 | InputMobile: "Enter to send",
15 | Input:
16 | "Enter to send, Shift + Enter to wrap, enter URLs to add a PDF or HTML document to the context",
17 | Send: "Send",
18 | IsContext: "Contextual Prompt",
19 | LoadingURL: "Loading content...",
20 | LLMError:
21 | "There was an error calling the OpenAI API. Please try again later.",
22 | },
23 | Upload: {
24 | Invalid: (acceptTypes: string) =>
25 | `Invalid file type. Please select a file with one of these formats: ${acceptTypes}`,
26 | SizeExceeded: (limitSize: number) =>
27 | `File size exceeded. Limit is ${limitSize} MB`,
28 | Failed: (e: string) => `Error uploading file: ${e}`,
29 | ParseDataURLFailed: "Unable to read file: Please check if it's encrypted.",
30 | UnknownFileType: "Unknown file type",
31 | ModelDoesNotSupportImages: (acceptTypes: string) =>
32 | `Image upload is not supported for this model. Upload one of the supported types instead: ${acceptTypes}`,
33 | },
34 | Export: {
35 | Image: {
36 | Modal: "Long press or right click to save image",
37 | },
38 | },
39 | Memory: {
40 | Title: "Memory Prompt",
41 | Send: "Send Memory",
42 | },
43 | Home: {
44 | Github: "Github",
45 | Logout: "Logout",
46 | Settings: "Settings",
47 | },
48 | Settings: {
49 | Title: "Settings",
50 | SubTitle: "All Settings",
51 | Danger: {
52 | Clear: {
53 | Title: "Clear All Data",
54 | SubTitle: "Reset all bots and clear all messages",
55 | Action: "Clear",
56 | Confirm: "Confirm to clear all data?",
57 | },
58 | },
59 |
60 | Model: "Model",
61 | Temperature: {
62 | Title: "Temperature",
63 | SubTitle: "A larger value makes the more random output",
64 | },
65 | TopP: {
66 | Title: "Top P",
67 | SubTitle: "Do not alter this value together with temperature",
68 | },
69 | MaxTokens: {
70 | Title: "Max Tokens",
71 | SubTitle: "Maximum length of input tokens and generated tokens",
72 | },
73 | Backup: {
74 | Download: {
75 | Title: "Backup Bots",
76 | SutTitle: "Download the state of your bots to a JSON file",
77 | },
78 | Upload: {
79 | Title: "Restore Bots",
80 | SutTitle: "Upload the state of your bots from a JSON file",
81 | Success: "Successfully restored the bots from the JSON file",
82 | Failed: (e: string) => `Error importing the JSON file: ${e}`,
83 | },
84 | },
85 | },
86 | Store: {
87 | DefaultBotName: "New Bot",
88 | BotHello: "Hello! How can I assist you today?",
89 | },
90 | Copy: {
91 | Success: "Copied to clipboard",
92 | Failed: "Copy failed, please grant permission to access clipboard",
93 | },
94 | Context: {
95 | Add: "Add a Prompt",
96 | Clear: "Context Cleared",
97 | Revert: "Revert",
98 | Title: "Context Prompt Settings",
99 | },
100 | Share: {
101 | Title: "Share Bot",
102 | Url: {
103 | Title: "URL",
104 | Hint: "Use the URL to share your bot. The URL will be valid for 30 days.",
105 | Error: "Oops, something went wrong. Please try again later.",
106 | },
107 | },
108 | Bot: {
109 | Name: "Bot",
110 | Page: {
111 | Search: (count: number) => `Search Bot - ${count} bots`,
112 | Create: "Create bot",
113 | },
114 | Item: {
115 | Edit: "Edit",
116 | Delete: "Delete",
117 | DeleteConfirm: "Confirm to delete?",
118 | Share: "Share",
119 | },
120 | EditModal: {
121 | Title: `Edit Bot`,
122 | Clone: "Clone",
123 | },
124 | Config: {
125 | Name: "Bot Name",
126 | Title: "Bot Settings",
127 | Datasource: "Data Source",
128 | },
129 | },
130 |
131 | Welcome: {
132 | Title: "Chat LlamaIndex",
133 | SubTitle: "Create chat bots that know your data",
134 | Quote:
135 | "“This tool has saved me countless hours of work and helped me apply AI features to my work faster than ever before.”",
136 | LoginLinkedinTitle: "Login with LinkedIn",
137 | },
138 | };
139 |
140 | export type LocaleType = typeof en;
141 |
142 | export default en;
143 |
--------------------------------------------------------------------------------
/app/locales/index.ts:
--------------------------------------------------------------------------------
1 | import en from "./en";
2 |
3 | import type { LocaleType } from "./en";
4 | export type { LocaleType } from "./en";
5 |
6 | export default en as LocaleType;
7 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Analytics } from "@vercel/analytics/react";
2 |
3 | import { Home } from "./components/home";
4 |
5 | export default async function App() {
6 | return (
7 | <>
8 |
9 |
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/store/bot.data.ts:
--------------------------------------------------------------------------------
1 | import { Bot, ChatSession } from "@/app/store/bot";
2 | import { nanoid } from "nanoid";
3 | import Locale from "../locales";
4 |
5 | const toLlamaCloudDataSource = (pipeline: string) =>
6 | JSON.stringify({ pipeline });
7 |
8 | const TEMPLATE = (PERSONA: string) =>
9 | `I want you to act as a ${PERSONA}. I will provide you with the context needed to solve my problem. Use intelligent, simple, and understandable language. Be concise. It is helpful to explain your thoughts step by step and with bullet points.`;
10 |
11 | type DemoBot = Omit;
12 |
13 | export const DEMO_BOTS: DemoBot[] = [
14 | {
15 | id: "2",
16 | avatar: "1f916",
17 | name: "My Documents",
18 | botHello: "Hello! How can I assist you today?",
19 | context: [],
20 | modelConfig: {
21 | model: "gpt-4o-mini",
22 | temperature: 0.5,
23 | maxTokens: 4096,
24 | sendMemory: false,
25 | },
26 | readOnly: true,
27 | datasource: toLlamaCloudDataSource("documents"),
28 | },
29 | {
30 | id: "3",
31 | avatar: "1f5a5-fe0f",
32 | name: "Red Hat Linux Expert",
33 | botHello: "Hello! How can I help you with Red Hat Linux?",
34 | context: [
35 | {
36 | role: "system",
37 | content: TEMPLATE("Red Hat Linux Expert"),
38 | id: "demo-bot-3-system-message",
39 | },
40 | ],
41 | modelConfig: {
42 | model: "gpt-4o-mini",
43 | temperature: 0.1,
44 | maxTokens: 4096,
45 | sendMemory: false,
46 | },
47 | readOnly: true,
48 | datasource: toLlamaCloudDataSource("redhat"),
49 | },
50 | {
51 | id: "4",
52 | avatar: "1f454",
53 | name: "Apple Watch Genius",
54 | botHello: "Hello! How can I help you with Apple Watches?",
55 | context: [
56 | {
57 | role: "system",
58 | content: TEMPLATE("Apple Genius specialized in Apple Watches"),
59 | id: "demo-bot-4-system-message",
60 | },
61 | ],
62 | modelConfig: {
63 | model: "gpt-4o-mini",
64 | temperature: 0.1,
65 | maxTokens: 4096,
66 | sendMemory: false,
67 | },
68 | readOnly: true,
69 | datasource: toLlamaCloudDataSource("watchos"),
70 | },
71 | {
72 | id: "5",
73 | avatar: "1f4da",
74 | name: "German Basic Law Expert",
75 | botHello: "Hello! How can I assist you today?",
76 | context: [
77 | {
78 | role: "system",
79 | content: TEMPLATE("Lawyer specialized in the basic law of Germany"),
80 | id: "demo-bot-5-system-message",
81 | },
82 | ],
83 | modelConfig: {
84 | model: "gpt-4o-mini",
85 | temperature: 0.1,
86 | maxTokens: 4096,
87 | sendMemory: false,
88 | },
89 | readOnly: true,
90 | datasource: toLlamaCloudDataSource("basic_law_germany"),
91 | },
92 | ];
93 |
94 | export const createDemoBots = (): Record => {
95 | const map: Record = {};
96 | DEMO_BOTS.forEach((demoBot) => {
97 | const bot: Bot = JSON.parse(JSON.stringify(demoBot));
98 | bot.session = createEmptySession();
99 | map[bot.id] = bot;
100 | });
101 | return map;
102 | };
103 |
104 | export const createEmptyBot = (): Bot => ({
105 | id: nanoid(),
106 | avatar: "1f916",
107 | name: Locale.Store.DefaultBotName,
108 | context: [],
109 | modelConfig: {
110 | model: "gpt-4o-mini",
111 | temperature: 0.5,
112 | maxTokens: 4096,
113 | sendMemory: false,
114 | },
115 | readOnly: false,
116 | createdAt: Date.now(),
117 | botHello: Locale.Store.BotHello,
118 | session: createEmptySession(),
119 | });
120 |
121 | export function createEmptySession(): ChatSession {
122 | return {
123 | messages: [],
124 | };
125 | }
126 |
--------------------------------------------------------------------------------
/app/store/bot.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from "nanoid";
2 | import { create } from "zustand";
3 | import { persist } from "zustand/middleware";
4 | import {
5 | DEMO_BOTS,
6 | createDemoBots,
7 | createEmptyBot,
8 | createEmptySession,
9 | } from "./bot.data";
10 | import { Message } from "ai";
11 |
12 | export const MESSAGE_ROLES: Message["role"][] = [
13 | "system",
14 | "user",
15 | "assistant",
16 | "function",
17 | "data",
18 | "tool",
19 | ];
20 |
21 | export const AVAILABLE_DATASOURCES = [
22 | "documents",
23 | "redhat",
24 | "watchos",
25 | "basic_law_germany",
26 | ] as const;
27 |
28 | export const ALL_MODELS = ["gpt-4o-mini", "gpt-4-turbo", "gpt-4o"] as const;
29 |
30 | export type ModelType = (typeof ALL_MODELS)[number];
31 |
32 | export interface LLMConfig {
33 | model: ModelType;
34 | temperature?: number;
35 | topP?: number;
36 | sendMemory?: boolean;
37 | maxTokens?: number;
38 | }
39 |
40 | export interface ChatSession {
41 | messages: Message[];
42 | }
43 |
44 | export type Share = {
45 | id: string;
46 | };
47 |
48 | export type Bot = {
49 | id: string;
50 | avatar: string;
51 | name: string;
52 | context: Message[];
53 | modelConfig: LLMConfig;
54 | readOnly: boolean;
55 | botHello: string | null;
56 | datasource?: string;
57 | share?: Share;
58 | createdAt?: number;
59 | session: ChatSession;
60 | };
61 |
62 | type BotState = {
63 | bots: Record;
64 | currentBotId: string;
65 | };
66 |
67 | type BotStore = BotState & {
68 | currentBot: () => Bot;
69 | selectBot: (id: string) => void;
70 | currentSession: () => ChatSession;
71 | updateBotSession: (
72 | updater: (session: ChatSession) => void,
73 | botId: string,
74 | ) => void;
75 | get: (id: string) => Bot | undefined;
76 | getByShareId: (shareId: string) => Bot | undefined;
77 | getAll: () => Bot[];
78 | create: (
79 | bot?: Partial,
80 | options?: { readOnly?: boolean; reset?: boolean },
81 | ) => Bot;
82 | update: (id: string, updater: (bot: Bot) => void) => void;
83 | delete: (id: string) => void;
84 | restore: (state: BotState) => void;
85 | backup: () => BotState;
86 | clearAllData: () => void;
87 | };
88 |
89 | const demoBots = createDemoBots();
90 |
91 | export const useBotStore = create()(
92 | persist(
93 | (set, get) => ({
94 | bots: demoBots,
95 | currentBotId: Object.values(demoBots)[0].id,
96 |
97 | currentBot() {
98 | return get().bots[get().currentBotId];
99 | },
100 | selectBot(id) {
101 | set(() => ({ currentBotId: id }));
102 | },
103 | currentSession() {
104 | return get().currentBot().session;
105 | },
106 | updateBotSession(updater, botId) {
107 | const bots = get().bots;
108 | updater(bots[botId].session);
109 | set(() => ({ bots }));
110 | },
111 | get(id) {
112 | return get().bots[id] || undefined;
113 | },
114 | getAll() {
115 | const list = Object.values(get().bots).map((b) => ({
116 | ...b,
117 | createdAt: b.createdAt || 0,
118 | }));
119 | return list.sort((a, b) => b.createdAt - a.createdAt);
120 | },
121 | getByShareId(shareId) {
122 | return get()
123 | .getAll()
124 | .find((b) => shareId === b.share?.id);
125 | },
126 | create(bot, options) {
127 | const bots = get().bots;
128 | const id = nanoid();
129 | const session = createEmptySession();
130 | bots[id] = {
131 | ...createEmptyBot(),
132 | ...bot,
133 | id,
134 | session,
135 | readOnly: options?.readOnly || false,
136 | };
137 | if (options?.reset) {
138 | bots[id].share = undefined;
139 | }
140 | set(() => ({ bots }));
141 | return bots[id];
142 | },
143 | update(id, updater) {
144 | const bots = get().bots;
145 | const bot = bots[id];
146 | if (!bot) return;
147 | const updateBot = { ...bot };
148 | updater(updateBot);
149 | bots[id] = updateBot;
150 | set(() => ({ bots }));
151 | },
152 | delete(id) {
153 | const bots = JSON.parse(JSON.stringify(get().bots));
154 | delete bots[id];
155 |
156 | let nextId = get().currentBotId;
157 | if (nextId === id) {
158 | nextId = Object.keys(bots)[0];
159 | }
160 | set(() => ({ bots, currentBotId: nextId }));
161 | },
162 |
163 | backup() {
164 | return get();
165 | },
166 | restore(state: BotState) {
167 | if (!state.bots) {
168 | throw new Error("no state object");
169 | }
170 | set(() => ({ bots: state.bots }));
171 | },
172 | clearAllData() {
173 | localStorage.clear();
174 | location.reload();
175 | },
176 | }),
177 | {
178 | name: "bot-store",
179 | version: 1,
180 | migrate: (persistedState, version) => {
181 | const state = persistedState as BotState;
182 | if (version < 1) {
183 | DEMO_BOTS.forEach((demoBot) => {
184 | // check if there is a bot with the same name as the demo bot
185 | const bot = Object.values(state.bots).find(
186 | (b) => b.name === demoBot.name,
187 | );
188 | if (bot) {
189 | // if so, update the id of the bot to the demo bot id
190 | delete state.bots[bot.id];
191 | bot.id = demoBot.id;
192 | state.bots[bot.id] = bot;
193 | } else {
194 | // if not, store the new demo bot
195 | const bot: Bot = JSON.parse(JSON.stringify(demoBot));
196 | bot.session = createEmptySession();
197 | state.bots[bot.id] = bot;
198 | }
199 | });
200 | state.currentBotId = Object.values(state.bots)[0].id;
201 | }
202 | return state as any;
203 | },
204 | },
205 | ),
206 | );
207 |
--------------------------------------------------------------------------------
/app/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 142.1 76.2% 36.3%;
14 | --primary-foreground: 355.7 100% 97.3%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 142.1 76.2% 36.3%;
26 | --radius: 0.5rem;
27 |
28 | /* markdown variables */
29 | --color-prettylights-syntax-comment: #6e7781;
30 | --color-prettylights-syntax-constant: #0550ae;
31 | --color-prettylights-syntax-entity: #8250df;
32 | --color-prettylights-syntax-storage-modifier-import: #24292f;
33 | --color-prettylights-syntax-entity-tag: #116329;
34 | --color-prettylights-syntax-keyword: #cf222e;
35 | --color-prettylights-syntax-string: #0a3069;
36 | --color-prettylights-syntax-variable: #953800;
37 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
38 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
39 | --color-prettylights-syntax-invalid-illegal-bg: #82071e;
40 | --color-prettylights-syntax-carriage-return-text: #f6f8fa;
41 | --color-prettylights-syntax-carriage-return-bg: #cf222e;
42 | --color-prettylights-syntax-string-regexp: #116329;
43 | --color-prettylights-syntax-markup-list: #3b2300;
44 | --color-prettylights-syntax-markup-heading: #0550ae;
45 | --color-prettylights-syntax-markup-italic: #24292f;
46 | --color-prettylights-syntax-markup-bold: #24292f;
47 | --color-prettylights-syntax-markup-deleted-text: #82071e;
48 | --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
49 | --color-prettylights-syntax-markup-inserted-text: #116329;
50 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
51 | --color-prettylights-syntax-markup-changed-text: #953800;
52 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
53 | --color-prettylights-syntax-markup-ignored-text: #eaeef2;
54 | --color-prettylights-syntax-markup-ignored-bg: #0550ae;
55 | --color-prettylights-syntax-meta-diff-range: #8250df;
56 | --color-prettylights-syntax-brackethighlighter-angle: #57606a;
57 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
58 | --color-prettylights-syntax-constant-other-reference-link: #0a3069;
59 | --color-fg-default: #24292f;
60 | --color-fg-muted: #57606a;
61 | --color-fg-subtle: #6e7781;
62 | --color-canvas-default: transparent;
63 | --color-canvas-subtle: #f6f8fa;
64 | --color-border-default: #d0d7de;
65 | --color-border-muted: hsla(210, 18%, 87%, 1);
66 | --color-neutral-muted: rgba(175, 184, 193, 0.2);
67 | --color-accent-fg: #0969da;
68 | --color-accent-emphasis: #0969da;
69 | --color-attention-subtle: #fff8c5;
70 | --color-danger-fg: #cf222e;
71 | }
72 |
73 | .dark {
74 | --background: 20 14.3% 4.1%;
75 | --foreground: 0 0% 95%;
76 | --card: 24 9.8% 10%;
77 | --card-foreground: 0 0% 95%;
78 | --popover: 0 0% 9%;
79 | --popover-foreground: 0 0% 95%;
80 | --primary: 142.1 70.6% 45.3%;
81 | --primary-foreground: 144.9 80.4% 10%;
82 | --secondary: 240 3.7% 15.9%;
83 | --secondary-foreground: 0 0% 98%;
84 | --muted: 0 0% 15%;
85 | --muted-foreground: 240 5% 64.9%;
86 | --accent: 12 6.5% 15.1%;
87 | --accent-foreground: 0 0% 98%;
88 | --destructive: 0 62.8% 30.6%;
89 | --destructive-foreground: 0 85.7% 97.3%;
90 | --border: 240 3.7% 15.9%;
91 | --input: 240 3.7% 15.9%;
92 | --ring: 142.4 71.8% 29.2%;
93 |
94 | /* markdown variables */
95 | --color-prettylights-syntax-comment: #8b949e;
96 | --color-prettylights-syntax-constant: #79c0ff;
97 | --color-prettylights-syntax-entity: #d2a8ff;
98 | --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
99 | --color-prettylights-syntax-entity-tag: #7ee787;
100 | --color-prettylights-syntax-keyword: #ff7b72;
101 | --color-prettylights-syntax-string: #a5d6ff;
102 | --color-prettylights-syntax-variable: #ffa657;
103 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
104 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
105 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
106 | --color-prettylights-syntax-carriage-return-text: #f0f6fc;
107 | --color-prettylights-syntax-carriage-return-bg: #b62324;
108 | --color-prettylights-syntax-string-regexp: #7ee787;
109 | --color-prettylights-syntax-markup-list: #f2cc60;
110 | --color-prettylights-syntax-markup-heading: #1f6feb;
111 | --color-prettylights-syntax-markup-italic: #c9d1d9;
112 | --color-prettylights-syntax-markup-bold: #c9d1d9;
113 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
114 | --color-prettylights-syntax-markup-deleted-bg: #67060c;
115 | --color-prettylights-syntax-markup-inserted-text: #aff5b4;
116 | --color-prettylights-syntax-markup-inserted-bg: #033a16;
117 | --color-prettylights-syntax-markup-changed-text: #ffdfb6;
118 | --color-prettylights-syntax-markup-changed-bg: #5a1e02;
119 | --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
120 | --color-prettylights-syntax-markup-ignored-bg: #1158c7;
121 | --color-prettylights-syntax-meta-diff-range: #d2a8ff;
122 | --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
123 | --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
124 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
125 | --color-fg-default: #c9d1d9;
126 | --color-fg-muted: #8b949e;
127 | --color-fg-subtle: #6e7681;
128 | --color-canvas-default: transparent;
129 | --color-canvas-subtle: #161b22;
130 | --color-border-default: #30363d;
131 | --color-border-muted: #21262d;
132 | --color-neutral-muted: rgba(110, 118, 129, 0.4);
133 | --color-accent-fg: #58a6ff;
134 | --color-accent-emphasis: #1f6feb;
135 | --color-attention-subtle: rgba(187, 128, 9, 0.15);
136 | --color-danger-fg: #f85149;
137 | }
138 | }
139 |
140 | @layer base {
141 | * {
142 | @apply border-border;
143 | }
144 | body {
145 | @apply bg-background text-foreground;
146 | }
147 |
148 | .copy-code-button {
149 | @apply absolute cursor-pointer bg-secondary text-muted-foreground border-2 translate-x-2.5 opacity-0 transition-all duration-200 delay-300 px-[5px] py-0 rounded-[10px] right-2.5 top-[1em] after:content-["copy"] hover:opacity-100;
150 | }
151 |
152 | .custom-scrollarea-viewport > div {
153 | @apply !block;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/app/styles/lib/highlight.css:
--------------------------------------------------------------------------------
1 | .markdown-body pre {
2 | padding: 0;
3 | }
4 | .markdown-body pre,
5 | .markdown-body code {
6 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
7 | }
8 | .markdown-body pre code {
9 | display: block;
10 | overflow-x: auto;
11 | padding: 1em;
12 | }
13 | .markdown-body code {
14 | padding: 3px 5px;
15 | }
16 | .markdown-body .hljs,
17 | .markdown-body pre {
18 | background: #1a1b26;
19 | color: #cbd2ea;
20 | }
21 | .markdown-body .hljs-comment,
22 | .markdown-body .hljs-meta {
23 | color: #565f89;
24 | }
25 | .markdown-body .hljs-deletion,
26 | .markdown-body .hljs-doctag,
27 | .markdown-body .hljs-regexp,
28 | .markdown-body .hljs-selector-attr,
29 | .markdown-body .hljs-selector-class,
30 | .markdown-body .hljs-selector-id,
31 | .markdown-body .hljs-selector-pseudo,
32 | .markdown-body .hljs-tag,
33 | .markdown-body .hljs-template-tag,
34 | .markdown-body .hljs-variable.language_ {
35 | color: #f7768e;
36 | }
37 | .markdown-body .hljs-link,
38 | .markdown-body .hljs-literal,
39 | .markdown-body .hljs-number,
40 | .markdown-body .hljs-params,
41 | .markdown-body .hljs-template-variable,
42 | .markdown-body .hljs-type,
43 | .markdown-body .hljs-variable {
44 | color: #ff9e64;
45 | }
46 | .markdown-body .hljs-attribute,
47 | .markdown-body .hljs-built_in {
48 | color: #e0af68;
49 | }
50 | .markdown-body .hljs-keyword,
51 | .markdown-body .hljs-property,
52 | .markdown-body .hljs-subst,
53 | .markdown-body .hljs-title,
54 | .markdown-body .hljs-title.class_,
55 | .markdown-body .hljs-title.class_.inherited__,
56 | .markdown-body .hljs-title.function_ {
57 | color: #7dcfff;
58 | }
59 | .markdown-body .hljs-selector-tag {
60 | color: #73daca;
61 | }
62 | .markdown-body .hljs-addition,
63 | .markdown-body .hljs-bullet,
64 | .markdown-body .hljs-quote,
65 | .markdown-body .hljs-string,
66 | .markdown-body .hljs-symbol {
67 | color: #9ece6a;
68 | }
69 | .markdown-body .hljs-code,
70 | .markdown-body .hljs-formula,
71 | .markdown-body .hljs-section {
72 | color: #7aa2f7;
73 | }
74 | .markdown-body .hljs-attr,
75 | .markdown-body .hljs-char.escape_,
76 | .markdown-body .hljs-keyword,
77 | .markdown-body .hljs-name,
78 | .markdown-body .hljs-operator {
79 | color: #bb9af7;
80 | }
81 | .markdown-body .hljs-punctuation {
82 | color: #c0caf5;
83 | }
84 | .markdown-body .hljs-emphasis {
85 | font-style: italic;
86 | }
87 | .markdown-body .hljs-strong {
88 | font-weight: 700;
89 | }
90 |
--------------------------------------------------------------------------------
/app/styles/lib/markdown.css:
--------------------------------------------------------------------------------
1 | .markdown-body {
2 | -ms-text-size-adjust: 100%;
3 | -webkit-text-size-adjust: 100%;
4 | margin: 0;
5 | font-size: 14px;
6 | line-height: 1.5;
7 | word-wrap: break-word;
8 | }
9 | .markdown-body .octicon {
10 | display: inline-block;
11 | fill: currentColor;
12 | vertical-align: text-bottom;
13 | }
14 | .markdown-body h1:hover .anchor .octicon-link:before,
15 | .markdown-body h2:hover .anchor .octicon-link:before,
16 | .markdown-body h3:hover .anchor .octicon-link:before,
17 | .markdown-body h4:hover .anchor .octicon-link:before,
18 | .markdown-body h5:hover .anchor .octicon-link:before,
19 | .markdown-body h6:hover .anchor .octicon-link:before {
20 | width: 16px;
21 | height: 16px;
22 | content: " ";
23 | display: inline-block;
24 | background-color: currentColor;
25 | -webkit-bot-image: url("data:image/svg+xml,");
26 | bot-image: url("data:image/svg+xml,");
27 | }
28 | .markdown-body details,
29 | .markdown-body figcaption,
30 | .markdown-body figure {
31 | display: block;
32 | }
33 | .markdown-body summary {
34 | display: list-item;
35 | }
36 | .markdown-body [hidden] {
37 | display: none !important;
38 | }
39 | .markdown-body a {
40 | background-color: transparent;
41 | color: var(--color-accent-fg);
42 | text-decoration: none;
43 | }
44 | .markdown-body abbr[title] {
45 | border-bottom: none;
46 | text-decoration: underline dotted;
47 | }
48 | .markdown-body b,
49 | .markdown-body strong {
50 | font-weight: var(--base-text-weight-semibold, 600);
51 | }
52 | .markdown-body dfn {
53 | font-style: italic;
54 | }
55 | .markdown-body h1 {
56 | margin: 0.67em 0;
57 | font-weight: var(--base-text-weight-semibold, 600);
58 | padding-bottom: 0.3em;
59 | font-size: 2em;
60 | border-bottom: 1px solid var(--color-border-muted);
61 | }
62 | .markdown-body mark {
63 | background-color: var(--color-attention-subtle);
64 | color: var(--color-fg-default);
65 | }
66 | .markdown-body small {
67 | font-size: 90%;
68 | }
69 | .markdown-body sub,
70 | .markdown-body sup {
71 | font-size: 75%;
72 | line-height: 0;
73 | position: relative;
74 | vertical-align: baseline;
75 | }
76 | .markdown-body sub {
77 | bottom: -0.25em;
78 | }
79 | .markdown-body sup {
80 | top: -0.5em;
81 | }
82 | .markdown-body img {
83 | border-style: none;
84 | max-width: 100%;
85 | box-sizing: content-box;
86 | background-color: var(--color-canvas-default);
87 | }
88 | .markdown-body code,
89 | .markdown-body kbd,
90 | .markdown-body pre,
91 | .markdown-body samp {
92 | font-family: monospace;
93 | font-size: 1em;
94 | }
95 | .markdown-body figure {
96 | margin: 1em 40px;
97 | }
98 | .markdown-body hr {
99 | box-sizing: content-box;
100 | overflow: hidden;
101 | background: transparent;
102 | border-bottom: 1px solid var(--color-border-muted);
103 | height: 0.25em;
104 | padding: 0;
105 | margin: 24px 0;
106 | background-color: var(--color-border-default);
107 | border: 0;
108 | }
109 | .markdown-body input {
110 | font: inherit;
111 | margin: 0;
112 | overflow: visible;
113 | font-family: inherit;
114 | font-size: inherit;
115 | line-height: inherit;
116 | }
117 | .markdown-body [type="button"],
118 | .markdown-body [type="reset"],
119 | .markdown-body [type="submit"] {
120 | -webkit-appearance: button;
121 | }
122 | .markdown-body [type="checkbox"],
123 | .markdown-body [type="radio"] {
124 | box-sizing: border-box;
125 | padding: 0;
126 | }
127 | .markdown-body [type="number"]::-webkit-inner-spin-button,
128 | .markdown-body [type="number"]::-webkit-outer-spin-button {
129 | height: auto;
130 | }
131 | .markdown-body [type="search"]::-webkit-search-cancel-button,
132 | .markdown-body [type="search"]::-webkit-search-decoration {
133 | -webkit-appearance: none;
134 | }
135 | .markdown-body ::-webkit-input-placeholder {
136 | color: inherit;
137 | opacity: 0.54;
138 | }
139 | .markdown-body ::-webkit-file-upload-button {
140 | -webkit-appearance: button;
141 | font: inherit;
142 | }
143 | .markdown-body a:hover {
144 | text-decoration: underline;
145 | }
146 | .markdown-body ::placeholder {
147 | color: var(--color-fg-subtle);
148 | opacity: 1;
149 | }
150 | .markdown-body hr::before {
151 | display: table;
152 | content: "";
153 | }
154 | .markdown-body hr::after {
155 | display: table;
156 | clear: both;
157 | content: "";
158 | }
159 | .markdown-body table {
160 | border-spacing: 0;
161 | border-collapse: collapse;
162 | display: block;
163 | width: max-content;
164 | max-width: 100%;
165 | overflow: auto;
166 | }
167 | .markdown-body td,
168 | .markdown-body th {
169 | padding: 0;
170 | }
171 | .markdown-body details summary {
172 | cursor: pointer;
173 | }
174 | .markdown-body details:not([open]) > :not(summary) {
175 | display: none !important;
176 | }
177 | .markdown-body a:focus,
178 | .markdown-body [role="button"]:focus,
179 | .markdown-body input[type="radio"]:focus,
180 | .markdown-body input[type="checkbox"]:focus {
181 | outline: 2px solid var(--color-accent-fg);
182 | outline-offset: -2px;
183 | box-shadow: none;
184 | }
185 | .markdown-body a:focus:not(:focus-visible),
186 | .markdown-body [role="button"]:focus:not(:focus-visible),
187 | .markdown-body input[type="radio"]:focus:not(:focus-visible),
188 | .markdown-body input[type="checkbox"]:focus:not(:focus-visible) {
189 | outline: solid 1px transparent;
190 | }
191 | .markdown-body a:focus-visible,
192 | .markdown-body [role="button"]:focus-visible,
193 | .markdown-body input[type="radio"]:focus-visible,
194 | .markdown-body input[type="checkbox"]:focus-visible {
195 | outline: 2px solid var(--color-accent-fg);
196 | outline-offset: -2px;
197 | box-shadow: none;
198 | }
199 | .markdown-body a:not([class]):focus,
200 | .markdown-body a:not([class]):focus-visible,
201 | .markdown-body input[type="radio"]:focus,
202 | .markdown-body input[type="radio"]:focus-visible,
203 | .markdown-body input[type="checkbox"]:focus,
204 | .markdown-body input[type="checkbox"]:focus-visible {
205 | outline-offset: 0;
206 | }
207 | .markdown-body kbd {
208 | display: inline-block;
209 | padding: 3px 5px;
210 | font:
211 | 11px ui-monospace,
212 | SFMono-Regular,
213 | SF Mono,
214 | Menlo,
215 | Consolas,
216 | Liberation Mono,
217 | monospace;
218 | line-height: 10px;
219 | color: var(--color-fg-default);
220 | vertical-align: middle;
221 | background-color: var(--color-canvas-subtle);
222 | border: solid 1px var(--color-neutral-muted);
223 | border-bottom-color: var(--color-neutral-muted);
224 | border-radius: 6px;
225 | box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
226 | }
227 | .markdown-body h1,
228 | .markdown-body h2,
229 | .markdown-body h3,
230 | .markdown-body h4,
231 | .markdown-body h5,
232 | .markdown-body h6 {
233 | margin-top: 24px;
234 | margin-bottom: 16px;
235 | font-weight: var(--base-text-weight-semibold, 600);
236 | line-height: 1.25;
237 | }
238 | .markdown-body h2 {
239 | font-weight: var(--base-text-weight-semibold, 600);
240 | padding-bottom: 0.3em;
241 | font-size: 1.5em;
242 | border-bottom: 1px solid var(--color-border-muted);
243 | }
244 | .markdown-body h3 {
245 | font-weight: var(--base-text-weight-semibold, 600);
246 | font-size: 1.25em;
247 | }
248 | .markdown-body h4 {
249 | font-weight: var(--base-text-weight-semibold, 600);
250 | font-size: 1em;
251 | }
252 | .markdown-body h5 {
253 | font-weight: var(--base-text-weight-semibold, 600);
254 | font-size: 0.875em;
255 | }
256 | .markdown-body h6 {
257 | font-weight: var(--base-text-weight-semibold, 600);
258 | font-size: 0.85em;
259 | color: var(--color-fg-muted);
260 | }
261 | .markdown-body p {
262 | margin-top: 0;
263 | margin-bottom: 10px;
264 | }
265 | .markdown-body blockquote {
266 | margin: 0;
267 | padding: 0 1em;
268 | color: var(--color-fg-muted);
269 | border-left: 0.25em solid var(--color-border-default);
270 | }
271 | .markdown-body ul,
272 | .markdown-body ol {
273 | margin-top: 0;
274 | margin-bottom: 0;
275 | padding-left: 2em;
276 | }
277 | .markdown-body ol ol,
278 | .markdown-body ul ol {
279 | list-style-type: lower-roman;
280 | }
281 | .markdown-body ul ul ol,
282 | .markdown-body ul ol ol,
283 | .markdown-body ol ul ol,
284 | .markdown-body ol ol ol {
285 | list-style-type: lower-alpha;
286 | }
287 | .markdown-body dd {
288 | margin-left: 0;
289 | }
290 | .markdown-body tt,
291 | .markdown-body code,
292 | .markdown-body samp {
293 | font-family:
294 | ui-monospace,
295 | SFMono-Regular,
296 | SF Mono,
297 | Menlo,
298 | Consolas,
299 | Liberation Mono,
300 | monospace;
301 | font-size: 12px;
302 | }
303 | .markdown-body pre {
304 | margin-top: 0;
305 | margin-bottom: 0;
306 | font-family:
307 | ui-monospace,
308 | SFMono-Regular,
309 | SF Mono,
310 | Menlo,
311 | Consolas,
312 | Liberation Mono,
313 | monospace;
314 | font-size: 12px;
315 | word-wrap: normal;
316 | }
317 | .markdown-body .octicon {
318 | display: inline-block;
319 | overflow: visible !important;
320 | vertical-align: text-bottom;
321 | fill: currentColor;
322 | }
323 | .markdown-body input::-webkit-outer-spin-button,
324 | .markdown-body input::-webkit-inner-spin-button {
325 | margin: 0;
326 | -webkit-appearance: none;
327 | appearance: none;
328 | }
329 | .markdown-body::before {
330 | display: table;
331 | content: "";
332 | }
333 | .markdown-body::after {
334 | display: table;
335 | clear: both;
336 | content: "";
337 | }
338 | .markdown-body > :first-child {
339 | margin-top: 0 !important;
340 | }
341 | .markdown-body > :last-child {
342 | margin-bottom: 0 !important;
343 | }
344 | .markdown-body a:not([href]) {
345 | color: inherit;
346 | text-decoration: none;
347 | }
348 | .markdown-body .absent {
349 | color: var(--color-danger-fg);
350 | }
351 | .markdown-body .anchor {
352 | float: left;
353 | padding-right: 4px;
354 | margin-left: -20px;
355 | line-height: 1;
356 | }
357 | .markdown-body .anchor:focus {
358 | outline: none;
359 | }
360 | .markdown-body p,
361 | .markdown-body blockquote,
362 | .markdown-body ul,
363 | .markdown-body ol,
364 | .markdown-body dl,
365 | .markdown-body table,
366 | .markdown-body pre,
367 | .markdown-body details {
368 | margin-top: 0;
369 | margin-bottom: 16px;
370 | }
371 | .markdown-body blockquote > :first-child {
372 | margin-top: 0;
373 | }
374 | .markdown-body blockquote > :last-child {
375 | margin-bottom: 0;
376 | }
377 | .markdown-body h1 .octicon-link,
378 | .markdown-body h2 .octicon-link,
379 | .markdown-body h3 .octicon-link,
380 | .markdown-body h4 .octicon-link,
381 | .markdown-body h5 .octicon-link,
382 | .markdown-body h6 .octicon-link {
383 | color: var(--color-fg-default);
384 | vertical-align: middle;
385 | visibility: hidden;
386 | }
387 | .markdown-body h1:hover .anchor,
388 | .markdown-body h2:hover .anchor,
389 | .markdown-body h3:hover .anchor,
390 | .markdown-body h4:hover .anchor,
391 | .markdown-body h5:hover .anchor,
392 | .markdown-body h6:hover .anchor {
393 | text-decoration: none;
394 | }
395 | .markdown-body h1:hover .anchor .octicon-link,
396 | .markdown-body h2:hover .anchor .octicon-link,
397 | .markdown-body h3:hover .anchor .octicon-link,
398 | .markdown-body h4:hover .anchor .octicon-link,
399 | .markdown-body h5:hover .anchor .octicon-link,
400 | .markdown-body h6:hover .anchor .octicon-link {
401 | visibility: visible;
402 | }
403 | .markdown-body h1 tt,
404 | .markdown-body h1 code,
405 | .markdown-body h2 tt,
406 | .markdown-body h2 code,
407 | .markdown-body h3 tt,
408 | .markdown-body h3 code,
409 | .markdown-body h4 tt,
410 | .markdown-body h4 code,
411 | .markdown-body h5 tt,
412 | .markdown-body h5 code,
413 | .markdown-body h6 tt,
414 | .markdown-body h6 code {
415 | padding: 0 0.2em;
416 | font-size: inherit;
417 | }
418 | .markdown-body summary h1,
419 | .markdown-body summary h2,
420 | .markdown-body summary h3,
421 | .markdown-body summary h4,
422 | .markdown-body summary h5,
423 | .markdown-body summary h6 {
424 | display: inline-block;
425 | }
426 | .markdown-body summary h1 .anchor,
427 | .markdown-body summary h2 .anchor,
428 | .markdown-body summary h3 .anchor,
429 | .markdown-body summary h4 .anchor,
430 | .markdown-body summary h5 .anchor,
431 | .markdown-body summary h6 .anchor {
432 | margin-left: -40px;
433 | }
434 | .markdown-body summary h1,
435 | .markdown-body summary h2 {
436 | padding-bottom: 0;
437 | border-bottom: 0;
438 | }
439 | .markdown-body ul.no-list,
440 | .markdown-body ol.no-list {
441 | padding: 0;
442 | list-style-type: none;
443 | }
444 | .markdown-body ol[type="a"] {
445 | list-style-type: lower-alpha;
446 | }
447 | .markdown-body ol[type="A"] {
448 | list-style-type: upper-alpha;
449 | }
450 | .markdown-body ol[type="i"] {
451 | list-style-type: lower-roman;
452 | }
453 | .markdown-body ol[type="I"] {
454 | list-style-type: upper-roman;
455 | }
456 | .markdown-body ol[type="1"] {
457 | list-style-type: decimal;
458 | }
459 | .markdown-body div > ol:not([type]) {
460 | list-style-type: decimal;
461 | }
462 | .markdown-body ul ul,
463 | .markdown-body ul ol,
464 | .markdown-body ol ol,
465 | .markdown-body ol ul {
466 | margin-top: 0;
467 | margin-bottom: 0;
468 | }
469 | .markdown-body li > p {
470 | margin-top: 16px;
471 | }
472 | .markdown-body li + li {
473 | margin-top: 0.25em;
474 | }
475 | .markdown-body dl {
476 | padding: 0;
477 | }
478 | .markdown-body dl dt {
479 | padding: 0;
480 | margin-top: 16px;
481 | font-size: 1em;
482 | font-style: italic;
483 | font-weight: var(--base-text-weight-semibold, 600);
484 | }
485 | .markdown-body dl dd {
486 | padding: 0 16px;
487 | margin-bottom: 16px;
488 | }
489 | .markdown-body table th {
490 | font-weight: var(--base-text-weight-semibold, 600);
491 | }
492 | .markdown-body table th,
493 | .markdown-body table td {
494 | padding: 6px 13px;
495 | border: 1px solid var(--color-border-default);
496 | }
497 | .markdown-body table tr {
498 | background-color: var(--color-canvas-default);
499 | border-top: 1px solid var(--color-border-muted);
500 | }
501 | .markdown-body table tr:nth-child(2n) {
502 | background-color: var(--color-canvas-subtle);
503 | }
504 | .markdown-body table img {
505 | background-color: transparent;
506 | }
507 | .markdown-body img[align="right"] {
508 | padding-left: 20px;
509 | }
510 | .markdown-body img[align="left"] {
511 | padding-right: 20px;
512 | }
513 | .markdown-body .emoji {
514 | max-width: none;
515 | vertical-align: text-top;
516 | background-color: transparent;
517 | }
518 | .markdown-body span.frame {
519 | display: block;
520 | overflow: hidden;
521 | }
522 | .markdown-body span.frame > span {
523 | display: block;
524 | float: left;
525 | width: auto;
526 | padding: 7px;
527 | margin: 13px 0 0;
528 | overflow: hidden;
529 | border: 1px solid var(--color-border-default);
530 | }
531 | .markdown-body span.frame span img {
532 | display: block;
533 | float: left;
534 | }
535 | .markdown-body span.frame span span {
536 | display: block;
537 | padding: 5px 0 0;
538 | clear: both;
539 | color: var(--color-fg-default);
540 | }
541 | .markdown-body span.align-center {
542 | display: block;
543 | overflow: hidden;
544 | clear: both;
545 | }
546 | .markdown-body span.align-center > span {
547 | display: block;
548 | margin: 13px auto 0;
549 | overflow: hidden;
550 | text-align: center;
551 | }
552 | .markdown-body span.align-center span img {
553 | margin: 0 auto;
554 | text-align: center;
555 | }
556 | .markdown-body span.align-right {
557 | display: block;
558 | overflow: hidden;
559 | clear: both;
560 | }
561 | .markdown-body span.align-right > span {
562 | display: block;
563 | margin: 13px 0 0;
564 | overflow: hidden;
565 | text-align: right;
566 | }
567 | .markdown-body span.align-right span img {
568 | margin: 0;
569 | text-align: right;
570 | }
571 | .markdown-body span.float-left {
572 | display: block;
573 | float: left;
574 | margin-right: 13px;
575 | overflow: hidden;
576 | }
577 | .markdown-body span.float-left span {
578 | margin: 13px 0 0;
579 | }
580 | .markdown-body span.float-right {
581 | display: block;
582 | float: right;
583 | margin-left: 13px;
584 | overflow: hidden;
585 | }
586 | .markdown-body span.float-right > span {
587 | display: block;
588 | margin: 13px auto 0;
589 | overflow: hidden;
590 | text-align: right;
591 | }
592 | .markdown-body code,
593 | .markdown-body tt {
594 | padding: 0.2em 0.4em;
595 | margin: 0;
596 | font-size: 85%;
597 | white-space: break-spaces;
598 | background-color: rgba(175, 184, 193, 0.4);
599 | border-radius: 6px;
600 | }
601 | .markdown-body code br,
602 | .markdown-body tt br {
603 | display: none;
604 | }
605 | .markdown-body del code {
606 | text-decoration: inherit;
607 | }
608 | .markdown-body samp {
609 | font-size: 85%;
610 | }
611 | .markdown-body pre code {
612 | font-size: 100%;
613 | }
614 | .markdown-body pre > code {
615 | padding: 0;
616 | margin: 0;
617 | word-break: normal;
618 | white-space: pre;
619 | background: transparent;
620 | border: 0;
621 | }
622 | .markdown-body .highlight {
623 | margin-bottom: 16px;
624 | }
625 | .markdown-body .highlight pre {
626 | margin-bottom: 0;
627 | word-break: normal;
628 | }
629 | .markdown-body .highlight pre,
630 | .markdown-body pre {
631 | padding: 16px 16px 8px;
632 | overflow: auto;
633 | font-size: 85%;
634 | line-height: 1.45;
635 | border-radius: 6px;
636 | direction: ltr;
637 | }
638 | .markdown-body pre code,
639 | .markdown-body pre tt {
640 | display: inline-block;
641 | max-width: 100%;
642 | padding: 0;
643 | margin: 0;
644 | overflow-x: scroll;
645 | line-height: inherit;
646 | word-wrap: normal;
647 | background-color: transparent;
648 | border: 0;
649 | }
650 | .markdown-body .csv-data td,
651 | .markdown-body .csv-data th {
652 | padding: 5px;
653 | overflow: hidden;
654 | font-size: 12px;
655 | line-height: 1;
656 | text-align: left;
657 | white-space: nowrap;
658 | }
659 | .markdown-body .csv-data .blob-num {
660 | padding: 10px 8px 9px;
661 | text-align: right;
662 | background: var(--color-canvas-default);
663 | border: 0;
664 | }
665 | .markdown-body .csv-data tr {
666 | border-top: 0;
667 | }
668 | .markdown-body .csv-data th {
669 | font-weight: var(--base-text-weight-semibold, 600);
670 | background: var(--color-canvas-subtle);
671 | border-top: 0;
672 | }
673 | .markdown-body [data-footnote-ref]::before {
674 | content: "[";
675 | }
676 | .markdown-body [data-footnote-ref]::after {
677 | content: "]";
678 | }
679 | .markdown-body .footnotes {
680 | font-size: 12px;
681 | color: var(--color-fg-muted);
682 | border-top: 1px solid var(--color-border-default);
683 | }
684 | .markdown-body .footnotes ol {
685 | padding-left: 16px;
686 | }
687 | .markdown-body .footnotes ol ul {
688 | display: inline-block;
689 | padding-left: 16px;
690 | margin-top: 16px;
691 | }
692 | .markdown-body .footnotes li {
693 | position: relative;
694 | }
695 | .markdown-body .footnotes li:target::before {
696 | position: absolute;
697 | top: -8px;
698 | right: -8px;
699 | bottom: -8px;
700 | left: -24px;
701 | pointer-events: none;
702 | content: "";
703 | border: 2px solid var(--color-accent-emphasis);
704 | border-radius: 6px;
705 | }
706 | .markdown-body .footnotes li:target {
707 | color: var(--color-fg-default);
708 | }
709 | .markdown-body .footnotes .data-footnote-backref g-emoji {
710 | font-family: monospace;
711 | }
712 | .markdown-body .pl-c {
713 | color: var(--color-prettylights-syntax-comment);
714 | }
715 | .markdown-body .pl-c1,
716 | .markdown-body .pl-s .pl-v {
717 | color: var(--color-prettylights-syntax-constant);
718 | }
719 | .markdown-body .pl-e,
720 | .markdown-body .pl-en {
721 | color: var(--color-prettylights-syntax-entity);
722 | }
723 | .markdown-body .pl-smi,
724 | .markdown-body .pl-s .pl-s1 {
725 | color: var(--color-prettylights-syntax-storage-modifier-import);
726 | }
727 | .markdown-body .pl-ent {
728 | color: var(--color-prettylights-syntax-entity-tag);
729 | }
730 | .markdown-body .pl-k {
731 | color: var(--color-prettylights-syntax-keyword);
732 | }
733 | .markdown-body .pl-s,
734 | .markdown-body .pl-pds,
735 | .markdown-body .pl-s .pl-pse .pl-s1,
736 | .markdown-body .pl-sr,
737 | .markdown-body .pl-sr .pl-cce,
738 | .markdown-body .pl-sr .pl-sre,
739 | .markdown-body .pl-sr .pl-sra {
740 | color: var(--color-prettylights-syntax-string);
741 | }
742 | .markdown-body .pl-v,
743 | .markdown-body .pl-smw {
744 | color: var(--color-prettylights-syntax-variable);
745 | }
746 | .markdown-body .pl-bu {
747 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
748 | }
749 | .markdown-body .pl-ii {
750 | color: var(--color-prettylights-syntax-invalid-illegal-text);
751 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
752 | }
753 | .markdown-body .pl-c2 {
754 | color: var(--color-prettylights-syntax-carriage-return-text);
755 | background-color: var(--color-prettylights-syntax-carriage-return-bg);
756 | }
757 | .markdown-body .pl-sr .pl-cce {
758 | font-weight: 700;
759 | color: var(--color-prettylights-syntax-string-regexp);
760 | }
761 | .markdown-body .pl-ml {
762 | color: var(--color-prettylights-syntax-markup-list);
763 | }
764 | .markdown-body .pl-mh,
765 | .markdown-body .pl-mh .pl-en,
766 | .markdown-body .pl-ms {
767 | font-weight: 700;
768 | color: var(--color-prettylights-syntax-markup-heading);
769 | }
770 | .markdown-body .pl-mi {
771 | font-style: italic;
772 | color: var(--color-prettylights-syntax-markup-italic);
773 | }
774 | .markdown-body .pl-mb {
775 | font-weight: 700;
776 | color: var(--color-prettylights-syntax-markup-bold);
777 | }
778 | .markdown-body .pl-md {
779 | color: var(--color-prettylights-syntax-markup-deleted-text);
780 | background-color: var(--color-prettylights-syntax-markup-deleted-bg);
781 | }
782 | .markdown-body .pl-mi1 {
783 | color: var(--color-prettylights-syntax-markup-inserted-text);
784 | background-color: var(--color-prettylights-syntax-markup-inserted-bg);
785 | }
786 | .markdown-body .pl-mc {
787 | color: var(--color-prettylights-syntax-markup-changed-text);
788 | background-color: var(--color-prettylights-syntax-markup-changed-bg);
789 | }
790 | .markdown-body .pl-mi2 {
791 | color: var(--color-prettylights-syntax-markup-ignored-text);
792 | background-color: var(--color-prettylights-syntax-markup-ignored-bg);
793 | }
794 | .markdown-body .pl-mdr {
795 | font-weight: 700;
796 | color: var(--color-prettylights-syntax-meta-diff-range);
797 | }
798 | .markdown-body .pl-ba {
799 | color: var(--color-prettylights-syntax-brackethighlighter-angle);
800 | }
801 | .markdown-body .pl-sg {
802 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
803 | }
804 | .markdown-body .pl-corl {
805 | text-decoration: underline;
806 | color: var(--color-prettylights-syntax-constant-other-reference-link);
807 | }
808 | .markdown-body g-emoji {
809 | display: inline-block;
810 | min-width: 1ch;
811 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
812 | font-size: 1em;
813 | font-style: normal !important;
814 | font-weight: var(--base-text-weight-normal, 400);
815 | line-height: 1;
816 | vertical-align: -0.075em;
817 | }
818 | .markdown-body g-emoji img {
819 | width: 1em;
820 | height: 1em;
821 | }
822 | .markdown-body .task-list-item {
823 | list-style-type: none;
824 | }
825 | .markdown-body .task-list-item label {
826 | font-weight: var(--base-text-weight-normal, 400);
827 | }
828 | .markdown-body .task-list-item.enabled label {
829 | cursor: pointer;
830 | }
831 | .markdown-body .task-list-item + .task-list-item {
832 | margin-top: 4px;
833 | }
834 | .markdown-body .task-list-item .handle {
835 | display: none;
836 | }
837 | .markdown-body .task-list-item-checkbox {
838 | margin: 0 0.2em 0.25em -1.4em;
839 | vertical-align: middle;
840 | }
841 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
842 | margin: 0 -1.6em 0.25em 0.2em;
843 | }
844 | .markdown-body .contains-task-list {
845 | position: relative;
846 | }
847 | .markdown-body .contains-task-list:hover .task-list-item-convert-container,
848 | .markdown-body
849 | .contains-task-list:focus-within
850 | .task-list-item-convert-container {
851 | display: block;
852 | width: auto;
853 | height: 24px;
854 | overflow: visible;
855 | clip: auto;
856 | }
857 | .markdown-body ::-webkit-calendar-picker-indicator {
858 | filter: invert(50%);
859 | }
860 | .markdown-body .mermaid {
861 | border: var(--border-in-light);
862 | margin-bottom: 10px;
863 | border-radius: 4px;
864 | padding: 10px;
865 | background-color: var(--white);
866 | }
867 | #dmermaid {
868 | display: none;
869 | }
870 |
871 | .markdown-body a {
872 | color: hsl(var(--primary-foreground));
873 | text-decoration: underline;
874 | }
875 |
876 | /* Custom CSS for chat message markdown */
877 | .custom-markdown ul {
878 | list-style-type: disc;
879 | margin-left: 20px;
880 | }
881 |
882 | .custom-markdown ol {
883 | list-style-type: decimal;
884 | margin-left: 20px;
885 | }
886 |
887 | .custom-markdown li {
888 | margin-bottom: 5px;
889 | }
890 |
891 | .custom-markdown ol ol {
892 | list-style: lower-alpha;
893 | }
894 |
895 | .custom-markdown ul ul,
896 | .custom-markdown ol ol {
897 | margin-left: 20px;
898 | }
899 |
--------------------------------------------------------------------------------
/app/utils/clipboard.ts:
--------------------------------------------------------------------------------
1 | import Locale from "../locales";
2 |
3 | type DisplayResultInput = {
4 | title: string;
5 | variant: "success" | "destructive" | "default";
6 | };
7 |
8 | export type DisplayResultFn = (input: DisplayResultInput) => void;
9 |
10 | export async function copyToClipboard(
11 | text: string,
12 | displayResult: DisplayResultFn,
13 | ) {
14 | try {
15 | await navigator.clipboard.writeText(text);
16 |
17 | displayResult({
18 | title: Locale.Copy.Success,
19 | variant: "success",
20 | });
21 | } catch (error) {
22 | const textArea = document.createElement("textarea");
23 | textArea.value = text;
24 | document.body.appendChild(textArea);
25 | textArea.focus();
26 | textArea.select();
27 | try {
28 | document.execCommand("copy");
29 | displayResult({
30 | title: Locale.Copy.Success,
31 | variant: "success",
32 | });
33 | } catch (error) {
34 | displayResult({
35 | title: Locale.Copy.Failed,
36 | variant: "destructive",
37 | });
38 | }
39 | document.body.removeChild(textArea);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/utils/download.ts:
--------------------------------------------------------------------------------
1 | export function downloadAs(text: string, filename: string) {
2 | const element = document.createElement("a");
3 | element.setAttribute(
4 | "href",
5 | "data:text/plain;charset=utf-8," + encodeURIComponent(text),
6 | );
7 | element.setAttribute("download", filename);
8 |
9 | element.style.display = "none";
10 | document.body.appendChild(element);
11 |
12 | element.click();
13 |
14 | document.body.removeChild(element);
15 | }
16 |
17 | export function readFromFile() {
18 | return new Promise((res, rej) => {
19 | const fileInput = document.createElement("input");
20 | fileInput.type = "file";
21 | fileInput.accept = "application/json";
22 |
23 | fileInput.onchange = (event: any) => {
24 | const file = event.target.files[0];
25 | const fileReader = new FileReader();
26 | fileReader.onload = (e: any) => {
27 | res(e.target.result);
28 | };
29 | fileReader.onerror = (e) => rej(e);
30 | fileReader.readAsText(file);
31 | };
32 |
33 | fileInput.click();
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/app/utils/mobile.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | function useWindowSize() {
4 | const [size, setSize] = useState({
5 | width: window.innerWidth,
6 | height: window.innerHeight,
7 | });
8 |
9 | useEffect(() => {
10 | const onResize = () => {
11 | setSize({
12 | width: window.innerWidth,
13 | height: window.innerHeight,
14 | });
15 | };
16 |
17 | window.addEventListener("resize", onResize);
18 |
19 | return () => {
20 | window.removeEventListener("resize", onResize);
21 | };
22 | }, []);
23 |
24 | return size;
25 | }
26 |
27 | export const MOBILE_MAX_WIDTH = 640; // based on tailwindcss breakpoint
28 | export function useMobileScreen() {
29 | const { width } = useWindowSize();
30 |
31 | return width <= MOBILE_MAX_WIDTH;
32 | }
33 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/styles/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/app/components",
14 | "utils": "@/app/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/datasources/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !*.md
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 |
3 | services:
4 | chat-llamaindex:
5 | build:
6 | context: .
7 | dockerfile: Dockerfile
8 | target: runtime
9 | container_name: chat-llamaindex
10 | ports:
11 | - "3000:3000"
12 | volumes:
13 | - ./cache:/usr/src/app/cache
14 | - ./datasources:/usr/src/app/datasources
15 | env_file:
16 | - .env.development.local
17 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { withSentryConfig } from "@sentry/nextjs";
2 | /** @type {import('next').NextConfig} */
3 | const nextConfig = {
4 | experimental: {
5 | serverComponentsExternalPackages: ["pdf-parse"],
6 | outputFileTracingIncludes: {
7 | "/*": ["./cache/**/*"],
8 | "/api/**/*": ["node_modules/tiktoken/tiktoken_bg.wasm"]
9 | },
10 | outputFileTracingExcludes: {
11 | "/api/files/*": [".next/**/*", "node_modules/**/*", "public/**/*", "app/**/*"],
12 | },
13 | },
14 | webpack: (config) => {
15 | // See https://webpack.js.org/configuration/resolve/#resolvealias
16 | config.resolve.alias = {
17 | ...config.resolve.alias,
18 | sharp$: false,
19 | "onnxruntime-node$": false,
20 | };
21 | config.resolve.fallback = {
22 | aws4: false,
23 | };
24 | return config;
25 | },
26 | images: {
27 | remotePatterns: [
28 | {
29 | protocol: "https",
30 | hostname: "*.public.blob.vercel-storage.com",
31 | },
32 | ],
33 | },
34 | };
35 |
36 | export default withSentryConfig(
37 | nextConfig,
38 | {
39 | // For all available options, see:
40 | // https://github.com/getsentry/sentry-webpack-plugin#options
41 |
42 | // Suppresses source map uploading logs during build
43 | silent: true,
44 | org: "llamaindex",
45 | project: "chat-llamaindex",
46 | },
47 | {
48 | // For all available options, see:
49 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
50 |
51 | // Upload a larger set of source maps for prettier stack traces (increases build time)
52 | widenClientFileUpload: true,
53 |
54 | // Transpiles SDK to be compatible with IE11 (increases bundle size)
55 | transpileClientSDK: true,
56 |
57 | // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
58 | tunnelRoute: "/monitoring",
59 |
60 | // Hides source maps from generated client bundles
61 | hideSourceMaps: true,
62 |
63 | // Automatically tree-shake Sentry logger statements to reduce bundle size
64 | disableLogger: true,
65 | },
66 | );
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-llamaindex",
3 | "private": false,
4 | "license": "MIT",
5 | "scripts": {
6 | "dev": "next dev",
7 | "create-llama": "bash scripts/create-llama.sh",
8 | "get-demo": "bash scripts/get-demo.sh",
9 | "generate-demo": "bash scripts/generate-demo.sh",
10 | "build": "npm run create-llama && next build",
11 | "start": "next start",
12 | "lint": "next lint",
13 | "format:check": "prettier --check --ignore-path .gitignore app",
14 | "format": "prettier --write --ignore-path .gitignore app",
15 | "prepare": "husky install",
16 | "generate:win": "tsx app\\api\\chat\\engine\\generate.ts",
17 | "generate": "tsx app/api/chat/engine/generate.ts"
18 | },
19 | "dependencies": {
20 | "@apidevtools/swagger-parser": "^10.1.0",
21 | "@e2b/code-interpreter": "^0.0.5",
22 | "@fortaine/fetch-event-source": "^3.0.6",
23 | "@google/generative-ai": "^0.1.3",
24 | "@llamaindex/cloud": "^0.2.2",
25 | "@llamaindex/pdf-viewer": "^1.1.1",
26 | "@radix-ui/react-alert-dialog": "1.0.4",
27 | "@radix-ui/react-checkbox": "^1.0.4",
28 | "@radix-ui/react-collapsible": "^1.0.3",
29 | "@radix-ui/react-dialog": "1.0.4",
30 | "@radix-ui/react-dropdown-menu": "^2.0.6",
31 | "@radix-ui/react-hover-card": "^1.0.7",
32 | "@radix-ui/react-popover": "^1.0.7",
33 | "@radix-ui/react-progress": "^1.0.3",
34 | "@radix-ui/react-scroll-area": "^1.0.5",
35 | "@radix-ui/react-select": "^1.2.2",
36 | "@radix-ui/react-separator": "^1.0.3",
37 | "@radix-ui/react-slot": "^1.0.2",
38 | "@radix-ui/react-toast": "^1.1.5",
39 | "@radix-ui/react-tooltip": "^1.0.7",
40 | "@sentry/nextjs": "^7.110.0",
41 | "@vercel/analytics": "^1.2.2",
42 | "@vercel/blob": "^0.14.1",
43 | "@vercel/kv": "^0.2.4",
44 | "ai": "^3.0.21",
45 | "ajv": "^8.12.0",
46 | "autoprefixer": "10.4.15",
47 | "axios": "^1.6.8",
48 | "class-variance-authority": "^0.7.0",
49 | "clsx": "^2.1.0",
50 | "dotenv": "^16.4.5",
51 | "emoji-picker-react": "^4.9.2",
52 | "encoding": "^0.1.13",
53 | "got": "10.7.0",
54 | "llamaindex": "0.5.19",
55 | "lucide-react": "^0.277.0",
56 | "mermaid": "^10.9.0",
57 | "nanoid": "^5.0.7",
58 | "next": "^14.2.1",
59 | "next-themes": "^0.2.1",
60 | "pdf-parse": "^1.1.1",
61 | "postcss": "8.4.29",
62 | "react": "^18.2.0",
63 | "react-dom": "^18.2.0",
64 | "react-markdown": "^8.0.7",
65 | "react-query": "^3.39.3",
66 | "react-router-dom": "^6.22.3",
67 | "react-syntax-highlighter": "^15.5.0",
68 | "rehype-highlight": "^6.0.0",
69 | "rehype-katex": "^6.0.3",
70 | "rehype-parse": "^8.0.5",
71 | "rehype-remark": "^9.1.2",
72 | "remark-breaks": "^3.0.3",
73 | "remark-gfm": "^3.0.1",
74 | "remark-math": "^5.1.1",
75 | "remark-stringify": "^10.0.3",
76 | "sass": "^1.75.0",
77 | "tailwind-merge": "^1.14.0",
78 | "tailwindcss": "3.3.3",
79 | "tailwindcss-animate": "^1.0.7",
80 | "tiktoken": "^1.0.15",
81 | "unified": "^10.1.2",
82 | "unist-util-remove": "^4.0.0",
83 | "use-debounce": "^9.0.4",
84 | "uuid": "^9.0.1",
85 | "vaul": "^0.9.1",
86 | "zustand": "^4.5.2"
87 | },
88 | "devDependencies": {
89 | "@types/node": "^20.12.7",
90 | "@types/pdf-parse": "^1.1.4",
91 | "@types/react": "^18.2.77",
92 | "@types/react-dom": "^18.2.25",
93 | "@types/react-katex": "^3.0.4",
94 | "@types/react-syntax-highlighter": "^15.5.11",
95 | "@types/uuid": "^9.0.8",
96 | "eslint": "^8.57.0",
97 | "eslint-config-next": "14.2.1",
98 | "eslint-config-prettier": "^9.1.0",
99 | "eslint-plugin-prettier": "^5.1.3",
100 | "husky": "^8.0.3",
101 | "lint-staged": "^13.3.0",
102 | "prettier": "^3.2.5",
103 | "tsx": "^4.7.2",
104 | "typescript": "5.1.6"
105 | }
106 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-2048x2048.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/favicon-2048x2048.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/favicon.ico
--------------------------------------------------------------------------------
/public/llama.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/llama.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | User-agent: vitals.vercel-insights.com
4 | Allow: /
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/run-llama/chat-llamaindex/179c907196cf4f63f6d3ae1beb192186a1e00a0c/public/screenshot.png
--------------------------------------------------------------------------------
/public/serviceWorker.js:
--------------------------------------------------------------------------------
1 | const UNC_WEB_CACHE = "chat-llamaindex-web-cache";
2 |
3 | self.addEventListener("activate", function (event) {
4 | console.log("ServiceWorker activated.");
5 | });
6 |
7 | self.addEventListener("install", function (event) {
8 | event.waitUntil(
9 | caches.open(UNC_WEB_CACHE).then(function (cache) {
10 | return cache.addAll([]);
11 | }),
12 | );
13 | });
14 |
15 | self.addEventListener("fetch", (e) => {});
16 |
--------------------------------------------------------------------------------
/public/serviceWorkerRegister.js:
--------------------------------------------------------------------------------
1 | if ('serviceWorker' in navigator) {
2 | window.addEventListener('load', function () {
3 | navigator.serviceWorker.register('/serviceWorker.js').then(function (registration) {
4 | console.log('ServiceWorker registration successful with scope: ', registration.scope);
5 | }, function (err) {
6 | console.error('ServiceWorker registration failed: ', err);
7 | });
8 | });
9 | }
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-llamaindex",
3 | "short_name": "Llama Chat",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "/",
17 | "theme_color": "#ffffff",
18 | "background_color": "#ffffff",
19 | "display": "standalone"
20 | }
--------------------------------------------------------------------------------
/scripts/create-llama.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo -e "\nAdding sources from create-llama..."
4 |
5 | # Remove current create-llama folder
6 | rm -rf app/api/chat/config/
7 | rm -rf app/api/files
8 | rm -rf cl
9 |
10 | # Run the node command with specified options
11 | npx -y create-llama@0.1.39 \
12 | --framework nextjs \
13 | --template streaming \
14 | --engine context \
15 | --frontend \
16 | --ui shadcn \
17 | --observability none \
18 | --open-ai-key "Set your OpenAI key here" \
19 | --llama-cloud-key "Set your LlamaCloud API key here" \
20 | --tools none \
21 | --post-install-action none \
22 | --no-llama-parse \
23 | --example-file \
24 | --vector-db llamacloud \
25 | --use-pnpm \
26 | -- cl >/dev/null
27 |
28 | # copy routes from create-llama to app
29 | # Note: if changes on these routes are needed, copy them to the project's app folder
30 | cp -r cl/app/api/chat/config app/api/chat/config
31 |
32 | # copy example .env file
33 | cp cl/.env .env.development.local
34 |
--------------------------------------------------------------------------------
/scripts/generate-demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e # Exit immediately if a command exits with a non-zero status
3 | # uploads demo datasources to LlamaCloud
4 |
5 | pnpm run generate documents
6 | pnpm run generate watchos
7 | pnpm run generate redhat
8 | pnpm run generate basic_law_germany
9 |
--------------------------------------------------------------------------------
/scripts/get-demo.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # downloads demo datasources
3 |
4 | pushd datasources
5 |
6 | # redhat
7 | mkdir redhat
8 | pushd redhat
9 | wget -nc https://docs.redhat.com/en-us/documentation/red_hat_enterprise_linux/9/pdf/configuring_basic_system_settings/Red_Hat_Enterprise_Linux-9-Configuring_basic_system_settings-en-US.pdf
10 | popd
11 |
12 | # watchos
13 | mkdir watchos
14 | pushd watchos
15 | wget -nc https://help.apple.com/pdf/watch/10/en_US/apple-watch-user-guide-watchos10.pdf
16 | popd
17 |
18 | # basic_law_germany
19 | mkdir basic_law_germany
20 | pushd basic_law_germany
21 | wget -nc https://www.gesetze-im-internet.de/englisch_gg/englisch_gg.html
22 | popd
23 |
24 | # documents (empty folder)
25 | mkdir documents
26 |
27 | popd
28 |
--------------------------------------------------------------------------------
/sentry.client.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the client.
2 | // The config you add here will be used whenever a users loads a page in their browser.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs";
6 |
7 | Sentry.init({
8 | // Adjust this value in production, or use tracesSampler for greater control
9 | tracesSampleRate: 1,
10 |
11 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
12 | debug: false,
13 |
14 | replaysOnErrorSampleRate: 1.0,
15 |
16 | // This sets the sample rate to be 10%. You may want this to be 100% while
17 | // in development and sample at a lower rate in production
18 | replaysSessionSampleRate: 0.1,
19 |
20 | // You can remove this option if you're not planning to use the Sentry Session Replay feature:
21 | integrations: [
22 | new Sentry.Replay({
23 | // Additional Replay configuration goes in here, for example:
24 | maskAllText: true,
25 | blockAllMedia: true,
26 | }),
27 | ],
28 | });
29 |
--------------------------------------------------------------------------------
/sentry.edge.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
2 | // The config you add here will be used whenever one of the edge features is loaded.
3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
5 |
6 | import * as Sentry from "@sentry/nextjs";
7 |
8 | Sentry.init({
9 | // Adjust this value in production, or use tracesSampler for greater control
10 | tracesSampleRate: 1,
11 |
12 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
13 | debug: false,
14 | });
15 |
--------------------------------------------------------------------------------
/sentry.server.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the server.
2 | // The config you add here will be used whenever the server handles a request.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs";
6 |
7 | Sentry.init({
8 | // Adjust this value in production, or use tracesSampler for greater control
9 | tracesSampleRate: 1,
10 |
11 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
12 | debug: false,
13 | });
14 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | './cl/app/components/**/*.{ts,tsx}'
10 | ],
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: "2rem",
15 | screens: {
16 | "2xl": "1400px",
17 | },
18 | },
19 | extend: {
20 | colors: {
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
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: [require("tailwindcss-animate")],
77 | }
--------------------------------------------------------------------------------
/test/data/.gitignore:
--------------------------------------------------------------------------------
1 | *.pdf
2 | *.pdf.txt
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2015",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "bundler",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "paths": {
27 | "@/*": [
28 | "./*"
29 | ]
30 | }
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts",
37 | ],
38 | "exclude": [
39 | "node_modules",
40 | "cl/**/*"
41 | ]
42 | }
--------------------------------------------------------------------------------