├── .dockerignore ├── .eslintrc.json ├── .eslintrc.verbose.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-main.yml │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── chat.png ├── collections.png ├── documents.png └── login.png ├── components.json ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── env-config.js ├── favicon.ico └── images │ ├── default_profile.svg │ ├── favicon.ico │ ├── github-mark.svg │ ├── google-logo.svg │ ├── hatchet-logo.svg │ ├── javascript-logo.svg │ ├── python-logo.svg │ └── sciphi.svg ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── components │ ├── AverageScoreCard.tsx │ ├── ChatDemo │ │ ├── ConfigurationSheet.tsx │ │ ├── CreateDialog.tsx │ │ ├── DefaultQueries.tsx │ │ ├── DocumentsTable.tsx │ │ ├── DownloadFileContainer.tsx │ │ ├── ExtractContainer.tsx │ │ ├── KGDescriptionDialog.tsx │ │ ├── MessageBubble.tsx │ │ ├── PipelineStatus.tsx │ │ ├── ServerCard.tsx │ │ ├── SingleSwitch.tsx │ │ ├── SwitchManager.tsx │ │ ├── Table.tsx │ │ ├── UpdateButtonContainer.tsx │ │ ├── UploadDialog.tsx │ │ ├── answer.tsx │ │ ├── createFile.tsx │ │ ├── deleteButton.tsx │ │ ├── popover.tsx │ │ ├── remove.tsx │ │ ├── result.tsx │ │ ├── search.tsx │ │ ├── skeleton.tsx │ │ ├── title.tsx │ │ ├── update.tsx │ │ ├── upload.tsx │ │ └── utils │ │ │ ├── AssignDocumentToCollectionDialog.tsx │ │ │ ├── AssignUserToCollectionDialog.tsx │ │ │ ├── cn.ts │ │ │ ├── collectionCreationModal.tsx │ │ │ ├── collectionDialog.tsx │ │ │ ├── containerObjectCreationModal.tsx │ │ │ ├── documentDialogInfo.tsx │ │ │ ├── documentSorter.ts │ │ │ ├── editPromptDialog.tsx │ │ │ ├── formatUptime.ts │ │ │ ├── get-search-url.ts │ │ │ └── userDialogInfo.tsx │ ├── ContainerObjectCard.tsx │ ├── DAUCard.tsx │ ├── ErrorsCard.tsx │ ├── Graph.txt │ ├── LLMCompletionCard.tsx │ ├── Layout │ │ └── index.tsx │ ├── OverlayWrapper.tsx │ ├── RequestsCard.tsx │ ├── SearchResults │ │ └── index.tsx │ ├── Sidebar.tsx │ ├── Spinner.tsx │ ├── ThemeProvider │ │ └── index.tsx │ ├── USMapCard.tsx │ ├── UserForm.tsx │ ├── WAUCard.tsx │ ├── WorldMapCard.tsx │ ├── knowledgeGraph.tsx │ ├── knowledgeGraphD3.tsx │ ├── shared │ │ ├── Footer.tsx │ │ ├── Logo.tsx │ │ ├── NavBar.tsx │ │ └── ThemeToggle.tsx │ ├── ui │ │ ├── BarChart.tsx │ │ ├── Button.tsx │ │ ├── CopyableContent.tsx │ │ ├── InfoIcon.tsx │ │ ├── ModelSelector.tsx │ │ ├── ShadcnButton.tsx │ │ ├── UserSelector.tsx │ │ ├── WatchButton.tsx │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── highlight.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── logtable.tsx │ │ ├── menubar.tsx │ │ ├── multi-select.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts │ ├── us-abbr.json │ ├── usa-topo.json │ └── world-topo.json ├── config │ ├── brandingConfig.ts │ └── brandingOverride.ts ├── context │ └── UserContext.tsx ├── hooks │ └── usePagination.ts ├── instrumentation.ts ├── lib │ ├── CustomErrors.ts │ ├── debounce.ts │ ├── posthog-client.tsx │ ├── remToPx.ts │ ├── supabase.ts │ └── utils.ts ├── pages │ ├── _app.tsx │ ├── _error.jsx │ ├── account.tsx │ ├── analytics.tsx │ ├── api │ │ └── sentry-example-api.js │ ├── auth │ │ ├── login.tsx │ │ └── signup.tsx │ ├── chat.tsx │ ├── collections.tsx │ ├── collections │ │ └── [id].tsx │ ├── documents.tsx │ ├── error.tsx │ ├── index.tsx │ ├── indexLogs.tsx │ ├── search.tsx │ ├── settings.tsx │ └── users.tsx ├── styles │ ├── Index.module.scss │ └── globals.css └── types.ts ├── startup.sh ├── tailwind.config.js ├── tsconfig.json └── types └── globals.d.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | /assets 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["import", "@typescript-eslint"], 3 | "extends": [ 4 | "next/core-web-vitals", 5 | "plugin:prettier/recommended", 6 | "plugin:@typescript-eslint/recommended" 7 | ], 8 | "rules": { 9 | "@typescript-eslint/no-explicit-any": "off", 10 | "@typescript-eslint/no-var-requires": "off", 11 | "no-unused-vars": "off", 12 | "@typescript-eslint/no-unused-vars": "off", 13 | "@next/next/no-page-custom-font": "off", 14 | "react-hooks/exhaustive-deps": "off", 15 | "react/no-unescaped-entities": "off", 16 | "@next/next/no-img-element": "off", 17 | "jsx-a11y/alt-text": "off", 18 | "import/order": [ 19 | "error", 20 | { 21 | "groups": [ 22 | "builtin", 23 | "external", 24 | "internal", 25 | "parent", 26 | "sibling", 27 | "index" 28 | ], 29 | "pathGroups": [ 30 | { 31 | "pattern": "@/components/**", 32 | "group": "internal" 33 | }, 34 | { 35 | "pattern": "@/ui/**", 36 | "group": "internal" 37 | } 38 | ], 39 | "pathGroupsExcludedImportTypes": ["builtin"], 40 | "newlines-between": "always", 41 | "alphabetize": { 42 | "order": "asc", 43 | "caseInsensitive": true 44 | } 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.eslintrc.verbose.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./.eslintrc.json"], 3 | "rules": { 4 | "@typescript-eslint/no-unused-vars": "warn", 5 | "no-unused-vars": "warn", 6 | "react-hooks/exhaustive-deps": "warn", 7 | "@next/next/no-img-element": "warn", 8 | "jsx-a11y/alt-text": "warn" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 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 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the R2R Dashboard! 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to publish (leave empty to use package.json version)' 8 | required: false 9 | type: string 10 | 11 | env: 12 | REGISTRY_IMAGE: sciphiai/r2r-dashboard 13 | 14 | jobs: 15 | prepare: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | release_version: ${{ steps.version.outputs.RELEASE_VERSION }} 19 | matrix: ${{ steps.set-matrix.outputs.matrix }} 20 | steps: 21 | - name: Check out the repo 22 | uses: actions/checkout@v4 23 | 24 | - name: Get version from package.json 25 | id: package-version 26 | run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 27 | 28 | - name: Determine version to use 29 | id: version 30 | run: | 31 | if [ -n "${{ github.event.inputs.version }}" ]; then 32 | echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT 33 | else 34 | echo "RELEASE_VERSION=${{ steps.package-version.outputs.VERSION }}" >> $GITHUB_OUTPUT 35 | fi 36 | 37 | - name: Set matrix 38 | id: set-matrix 39 | run: | 40 | echo "matrix={\"include\":[{\"platform\":\"amd64\",\"runner\":\"ubuntu-latest\"},{\"platform\":\"arm64\",\"runner\":\"arm64\"}]}" >> $GITHUB_OUTPUT 41 | 42 | build: 43 | needs: prepare 44 | strategy: 45 | fail-fast: false 46 | matrix: ${{fromJson(needs.prepare.outputs.matrix)}} 47 | runs-on: ${{ matrix.runner }} 48 | permissions: 49 | packages: write 50 | contents: read 51 | id-token: write 52 | actions: write 53 | steps: 54 | - name: Check out the repo 55 | uses: actions/checkout@v4 56 | 57 | - name: Set up Docker Buildx 58 | uses: docker/setup-buildx-action@v3 59 | 60 | - name: Log in to Docker Hub 61 | uses: docker/login-action@v3 62 | with: 63 | username: ${{ secrets.DOCKER_USERNAME }} 64 | password: ${{ secrets.DOCKER_TOKEN }} 65 | 66 | - name: Build and push Docker image 67 | uses: docker/build-push-action@v5 68 | with: 69 | context: . 70 | file: ./Dockerfile 71 | platforms: linux/${{ matrix.platform }} 72 | push: true 73 | tags: ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }}-${{ matrix.platform }} 74 | provenance: false 75 | sbom: false 76 | 77 | create-manifest: 78 | needs: [prepare, build] 79 | runs-on: ubuntu-latest 80 | steps: 81 | - name: Log in to Docker Hub 82 | uses: docker/login-action@v3 83 | with: 84 | username: ${{ secrets.DOCKER_USERNAME }} 85 | password: ${{ secrets.DOCKER_TOKEN }} 86 | 87 | - name: Create and push manifest 88 | run: | 89 | docker buildx imagetools create -t ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }} \ 90 | ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }}-amd64 \ 91 | ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }}-arm64 92 | 93 | docker buildx imagetools create -t ${{ env.REGISTRY_IMAGE }}:latest \ 94 | ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }}-amd64 \ 95 | ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }}-arm64 96 | 97 | - name: Verify manifests 98 | run: | 99 | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ needs.prepare.outputs.release_version }} 100 | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:latest 101 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18, 19, 20, 21, 22, 23] 15 | fail-fast: false 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install pnpm 26 | uses: pnpm/action-setup@v2 27 | with: 28 | version: latest 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Check formatting 34 | run: pnpm format:check 35 | 36 | - name: Build 37 | run: pnpm build 38 | 39 | - name: Start server and run for 20 seconds 40 | run: | 41 | timeout 20s pnpm start || exit 0 42 | 43 | - name: Check server logs 44 | run: | 45 | if grep -q "error" .next/server.log; then 46 | echo "Server encountered errors during startup" 47 | exit 1 48 | fi 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .env* 3 | .DS_Store 4 | 5 | #vscode 6 | .vscode/settings.json 7 | 8 | #logs 9 | logs.json 10 | 11 | # dependencies 12 | /node_modules 13 | /cloud-docs/node_modules 14 | /.pnp 15 | .pnp.js 16 | .next 17 | 18 | # testing 19 | /coverage 20 | 21 | # next.js 22 | /.next/ 23 | /out/ 24 | 25 | # production 26 | /build 27 | 28 | # misc 29 | .DS_Store 30 | *.pem 31 | 32 | # debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # local env files 38 | .env.local 39 | .env.development.local 40 | .env.test.local 41 | .env.production.local 42 | 43 | # typescript 44 | *.tsbuildinfo 45 | .vercel 46 | 47 | # Sentry Config File 48 | .env.sentry-build-plugin 49 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder Stage 2 | FROM node:22-alpine AS builder 3 | WORKDIR /app 4 | 5 | # Install pnpm 6 | RUN npm install -g pnpm 7 | 8 | # Copy package.json and pnpm-lock.yaml 9 | COPY package.json pnpm-lock.yaml* ./ 10 | 11 | # Install dependencies 12 | RUN pnpm install --frozen-lockfile 13 | 14 | # Copy the rest of the application code 15 | COPY . . 16 | 17 | # Build the Next.js application 18 | RUN pnpm build 19 | 20 | # Production Stage 21 | FROM node:22-alpine AS runner 22 | WORKDIR /app 23 | 24 | # Install pnpm 25 | RUN npm install -g pnpm 26 | 27 | # Set node environment to production 28 | ENV NODE_ENV=production 29 | ENV HOSTNAME="0.0.0.0" 30 | 31 | # Copy necessary files from builder stage 32 | COPY --from=builder /app/next.config.js ./ 33 | COPY --from=builder /app/public ./public 34 | COPY --from=builder /app/.next/standalone ./ 35 | COPY --from=builder /app/.next/static ./.next/static 36 | 37 | # Copy the startup script 38 | COPY startup.sh /app/startup.sh 39 | 40 | # Ensure the startup script is executable 41 | RUN chmod +x /app/startup.sh 42 | 43 | # Expose the port the app runs on 44 | EXPOSE 3000 45 | 46 | # Define the command to run the startup script 47 | CMD ["/app/startup.sh"] 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Acme Co. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | R2R Dashboard 3 |

4 | r2r 5 |

6 | Manage and Monitor Your R2R RAG Applications with Ease 7 |

8 | 9 | # About 10 | 11 | The R2R Dashboard is an open-source React+Next.js application designed to provide [R2R](https://github.com/SciPhi-AI/R2R) developers with an easy interface to interact with their pipelines. This dashboard aims to reduce development and iteration time by offering a user-friendly environment. 12 | 13 | ## Key Features 14 | 15 | - **🗂️ Document Management**: Upload, update, and delete documents and their metadata. 16 | - **🛝 Playground**: Stream RAG responses with different models and configurable settings. 17 | - **📊 Analytics**: View aggregate statistics around latencies and metrics with detailed histograms. 18 | - **📜 Logs**: Track user queries, search results, and LLM responses. 19 | - **🔧 Development Tools**: Easily start a development server, format code, and run lint checks. 20 | 21 | ## Table of Contents 22 | 23 | 1. [Quick Install](#quick-install) 24 | 2. [Links](#links) 25 | 3. [Screenshots](#screenshots) 26 | 4. [Core Abstractions](#core-abstractions) 27 | 5. [Summary](#summary) 28 | 29 | # Quick Install 30 | 31 | ### Install PNPM 32 | 33 | PNPM is a fast, disk space-efficient package manager that helps you manage your project dependencies. To install PNPM, visit the [official PNPM installation page](https://pnpm.io/installation) for the latest instructions, or follow the instructions outlined below: 34 | 35 |
36 | PNPM Installation 37 | 38 | For Unix-based systems (Linux, macOS): 39 | 40 | ```bash 41 | curl -fsSL https://get.pnpm.io/install.sh | sh - 42 | ``` 43 | 44 | For Windows: 45 | 46 | ```powershell 47 | iwr https://get.pnpm.io/install.ps1 -useb | iex 48 | ``` 49 | 50 | After installing PNPM, you may need to add it to your system's PATH. Follow the instructions provided on the PNPM installation page to ensure it's properly set up. 51 | 52 |
53 | 54 | ### Clone the R2R Dashboard and Install Dependencies 55 | 56 | 1. **Clone the project repository and navigate to the project directory:** 57 | 58 | ```bash 59 | git clone git@github.com:SciPhi-AI/R2R-Application.git 60 | cd R2R-Application 61 | ``` 62 | 63 | 2. **Install the project dependencies using PNPM:** 64 | 65 | ```bash 66 | pnpm install 67 | ``` 68 | 69 | 3. **Build and start the application for production:** 70 | 71 | ```bash 72 | pnpm build 73 | pnpm start 74 | ``` 75 | 76 | This will build the application on port 3000. After `pnpm start` runs successfully, the dashboard can be viewed at [http://localhost:3000](http://localhost:3000). 77 | 78 | ### Developing with the R2R Dashboard 79 | 80 | If you'd like to develop the R2R dashboard, you can do so by starting a development server: 81 | 82 | 1. **Start the development server:** 83 | 84 | ```bash 85 | pnpm dev 86 | ``` 87 | 88 | 2. **Pre-commit checks (optional but recommended):** 89 | 90 | Ensure your code is properly formatted and free of linting issues before committing: 91 | 92 | ```bash 93 | pnpm format 94 | pnpm lint 95 | ``` 96 | 97 | # Links 98 | 99 | - [Join the Discord server](https://discord.gg/p6KqD2kjtB) 100 | - [R2R Docs Quickstart](https://r2r-docs.sciphi.ai/getting-started/quick-install) 101 | 102 | ## Docs 103 | 104 | - [R2R Dashboard](https://r2r-docs.sciphi.ai/cookbooks/dashboard): A how-to guide on connecting with the R2R Dashboard. 105 | - [R2R Demo](https://r2r-docs.sciphi.ai/getting-started/r2r-demo): A basic demo script designed to get you started with an R2R RAG application. 106 | - [R2R Client-Server](https://r2r-docs.sciphi.ai/cookbooks/client-server): An extension of the basic `R2R Demo` with client-server interactions. 107 | - [Local RAG](https://r2r-docs.sciphi.ai/cookbooks/local-rag): A quick cookbook demonstration of how to run R2R with local LLMs. 108 | - [Hybrid Search](https://r2r-docs.sciphi.ai/cookbooks/hybrid-search): A brief introduction to running hybrid search with R2R. 109 | - [Reranking](https://r2r-docs.sciphi.ai/cookbooks/rerank-search): A short guide on how to apply reranking to R2R results. 110 | - [SciPhi Cloud Docs](https://docs.sciphi.ai/): SciPhi Cloud documentation. 111 | 112 | # Screenshots 113 | 114 | ![Login](assets/login.png) 115 | ![Chat](assets/chat.png) 116 | ![Documents](assets/documents.png) 117 | ![Collections](assets/collections.png) 118 | 119 | # Summary 120 | 121 | The R2R Dashboard is a comprehensive tool designed to streamline the management and monitoring of Retrieval-Augmented Generation (RAG) pipelines built with the R2R framework. By providing a user-friendly interface and robust core features, the dashboard helps developers efficiently interact with their RAG systems, enhancing development and operational workflows. 122 | -------------------------------------------------------------------------------- /assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SciPhi-AI/R2R-Application/c4612ab48af002d4cd078659d11db88ed61d53b8/assets/chat.png -------------------------------------------------------------------------------- /assets/collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SciPhi-AI/R2R-Application/c4612ab48af002d4cd078659d11db88ed61d53b8/assets/collections.png -------------------------------------------------------------------------------- /assets/documents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SciPhi-AI/R2R-Application/c4612ab48af002d4cd078659d11db88ed61d53b8/assets/documents.png -------------------------------------------------------------------------------- /assets/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SciPhi-AI/R2R-Application/c4612ab48af002d4cd078659d11db88ed61d53b8/assets/login.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: false, 3 | output: 'standalone', 4 | webpack: (config, { isServer }) => { 5 | if (!isServer) { 6 | config.resolve.fallback = { 7 | ...config.resolve.fallback, 8 | fs: false, 9 | }; 10 | } 11 | return config; 12 | }, 13 | images: { 14 | remotePatterns: [ 15 | { 16 | protocol: 'https', 17 | hostname: 'github.com', 18 | }, 19 | { 20 | protocol: 'https', 21 | hostname: 'lh3.googleusercontent.com', 22 | }, 23 | ], 24 | }, 25 | }; 26 | 27 | // Injected content via Sentry wizard below 28 | 29 | const { withSentryConfig } = require('@sentry/nextjs'); 30 | 31 | module.exports = withSentryConfig(module.exports, { 32 | // For all available options, see: 33 | // https://www.npmjs.com/package/@sentry/webpack-plugin#options 34 | 35 | org: 'sciphi', 36 | project: 'r2r-dashboard', 37 | 38 | // Only print logs for uploading source maps in CI 39 | silent: !process.env.CI, 40 | 41 | // For all available options, see: 42 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 43 | 44 | // Upload a larger set of source maps for prettier stack traces (increases build time) 45 | widenClientFileUpload: true, 46 | 47 | // Automatically annotate React components to show their full name in breadcrumbs and session replay 48 | reactComponentAnnotation: { 49 | enabled: true, 50 | }, 51 | 52 | // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 53 | // This can increase your server load as well as your hosting bill. 54 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 55 | // side errors will fail. 56 | tunnelRoute: '/monitoring', 57 | 58 | // Automatically tree-shake Sentry logger statements to reduce bundle size 59 | disableLogger: true, 60 | 61 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) 62 | // See the following for more information: 63 | // https://docs.sentry.io/product/crons/ 64 | // https://vercel.com/docs/cron-jobs 65 | automaticVercelMonitors: true, 66 | }); 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r2r-dashboard", 3 | "version": "1.0.4", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3005", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint --fix .", 10 | "lint:verbose": "eslint --config .eslintrc.verbose.json .", 11 | "format": "prettier --write .", 12 | "format:check": "prettier --check ." 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "lint-staged" 17 | } 18 | }, 19 | "lint-staged": { 20 | "*.{js,jsx,ts,tsx}": [ 21 | "eslint --fix", 22 | "prettier --write" 23 | ], 24 | "*.{json,css,scss,md}": [ 25 | "prettier --write" 26 | ] 27 | }, 28 | "license": "MIT", 29 | "dependencies": { 30 | "@radix-ui/react-accordion": "^1.2.2", 31 | "@radix-ui/react-alert-dialog": "^1.1.5", 32 | "@radix-ui/react-avatar": "^1.1.2", 33 | "@radix-ui/react-checkbox": "^1.1.3", 34 | "@radix-ui/react-collapsible": "^1.1.2", 35 | "@radix-ui/react-dialog": "^1.1.5", 36 | "@radix-ui/react-dropdown-menu": "^2.1.5", 37 | "@radix-ui/react-icons": "^1.3.2", 38 | "@radix-ui/react-label": "^2.1.1", 39 | "@radix-ui/react-menubar": "^1.1.5", 40 | "@radix-ui/react-popover": "^1.1.5", 41 | "@radix-ui/react-progress": "^1.1.1", 42 | "@radix-ui/react-select": "^2.1.5", 43 | "@radix-ui/react-slider": "^1.2.2", 44 | "@radix-ui/react-slot": "^1.1.1", 45 | "@radix-ui/react-switch": "^1.1.2", 46 | "@radix-ui/react-tabs": "^1.1.2", 47 | "@radix-ui/react-toast": "^1.2.5", 48 | "@radix-ui/react-tooltip": "^1.1.7", 49 | "@react-spring/web": "^9.7.5", 50 | "@sentry/nextjs": "^9", 51 | "@supabase/supabase-js": "^2.48.1", 52 | "@tailwindcss/forms": "^0.5.10", 53 | "@types/topojson-specification": "^1.0.5", 54 | "@visx/axis": "^3.12.0", 55 | "@visx/curve": "^3.12.0", 56 | "@visx/event": "^3.12.0", 57 | "@visx/geo": "^3.12.0", 58 | "@visx/gradient": "^3.12.0", 59 | "@visx/grid": "^3.12.0", 60 | "@visx/group": "^3.12.0", 61 | "@visx/pattern": "^3.12.0", 62 | "@visx/scale": "^3.12.0", 63 | "@visx/shape": "^3.12.0", 64 | "@visx/stats": "^3.12.0", 65 | "@visx/tooltip": "^3.12.0", 66 | "@visx/vendor": "^3.12.0", 67 | "ansi_up": "^6.0.2", 68 | "chart.js": "^4.4.7", 69 | "class-variance-authority": "^0.7.1", 70 | "clsx": "^2.1.1", 71 | "d3": "^7.9.0", 72 | "d3-array": "^3.2.4", 73 | "date-fns": "^3.6.0", 74 | "embla-carousel-react": "^8.5.2", 75 | "framer-motion": "^11.18.2", 76 | "lucide-react": "^0.395.0", 77 | "next": "14.2.5", 78 | "next-themes": "^0.3.0", 79 | "posthog-js": "^1.212.1", 80 | "r2r-js": "^0.4.34", 81 | "react": "18.3.1", 82 | "react-chartjs-2": "^5.3.0", 83 | "react-dom": "18.3.1", 84 | "react-dropzone": "^14.3.5", 85 | "react-markdown": "^9.0.3", 86 | "sass": "^1.83.4", 87 | "tailwind-merge": "^2.6.0", 88 | "tailwindcss-animate": "^1.0.7", 89 | "topojson-client": "^3.1.0", 90 | "uuid": "^10.0.0" 91 | }, 92 | "devDependencies": { 93 | "@types/d3": "^7.4.3", 94 | "@types/d3-array": "^3.2.1", 95 | "@types/react": "18.3.3", 96 | "@types/topojson-client": "^3.1.5", 97 | "@types/uuid": "^9.0.8", 98 | "@typescript-eslint/eslint-plugin": "^7.18.0", 99 | "@typescript-eslint/parser": "^7.18.0", 100 | "autoprefixer": "^10.4.20", 101 | "eslint": "^8.57.1", 102 | "eslint-config-next": "14.2.4", 103 | "eslint-config-prettier": "^9.1.0", 104 | "eslint-plugin-import": "^2.31.0", 105 | "eslint-plugin-prettier": "^5.2.3", 106 | "husky": "^9.1.7", 107 | "lint-staged": "^15.4.3", 108 | "prettier": "^3.4.2", 109 | "tailwindcss": "^3.4.17", 110 | "typescript": "5.5.2" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/env-config.js: -------------------------------------------------------------------------------- 1 | window.__RUNTIME_CONFIG__ = { 2 | NEXT_PUBLIC_R2R_DEPLOYMENT_URL: '__NEXT_PUBLIC_R2R_DEPLOYMENT_URL__', 3 | R2R_DASHBOARD_DISABLE_TELEMETRY: '__R2R_DASHBOARD_DISABLE_TELEMETRY__', 4 | SUPABASE_URL: '__SUPABASE_URL__', 5 | SUPABASE_ANON_KEY: '__SUPABASE_ANON_KEY__', 6 | NEXT_PUBLIC_HATCHET_DASHBOARD_URL: '__NEXT_PUBLIC_HATCHET_DASHBOARD_URL__', 7 | NEXT_PUBLIC_SENTRY_DSN: '__NEXT_PUBLIC_SENTRY_DSN__', 8 | }; 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SciPhi-AI/R2R-Application/c4612ab48af002d4cd078659d11db88ed61d53b8/public/favicon.ico -------------------------------------------------------------------------------- /public/images/default_profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SciPhi-AI/R2R-Application/c4612ab48af002d4cd078659d11db88ed61d53b8/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/google-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/hatchet-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/images/javascript-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/sciphi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '', 9 | 10 | // Add optional integrations for additional features 11 | integrations: [Sentry.replayIntegration()], 12 | 13 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 14 | tracesSampleRate: 1, 15 | 16 | // Define how likely Replay events are sampled. 17 | // This sets the sample rate to be 10%. You may want this to be 100% while 18 | // in development and sample at a lower rate in production 19 | replaysSessionSampleRate: 0.1, 20 | 21 | // Define how likely Replay events are sampled when an error occurs. 22 | replaysOnErrorSampleRate: 1.0, 23 | 24 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 25 | debug: false, 26 | }); 27 | -------------------------------------------------------------------------------- /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 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '', 10 | 11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /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 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || '', 9 | 10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/ChatDemo/DefaultQueries.tsx: -------------------------------------------------------------------------------- 1 | import { Lightbulb, FlaskConical, Flame, Earth } from 'lucide-react'; 2 | import { FC } from 'react'; 3 | 4 | import { Logo } from '@/components/shared/Logo'; 5 | import { Alert, AlertDescription } from '@/components/ui/alert'; 6 | import { DefaultQueriesProps } from '@/types'; 7 | 8 | export const DefaultQueries: FC = ({ setQuery, mode }) => { 9 | const defaultRagQueries = [ 10 | { 11 | query: 'What is the main topic of the uploaded documents?', 12 | icon: , 13 | }, 14 | { 15 | query: 'Summarize key points for me.', 16 | icon: , 17 | }, 18 | { 19 | query: 'What issues do you see with the documents?', 20 | icon: , 21 | }, 22 | { 23 | query: 'How are these documents interrelated?', 24 | icon: , 25 | }, 26 | ]; 27 | 28 | const defaultAgentQueries = [ 29 | { 30 | query: 'Hey! How are you today?', 31 | icon: , 32 | }, 33 | { 34 | query: 'Can you help me understand my documents better?', 35 | icon: , 36 | }, 37 | { 38 | query: 'How might agentic RAG help me in the long run?', 39 | icon: , 40 | }, 41 | { 42 | query: 'What is the coolest thing you can do?', 43 | icon: , 44 | }, 45 | ]; 46 | 47 | const getQueriesBasedOnMode = (mode: 'rag' | 'rag_agent') => { 48 | if (mode === 'rag') { 49 | return defaultRagQueries; 50 | } else if (mode === 'rag_agent') { 51 | return defaultAgentQueries; 52 | } else { 53 | throw new Error('Invalid mode'); 54 | } 55 | }; 56 | 57 | const defaultQueries = getQueriesBasedOnMode(mode); 58 | 59 | return ( 60 |
61 | 62 |
63 | {defaultQueries.map(({ query, icon }, index) => ( 64 | setQuery(query)} 68 | > 69 |
{icon}
70 | 71 | {query} 72 | 73 |
74 | ))} 75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/ChatDemo/DownloadFileContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { FileDown } from 'lucide-react'; 3 | import React, { useState } from 'react'; 4 | 5 | import { Spinner } from '@/components/Spinner'; 6 | import { Button } from '@/components/ui/Button'; 7 | import { useUserContext } from '@/context/UserContext'; 8 | 9 | interface DownloadFileContainerProps { 10 | id: string; 11 | fileName: string; 12 | showToast: (message: { 13 | title: string; 14 | description: string; 15 | variant: 'default' | 'destructive' | 'success'; 16 | }) => void; 17 | } 18 | 19 | const DownloadButtonContainer: React.FC = ({ 20 | id, 21 | fileName, 22 | showToast, 23 | }) => { 24 | const [isDownloading, setIsDownloading] = useState(false); 25 | const { getClient } = useUserContext(); 26 | 27 | const handleDocumentDownload = async () => { 28 | setIsDownloading(true); 29 | 30 | try { 31 | const client = await getClient(); 32 | if (!client) { 33 | throw new Error('Failed to get authenticated client'); 34 | } 35 | 36 | const blob = await client.documents.download({ id: id }); 37 | 38 | const url = window.URL.createObjectURL(blob); 39 | 40 | const a = document.createElement('a'); 41 | a.style.display = 'none'; 42 | a.href = url; 43 | a.download = fileName; 44 | 45 | document.body.appendChild(a); 46 | a.click(); 47 | 48 | window.URL.revokeObjectURL(url); 49 | document.body.removeChild(a); 50 | 51 | showToast({ 52 | variant: 'success', 53 | title: 'Download Successful', 54 | description: 'The file has been downloaded successfully.', 55 | }); 56 | } catch (error: any) { 57 | console.error('Error downloading file:', error); 58 | showToast({ 59 | variant: 'destructive', 60 | title: 'Download Failed', 61 | description: error.message || 'An unknown error occurred', 62 | }); 63 | } finally { 64 | setIsDownloading(false); 65 | } 66 | }; 67 | 68 | return ( 69 |
70 | 83 |
84 | ); 85 | }; 86 | 87 | export default DownloadButtonContainer; 88 | -------------------------------------------------------------------------------- /src/components/ChatDemo/ExtractContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { FileOutput } from 'lucide-react'; 3 | import React, { useState } from 'react'; 4 | 5 | import { Spinner } from '@/components/Spinner'; 6 | import { Button } from '@/components/ui/Button'; 7 | import { useUserContext } from '@/context/UserContext'; 8 | 9 | interface ExtractContainerProps { 10 | id: string; 11 | ingestionStatus: string; 12 | showToast: (message: { 13 | title: string; 14 | description: string; 15 | variant: 'default' | 'destructive' | 'success'; 16 | }) => void; 17 | } 18 | 19 | const ExtractButtonContainer: React.FC = ({ 20 | id, 21 | ingestionStatus, 22 | showToast, 23 | }) => { 24 | const [isExtracting, setIsExtracting] = useState(false); 25 | const { getClient } = useUserContext(); 26 | 27 | const isIngestionValid = () => { 28 | const status = ingestionStatus.toUpperCase(); 29 | return status === 'SUCCESS'; 30 | }; 31 | 32 | const handleDocumentExtraction = async () => { 33 | setIsExtracting(true); 34 | 35 | try { 36 | const client = await getClient(); 37 | if (!client) { 38 | throw new Error('Failed to get authenticated client'); 39 | } 40 | 41 | client.documents.extract({ id: id }); 42 | 43 | showToast({ 44 | variant: 'success', 45 | title: 'Extraction Started', 46 | description: 47 | 'The extraction request has been sent and will be processed in the background.', 48 | }); 49 | } catch (error: any) { 50 | console.error('Error initiating extraction:', error); 51 | showToast({ 52 | variant: 'destructive', 53 | title: 'Extraction Failed', 54 | description: error.message || 'An unknown error occurred', 55 | }); 56 | } finally { 57 | setIsExtracting(false); 58 | } 59 | }; 60 | 61 | return ( 62 |
63 | 76 |
77 | ); 78 | }; 79 | export default ExtractButtonContainer; 80 | -------------------------------------------------------------------------------- /src/components/ChatDemo/MessageBubble.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Message } from '@/types'; 4 | 5 | const MessageBubble: React.FC<{ message: Message; isStreaming?: boolean }> = ({ 6 | message, 7 | isStreaming, 8 | }) => { 9 | if (message.role === 'user') { 10 | return ( 11 |
12 |
13 |

{message.content}

14 |
15 |
16 | ); 17 | } else if (message.role === 'assistant') { 18 | return ( 19 |
20 |
23 |

24 | {message.content} 25 | {message.isStreaming && ( 26 | 27 | )} 28 |

29 |
30 |
31 | ); 32 | } 33 | return null; 34 | }; 35 | 36 | export default MessageBubble; 37 | -------------------------------------------------------------------------------- /src/components/ChatDemo/PipelineStatus.tsx: -------------------------------------------------------------------------------- 1 | import { ServerStats } from 'r2r-js'; 2 | import React, { useState, useEffect, useCallback } from 'react'; 3 | 4 | import { useUserContext } from '@/context/UserContext'; 5 | import { PipelineStatusProps } from '@/types'; 6 | 7 | export function useConnectionStatus( 8 | deploymentUrl?: string, 9 | onStatusChange?: (isConnected: boolean) => void 10 | ) { 11 | const { getClient } = useUserContext(); 12 | const [isConnected, setIsConnected] = useState(false); 13 | const [serverStats, setServerStats] = useState(null); 14 | const [localUptime, setLocalUptime] = useState(0); 15 | 16 | const checkStatusAndFetchStats = useCallback(async () => { 17 | if (!deploymentUrl) { 18 | setIsConnected(false); 19 | onStatusChange?.(false); 20 | return; 21 | } 22 | 23 | const client = getClient(); 24 | if (!client) { 25 | setIsConnected(false); 26 | onStatusChange?.(false); 27 | return; 28 | } 29 | 30 | try { 31 | await client.system.health(); 32 | setIsConnected(true); 33 | onStatusChange?.(true); 34 | 35 | const stats = await client.system.status(); 36 | setServerStats(stats.results); 37 | setLocalUptime(stats.results.uptimeSeconds); 38 | } catch (error) { 39 | console.error('Error checking status or fetching stats:', error); 40 | setIsConnected(false); 41 | onStatusChange?.(false); 42 | } 43 | }, [deploymentUrl, getClient, onStatusChange]); 44 | 45 | useEffect(() => { 46 | checkStatusAndFetchStats(); 47 | const intervalId = setInterval(checkStatusAndFetchStats, 60000); // Check every minute 48 | 49 | return () => clearInterval(intervalId); 50 | }, [checkStatusAndFetchStats]); 51 | 52 | useEffect(() => { 53 | const uptimeIntervalId = setInterval(() => { 54 | setLocalUptime((prevUptime) => prevUptime + 1); 55 | }, 1000); 56 | 57 | return () => clearInterval(uptimeIntervalId); 58 | }, []); 59 | 60 | return { isConnected, serverStats, localUptime }; 61 | } 62 | 63 | export function PipelineStatus({ 64 | className = '', 65 | onStatusChange, 66 | }: PipelineStatusProps) { 67 | const { pipeline } = useUserContext(); 68 | const { isConnected, localUptime } = useConnectionStatus( 69 | pipeline?.deploymentUrl, 70 | onStatusChange 71 | ); 72 | 73 | return ( 74 |
75 |
78 | 79 | Status: {isConnected ? 'Connected' : 'No Connection'} 80 | 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/components/ChatDemo/ServerCard.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Check, ClipboardCheck } from 'lucide-react'; 2 | import React, { useState } from 'react'; 3 | 4 | import { 5 | PipelineStatus, 6 | useConnectionStatus, 7 | } from '@/components/ChatDemo/PipelineStatus'; 8 | import { formatUptime } from '@/components/ChatDemo/utils/formatUptime'; 9 | import { 10 | Card, 11 | CardHeader, 12 | CardTitle, 13 | CardDescription, 14 | CardContent, 15 | } from '@/components/ui/card'; 16 | import { brandingConfig } from '@/config/brandingConfig'; 17 | import { R2RServerCardProps } from '@/types'; 18 | 19 | const R2RServerCard: React.FC = ({ 20 | pipeline, 21 | onStatusChange, 22 | }) => { 23 | const [copied, setCopied] = useState(false); 24 | const { isConnected, serverStats, localUptime } = useConnectionStatus( 25 | pipeline?.deploymentUrl, 26 | onStatusChange 27 | ); 28 | 29 | const handleCopy = (text: string) => { 30 | navigator.clipboard.writeText(text).then( 31 | () => { 32 | setCopied(true); 33 | setTimeout(() => setCopied(false), 2000); 34 | }, 35 | (err) => console.error('Failed to copy text: ', err) 36 | ); 37 | }; 38 | 39 | return ( 40 | 41 | 42 |
43 | 44 | {brandingConfig.deploymentName} Server 45 | 46 | 47 |
48 | 49 | Your {brandingConfig.deploymentName} server deployment. 50 | 51 |
52 | 53 |
54 |
55 |

Uptime:

56 |

{isConnected ? formatUptime(localUptime) : 'N/A'}

57 |
58 |
59 |

CPU Usage:

60 |

{serverStats ? `${serverStats.cpuUsage.toFixed(1)}%` : 'N/A'}

61 |
62 |
63 |

Start Time:

64 |

65 | {serverStats 66 | ? new Date(serverStats.startTime).toLocaleString() 67 | : 'N/A'} 68 |

69 |
70 |
71 |

Memory Usage:

72 |

73 | {serverStats ? `${serverStats.memoryUsage.toFixed(1)}%` : 'N/A'} 74 |

75 |
76 |
77 |
78 | 79 | 80 | {pipeline?.deploymentUrl} 81 | 82 | {copied ? ( 83 | 84 | ) : ( 85 | handleCopy(pipeline?.deploymentUrl || '')} 88 | /> 89 | )} 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export default R2RServerCard; 97 | -------------------------------------------------------------------------------- /src/components/ChatDemo/SingleSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | import { Switch } from '@/components/ui/switch'; 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipProvider, 8 | TooltipTrigger, 9 | } from '@/components/ui/tooltip'; 10 | import { SingleSwitchProps } from '@/types'; 11 | 12 | const SingleSwitch: React.FC = ({ 13 | id, 14 | initialChecked, 15 | onChange, 16 | label, 17 | tooltipText, 18 | }) => { 19 | const [isChecked, setIsChecked] = useState(initialChecked); 20 | 21 | useEffect(() => { 22 | setIsChecked(initialChecked); 23 | }, [initialChecked]); 24 | 25 | const handleSwitchChange = (checked: boolean) => { 26 | setIsChecked(checked); 27 | onChange(id, checked); 28 | }; 29 | 30 | return ( 31 |
32 | {label && ( 33 | 36 | )} 37 | 38 | 39 | 40 | 45 | 46 | 47 |

{tooltipText}

48 |
49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default SingleSwitch; 56 | -------------------------------------------------------------------------------- /src/components/ChatDemo/SwitchManager.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | 3 | import { Switch } from '@/types'; 4 | 5 | const useSwitchManager = () => { 6 | const [switches, setSwitches] = useState>({}); 7 | 8 | const initializeSwitch = useCallback( 9 | ( 10 | id: string, 11 | initialChecked: boolean, 12 | label: string, 13 | tooltipText: string 14 | ) => { 15 | setSwitches((prevSwitches) => ({ 16 | ...prevSwitches, 17 | [id]: { checked: initialChecked, label, tooltipText }, 18 | })); 19 | }, 20 | [] 21 | ); 22 | 23 | const updateSwitch = useCallback((id: string, checked: boolean) => { 24 | setSwitches((prevSwitches) => ({ 25 | ...prevSwitches, 26 | [id]: { ...prevSwitches[id], checked }, 27 | })); 28 | }, []); 29 | 30 | return { 31 | switches, 32 | initializeSwitch, 33 | updateSwitch, 34 | }; 35 | }; 36 | 37 | export default useSwitchManager; 38 | -------------------------------------------------------------------------------- /src/components/ChatDemo/UpdateButtonContainer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { FileUp } from 'lucide-react'; 3 | import React, { useState, useRef } from 'react'; 4 | 5 | import { Spinner } from '@/components/Spinner'; 6 | import { Button } from '@/components/ui/Button'; 7 | import { useUserContext } from '@/context/UserContext'; 8 | import { UpdateButtonContainerProps } from '@/types'; 9 | 10 | const UpdateButtonContainer: React.FC = ({ 11 | id, 12 | onUpdateSuccess, 13 | showToast, 14 | }) => { 15 | const [isUpdating, setIsUpdating] = useState(false); 16 | const fileInputRef = useRef(null); 17 | const { getClient } = useUserContext(); 18 | 19 | const handleDocumentUpdate = async ( 20 | event: React.ChangeEvent 21 | ) => { 22 | event.preventDefault(); 23 | if ( 24 | fileInputRef.current && 25 | fileInputRef.current.files && 26 | fileInputRef.current.files.length 27 | ) { 28 | setIsUpdating(true); 29 | const file = fileInputRef.current.files[0]; 30 | 31 | try { 32 | const client = await getClient(); 33 | if (!client) { 34 | throw new Error('Failed to get authenticated client'); 35 | } 36 | 37 | const metadata = { title: file.name }; 38 | 39 | await client.documents.create({ 40 | id: id, 41 | file: file, 42 | metadata: [metadata], 43 | }); 44 | 45 | showToast({ 46 | variant: 'success', 47 | title: 'Upload Successful', 48 | description: 'All files have been uploaded successfully.', 49 | }); 50 | onUpdateSuccess(); 51 | } catch (error: any) { 52 | console.error('Error updating file:', error); 53 | console.error('Error details:', error.response?.data); 54 | showToast({ 55 | variant: 'destructive', 56 | title: 'Upload Failed', 57 | description: error.message || 'An unknown error occurred', 58 | }); 59 | } finally { 60 | setIsUpdating(false); 61 | if (fileInputRef.current) { 62 | fileInputRef.current.value = ''; 63 | } 64 | } 65 | } 66 | }; 67 | 68 | const handleUpdateButtonClick = () => { 69 | if (fileInputRef.current) { 70 | fileInputRef.current.click(); 71 | } 72 | }; 73 | 74 | return ( 75 |
76 | 89 | 95 |
96 | ); 97 | }; 98 | 99 | export default UpdateButtonContainer; 100 | -------------------------------------------------------------------------------- /src/components/ChatDemo/UploadDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Upload, X } from 'lucide-react'; 2 | import React, { useCallback, useState } from 'react'; 3 | import { useDropzone } from 'react-dropzone'; 4 | 5 | import { Button } from '@/components/ui/Button'; 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogHeader, 10 | DialogTitle, 11 | } from '@/components/ui/dialog'; 12 | import { UploadDialogProps } from '@/types'; 13 | 14 | export const UploadDialog: React.FC = ({ 15 | isOpen, 16 | onClose, 17 | onUpload, 18 | }) => { 19 | const [files, setFiles] = useState([]); 20 | 21 | const onDrop = useCallback((acceptedFiles: File[]) => { 22 | setFiles((prevFiles) => { 23 | const newFiles = acceptedFiles.filter((newFile) => { 24 | const isDuplicate = prevFiles.some( 25 | (existingFile) => 26 | existingFile.name === newFile.name && 27 | existingFile.size === newFile.size 28 | ); 29 | return !isDuplicate; 30 | }); 31 | return [...prevFiles, ...newFiles]; 32 | }); 33 | }, []); 34 | 35 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 36 | onDrop, 37 | multiple: true, 38 | }); 39 | 40 | const handleUpload = () => { 41 | onUpload(files); 42 | setFiles([]); 43 | onClose(); 44 | }; 45 | 46 | const removeFile = (index: number) => { 47 | setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | 54 | Upload Files or Folders 55 | 56 |
62 | 63 | {isDragActive ? ( 64 |

Drop the files or folders here ...

65 | ) : ( 66 |
67 | 68 |

Drag and drop files or folders here, or click to select

69 |
70 | )} 71 |
72 | {files.length > 0 && ( 73 |
74 |

Selected files:

75 |
    76 | {files.map((file, index) => ( 77 |
  • 81 | 82 | {(file as any).webkitRelativePath || file.name} 83 | 84 | 90 |
  • 91 | ))} 92 |
93 |
94 | )} 95 | 103 |
104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/ChatDemo/createFile.tsx: -------------------------------------------------------------------------------- 1 | import { FileUp } from 'lucide-react'; 2 | import React, { useState, Dispatch, SetStateAction } from 'react'; 3 | 4 | import { Button } from '@/components/ui/Button'; 5 | import { useUserContext } from '@/context/UserContext'; 6 | import { generateIdFromLabel } from '@/lib/utils'; 7 | 8 | import { UploadDialog } from './UploadDialog'; 9 | 10 | export interface CreateFileButton { 11 | userId: string | null; 12 | onUploadSuccess?: () => Promise; 13 | showToast?: (message: { 14 | title: string; 15 | description: string; 16 | variant: 'default' | 'destructive' | 'success'; 17 | }) => void; 18 | setCurrentPage?: React.Dispatch>; 19 | documentsPerPage?: number; 20 | } 21 | 22 | export const CreateFileButton: React.FC = ({ 23 | userId, 24 | onUploadSuccess, 25 | showToast = () => {}, 26 | setCurrentPage, 27 | documentsPerPage, 28 | }) => { 29 | const [isUploading, setIsUploading] = useState(false); 30 | const [isDialogOpen, setIsDialogOpen] = useState(false); 31 | const { getClient } = useUserContext(); 32 | 33 | const handleCreateDocument = async (files: File[]) => { 34 | setIsUploading(true); 35 | const client = await getClient(); 36 | if (!client) { 37 | throw new Error('Failed to get authenticated client'); 38 | } 39 | 40 | try { 41 | const uploadedFiles: any[] = []; 42 | const metadatas: Record[] = []; 43 | const userIds: (string | null)[] = []; 44 | 45 | for (const file of files) { 46 | const fileId = generateIdFromLabel(file.name); 47 | uploadedFiles.push({ documentId: fileId, title: file.name }); 48 | metadatas.push({ title: file.name }); 49 | userIds.push(userId); 50 | } 51 | 52 | for (const file of files) { 53 | await client.documents.create({ 54 | file: file, 55 | }); 56 | } 57 | 58 | showToast({ 59 | variant: 'success', 60 | title: 'Upload Successful', 61 | description: 'All files have been uploaded successfully.', 62 | }); 63 | 64 | if (onUploadSuccess) { 65 | const updatedDocuments = await onUploadSuccess(); 66 | if (updatedDocuments.length > 0 && setCurrentPage && documentsPerPage) { 67 | const totalPages = Math.ceil( 68 | updatedDocuments.length / documentsPerPage 69 | ); 70 | setCurrentPage(1); 71 | } else if (setCurrentPage) { 72 | setCurrentPage(1); 73 | } 74 | } 75 | } catch (error: any) { 76 | console.error('Error uploading files:', error); 77 | showToast({ 78 | variant: 'destructive', 79 | title: 'Upload Failed', 80 | description: error.message, 81 | }); 82 | } finally { 83 | setIsUploading(false); 84 | } 85 | }; 86 | 87 | return ( 88 | <> 89 | 101 | setIsDialogOpen(false)} 104 | onUpload={handleCreateDocument} 105 | /> 106 | 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /src/components/ChatDemo/popover.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 4 | import * as React from 'react'; 5 | 6 | import { cn } from './utils/cn'; 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 | -------------------------------------------------------------------------------- /src/components/ChatDemo/remove.tsx: -------------------------------------------------------------------------------- 1 | import { FileMinus, UserMinus, Minus } from 'lucide-react'; 2 | import React from 'react'; 3 | 4 | import { 5 | AlertDialog, 6 | AlertDialogAction, 7 | AlertDialogCancel, 8 | AlertDialogContent, 9 | AlertDialogDescription, 10 | AlertDialogFooter, 11 | AlertDialogHeader, 12 | AlertDialogTitle, 13 | AlertDialogTrigger, 14 | } from '@/components/ui/alert-dialog'; 15 | import { Button } from '@/components/ui/Button'; 16 | import { useUserContext } from '@/context/UserContext'; 17 | 18 | export interface RemoveButtonProps { 19 | itemId: string; 20 | collectionId: string; 21 | itemType: 'document' | 'user' | 'entity' | 'relationship' | 'community'; 22 | onSuccess: () => void; 23 | showToast: (message: { 24 | title: string; 25 | description: string; 26 | variant: 'default' | 'destructive' | 'success'; 27 | }) => void; 28 | } 29 | 30 | export const RemoveButton: React.FC = ({ 31 | itemId, 32 | collectionId, 33 | itemType, 34 | onSuccess, 35 | showToast, 36 | }) => { 37 | const { getClient } = useUserContext(); 38 | 39 | const handleRemove = async () => { 40 | try { 41 | const client = await getClient(); 42 | if (!client) { 43 | throw new Error('Failed to get authenticated client'); 44 | } 45 | 46 | if (itemType === 'document') { 47 | await client.collections.removeDocument({ 48 | id: collectionId, 49 | documentId: itemId, 50 | }); 51 | } else if (itemType === 'user') { 52 | await client.collections.removeUser({ 53 | id: collectionId, 54 | userId: itemId, 55 | }); 56 | } else if (itemType === 'entity') { 57 | await client.graphs.removeEntity({ 58 | collectionId: collectionId, 59 | entityId: itemId, 60 | }); 61 | } else if (itemType === 'relationship') { 62 | await client.graphs.removeRelationship({ 63 | collectionId: collectionId, 64 | relationshipId: itemId, 65 | }); 66 | } else if (itemType === 'community') { 67 | await client.graphs.deleteCommunity({ 68 | collectionId: collectionId, 69 | communityId: itemId, 70 | }); 71 | } 72 | 73 | showToast({ 74 | variant: 'success', 75 | title: `${itemType.charAt(0).toUpperCase() + itemType.slice(1)} removed`, 76 | description: `The ${itemType} has been successfully removed from the collection`, 77 | }); 78 | onSuccess(); 79 | } catch (error: unknown) { 80 | console.error(`Error removing ${itemType}:`, error); 81 | if (error instanceof Error) { 82 | showToast({ 83 | variant: 'destructive', 84 | title: `Failed to remove ${itemType}`, 85 | description: error.message, 86 | }); 87 | } else { 88 | showToast({ 89 | variant: 'destructive', 90 | title: `Failed to remove ${itemType}`, 91 | description: 'An unknown error occurred', 92 | }); 93 | } 94 | } 95 | }; 96 | 97 | const Icon = 98 | itemType === 'document' 99 | ? FileMinus 100 | : itemType === 'user' 101 | ? UserMinus 102 | : Minus; 103 | 104 | return ( 105 | 106 | 107 | 114 | 115 | 116 | 117 | 118 | Are you sure you want to remove this {itemType} from the collection? 119 | 120 | 121 | This action will remove the {itemType} from the current collection. 122 | The {itemType} will not be deleted from the system. 123 | 124 | 125 | 126 | Cancel 127 | Remove 128 | 129 | 130 | 131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /src/components/ChatDemo/search.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRight } from 'lucide-react'; 2 | import { useRouter } from 'next/navigation'; 3 | import React, { FC, useState, useCallback } from 'react'; 4 | 5 | import { Button } from '@/components/ui/Button'; 6 | import { SearchProps } from '@/types'; 7 | 8 | function debounce void>( 9 | func: T, 10 | wait: number 11 | ): (...args: Parameters) => void { 12 | let timeout: NodeJS.Timeout | null = null; 13 | return function (this: ThisParameterType, ...args: Parameters) { 14 | if (timeout) { 15 | clearTimeout(timeout); 16 | } 17 | timeout = setTimeout(() => func.apply(this, args), wait); 18 | }; 19 | } 20 | 21 | export const Search: FC = ({ 22 | pipeline, 23 | setQuery, 24 | placeholder, 25 | disabled = false, 26 | }) => { 27 | const [value, setValue] = useState(''); 28 | const router = useRouter(); 29 | 30 | if (!placeholder) { 31 | placeholder = 'Search over your documents…'; 32 | } 33 | 34 | const navigateToSearch = useCallback( 35 | debounce((searchValue: string) => { 36 | if (pipeline) { 37 | router.push(`/chat/?q=${encodeURIComponent(searchValue)}`); 38 | } 39 | }, 50), 40 | [router, pipeline] 41 | ); 42 | 43 | const handleSubmit = (e: React.FormEvent) => { 44 | e.preventDefault(); 45 | if (value.trim()) { 46 | navigateToSearch(value.trim()); 47 | setQuery(value.trim()); 48 | setValue(''); 49 | } 50 | }; 51 | 52 | return ( 53 |
54 |
55 | ) => 59 | setValue(e.target.value) 60 | } 61 | autoFocus 62 | placeholder={placeholder} 63 | className="w-full px-4 py-2 h-10 bg-zinc-700 text-zinc-200 rounded-l-full focus:outline-none" 64 | disabled={disabled} 65 | /> 66 | 74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/ChatDemo/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | 3 | import { cn } from './utils/cn'; 4 | 5 | function Skeleton({ className, ...props }: HTMLAttributes) { 6 | return ( 7 |
11 | ); 12 | } 13 | 14 | export { Skeleton }; 15 | -------------------------------------------------------------------------------- /src/components/ChatDemo/title.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useRouter } from 'next/navigation'; 3 | 4 | import { getSearchUrl } from './utils/get-search-url'; 5 | 6 | export const Title = ({ 7 | query, 8 | userId, 9 | model, 10 | setModel, 11 | }: { 12 | query: string; 13 | userId: string; 14 | model: string; 15 | setModel: any; 16 | }) => { 17 | const router = useRouter(); 18 | 19 | return ( 20 |
21 |
22 | 25 |
29 | {query} 30 |
31 | 32 |
33 |
34 |
35 | {/* */} 44 | 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/ChatDemo/update.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useState, useRef, FormEvent, ChangeEvent } from 'react'; 3 | 4 | import { useUserContext } from '@/context/UserContext'; 5 | import { UpdateButtonProps } from '@/types'; 6 | 7 | export const UpdateButton: React.FC = ({ 8 | id, 9 | onUpdateSuccess, 10 | showToast = () => {}, 11 | }) => { 12 | const [isUpdating, setIsUpdating] = useState(false); 13 | const fileInputRef = useRef(null); 14 | const { getClient } = useUserContext(); 15 | 16 | const handleDocumentUpdate = async ( 17 | event: FormEvent | ChangeEvent 18 | ) => { 19 | event.preventDefault(); 20 | if ( 21 | fileInputRef.current && 22 | fileInputRef.current.files && 23 | fileInputRef.current.files.length 24 | ) { 25 | setIsUpdating(true); 26 | const file = fileInputRef.current.files[0]; 27 | 28 | try { 29 | const client = await getClient(); 30 | if (!client) { 31 | throw new Error('Failed to get authenticated client'); 32 | } 33 | 34 | const metadata = { title: file.name }; 35 | 36 | // Updating is currently not supported in the SDK 37 | 38 | await client.documents.create({ 39 | id: id, 40 | file: file, 41 | metadata: [metadata], 42 | }); 43 | showToast({ 44 | variant: 'success', 45 | title: 'Upload Successful', 46 | description: 'All files have been uploaded successfully.', 47 | }); 48 | onUpdateSuccess(); 49 | } catch (error: unknown) { 50 | console.error('Error updating file:', error); 51 | let errorMessage = 'An unknown error occurred'; 52 | if (error instanceof Error) { 53 | errorMessage = error.message; 54 | } 55 | showToast({ 56 | variant: 'destructive', 57 | title: 'Upload Failed', 58 | description: errorMessage, 59 | }); 60 | } finally { 61 | setIsUpdating(false); 62 | if (fileInputRef.current) { 63 | fileInputRef.current.value = ''; 64 | } 65 | } 66 | } 67 | }; 68 | 69 | const handleUpdateButtonClick = () => { 70 | if (fileInputRef.current) { 71 | fileInputRef.current.click(); 72 | } 73 | }; 74 | 75 | return ( 76 |
77 |
78 | 84 | 96 |
97 |
98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/components/ChatDemo/utils/cn.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 | -------------------------------------------------------------------------------- /src/components/ChatDemo/utils/collectionCreationModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { Button } from '@/components/ui/Button'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogFooter, 10 | } from '@/components/ui/dialog'; 11 | import { Input } from '@/components/ui/input'; 12 | import { Textarea } from '@/components/ui/textarea'; 13 | import { useToast } from '@/components/ui/use-toast'; 14 | import { useUserContext } from '@/context/UserContext'; 15 | 16 | interface CollectionCreationModalProps { 17 | open: boolean; 18 | onClose: () => void; 19 | onCollectionCreated: () => void; 20 | } 21 | 22 | const CollectionCreationModal: React.FC = ({ 23 | open, 24 | onClose, 25 | onCollectionCreated, 26 | }) => { 27 | const { getClient } = useUserContext(); 28 | const { toast } = useToast(); 29 | 30 | const [name, setName] = useState(''); 31 | const [description, setDescription] = useState(''); 32 | const [isCreating, setIsCreating] = useState(false); 33 | 34 | const handleCreate = async () => { 35 | if (!name.trim()) { 36 | toast({ 37 | title: 'Validation Error', 38 | description: 'Collection name is required.', 39 | variant: 'destructive', 40 | }); 41 | return; 42 | } 43 | 44 | setIsCreating(true); 45 | try { 46 | const client = await getClient(); 47 | if (!client) { 48 | throw new Error('Failed to get authenticated client'); 49 | } 50 | 51 | await client.collections.create({ 52 | name: name.trim(), 53 | description: description.trim() || undefined, 54 | }); 55 | toast({ 56 | title: 'Collection Created', 57 | description: `Collection "${name}" has been successfully created.`, 58 | variant: 'success', 59 | }); 60 | setName(''); 61 | setDescription(''); 62 | onClose(); 63 | onCollectionCreated(); 64 | } catch (error: any) { 65 | console.error('Error creating collection:', error); 66 | toast({ 67 | title: 'Error', 68 | description: 69 | error?.message || 70 | 'An unexpected error occurred while creating the collection.', 71 | variant: 'destructive', 72 | }); 73 | } finally { 74 | setIsCreating(false); 75 | } 76 | }; 77 | 78 | const handleClose = () => { 79 | if (!isCreating) { 80 | setName(''); 81 | setDescription(''); 82 | onClose(); 83 | } 84 | }; 85 | 86 | return ( 87 | 88 | 89 | 90 | Create New Collection 91 | 92 |
93 | 96 | setName(e.target.value)} 99 | placeholder="Enter collection name" 100 | required 101 | /> 102 |
103 |
104 | 105 |