├── .circleci └── config.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE.txt ├── README.md ├── app ├── (auth) │ ├── change-password │ │ ├── actions.ts │ │ ├── change-password.tsx │ │ └── page.tsx │ ├── common.tsx │ ├── google-sign-in │ │ └── index.tsx │ ├── layout.tsx │ ├── reset │ │ ├── actions.ts │ │ ├── page.tsx │ │ └── reset.tsx │ ├── sign-in │ │ ├── page.tsx │ │ └── sign-in.tsx │ ├── sign-up │ │ ├── page.tsx │ │ └── sign-up.tsx │ └── utils.ts ├── (main) │ └── o │ │ └── [slug] │ │ ├── banner.tsx │ │ ├── context.tsx │ │ ├── conversation-history.tsx │ │ ├── conversations │ │ └── [id] │ │ │ ├── conversation.tsx │ │ │ ├── page.tsx │ │ │ ├── summary.tsx │ │ │ └── types.ts │ │ ├── data │ │ ├── add-connection-menu.tsx │ │ ├── connections-table.tsx │ │ ├── data-page-client.tsx │ │ ├── file-dropzone.tsx │ │ ├── files-table.tsx │ │ ├── manage-connection-menu.tsx │ │ ├── manage-file-menu.tsx │ │ ├── page.tsx │ │ └── upload-file-button.tsx │ │ ├── footer.tsx │ │ ├── header.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── settings │ │ ├── billing │ │ │ ├── billing-settings.tsx │ │ │ └── page.tsx │ │ ├── general-settings.tsx │ │ ├── help-grounding-prompt-dialog.tsx │ │ ├── help-system-prompt-dialog.tsx │ │ ├── help-welcome-message-dialog.tsx │ │ ├── models │ │ │ ├── model-settings.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── prompts │ │ │ ├── page.tsx │ │ │ └── prompt-settings.tsx │ │ ├── settings-nav.tsx │ │ └── users │ │ │ ├── page.tsx │ │ │ └── user-settings.tsx │ │ └── welcome.tsx ├── api │ ├── auth │ │ └── [...all] │ │ │ └── route.ts │ ├── connections │ │ └── [id] │ │ │ └── route.ts │ ├── conversations │ │ ├── [conversationId] │ │ │ ├── messages │ │ │ │ ├── [messageId] │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── utils.ts │ │ │ └── route.ts │ │ └── route.ts │ ├── documents │ │ └── [id] │ │ │ └── route.ts │ ├── invites │ │ ├── [id] │ │ │ └── route.ts │ │ └── route.ts │ ├── profiles │ │ ├── [id] │ │ │ └── route.ts │ │ └── route.ts │ ├── ragie │ │ ├── callback │ │ │ └── route.ts │ │ ├── connect │ │ │ └── [type] │ │ │ │ └── route.ts │ │ ├── stream │ │ │ └── route.ts │ │ ├── upload │ │ │ └── route.ts │ │ └── webhooks │ │ │ └── route.ts │ ├── setup │ │ └── route.ts │ └── tenants │ │ ├── check-slug │ │ └── route.ts │ │ ├── current │ │ ├── documents │ │ │ └── route.ts │ │ ├── members │ │ │ └── route.ts │ │ └── route.ts │ │ └── route.ts ├── check │ └── [slug] │ │ └── route.ts ├── empty │ ├── empty-form.tsx │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── healthz │ └── route.ts ├── invites │ └── accept │ │ └── page.tsx ├── layout.tsx ├── route.ts ├── setup │ ├── page.tsx │ └── setup-form.tsx └── start │ └── page.tsx ├── auth.ts ├── components.json ├── components ├── chatbot │ ├── assistant-message.tsx │ ├── chat-input.tsx │ ├── index.tsx │ ├── style.css │ └── types.ts ├── ga-tags.tsx ├── primary-button.tsx ├── ragie-logo.tsx ├── tenant │ └── logo │ │ ├── confirm-delete-dialog.tsx │ │ ├── create-logo-dialog.tsx │ │ ├── logo-changer.tsx │ │ ├── logo.tsx │ │ ├── server-actions.ts │ │ ├── uploadable-logo.tsx │ │ └── utils.ts ├── ui │ ├── autosize-textarea.tsx │ ├── button.tsx │ ├── checkbox.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form-cancel-button.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── radio-group.tsx │ ├── select.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── submit-button.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ └── tooltip.tsx ├── use-polling.ts └── warning-message.tsx ├── drizzle.config.ts ├── drizzle ├── 0000_clean_thing.sql ├── 0001_overrated_nick_fury.sql ├── 0002_brainy_thing.sql ├── 0003_plain_taskmaster.sql ├── 0004_even_the_leader.sql ├── 0005_wooden_black_bird.sql ├── 0006_classy_leopardon.sql ├── 0007_greedy_squadron_sinister.sql ├── 0008_late_klaw.sql ├── 0009_sparkling_odin.sql ├── 0010_messy_martin_li.sql ├── 0011_flat_husk.sql ├── 0012_mature_anita_blake.sql ├── 0013_yellow_agent_zero.sql ├── 0014_mighty_sersi.sql ├── 0015_left_lady_mastermind.sql ├── 0016_loving_captain_universe.sql ├── 0017_concerned_viper.sql ├── 0018_sturdy_captain_america.sql ├── 0019_lively_tarantula.sql ├── 0020_perfect_synch.sql ├── 0021_organic_madame_hydra.sql ├── 0022_dapper_squadron_sinister.sql ├── 0023_majestic_veda.sql ├── 0024_wandering_speed_demon.sql ├── 0025_organic_thunderbird.sql ├── 0026_colorful_the_executioner.sql ├── 0027_lowly_tana_nile.sql ├── 0028_petite_killmonger.sql ├── 0029_icy_james_howlett.sql ├── 0030_legal_namor.sql ├── 0031_tricky_blue_marvel.sql ├── 0032_dusty_morg.sql ├── 0033_fresh_baron_zemo.sql ├── 0034_abandoned_mindworm.sql ├── 0035_chilly_warpath.sql ├── 0036_amused_rhino.sql ├── 0037_icy_excalibur.sql ├── 0038_smooth_darkstar.sql ├── 0039_worthless_wendigo.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ ├── 0004_snapshot.json │ ├── 0005_snapshot.json │ ├── 0006_snapshot.json │ ├── 0007_snapshot.json │ ├── 0008_snapshot.json │ ├── 0009_snapshot.json │ ├── 0010_snapshot.json │ ├── 0011_snapshot.json │ ├── 0012_snapshot.json │ ├── 0013_snapshot.json │ ├── 0014_snapshot.json │ ├── 0015_snapshot.json │ ├── 0016_snapshot.json │ ├── 0017_snapshot.json │ ├── 0018_snapshot.json │ ├── 0019_snapshot.json │ ├── 0020_snapshot.json │ ├── 0021_snapshot.json │ ├── 0022_snapshot.json │ ├── 0023_snapshot.json │ ├── 0024_snapshot.json │ ├── 0025_snapshot.json │ ├── 0026_snapshot.json │ ├── 0027_snapshot.json │ ├── 0028_snapshot.json │ ├── 0029_snapshot.json │ ├── 0030_snapshot.json │ ├── 0031_snapshot.json │ ├── 0032_snapshot.json │ ├── 0033_snapshot.json │ ├── 0034_snapshot.json │ ├── 0035_snapshot.json │ ├── 0036_snapshot.json │ ├── 0037_snapshot.json │ ├── 0038_snapshot.json │ ├── 0039_snapshot.json │ └── _journal.json ├── env.example ├── eslint.config.mjs ├── image.png ├── jest.config.ts ├── lib ├── api.ts ├── auth-client.ts ├── connector-map.ts ├── constants.ts ├── file-utils.ts ├── llm │ └── types.ts ├── mail.tsx ├── paths.ts ├── query-client-provider.tsx ├── server │ ├── db │ │ ├── index.ts │ │ └── schema.ts │ ├── encryption.ts │ ├── ragie.ts │ ├── service.spec.ts │ ├── service.tsx │ ├── session.ts │ ├── settings.ts │ └── utils.ts └── utils.ts ├── middleware.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── anthropic.svg ├── deepseek.svg ├── file.svg ├── gemini.svg ├── globe.svg ├── google-mark.svg ├── icons │ ├── anonymous-profile.svg │ ├── chat-off.svg │ ├── chat-on.svg │ ├── check.svg │ ├── close.svg │ ├── connectors │ │ ├── confluence.svg │ │ ├── dropbox.svg │ │ ├── freshdesk.svg │ │ ├── gcs.svg │ │ ├── gmail.svg │ │ ├── google-drive.svg │ │ ├── hubspot.svg │ │ ├── jira.svg │ │ ├── notion.svg │ │ ├── onedrive.svg │ │ ├── s3.svg │ │ ├── salesforce.svg │ │ └── slack.svg │ ├── data-off.svg │ ├── data-on.svg │ ├── ellipses.svg │ ├── external-link.svg │ ├── forward_10.svg │ ├── full_screen.svg │ ├── gear.svg │ ├── hamburger.svg │ ├── log-out.svg │ ├── new-chat.svg │ ├── pause.svg │ ├── play.svg │ ├── plus.svg │ ├── replay_10.svg │ ├── settings-off.svg │ ├── settings-on.svg │ └── volume_up.svg ├── images │ ├── title-logo.png │ └── title-logo.svg ├── logo.svg ├── manage-data-preview-icons.svg ├── meta.svg ├── next.svg ├── openai.svg ├── vercel.svg └── window.svg ├── scripts ├── migrate.js ├── update-all-partition-limits.js ├── update-api-key.js └── update-partition-limit.js ├── tailwind.config.ts ├── tsconfig.json └── types.d.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@5.1.1 4 | gcp-cli: circleci/gcp-cli@3.1.1 5 | jobs: 6 | build: 7 | docker: 8 | - image: google/cloud-sdk 9 | steps: 10 | - checkout 11 | 12 | - run: 13 | name: Build Docker image 14 | command: docker build -t basechat:${CIRCLE_SHA1} . 15 | 16 | # Authenticate with Google Cloud 17 | - run: 18 | name: Authenticate with GCP 19 | command: | 20 | echo $GCLOUD_SERVICE_KEY | base64 --decode --ignore-garbage > ${HOME}/gcloud-service-key.json 21 | gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json 22 | gcloud auth configure-docker us-central1-docker.pkg.dev 23 | 24 | - setup_remote_docker 25 | 26 | # Push the Docker image to Google Artifact Registry 27 | - run: 28 | name: Push Docker image to Google Artifact Registry 29 | command: | 30 | docker tag basechat:${CIRCLE_SHA1} us-central1-docker.pkg.dev/ragie-common/images/basechat:${CIRCLE_SHA1} 31 | docker push us-central1-docker.pkg.dev/ragie-common/images/basechat:${CIRCLE_SHA1} 32 | 33 | workflows: 34 | build: 35 | jobs: 36 | - build: 37 | context: 38 | - gcloud 39 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # eslint 43 | .eslintcache 44 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "[typescriptreact]": { 5 | "editor.defaultFormatter": "vscode.typescript-language-features" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "vscode.typescript-language-features" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine AS base 2 | 3 | # Install dependencies only when needed 4 | FROM base AS deps 5 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 6 | RUN apk add --no-cache libc6-compat 7 | WORKDIR /app 8 | 9 | # Install dependencies based on the preferred package manager 10 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 11 | RUN \ 12 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 13 | elif [ -f package-lock.json ]; then npm ci --legacy-peer-deps; \ 14 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ 15 | else echo "Lockfile not found." && exit 1; \ 16 | fi 17 | 18 | 19 | # Rebuild the source code only when needed 20 | FROM base AS builder 21 | WORKDIR /app 22 | COPY --from=deps /app/node_modules ./node_modules 23 | COPY . . 24 | 25 | # Next.js collects completely anonymous telemetry data about general usage. 26 | # Learn more here: https://nextjs.org/telemetry 27 | # Uncomment the following line in case you want to disable telemetry during the build. 28 | # ENV NEXT_TELEMETRY_DISABLED 1 29 | 30 | # FIXME: import/order rule is currently failing in docker build 31 | ENV DISABLE_IMPORT_ORDER=true 32 | 33 | RUN \ 34 | if [ -f yarn.lock ]; then yarn run build; \ 35 | elif [ -f package-lock.json ]; then npm run build; \ 36 | elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ 37 | else echo "Lockfile not found." && exit 1; \ 38 | fi 39 | 40 | # Production image, copy all the files and run next 41 | FROM base AS runner 42 | WORKDIR /app 43 | 44 | ENV NODE_ENV=production 45 | # Uncomment the following line in case you want to disable telemetry during runtime. 46 | # ENV NEXT_TELEMETRY_DISABLED 1 47 | 48 | RUN addgroup --system --gid 1001 nodejs 49 | RUN adduser --system --uid 1001 nextjs 50 | 51 | COPY --from=builder /app/public ./public 52 | 53 | # Set the correct permission for prerender cache 54 | RUN mkdir .next 55 | RUN chown nextjs:nodejs .next 56 | 57 | # Automatically leverage output traces to reduce image size 58 | # https://nextjs.org/docs/advanced-features/output-file-tracing 59 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 60 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 61 | 62 | # Copy drizzle migration files 63 | RUN npm install drizzle-orm --legacy-peer-deps 64 | COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle 65 | COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts 66 | 67 | USER nextjs 68 | 69 | EXPOSE 3000 70 | 71 | ENV PORT=3000 72 | 73 | # server.js is created by next build from the standalone output 74 | # https://nextjs.org/docs/pages/api-reference/next-config-js/output 75 | CMD HOSTNAME="0.0.0.0" node server.js 76 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2025 Ragie Corp. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /app/(auth)/change-password/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { z, ZodError } from "zod"; 5 | 6 | import auth from "@/auth"; 7 | import { getSignInPath } from "@/lib/paths"; 8 | 9 | import { extendPasswordSchema } from "../utils"; 10 | 11 | interface ChangePasswordFormState { 12 | error?: string[]; 13 | } 14 | 15 | const changePasswordSchema = extendPasswordSchema({ 16 | token: z.string(), 17 | }); 18 | 19 | const verificationTokenSchema = z.object({ 20 | sub: z.string().email(), 21 | }); 22 | 23 | export async function handleChangePassword( 24 | prevState: ChangePasswordFormState, 25 | formData: FormData, 26 | ): Promise { 27 | let data; 28 | try { 29 | data = changePasswordSchema.parse({ 30 | token: formData.get("token"), 31 | password: formData.get("password"), 32 | confirm: formData.get("confirm"), 33 | }); 34 | } catch (e) { 35 | if (!(e instanceof ZodError)) throw e; 36 | return { error: e.errors.map((error) => error.message) }; 37 | } 38 | 39 | await auth.api.resetPassword({ 40 | body: { 41 | token: data.token, 42 | newPassword: data.password, 43 | }, 44 | }); 45 | 46 | redirect(getSignInPath({ reset: true })); 47 | } 48 | -------------------------------------------------------------------------------- /app/(auth)/change-password/change-password.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActionState } from "react"; 4 | 5 | import { Button, Error } from "../common"; 6 | 7 | import { handleChangePassword } from "./actions"; 8 | 9 | interface Props { 10 | token: string; 11 | } 12 | 13 | export default function ChangePassword({ token }: Props) { 14 | const [{ error }, changePasswordAction, pending] = useActionState(handleChangePassword, {}); 15 | return ( 16 | <> 17 |
18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/(auth)/change-password/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Title } from "../common"; 4 | 5 | import ChangePassword from "./change-password"; 6 | 7 | interface Props { 8 | searchParams: Promise<{ token: string }>; 9 | } 10 | 11 | export default async function ChangePasswordPage({ searchParams }: Props) { 12 | const { token } = await searchParams; 13 | 14 | return ( 15 | <> 16 | Enter new password: 17 | 18 | 19 | Back to reset 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/(auth)/common.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export function Title({ children, className }: { children: ReactNode; className?: string }) { 6 | return
{children}
; 7 | } 8 | 9 | export function Button({ children, className }: { children: ReactNode; className?: string }) { 10 | return ( 11 | 16 | ); 17 | } 18 | 19 | export function Error({ error, className }: { error: string[] | undefined; className?: string }) { 20 | return ( 21 | error && ( 22 |
    23 | {error.map((e, i) => ( 24 |
  • 25 | {e} 26 |
  • 27 | ))} 28 |
29 | ) 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/(auth)/google-sign-in/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import Image from "next/image"; 5 | 6 | import { signIn } from "@/lib/auth-client"; 7 | 8 | import GoogleMarkSVG from "../../../public/google-mark.svg"; 9 | 10 | interface Props { 11 | redirectTo?: string; 12 | } 13 | 14 | const inter = Inter({ subsets: ["latin"] }); 15 | 16 | export default function GoogleSignIn({ redirectTo }: Props) { 17 | async function handleClick() { 18 | const { data, error } = await signIn.social({ 19 | provider: "google", 20 | callbackURL: redirectTo, 21 | }); 22 | } 23 | 24 | return ( 25 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter, Inter_Tight } from "next/font/google"; 2 | import Image from "next/image"; 3 | 4 | import RagieLogo from "@/components/ragie-logo"; 5 | import * as settings from "@/lib/server/settings"; 6 | 7 | const inter_tight = Inter_Tight({ 8 | subsets: ["latin"], 9 | display: "swap", 10 | }); 11 | 12 | const inter = Inter({ subsets: ["latin"] }); 13 | 14 | export default function Layout({ children }: Readonly<{ children: React.ReactNode }>) { 15 | return ( 16 |
17 |
18 |
19 |
20 | {settings.APP_NAME} 28 |
29 |
{children}
30 |
31 |
32 |
33 |
Powered by
34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/(auth)/reset/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { z, ZodError } from "zod"; 4 | 5 | import auth from "@/auth"; 6 | import { getChangePasswordPath } from "@/lib/paths"; 7 | import { findUserByEmail } from "@/lib/server/service"; 8 | 9 | interface ResetFormState { 10 | error?: string[]; 11 | email?: string; 12 | } 13 | 14 | export async function handleResetPassword(prevState: ResetFormState, formData: FormData): Promise { 15 | let email; 16 | try { 17 | email = z.string().email().parse(formData.get("email")); 18 | } catch (e) { 19 | if (!(e instanceof ZodError)) throw e; 20 | return { error: e.errors.map((e) => e.message) }; 21 | } 22 | 23 | try { 24 | // First check if user exists 25 | const user = await findUserByEmail(email); 26 | if (!user) { 27 | return { 28 | error: ["No account found with this email address"], 29 | }; 30 | } 31 | 32 | await auth.api.forgetPassword({ 33 | body: { 34 | email, 35 | callbackURL: getChangePasswordPath(), 36 | }, 37 | }); 38 | return { email }; 39 | } catch (error) { 40 | return { 41 | error: ["An error occurred while processing your request"], 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/(auth)/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Title } from "../common"; 4 | 5 | import Reset from "./reset"; 6 | 7 | export default function ResetPage() { 8 | return ( 9 | <> 10 | Reset your password 11 | 12 |
13 | Enter the email address associated with your account. If you have an account, we’ll send a reset link to your 14 | email. 15 |
16 | 17 | 18 | 19 | 20 | Back to sign in 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/(auth)/reset/reset.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActionState, useEffect } from "react"; 4 | import { toast } from "sonner"; 5 | 6 | import { Button, Error } from "../common"; 7 | 8 | import { handleResetPassword } from "./actions"; 9 | 10 | export default function Reset() { 11 | const [state, resetPasswordAction, pending] = useActionState(handleResetPassword, {}); 12 | 13 | useEffect(() => { 14 | if (state.email && !state.error) { 15 | toast.info(`Email sent to ${state.email}`); 16 | } 17 | }, [state.email, state.error]); 18 | 19 | return ( 20 |
21 | 22 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import * as settings from "@/lib/server/settings"; 4 | 5 | import { Title } from "../common"; 6 | import GoogleSignIn from "../google-sign-in"; 7 | 8 | import SignIn from "./sign-in"; 9 | 10 | interface Params { 11 | redirectTo?: string; 12 | reset?: string; 13 | } 14 | 15 | export default async function SignInPage({ searchParams }: { searchParams: Promise }) { 16 | const { reset, redirectTo } = await searchParams; 17 | const signUpUrl = new URL("/sign-up", settings.BASE_URL); 18 | if (redirectTo) { 19 | signUpUrl.searchParams.set("redirectTo", redirectTo); 20 | } 21 | 22 | return ( 23 | <> 24 | 25 | Welcome back. 26 | <br /> 27 | Log in to your account below. 28 | 29 | 30 |
31 | 32 |
33 | 34 |
35 |
36 |
or
37 |
38 | 39 | 40 | 41 | 42 | Forgot password? 43 | 44 | 45 |
46 | Need to create a new organization?  47 | 48 | Sign up 49 | 50 |
51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/sign-in.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | import { signIn } from "@/lib/auth-client"; 6 | import { getStartPath } from "@/lib/paths"; 7 | 8 | import { Button } from "../common"; 9 | 10 | export default function SignIn({ redirectTo, reset }: { redirectTo?: string; reset?: boolean }) { 11 | const handleSubmit = async (e: React.FormEvent) => { 12 | e.preventDefault(); 13 | 14 | await signIn.email({ 15 | email, 16 | password, 17 | callbackURL: redirectTo || getStartPath(), 18 | fetchOptions: { 19 | onError: (error) => { 20 | setError(error.error.message); 21 | }, 22 | }, 23 | }); 24 | }; 25 | 26 | const [email, setEmail] = useState(""); 27 | const [password, setPassword] = useState(""); 28 | const [error, setError] = useState(reset ? "Your password has been reset. Please sign in again." : ""); 29 | 30 | const handleEmailChange = (e: React.ChangeEvent) => { 31 | setEmail(e.target.value); 32 | setError(""); 33 | }; 34 | 35 | const handlePasswordChange = (e: React.ChangeEvent) => { 36 | setPassword(e.target.value); 37 | setError(""); 38 | }; 39 | 40 | return ( 41 |
42 | {error &&
{error}
} 43 | 50 | 57 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { getSignInPath } from "@/lib/paths"; 4 | import * as settings from "@/lib/server/settings"; 5 | 6 | import { Title } from "../common"; 7 | import GoogleSignIn from "../google-sign-in"; 8 | 9 | import SignUp from "./sign-up"; 10 | 11 | export default async function SignUpPage({ searchParams }: { searchParams: Promise<{ redirectTo?: string }> }) { 12 | const { redirectTo } = await searchParams; 13 | const signInUrl = new URL(getSignInPath(), settings.BASE_URL); 14 | if (redirectTo) { 15 | signInUrl.searchParams.set("redirectTo", redirectTo); 16 | } 17 | 18 | return ( 19 | <> 20 | 21 | Welcome to {settings.APP_NAME}.<br /> 22 | Sign up to build your chatbot. 23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 |
or
32 |
33 | 34 | 35 | 36 |
37 | Already using {settings.APP_NAME}?  38 | 39 | Sign in 40 | 41 |
42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/(auth)/utils.ts: -------------------------------------------------------------------------------- 1 | import { z, ZodRawShape } from "zod"; 2 | 3 | const passwordSchema = z.object({ 4 | password: z.string().min(6, { message: "Password is required and must be at least 6 characters" }), 5 | confirm: z.string(), 6 | }); 7 | 8 | export function extendPasswordSchema(schema: ZodRawShape) { 9 | return passwordSchema.extend(schema).refine((data) => data.password === data.confirm, { 10 | message: "Passwords do not match", 11 | path: ["confirmPassword"], 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/banner.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface BannerProps { 6 | className?: string; 7 | children: React.ReactNode; 8 | bubble?: boolean; 9 | } 10 | 11 | export function Banner({ className, children, bubble = false }: BannerProps) { 12 | return ( 13 |
20 | {bubble && ( 21 |
22 | Free Trial 23 |
24 | )} 25 |
{children}
26 |
27 | ); 28 | } 29 | 30 | export function BannerLink({ href, children }: { href: string; children: React.ReactNode }) { 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/context.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { createContext, useState, useContext, ReactNode } from "react"; 4 | 5 | import { DEFAULT_MODEL, LLMModel } from "@/lib/llm/types"; 6 | 7 | interface GlobalState { 8 | initialMessage: string; 9 | setInitialMessage: (content: string) => void; 10 | initialModel: LLMModel; 11 | setInitialModel: (model: LLMModel) => void; 12 | isMessageConsumed: boolean; 13 | setMessageConsumed: (consumed: boolean) => void; 14 | clearInitialMessage: () => void; 15 | } 16 | 17 | const GlobalStateContext = createContext(undefined); 18 | 19 | export const GlobalStateProvider = ({ children }: { children: ReactNode }) => { 20 | const [initialMessage, setInitialMessage] = useState(""); 21 | const [initialModel, setInitialModel] = useState(DEFAULT_MODEL); 22 | const [isMessageConsumed, setMessageConsumed] = useState(false); 23 | 24 | const clearInitialMessage = () => { 25 | setInitialMessage(""); 26 | setMessageConsumed(false); 27 | }; 28 | 29 | return ( 30 | 41 | {children} 42 | 43 | ); 44 | }; 45 | 46 | export const useGlobalState = () => { 47 | const context = useContext(GlobalStateContext); 48 | if (!context) { 49 | throw new Error("useGlobalState must be used within a GlobalStateProvider"); 50 | } 51 | return context; 52 | }; 53 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/conversations/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { authOrRedirect } from "@/lib/server/utils"; 2 | 3 | import Conversation from "./conversation"; 4 | 5 | interface Props { 6 | params: Promise<{ id: string; slug: string }>; 7 | } 8 | 9 | export default async function ConversationPage({ params }: Props) { 10 | const p = await params; 11 | const { tenant } = await authOrRedirect(p.slug); 12 | const { id } = p; 13 | 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/conversations/[id]/types.ts: -------------------------------------------------------------------------------- 1 | export interface DocumentResponse { 2 | name: string; 3 | metadata: { 4 | source_type: string; 5 | source_url: string; 6 | }; 7 | updatedAt: string; 8 | summary: string; 9 | } 10 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/data/file-dropzone.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import Dropzone from "react-dropzone"; 5 | import { toast } from "sonner"; 6 | 7 | import { MAX_FILE_SIZE, getDropzoneAcceptConfig, uploadFile, validateFile } from "@/lib/file-utils"; 8 | 9 | interface FileDropzoneProps { 10 | tenant: { 11 | slug: string; 12 | }; 13 | userName: string; 14 | onUploadComplete: () => void; 15 | } 16 | 17 | export default function FileDropzone({ tenant, userName, onUploadComplete }: FileDropzoneProps) { 18 | const router = useRouter(); 19 | 20 | return ( 21 | { 23 | const uploadPromises = acceptedFiles.map(async (file) => { 24 | const validation = validateFile(file); 25 | if (!validation.isValid) { 26 | toast.error(validation.error); 27 | return; 28 | } 29 | // Show a persistent toast for the upload 30 | const toastId = toast.loading(`Uploading ${file.name}...`); 31 | 32 | try { 33 | await uploadFile(file, tenant.slug, userName); 34 | toast.success(`Successfully uploaded ${file.name}`, { 35 | id: toastId, 36 | }); 37 | } catch (err) { 38 | toast.error(`Failed to upload ${file.name}`, { 39 | id: toastId, 40 | }); 41 | } 42 | }); 43 | 44 | await Promise.all(uploadPromises); 45 | router.refresh(); 46 | onUploadComplete(); 47 | }} 48 | accept={getDropzoneAcceptConfig()} 49 | maxSize={MAX_FILE_SIZE} 50 | > 51 | {({ getRootProps, getInputProps, isDragActive }) => ( 52 |
53 |
57 | 58 |

59 | {isDragActive ? ( 60 | "Drop files here..." 61 | ) : ( 62 | <> 63 | Drop anything here or upload a file 64 | 65 | )} 66 |

67 |
68 |
69 | )} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/data/manage-connection-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; 4 | import { MoreHorizontal, Trash } from "lucide-react"; 5 | import { Inter } from "next/font/google"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | interface Props { 13 | id: string; 14 | tenant: { 15 | slug: string; 16 | }; 17 | } 18 | 19 | export default function ManageConnectionMenu({ id, tenant }: Props) { 20 | const router = useRouter(); 21 | 22 | async function deleteConnection() { 23 | const res = await fetch(`/api/connections/${id}`, { 24 | headers: { tenant: tenant.slug }, 25 | method: "DELETE", 26 | }); 27 | 28 | if (!res.ok) throw new Error("delete failed"); 29 | router.refresh(); 30 | } 31 | 32 | return ( 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | Delete 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/data/manage-file-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; 4 | import { MoreHorizontal, Trash } from "lucide-react"; 5 | import { Inter } from "next/font/google"; 6 | import { useRouter } from "next/navigation"; 7 | import { toast } from "sonner"; 8 | 9 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; 10 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 11 | import { cn } from "@/lib/utils"; 12 | 13 | const inter = Inter({ subsets: ["latin"] }); 14 | 15 | interface Props { 16 | id: string; 17 | tenant: { 18 | slug: string; 19 | }; 20 | isConnectorFile: boolean; 21 | onFileRemoved: (fileId: string) => void; 22 | } 23 | 24 | export default function ManageFileMenu({ id, tenant, isConnectorFile, onFileRemoved }: Props) { 25 | const router = useRouter(); 26 | 27 | async function deleteFile() { 28 | if (isConnectorFile) { 29 | toast.error("This file is from a connector and cannot be deleted"); 30 | return; 31 | } 32 | 33 | const toastId = toast.loading("Deleting file..."); 34 | 35 | try { 36 | const res = await fetch(`/api/documents/${id}`, { 37 | headers: { tenant: tenant.slug }, 38 | method: "DELETE", 39 | }); 40 | 41 | if (!res.ok) throw new Error("Failed to delete file"); 42 | 43 | toast.success("File deleted successfully", { 44 | id: toastId, 45 | }); 46 | onFileRemoved(id); 47 | router.refresh(); 48 | } catch (error) { 49 | toast.error("Failed to delete file", { 50 | id: toastId, 51 | }); 52 | } 53 | } 54 | 55 | return ( 56 | 57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | 70 | 71 | Delete 72 | 73 | 74 | {isConnectorFile && ( 75 | 76 |

This file is from a connector and cannot be deleted.

77 |
78 | )} 79 |
80 |
81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/data/page.tsx: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import db from "@/lib/server/db"; 5 | import * as schema from "@/lib/server/db/schema"; 6 | import { getRagieClientAndPartition } from "@/lib/server/ragie"; 7 | import { DEFAULT_PARTITION_LIMIT } from "@/lib/server/settings"; 8 | import { adminOrRedirect } from "@/lib/server/utils"; 9 | 10 | import DataPageClient from "./data-page-client"; 11 | 12 | interface Props { 13 | params: Promise<{ slug: string }>; 14 | searchParams: Promise<{ cursor?: string }>; 15 | } 16 | 17 | export default async function DataIndexPage({ params, searchParams }: Props) { 18 | const p = await params; 19 | const sp = await searchParams; 20 | const { tenant, session } = await adminOrRedirect(p.slug); 21 | const connections = await db.select().from(schema.connections).where(eq(schema.connections.tenantId, tenant.id)); 22 | 23 | // Create a map of source types to connections for quick lookup 24 | const connectionMap = connections.reduce( 25 | (acc, connection) => { 26 | acc[connection.sourceType] = connection; 27 | return acc; 28 | }, 29 | {} as Record, 30 | ); 31 | 32 | let files: any[] = []; 33 | let nextCursor: string | null = null; 34 | let totalDocuments: number = 0; 35 | try { 36 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 37 | const res = await client.documents.list({ 38 | partition, 39 | pageSize: 50, 40 | cursor: sp.cursor || undefined, 41 | }); 42 | files = res.result.documents; 43 | nextCursor = res.result.pagination.nextCursor || null; 44 | totalDocuments = res.result.pagination.totalCount; 45 | } catch (error) { 46 | console.error("Error fetching documents:", error); 47 | // If there's an error and we have a cursor, redirect to the base data page 48 | if (sp.cursor) { 49 | redirect(`/o/${p.slug}/data`); 50 | } 51 | } 52 | 53 | return ( 54 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/data/upload-file-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { toast } from "sonner"; 5 | 6 | import { VALID_FILE_TYPES, uploadFile, validateFile } from "@/lib/file-utils"; 7 | 8 | interface Props { 9 | tenant: { 10 | slug: string; 11 | }; 12 | userName: string; 13 | onUploadComplete: () => void; 14 | } 15 | 16 | export default function UploadFileButton({ tenant, userName, onUploadComplete }: Props) { 17 | const router = useRouter(); 18 | 19 | const handleUpload = () => { 20 | const input = document.createElement("input"); 21 | input.type = "file"; 22 | input.multiple = true; 23 | input.accept = [...Object.keys(VALID_FILE_TYPES), ...Object.values(VALID_FILE_TYPES)].join(","); 24 | input.click(); 25 | 26 | input.onchange = async (e) => { 27 | const files = (e.target as HTMLInputElement).files; 28 | if (!files || files.length === 0) return; 29 | 30 | const uploadPromises = Array.from(files).map(async (file) => { 31 | const validation = validateFile(file); 32 | if (!validation.isValid) { 33 | toast.error(validation.error); 34 | return; 35 | } 36 | 37 | // Show a persistent toast for the upload 38 | const toastId = toast.loading(`Uploading ${file.name}...`); 39 | 40 | try { 41 | await uploadFile(file, tenant.slug, userName); 42 | toast.success(`Successfully uploaded ${file.name}`, { 43 | id: toastId, 44 | }); 45 | } catch (err) { 46 | toast.error(`Failed to upload ${file.name}`, { 47 | id: toastId, 48 | }); 49 | } 50 | }); 51 | 52 | await Promise.all(uploadPromises); 53 | router.refresh(); 54 | onUploadComplete(); 55 | }; 56 | }; 57 | 58 | return ( 59 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/footer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | import { getDataPath, getSettingsPath, getTenantPath } from "@/lib/paths"; 8 | import { cn } from "@/lib/utils"; 9 | import ChatIconOff from "@/public/icons/chat-off.svg"; 10 | import ChatIconOn from "@/public/icons/chat-on.svg"; 11 | import DataIconOff from "@/public/icons/data-off.svg"; 12 | import DataIconOn from "@/public/icons/data-on.svg"; 13 | import SettingsIconOff from "@/public/icons/settings-off.svg"; 14 | import SettingsIconOn from "@/public/icons/settings-on.svg"; 15 | 16 | export enum AppLocation { 17 | CHAT, 18 | DATA, 19 | SETTINGS, 20 | SETTINGS_USERS, 21 | SETTINGS_MODELS, 22 | SETTINGS_PROMPTS, 23 | SETTINGS_BILLING, 24 | } 25 | 26 | export function NavButton({ alt, src, className }: { alt: string; src: any; className?: string }) { 27 | return ( 28 |
29 | {alt} 30 |
{alt}
31 |
32 | ); 33 | } 34 | 35 | interface Props { 36 | className?: string; 37 | tenant: { slug: string }; 38 | } 39 | 40 | export default function Footer({ className, tenant }: Props) { 41 | const pathname = usePathname(); 42 | 43 | let appLocation = AppLocation.CHAT; 44 | if (pathname.startsWith(getDataPath(tenant.slug))) { 45 | appLocation = AppLocation.DATA; 46 | } else if (pathname.startsWith(getSettingsPath(tenant.slug))) { 47 | appLocation = AppLocation.SETTINGS; 48 | } 49 | 50 | const chatIcon = appLocation === AppLocation.CHAT ? ChatIconOn : ChatIconOff; 51 | const chatClassName = appLocation === AppLocation.CHAT ? "mr-5 font-semibold" : "mr-5"; 52 | 53 | const dataIcon = appLocation === AppLocation.DATA ? DataIconOn : DataIconOff; 54 | const dataClassName = appLocation === AppLocation.DATA ? "mr-5 font-semibold" : "mr-5"; 55 | 56 | const settingsIcon = appLocation === AppLocation.SETTINGS ? SettingsIconOn : SettingsIconOff; 57 | const settingsClassName = appLocation === AppLocation.SETTINGS ? "mr-5 font-semibold" : "mr-5"; 58 | 59 | return ( 60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import RagieLogo from "@/components/ragie-logo"; 4 | import { getUserById } from "@/lib/server/service"; 5 | import { BILLING_ENABLED } from "@/lib/server/settings"; 6 | import { authOrRedirect } from "@/lib/server/utils"; 7 | 8 | import Footer from "./footer"; 9 | import Header from "./header"; 10 | 11 | interface Props { 12 | params: Promise<{ slug: string }>; 13 | children?: ReactNode; 14 | } 15 | 16 | export default async function MainLayout({ children, params }: Props) { 17 | const { slug } = await params; 18 | const { tenant, profile, session } = await authOrRedirect(slug); 19 | const user = await getUserById(session.user.id); 20 | 21 | return ( 22 |
23 |
31 |
32 |
33 | {children} 34 |
35 |
36 | {profile.role == "admin" && ( 37 |
38 | )} 39 | {profile.role == "guest" && ( 40 |
41 |
Powered by
42 |
43 | 44 | 45 | 46 |
47 |
48 | )} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { authOrRedirect } from "@/lib/server/utils"; 2 | 3 | import Welcome from "./welcome"; 4 | 5 | export default async function Home({ params }: { params: Promise<{ slug: string }> }) { 6 | const p = await params; 7 | const context = await authOrRedirect(p.slug); 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import { getRagieClientAndPartition } from "@/lib/server/ragie"; 2 | import { BILLING_ENABLED, DEFAULT_PARTITION_LIMIT } from "@/lib/server/settings"; 3 | import { adminOrRedirect } from "@/lib/server/utils"; 4 | 5 | import SettingsNav from "../settings-nav"; 6 | 7 | import BillingSettings from "./billing-settings"; 8 | 9 | interface Props { 10 | params: Promise<{ slug: string }>; 11 | } 12 | 13 | export default async function BillingSettingsPage({ params }: Props) { 14 | const p = await params; 15 | const { tenant } = await adminOrRedirect(p.slug); 16 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 17 | 18 | const partitionInfo = await client.partitions.get({ partitionId: partition }); 19 | 20 | return ( 21 |
22 |
23 | 24 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/help-grounding-prompt-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CircleHelp } from "lucide-react"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; 7 | 8 | export function HelpGroundingPromptDialog() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | Grounding Prompt 17 | 18 |
19 |

20 | The Grounding Prompt is content first sent to an LLM before any message. This is used to help tune the 21 | responses the LLM outputs. You can use this to change the language, tone, or any style of the response. 22 |

23 |

Variables

24 |

25 | Each prompt has access to some variables like the company's name. To use these as part of your prompt 26 | wrap the variable in curly braces like so: {{company.name}} Here are the variables 27 | available 28 |

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
NameDescriptionExample
company.nameYour company nameAcme Co.
nowThe current date and time2025-02-26T21:39:10.969Z
51 | 52 | 55 | 56 |
57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/help-system-prompt-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CircleHelp } from "lucide-react"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; 7 | 8 | export function HelpSystemPromptDialog() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | System Prompt 17 | 18 |
19 |

20 | The System Prompt is sent to the LLM when the relevant chunks for the query are received. It wraps the 21 | chunks in a message to better format the responses. 22 |

23 |

Variables

24 |

The System Prompt has access to the available chunks. Here is a sample use of it

25 | 26 |
27 |

28 | Here is some additional context you can use in your response {{chunks}}. 29 |

30 | 31 | IMPORTANT RULES: 32 |
    33 |
  • Be concise
  • 34 |
  • REFUSE to sing songs
  • 35 |
36 |
37 | 38 | 39 | 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/help-welcome-message-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CircleHelp } from "lucide-react"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; 7 | 8 | export function HelpWelcomeMessageDialog() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | Welcome Message 17 | 18 |
19 |

The Welcome Message is displayed when creating a new conversation.

20 |

Variables

21 |

The Welcome Message can access the company's name. Here is an example

22 | 23 |
24 |

{`Hello, I'm {{ company.name }}'s AI. What would you like to know?`}

25 |
26 | 27 | 30 | 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/models/page.tsx: -------------------------------------------------------------------------------- 1 | import { BILLING_ENABLED } from "@/lib/server/settings"; 2 | import { adminOrRedirect } from "@/lib/server/utils"; 3 | 4 | import SettingsNav from "../settings-nav"; 5 | 6 | import ModelSettings from "./model-settings"; 7 | 8 | interface Props { 9 | params: Promise<{ slug: string }>; 10 | } 11 | 12 | export default async function ModelSettingsPage({ params }: Props) { 13 | const p = await params; 14 | const { tenant } = await adminOrRedirect(p.slug); 15 | 16 | return ( 17 |
18 |
19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { BILLING_ENABLED } from "@/lib/server/settings"; 2 | import { adminOrRedirect } from "@/lib/server/utils"; 3 | 4 | import GeneralSettings from "./general-settings"; 5 | import SettingsNav from "./settings-nav"; 6 | 7 | interface Props { 8 | params: Promise<{ slug: string }>; 9 | } 10 | 11 | export default async function SettingsIndexPage({ params }: Props) { 12 | const p = await params; 13 | const { tenant } = await adminOrRedirect(p.slug); 14 | const canUploadLogo = !!process.env.STORAGE_ENDPOINT; 15 | 16 | return ( 17 |
18 |
19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/prompts/page.tsx: -------------------------------------------------------------------------------- 1 | import { BILLING_ENABLED } from "@/lib/server/settings"; 2 | import { adminOrRedirect } from "@/lib/server/utils"; 3 | 4 | import SettingsNav from "../settings-nav"; 5 | 6 | import PromptSettings from "./prompt-settings"; 7 | 8 | interface Props { 9 | params: Promise<{ slug: string }>; 10 | } 11 | 12 | export default async function PromptsSettingsPage({ params }: Props) { 13 | const p = await params; 14 | const { tenant } = await adminOrRedirect(p.slug); 15 | 16 | return ( 17 |
18 |
19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/settings-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | import { ReactNode } from "react"; 6 | 7 | import { 8 | getModelSettingsPath, 9 | getPromptSettingsPath, 10 | getSettingsPath, 11 | getUserSettingsPath, 12 | getBillingSettingsPath, 13 | } from "@/lib/paths"; 14 | import { cn } from "@/lib/utils"; 15 | 16 | import { AppLocation } from "../footer"; 17 | 18 | const NavItem = ({ children, selected }: { children: ReactNode; selected?: boolean }) => ( 19 |
{children}
20 | ); 21 | 22 | function getAppLocation(path: string, slug: string, billingEnabled: boolean): AppLocation { 23 | if (path.startsWith(getUserSettingsPath(slug))) { 24 | return AppLocation.SETTINGS_USERS; 25 | } 26 | if (path.startsWith(getModelSettingsPath(slug))) { 27 | return AppLocation.SETTINGS_MODELS; 28 | } 29 | if (path.startsWith(getPromptSettingsPath(slug))) { 30 | return AppLocation.SETTINGS_PROMPTS; 31 | } 32 | if (billingEnabled && path.startsWith(getBillingSettingsPath(slug))) { 33 | return AppLocation.SETTINGS_BILLING; 34 | } 35 | return AppLocation.SETTINGS; 36 | } 37 | 38 | interface Props { 39 | tenant: { slug: string }; 40 | billingEnabled: boolean; 41 | } 42 | 43 | export default function SettingsNav({ tenant, billingEnabled }: Props) { 44 | const pathname = usePathname(); 45 | const appLocation = getAppLocation(pathname, tenant.slug, billingEnabled); 46 | 47 | return ( 48 |
49 | 50 | General 51 | 52 | 53 | Users 54 | 55 | 56 | Models 57 | 58 | 59 | Prompts 60 | 61 | {billingEnabled && ( 62 | 63 | Billing 64 | 65 | )} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/(main)/o/[slug]/settings/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMembersByTenantId } from "@/lib/server/service"; 2 | import { BILLING_ENABLED } from "@/lib/server/settings"; 3 | import { adminOrRedirect } from "@/lib/server/utils"; 4 | 5 | import SettingsNav from "../settings-nav"; 6 | 7 | import UserSettings from "./user-settings"; 8 | 9 | interface Props { 10 | params: Promise<{ slug: string }>; 11 | } 12 | 13 | export default async function SettingsUsersIndexPage({ params }: Props) { 14 | const p = await params; 15 | const { tenant } = await adminOrRedirect(p.slug); 16 | const { members, totalUsers, totalInvites } = await getMembersByTenantId(tenant.id, 1, 10); 17 | 18 | return ( 19 |
20 | 21 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | 3 | import auth from "@/auth"; 4 | 5 | export const { POST, GET } = toNextJsHandler(auth); 6 | -------------------------------------------------------------------------------- /app/api/connections/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { deleteConnection } from "@/lib/server/service"; 2 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 3 | 4 | export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { 5 | const { id } = await params; 6 | const { tenant } = await requireAdminContextFromRequest(request); 7 | 8 | await deleteConnection(tenant.id, id); 9 | 10 | return Response.json(200, {}); 11 | } 12 | -------------------------------------------------------------------------------- /app/api/conversations/[conversationId]/messages/[messageId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | import { getConversationMessage } from "@/lib/server/service"; 4 | import { requireAuthContextFromRequest } from "@/lib/server/utils"; 5 | 6 | type Params = { conversationId: string; messageId: string }; 7 | 8 | export async function GET(request: NextRequest, { params }: { params: Promise }) { 9 | const { profile, tenant } = await requireAuthContextFromRequest(request); 10 | const { conversationId, messageId } = await params; 11 | const message = await getConversationMessage(tenant.id, profile.id, conversationId, messageId); 12 | 13 | return Response.json(message); 14 | } 15 | -------------------------------------------------------------------------------- /app/api/conversations/[conversationId]/route.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { z } from "zod"; 4 | 5 | import db from "@/lib/server/db"; 6 | import { conversations } from "@/lib/server/db/schema"; 7 | import { requireAuthContext } from "@/lib/server/utils"; 8 | 9 | // Schema for validating request body 10 | const deleteConversationSchema = z.object({ 11 | tenantSlug: z.string(), 12 | }); 13 | 14 | export async function DELETE(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { 15 | const { conversationId } = await params; 16 | try { 17 | // Parse the request body 18 | const body = await request.json().catch(() => ({})); 19 | const validationResult = deleteConversationSchema.safeParse(body); 20 | if (!validationResult.success) return new NextResponse("Invalid request body", { status: 400 }); 21 | 22 | const { tenantSlug } = validationResult.data; 23 | 24 | const { profile, tenant } = await requireAuthContext(tenantSlug); 25 | 26 | const deleteResults = await db 27 | .delete(conversations) 28 | .where( 29 | and( 30 | eq(conversations.id, conversationId), 31 | eq(conversations.tenantId, tenant.id), 32 | eq(conversations.profileId, profile.id), 33 | ), 34 | ) 35 | .returning({ id: conversations.id }); 36 | 37 | // Check if we actually deleted anything 38 | if (deleteResults.length === 0) { 39 | return new NextResponse("Conversation not found or not authorized", { status: 404 }); 40 | } 41 | 42 | return new NextResponse(null, { status: 204 }); 43 | } catch (error) { 44 | console.error("Failed to delete conversation:", error); 45 | return new NextResponse("Internal Server Error", { status: 500 }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/api/documents/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | import { getRagieClientAndPartition } from "@/lib/server/ragie"; 4 | import { requireAuthContextFromRequest, requireAdminContextFromRequest } from "@/lib/server/utils"; 5 | 6 | export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { 7 | const { tenant } = await requireAuthContextFromRequest(request); 8 | const { id } = await params; 9 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 10 | let document; 11 | let summary; 12 | try { 13 | document = await client.documents.get({ partition, documentId: id }); 14 | } catch (error) { 15 | if (error instanceof Error && error.message.includes("not found")) { 16 | return new Response("Document not found", { status: 404 }); 17 | } 18 | throw error; 19 | } 20 | try { 21 | summary = await client.documents.getSummary({ partition, documentId: id }); 22 | } catch (error) { 23 | if (error instanceof Error && error.message.includes("not found")) { 24 | return new Response("Document summary not found", { status: 404 }); 25 | } 26 | throw error; 27 | } 28 | return Response.json({ ...document, summary: summary.summary }); 29 | } 30 | 31 | export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { 32 | const { tenant } = await requireAdminContextFromRequest(request); 33 | const { id } = await params; 34 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 35 | const result = await client.documents.delete({ partition, documentId: id }); 36 | return Response.json(result); 37 | } 38 | -------------------------------------------------------------------------------- /app/api/invites/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { z } from "zod"; 3 | 4 | import { deleteInviteById, updateInviteRoleById } from "@/lib/server/service"; 5 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 6 | 7 | type Params = Promise<{ id: string }>; 8 | 9 | export async function DELETE(request: NextRequest, { params }: { params: Params }) { 10 | const { tenant } = await requireAdminContextFromRequest(request); 11 | const { id } = await params; 12 | try { 13 | await deleteInviteById(tenant.id, id); 14 | } catch (error) { 15 | console.error(error); 16 | return new Response(null, { status: 500 }); 17 | } 18 | 19 | return new Response(null, { status: 200 }); 20 | } 21 | 22 | const updateInviteRoleByIdSchema = z 23 | .object({ 24 | role: z.union([z.literal("admin"), z.literal("user")]), 25 | }) 26 | .strict(); 27 | 28 | export async function PATCH(request: NextRequest, { params }: { params: Params }) { 29 | const { tenant } = await requireAdminContextFromRequest(request); 30 | const { id } = await params; 31 | 32 | const json = await request.json(); 33 | const payload = updateInviteRoleByIdSchema.parse(json); 34 | await updateInviteRoleById(tenant.id, id, payload.role); 35 | return new Response(null, { status: 200 }); 36 | } 37 | -------------------------------------------------------------------------------- /app/api/invites/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { z } from "zod"; 3 | 4 | import { createInvites } from "@/lib/server/service"; 5 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 6 | 7 | const inviteSchema = z 8 | .object({ 9 | emails: z.array(z.string().email()).min(1), 10 | role: z.union([z.literal("admin"), z.literal("user")]), 11 | }) 12 | .strict(); 13 | 14 | export async function POST(request: NextRequest) { 15 | const { profile, tenant } = await requireAdminContextFromRequest(request); 16 | const json = await request.json(); 17 | const payload = inviteSchema.parse(json); 18 | 19 | const invites = await createInvites(tenant.id, profile.id, payload.emails, payload.role); 20 | 21 | return Response.json(invites); 22 | } 23 | -------------------------------------------------------------------------------- /app/api/profiles/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { unauthorized } from "next/navigation"; 2 | import { NextRequest } from "next/server"; 3 | import { z } from "zod"; 4 | 5 | import { changeRole, deleteProfile, isProfileDeleteAllowed, ServiceError } from "@/lib/server/service"; 6 | import { requireAdminContextFromRequest, requireAuthContextFromRequest } from "@/lib/server/utils"; 7 | 8 | type Params = Promise<{ id: string }>; 9 | 10 | function unwrap(e: unknown): Error { 11 | if (!(e instanceof Error)) throw e; 12 | return e; 13 | } 14 | 15 | function renderError(e: unknown) { 16 | const err = unwrap(e); 17 | const message = err instanceof ServiceError ? err.message : "Unexpected error"; 18 | return new Response(JSON.stringify({ error: message }), { status: 500 }); 19 | } 20 | 21 | export async function DELETE(request: NextRequest, { params }: { params: Params }) { 22 | const { profile, tenant } = await requireAuthContextFromRequest(request); 23 | const { id } = await params; 24 | 25 | try { 26 | await deleteProfile(tenant.id, id, profile); 27 | } catch (e) { 28 | return renderError(e); 29 | } 30 | return new Response(null, { status: 200 }); 31 | } 32 | 33 | const updateProfileRoleByIdSchema = z 34 | .object({ 35 | role: z.union([z.literal("admin"), z.literal("user")]), 36 | }) 37 | .strict(); 38 | 39 | export async function PATCH(request: NextRequest, { params }: { params: Params }) { 40 | const { tenant } = await requireAdminContextFromRequest(request); 41 | const { id } = await params; 42 | 43 | const json = await request.json(); 44 | const payload = updateProfileRoleByIdSchema.parse(json); 45 | 46 | try { 47 | await changeRole(tenant.id, id, payload.role); 48 | } catch (e) { 49 | return renderError(e); 50 | } 51 | return new Response(null, { status: 200 }); 52 | } 53 | -------------------------------------------------------------------------------- /app/api/profiles/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | import { updateCurrentProfileSchema } from "@/lib/api"; 4 | import { setUserTenant } from "@/lib/server/service"; 5 | import { requireSession } from "@/lib/server/utils"; 6 | 7 | export async function POST(request: NextRequest) { 8 | const session = await requireSession(); 9 | const json = await request.json(); 10 | const payload = updateCurrentProfileSchema.parse(json); 11 | 12 | await setUserTenant(session.user.id, payload.tenantId); 13 | 14 | return new Response(null, { status: 200 }); 15 | } 16 | -------------------------------------------------------------------------------- /app/api/ragie/callback/route.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { NextRequest } from "next/server"; 4 | import { z } from "zod"; 5 | 6 | import { getDataPath } from "@/lib/paths"; 7 | import { saveConnection } from "@/lib/server/service"; 8 | import * as settings from "@/lib/server/settings"; 9 | import { requireAdminContext } from "@/lib/server/utils"; 10 | 11 | const querySchema = z.object({ 12 | tenant: z.string(), 13 | connection_id: z.string(), 14 | }); 15 | 16 | export async function GET(request: NextRequest) { 17 | const params = querySchema.parse({ 18 | tenant: request.nextUrl.searchParams.get("tenant"), 19 | connection_id: request.nextUrl.searchParams.get("connection_id"), 20 | }); 21 | 22 | const { tenant, session } = await requireAdminContext(params.tenant); 23 | await saveConnection(tenant.id, params.connection_id, "syncing", session.user.name); 24 | return Response.redirect(new URL(getDataPath(tenant.slug), settings.BASE_URL)); 25 | } 26 | -------------------------------------------------------------------------------- /app/api/ragie/connect/[type]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { ConnectorSource } from "ragie/models/components"; 3 | 4 | import { getRagieClientAndPartition } from "@/lib/server/ragie"; 5 | import * as settings from "@/lib/server/settings"; 6 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 7 | 8 | export const dynamic = "force-dynamic"; // no caching 9 | 10 | interface Params { 11 | type: string; 12 | } 13 | 14 | export async function GET(request: NextRequest, { params }: { params: Promise }) { 15 | const { tenant } = await requireAdminContextFromRequest(request); 16 | 17 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 18 | const { type } = await params; 19 | 20 | const redirectUri = new URL("/api/ragie/callback", settings.BASE_URL!); 21 | redirectUri.searchParams.set("tenant", tenant.slug); 22 | 23 | const payload = await client.connections.createOAuthRedirectUrl({ 24 | redirectUri: redirectUri.toString(), 25 | sourceType: type as ConnectorSource | undefined, 26 | partition, 27 | mode: "hi_res", 28 | theme: "light", 29 | }); 30 | 31 | return Response.json(payload); 32 | } 33 | -------------------------------------------------------------------------------- /app/api/ragie/stream/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { z } from "zod"; 3 | 4 | import { getRagieApiKey } from "@/lib/server/ragie"; 5 | import { RAGIE_API_BASE_URL } from "@/lib/server/settings"; 6 | import { requireAuthContext } from "@/lib/server/utils"; 7 | 8 | const paramsSchema = z.object({ 9 | tenant: z.string(), 10 | url: z.string(), 11 | }); 12 | 13 | // Important: The next edge runtime strips "simple headers" like "Range" from the request, 14 | // so we need to use the Node.js runtime to preserve them. 15 | export const runtime = "nodejs"; 16 | 17 | export async function GET(request: NextRequest) { 18 | const params = paramsSchema.parse({ 19 | tenant: request.nextUrl.searchParams.get("tenant"), 20 | url: request.nextUrl.searchParams.get("url"), 21 | }); 22 | 23 | if (!params.url.startsWith(RAGIE_API_BASE_URL)) { 24 | return new Response("Invalid URL", { status: 400 }); 25 | } 26 | 27 | const { tenant } = await requireAuthContext(params.tenant); 28 | 29 | try { 30 | const ragieApiKey = await getRagieApiKey(tenant); 31 | // Forward Range if present 32 | const reqRange = request.headers.get("range"); 33 | // Propagate stream cancel from player 34 | const controller = new AbortController(); 35 | request.signal.addEventListener("abort", () => controller.abort()); 36 | 37 | const upstreamResponse = await fetch(params.url, { 38 | headers: { 39 | authorization: `Bearer ${ragieApiKey}`, 40 | partition: tenant.ragiePartition || tenant.id, 41 | ...(reqRange ? { Range: reqRange } : {}), 42 | }, 43 | signal: controller.signal, 44 | }); 45 | 46 | // If there's no body, bail out: 47 | if (!upstreamResponse.body) { 48 | console.error("No body in upstream response"); 49 | return new Response("No body in upstream response", { status: 500 }); 50 | } 51 | 52 | // Stream the upstream response directly back to the client preserving status, headers, etc... 53 | const passedThroughHeaders = [ 54 | "Content-Type", 55 | "Accept-Ranges", 56 | "Content-Length", 57 | "Content-Range", 58 | "Transfer-Encoding", 59 | ]; 60 | const headers = new Headers(); 61 | passedThroughHeaders.forEach((header) => { 62 | const value = upstreamResponse.headers.get(header); 63 | if (value) { 64 | headers.set(header, value); 65 | } 66 | }); 67 | 68 | return new Response(upstreamResponse.body, { 69 | status: upstreamResponse.status, 70 | headers, 71 | }); 72 | } catch (error) { 73 | console.error("Error in transcription stream route:", error); 74 | return new Response("Error fetching transcription stream", { status: 500 }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/api/ragie/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | 3 | import { getRagieClientAndPartition } from "@/lib/server/ragie"; 4 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export async function POST(request: NextRequest) { 9 | try { 10 | const { tenant } = await requireAdminContextFromRequest(request); 11 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 12 | 13 | // Get the form data 14 | const formData = await request.formData(); 15 | const file = formData.get("file") as File; 16 | const metadata = JSON.parse(formData.get("metadata") as string); 17 | if (!file) { 18 | return Response.json({ error: "No file provided" }, { status: 400 }); 19 | } 20 | 21 | const mode = { 22 | static: "hi_res", 23 | audio: true, 24 | video: "audio_video", 25 | }; 26 | 27 | const res = await client.documents.create({ 28 | file: file, 29 | partition, 30 | mode, 31 | metadata, 32 | }); 33 | 34 | return Response.json(res); 35 | } catch (error) { 36 | console.error("Error uploading file:", JSON.stringify(error)); 37 | if (error instanceof Error && error.message.includes("limit for this partition")) { 38 | return Response.json({ error: error.message }, { status: 402 }); 39 | } 40 | return Response.json({ error: "Failed to upload file" }, { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/api/setup/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { z } from "zod"; 3 | 4 | import { createTenant, setCurrentProfileId } from "@/lib/server/service"; 5 | import { requireSession } from "@/lib/server/utils"; 6 | 7 | const setupSchema = z.object({ 8 | name: z.string().trim().min(1), 9 | }); 10 | 11 | export async function POST(request: NextRequest) { 12 | const session = await requireSession(); 13 | const payload = setupSchema.parse(await request.json()); 14 | const { profile, tenant } = await createTenant(session.user.id, payload.name); 15 | 16 | await setCurrentProfileId(session.user.id, profile.id); 17 | 18 | return Response.json({ profile, tenant }); 19 | } 20 | -------------------------------------------------------------------------------- /app/api/tenants/check-slug/route.ts: -------------------------------------------------------------------------------- 1 | import { and, eq, not } from "drizzle-orm"; 2 | import { NextResponse } from "next/server"; 3 | import { z } from "zod"; 4 | 5 | import db from "@/lib/server/db"; 6 | import { tenants } from "@/lib/server/db/schema"; 7 | import { requireSession } from "@/lib/server/utils"; 8 | 9 | const checkSlugSchema = z.object({ 10 | slug: z.string().min(1), 11 | tenantId: z.string().optional(), 12 | }); 13 | 14 | // returns available: boolean 15 | // true if slug does not exist in any tenant besides the tenantId passed in 16 | 17 | export async function POST(req: Request) { 18 | await requireSession(); 19 | try { 20 | const body = await req.json(); 21 | const { slug, tenantId: tenantIdToExclude } = checkSlugSchema.parse(body); 22 | 23 | // If tenantId is provided, exclude that tenant from the check 24 | // This allows us to check if the slug is unique among other tenants 25 | const query = tenantIdToExclude 26 | ? and(eq(tenants.slug, slug), not(eq(tenants.id, tenantIdToExclude))) 27 | : eq(tenants.slug, slug); 28 | 29 | const existingTenant = await db.select().from(tenants).where(query).limit(1); 30 | 31 | return NextResponse.json({ 32 | available: existingTenant.length === 0, 33 | }); 34 | } catch (error) { 35 | if (error instanceof z.ZodError) { 36 | return NextResponse.json({ error: "Invalid request body", details: error.errors }, { status: 400 }); 37 | } 38 | 39 | return NextResponse.json({ error: "Internal server error" }, { status: 500 }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/api/tenants/current/documents/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import { getRagieClientAndPartition } from "@/lib/server/ragie"; 4 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const { tenant } = await requireAdminContextFromRequest(request); 8 | const { client, partition } = await getRagieClientAndPartition(tenant.id); 9 | 10 | // Get cursor from query params 11 | const searchParams = request.nextUrl.searchParams; 12 | const cursor = searchParams.get("cursor") || undefined; 13 | 14 | try { 15 | const res = await client.documents.list({ 16 | partition, 17 | pageSize: 50, 18 | cursor, 19 | }); 20 | 21 | return NextResponse.json({ 22 | documents: res.result.documents, 23 | nextCursor: res.result.pagination.nextCursor, 24 | totalCount: res.result.pagination.totalCount, 25 | }); 26 | } catch (error) { 27 | console.error("Error fetching documents:", error); 28 | return NextResponse.json({ error: "Failed to fetch documents" }, { status: 500 }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/api/tenants/current/members/route.ts: -------------------------------------------------------------------------------- 1 | import { getMembersByTenantId } from "@/lib/server/service"; 2 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams } = new URL(request.url); 6 | const page = parseInt(searchParams.get("page") || "1"); 7 | const pageSize = parseInt(searchParams.get("pageSize") || "10"); 8 | 9 | const { tenant } = await requireAdminContextFromRequest(request); 10 | const { members, totalUsers, totalInvites } = await getMembersByTenantId(tenant.id, page, pageSize); 11 | 12 | return Response.json({ members, totalUsers, totalInvites, page, pageSize }); 13 | } 14 | -------------------------------------------------------------------------------- /app/api/tenants/current/route.ts: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm"; 2 | import { NextRequest } from "next/server"; 3 | 4 | import { updateTenantSchema } from "@/lib/api"; 5 | import { modelArraySchema } from "@/lib/llm/types"; 6 | import db from "@/lib/server/db"; 7 | import * as schema from "@/lib/server/db/schema"; 8 | import { requireAdminContextFromRequest } from "@/lib/server/utils"; 9 | 10 | export async function PATCH(request: NextRequest) { 11 | const { tenant } = await requireAdminContextFromRequest(request); 12 | 13 | const json = await request.json(); 14 | const update = updateTenantSchema.partial().parse(json); 15 | 16 | // Additional validation for enabledModels if it's being updated 17 | if (update.enabledModels !== undefined) { 18 | const modelParseResult = modelArraySchema.safeParse(update.enabledModels); 19 | if (!modelParseResult.success) { 20 | return Response.json({ error: "Invalid model array" }, { status: 400 }); 21 | } 22 | } 23 | 24 | try { 25 | await db.update(schema.tenants).set(update).where(eq(schema.tenants.id, tenant.id)); 26 | } catch (error) { 27 | console.error("Failed to update tenant:", error); 28 | return Response.json({ error: "Failed to update tenant settings" }, { status: 500 }); 29 | } 30 | 31 | return new Response(); 32 | } 33 | -------------------------------------------------------------------------------- /app/api/tenants/route.ts: -------------------------------------------------------------------------------- 1 | import { tenantListResponseSchema } from "@/lib/api"; 2 | import { getTenantsByUserId } from "@/lib/server/service"; 3 | import { requireSession } from "@/lib/server/utils"; 4 | 5 | export async function GET() { 6 | const session = await requireSession(); 7 | const tenants = await getTenantsByUserId(session.user.id); 8 | return Response.json(tenantListResponseSchema.parse(tenants)); 9 | } 10 | -------------------------------------------------------------------------------- /app/check/[slug]/route.ts: -------------------------------------------------------------------------------- 1 | import auth from "@/auth"; 2 | import { createProfile, findProfileByTenantIdAndUserId, findTenantBySlug } from "@/lib/server/service"; 3 | import getSession from "@/lib/server/session"; 4 | import { BASE_URL } from "@/lib/server/settings"; 5 | 6 | interface Params { 7 | params: Promise<{ slug: string }>; 8 | } 9 | 10 | export async function GET(request: Request, { params }: Params) { 11 | const { slug } = await params; 12 | const tenant = await findTenantBySlug(slug); 13 | 14 | if (!tenant?.isPublic) { 15 | return Response.redirect(getSignInUrl(request.url)); 16 | } 17 | 18 | const session = await getSession(); 19 | 20 | if (session) { 21 | const profile = await findProfileByTenantIdAndUserId(tenant.id, session.user.id); 22 | if (!profile) { 23 | await createProfile(tenant.id, session.user.id, "guest"); 24 | } 25 | } else { 26 | const data = await auth.api.signInAnonymous(); 27 | if (!data) { 28 | throw new Error("Could not sign in"); 29 | } 30 | const userId = data.user.id; 31 | await createProfile(tenant.id, userId, "guest"); 32 | } 33 | return Response.redirect(new URL(`/o/${slug}`, BASE_URL)); 34 | } 35 | 36 | function getSignInUrl(requestUrl: string) { 37 | const url = new URL(requestUrl); 38 | const redirectToParam = url.searchParams.get("redirectTo"); 39 | 40 | const signInUrl = new URL("/sign-in", BASE_URL); 41 | if (redirectToParam) { 42 | signInUrl.searchParams.set("redirectTo", redirectToParam); 43 | } 44 | return signInUrl; 45 | } 46 | -------------------------------------------------------------------------------- /app/empty/empty-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | export default function EmptyForm() { 6 | const router = useRouter(); 7 | 8 | return ( 9 |
10 |

11 | You don't have a chatbot right now. 12 |
13 | Let's get you started on your next one. 14 |

15 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/empty/page.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | 3 | import { requireSession } from "@/lib/server/utils"; 4 | 5 | import EmptyForm from "./empty-form"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export default async function EmptyPage() { 10 | const session = await requireSession(); 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ragieai/basechat/30b30f2ffff5a5cb736945cebf8f913171db6874/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ragieai/basechat/30b30f2ffff5a5cb736945cebf8f913171db6874/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ragieai/basechat/30b30f2ffff5a5cb736945cebf8f913171db6874/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/healthz/route.ts: -------------------------------------------------------------------------------- 1 | export function GET() { 2 | return Response.json({ status: "ok" }); 3 | } 4 | -------------------------------------------------------------------------------- /app/invites/accept/page.tsx: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { redirect } from "next/navigation"; 4 | 5 | import { acceptInvite, findInviteById, setCurrentProfileId } from "@/lib/server/service"; 6 | import { requireSession } from "@/lib/server/utils"; 7 | 8 | interface Props { 9 | searchParams: Promise<{ invite?: string }>; 10 | } 11 | 12 | export default async function AcceptInvitePage({ searchParams }: Props) { 13 | const session = await requireSession(); 14 | const params = await searchParams; 15 | 16 | assert(params.invite, "Bad request"); 17 | 18 | const invite = await findInviteById(params.invite); 19 | if (!invite) { 20 | return ( 21 |
22 |

Could not find invite.

23 |
24 | ); 25 | } 26 | 27 | try { 28 | const profile = await acceptInvite(session.user.id, params.invite); 29 | await setCurrentProfileId(session.user.id, profile.id); 30 | redirect("/"); 31 | } catch (e) { 32 | // NEXT_REDIRECT is thrown by redirect("/") and should be rethrown 33 | if (e instanceof Error && e.message === "NEXT_REDIRECT") { 34 | throw e; 35 | } else { 36 | // some other error occurred 37 | console.error(e); 38 | } 39 | } 40 | 41 | return ( 42 |
43 |

Could not process invite.

44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "sonner"; 5 | 6 | import { GaTags } from "@/components/ga-tags"; 7 | 8 | import { QueryClientProvider } from "../lib/query-client-provider"; 9 | 10 | import { GlobalStateProvider } from "./(main)/o/[slug]/context"; 11 | 12 | const inter = Inter({ subsets: ["latin"] }); 13 | 14 | export const dynamic = "force-dynamic"; 15 | 16 | export const metadata: Metadata = { 17 | title: "Base Chat", 18 | description: "Base Chat powered by Ragie", 19 | }; 20 | 21 | export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { 22 | return ( 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/route.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { getSignUpPath, getStartPath } from "@/lib/paths"; 4 | import { findUserById } from "@/lib/server/service"; 5 | import getSession from "@/lib/server/session"; 6 | 7 | export async function GET() { 8 | const session = await getSession(); 9 | 10 | if (session?.user.id) { 11 | const user = await findUserById(session.user.id); 12 | if (user?.isAnonymous) { 13 | return redirect(getSignUpPath()); 14 | } 15 | } 16 | return redirect(getStartPath()); 17 | } 18 | -------------------------------------------------------------------------------- /app/setup/page.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | 3 | import { requireSession } from "@/lib/server/utils"; 4 | 5 | import SetupForm from "./setup-form"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export default async function SetupPage() { 10 | const session = await requireSession(); 11 | 12 | return ( 13 |
14 |
15 |

Welcome, {session.user.name}!

16 |

Just a few more details below.

17 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/setup/setup-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useRouter } from "next/navigation"; 5 | import { useState } from "react"; 6 | import { useForm } from "react-hook-form"; 7 | import { z } from "zod"; 8 | 9 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; 10 | import { Input } from "@/components/ui/input"; 11 | import { setupSchema } from "@/lib/api"; 12 | import { getTenantPath } from "@/lib/paths"; 13 | 14 | const formSchema = z.object({ 15 | name: z.string().trim().min(1, "Name is required"), 16 | }); 17 | 18 | export default function SetupForm() { 19 | const form = useForm>({ 20 | resolver: zodResolver(formSchema), 21 | defaultValues: { name: "" }, 22 | }); 23 | 24 | const router = useRouter(); 25 | 26 | const [failureMessage, setFailureMessage] = useState(null); 27 | 28 | async function onSubmit(values: z.infer) { 29 | setFailureMessage(null); 30 | 31 | const res = await fetch("/api/setup", { method: "POST", body: JSON.stringify(values) }); 32 | if (res.status < 200 || res.status >= 300) { 33 | setFailureMessage("An unexpected error occurred. Could not finish setup."); 34 | return; 35 | } 36 | 37 | const data = await res.json(); 38 | const { tenant } = setupSchema.parse(data); 39 | router.push(getTenantPath(tenant.slug)); 40 | } 41 | 42 | return ( 43 |
44 | {failureMessage &&
{failureMessage}
} 45 | 46 | ( 50 | 51 | Company name 52 | 53 | 59 | 60 | 61 | 62 | )} 63 | > 64 | 70 |
71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /app/start/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { getSetupPath, getTenantPath } from "@/lib/paths"; 4 | import { getFirstTenantByUserId } from "@/lib/server/service"; 5 | import { requireSession } from "@/lib/server/utils"; 6 | 7 | export default async function StartPage() { 8 | const session = await requireSession(); 9 | const tenant = await getFirstTenantByUserId(session.user.id); 10 | 11 | if (!tenant) { 12 | redirect(getSetupPath()); 13 | } else { 14 | redirect(getTenantPath(tenant.slug)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import { betterAuth } from "better-auth"; 2 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 3 | import { nextCookies } from "better-auth/next-js"; 4 | import { anonymous } from "better-auth/plugins"; 5 | import { eq } from "drizzle-orm"; 6 | 7 | import db from "@/lib/server/db"; 8 | import * as schema from "@/lib/server/db/schema"; 9 | import * as settings from "@/lib/server/settings"; 10 | 11 | import { linkUsers, sendResetPasswordEmail } from "./lib/server/service"; 12 | import { hashPassword, verifyPassword } from "./lib/server/utils"; 13 | 14 | const socialProviders: Record = {}; 15 | 16 | if (settings.AUTH_GOOGLE_ID && settings.AUTH_GOOGLE_SECRET) { 17 | socialProviders.google = { 18 | clientId: settings.AUTH_GOOGLE_ID, 19 | clientSecret: settings.AUTH_GOOGLE_SECRET, 20 | }; 21 | } 22 | 23 | export const auth = betterAuth({ 24 | database: drizzleAdapter(db, { 25 | provider: "pg", 26 | schema, 27 | usePlural: true, 28 | }), 29 | advanced: { generateId: false }, 30 | socialProviders, 31 | emailAndPassword: { 32 | enabled: true, 33 | minPasswordLength: 6, 34 | sendResetPassword: ({ user, url, token }) => sendResetPasswordEmail(user, url, token), 35 | resetPasswordTokenExpiresIn: 36000, // seconds 36 | password: { 37 | hash: (password) => hashPassword(password), 38 | verify: ({ hash, password }) => verifyPassword(hash, password), 39 | }, 40 | }, 41 | plugins: [ 42 | anonymous({ 43 | emailDomainName: "example.com", 44 | onLinkAccount: async ({ anonymousUser, newUser }) => { 45 | await linkUsers(anonymousUser.user.id, newUser.user.id); 46 | }, 47 | }), 48 | nextCookies(), // This must be the last plugin 49 | ], 50 | }); 51 | 52 | export default auth; 53 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks", 19 | "public": "@/public" 20 | }, 21 | "iconLibrary": "lucide" 22 | } 23 | -------------------------------------------------------------------------------- /components/chatbot/style.css: -------------------------------------------------------------------------------- 1 | .dot-pulse { 2 | position: relative; 3 | left: -9999px; 4 | width: 10px; 5 | height: 10px; 6 | border-radius: 5px; 7 | background-color: #212020; 8 | color: #212020; 9 | box-shadow: 9999px 0 0 -5px; 10 | animation: dot-pulse 1.5s infinite linear; 11 | animation-delay: 0.25s; 12 | } 13 | 14 | .dot-pulse::before, 15 | .dot-pulse::after { 16 | content: ""; 17 | display: inline-block; 18 | position: absolute; 19 | top: 0; 20 | width: 10px; 21 | height: 10px; 22 | border-radius: 5px; 23 | background-color: #aeabab; 24 | color: #aeabab; 25 | } 26 | 27 | .dot-pulse::before { 28 | box-shadow: 9984px 0 0 -5px; 29 | animation: dot-pulse-before 1.5s infinite linear; 30 | animation-delay: 0s; 31 | } 32 | 33 | .dot-pulse::after { 34 | box-shadow: 10014px 0 0 -5px; 35 | animation: dot-pulse-after 1.5s infinite linear; 36 | animation-delay: 0.5s; 37 | } 38 | 39 | @keyframes dot-pulse-before { 40 | 0% { 41 | box-shadow: 9984px 0 0 -5px; 42 | } 43 | 44 | 30% { 45 | box-shadow: 9984px 0 0 2px; 46 | } 47 | 48 | 60%, 49 | 100% { 50 | box-shadow: 9984px 0 0 -5px; 51 | } 52 | } 53 | 54 | @keyframes dot-pulse { 55 | 0% { 56 | box-shadow: 9999px 0 0 -5px; 57 | } 58 | 59 | 30% { 60 | box-shadow: 9999px 0 0 2px; 61 | } 62 | 63 | 60%, 64 | 100% { 65 | box-shadow: 9999px 0 0 -5px; 66 | } 67 | } 68 | 69 | @keyframes dot-pulse-after { 70 | 0% { 71 | box-shadow: 10014px 0 0 -5px; 72 | } 73 | 74 | 30% { 75 | box-shadow: 10014px 0 0 2px; 76 | } 77 | 78 | 60%, 79 | 100% { 80 | box-shadow: 10014px 0 0 -5px; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /components/chatbot/types.ts: -------------------------------------------------------------------------------- 1 | export type SourceMetadata = { 2 | source_type: string; 3 | file_path: string; 4 | source_url: string; 5 | documentId: string; 6 | documentName: string; 7 | streamUrl?: string; 8 | downloadUrl?: string; 9 | documentStreamUrl?: string; 10 | startTime?: number; 11 | endTime?: number; 12 | }; 13 | -------------------------------------------------------------------------------- /components/ga-tags.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Script from "next/script"; 4 | 5 | export const GaTags = ({ gaKey }: { gaKey?: string }) => { 6 | if (!gaKey) return null; 7 | return ( 8 | <> 9 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /components/primary-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from "./ui/button"; 2 | 3 | interface PrimaryButtonProps extends ButtonProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function PrimaryButton({ children, ...props }: PrimaryButtonProps) { 8 | return ( 9 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/tenant/logo/confirm-delete-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useActionState, useEffect } from "react"; 4 | import { toast } from "sonner"; 5 | 6 | import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 7 | import { FormCancelButton } from "@/components/ui/form-cancel-button"; 8 | import { SubmitButton } from "@/components/ui/submit-button"; 9 | 10 | import { deleteLogo } from "./server-actions"; 11 | 12 | interface Props { 13 | onCancel: () => void; 14 | onSuccess: () => void; 15 | tenant: { 16 | slug: string; 17 | }; 18 | } 19 | 20 | export function ConfirmDeleteLogoDialog({ onCancel, onSuccess, tenant }: Props) { 21 | const [state, formAction] = useActionState(deleteLogo, { 22 | status: "pending" as const, 23 | }); 24 | 25 | useEffect(() => { 26 | if (state.status === "success") { 27 | onSuccess?.(); 28 | } else if (state.status === "error") { 29 | toast.error("Unable to delete logo"); 30 | } 31 | }, [state, onSuccess]); 32 | 33 | return ( 34 | { 37 | if (!open) { 38 | onCancel?.(); 39 | } 40 | }} 41 | > 42 | 43 | 44 | Delete logo 45 | 46 |
Are you sure you want to delete the logo?
47 |
48 | 49 | 50 | Cancel 51 | 52 | Delete 53 | 54 | 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/tenant/logo/logo-changer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useState } from "react"; 4 | import { toast } from "sonner"; 5 | 6 | import { ConfirmDeleteLogoDialog } from "./confirm-delete-dialog"; 7 | import CreateLogoDialog, { OnSuccessEvent } from "./create-logo-dialog"; 8 | import UploadableLogo, { FileCreateEvent, FileDeleteEvent } from "./uploadable-logo"; 9 | 10 | interface Props { 11 | tenant: { 12 | name: string; 13 | slug: string; 14 | logoName?: string | null; 15 | logoUrl?: string | null; 16 | }; 17 | } 18 | 19 | export default function LogoChanger({ tenant }: Props) { 20 | const [confirmDelete, setConfirmDelete] = useState(false); 21 | const [image, setImage] = useState(null); 22 | const [logoUrl, setLogoUrl] = useState(tenant.logoUrl); 23 | const [logoName, setLogoName] = useState(tenant.logoName); 24 | 25 | const onLogoChange = useCallback((event: FileCreateEvent | FileDeleteEvent) => { 26 | if (event.action === "create") { 27 | setImage(event.data); 28 | setLogoName(event.fileName); 29 | return; 30 | } 31 | 32 | if (event.action === "delete") { 33 | setConfirmDelete(true); 34 | } 35 | }, []); 36 | 37 | const onSetLogoCancel = () => setImage(null); 38 | const onSetLogoSuccess = ({ url, fileName }: OnSuccessEvent) => { 39 | toast.success("Logo updated"); 40 | setLogoUrl(url); 41 | setLogoName(fileName); 42 | setImage(null); 43 | }; 44 | 45 | const onDeleteLogoSuccess = () => { 46 | setConfirmDelete(false); 47 | setImage(null); 48 | setLogoUrl(null); 49 | toast.success("Logo successfully deleted"); 50 | }; 51 | 52 | return ( 53 | <> 54 | 55 | {image && ( 56 | 63 | )} 64 | {confirmDelete && ( 65 | setConfirmDelete(false)} 68 | onSuccess={() => onDeleteLogoSuccess()} 69 | /> 70 | )} 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/tenant/logo/logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { getInitials, getAvatarNumber } from "@/lib/utils"; 5 | 6 | interface Props { 7 | name?: string | null; 8 | url?: string | null; 9 | className?: string; 10 | width: number; 11 | height: number; 12 | initialCount?: number; 13 | tenantId?: string; 14 | } 15 | 16 | export default function Logo({ name, url, width, height, className, initialCount = 2, tenantId }: Props) { 17 | const formattedName = name ? getInitials(name, initialCount) : ""; 18 | const avatarClass = tenantId ? `avatar-${getAvatarNumber(tenantId)}` : ""; 19 | 20 | if (!url) { 21 | return ( 22 |
30 | {formattedName} 31 |
32 | ); 33 | } 34 | 35 | // These images could come from any source, would need additional set up per external resource 36 | // Just use for now. 37 | // https://nextjs.org/docs/pages/api-reference/components/image#remotepatterns 38 | // eslint-disable-next-line @next/next/no-img-element 39 | return {formattedName}; 40 | } 41 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { LoaderCircle } from "lucide-react"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 10 | { 11 | variants: { 12 | variant: { 13 | default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 15 | outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 16 | secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 17 | ghost: "hover:bg-accent hover:text-accent-foreground", 18 | link: "text-primary underline-offset-4 hover:underline", 19 | }, 20 | size: { 21 | default: "h-9 px-4 py-2", 22 | sm: "h-8 rounded-md px-3 text-xs", 23 | lg: "h-10 rounded-md px-8", 24 | icon: "h-9 w-9", 25 | }, 26 | }, 27 | defaultVariants: { 28 | variant: "default", 29 | size: "default", 30 | }, 31 | }, 32 | ); 33 | 34 | export interface ButtonProps 35 | extends React.ButtonHTMLAttributes, 36 | VariantProps { 37 | asChild?: boolean; 38 | loading?: boolean; 39 | } 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, children, asChild = false, loading, ...props }, ref) => { 43 | const Comp = asChild ? Slot : "button"; 44 | return ( 45 | 46 | {loading ? : children} 47 | 48 | ); 49 | }, 50 | ); 51 | Button.displayName = "Button"; 52 | 53 | export { Button, buttonVariants }; 54 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 | import { Check } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 22 | 23 | 24 | 25 | )); 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 | 28 | export { Checkbox }; 29 | -------------------------------------------------------------------------------- /components/ui/form-cancel-button.tsx: -------------------------------------------------------------------------------- 1 | import { useFormStatus } from "react-dom"; 2 | 3 | import { Button, ButtonProps } from "./button"; 4 | 5 | export function FormCancelButton({ children, disabled, ...props }: ButtonProps) { 6 | const { pending } = useFormStatus(); 7 | 8 | return ( 9 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & VariantProps 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )); 17 | Label.displayName = LabelPrimitive.Root.displayName; 18 | 19 | export { Label }; 20 | -------------------------------------------------------------------------------- /components/ui/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 "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor; 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )); 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 34 | -------------------------------------------------------------------------------- /components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; 4 | import { Circle } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => { 13 | return ; 14 | }); 15 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 16 | 17 | const RadioGroupItem = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => { 21 | return ( 22 | 30 | 31 | 32 | 33 | 34 | ); 35 | }); 36 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 37 | 38 | export { RadioGroup, RadioGroupItem }; 39 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
; 5 | } 6 | 7 | export { Skeleton }; 8 | -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SliderPrimitive from "@radix-ui/react-slider"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef & { 11 | colorClassName?: string; 12 | heightClassName?: string; 13 | widthClassName?: string; 14 | } 15 | >( 16 | ( 17 | { className, colorClassName = "bg-primary", heightClassName = "h-5", widthClassName = "w-[200px]", ...props }, 18 | ref, 19 | ) => ( 20 | 25 | 26 | 27 | 28 | 29 | 30 | ), 31 | ); 32 | 33 | Slider.displayName = SliderPrimitive.Root.displayName; 34 | 35 | export { Slider }; 36 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 26 | ); 27 | }; 28 | 29 | export { Toaster }; 30 | -------------------------------------------------------------------------------- /components/ui/submit-button.tsx: -------------------------------------------------------------------------------- 1 | import { useFormStatus } from "react-dom"; 2 | 3 | import { Button, ButtonProps } from "./button"; 4 | 5 | export function SubmitButton({ children, disabled, pendingText, ...props }: ButtonProps & { pendingText?: string }) { 6 | const { pending } = useFormStatus(); 7 | 8 | return ( 9 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/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 | 19 | 28 | 29 | )); 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 33 | -------------------------------------------------------------------------------- /components/use-polling.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | interface UsePollingOptions { 4 | pollFunction: () => Promise; // Function to execute periodically 5 | interval: number; // Polling interval in milliseconds 6 | onSuccess?: (data: T) => void; // Callback for successful responses 7 | onError?: (error: unknown) => void; // Callback for errors 8 | } 9 | 10 | export default function usePolling(options: UsePollingOptions) { 11 | const { pollFunction, interval, onSuccess, onError } = options; 12 | const [data, setData] = useState(null); 13 | const [error, setError] = useState(null); 14 | const [loading, setLoading] = useState(false); 15 | 16 | useEffect(() => { 17 | let isMounted = true; 18 | let timeoutId: ReturnType; 19 | 20 | const executeFunction = async () => { 21 | setLoading(true); 22 | try { 23 | const result = await pollFunction(); 24 | if (isMounted) { 25 | setData(result); 26 | setError(null); 27 | if (onSuccess) onSuccess(result); 28 | } 29 | } catch (err) { 30 | if (isMounted) { 31 | setError(err); 32 | if (onError) onError(err); 33 | } 34 | } finally { 35 | if (isMounted) { 36 | setLoading(false); 37 | timeoutId = setTimeout(executeFunction, interval); // Schedule the next tick 38 | } 39 | } 40 | }; 41 | 42 | // Start polling 43 | executeFunction(); 44 | 45 | // Cleanup 46 | return () => { 47 | isMounted = false; 48 | clearTimeout(timeoutId); 49 | }; 50 | }, [pollFunction, interval, onSuccess, onError]); 51 | 52 | return { data, error, loading }; 53 | } 54 | -------------------------------------------------------------------------------- /components/warning-message.tsx: -------------------------------------------------------------------------------- 1 | export default function WarningMessage({ 2 | children, 3 | className = "", 4 | }: { 5 | children: React.ReactNode; 6 | className?: string; 7 | }) { 8 | return ( 9 |
10 |

{children}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { defineConfig } from "drizzle-kit"; 4 | 5 | import "dotenv"; 6 | 7 | assert(process.env.DATABASE_URL); 8 | const DATABASE_URL = process.env.DATABASE_URL; 9 | 10 | export default defineConfig({ 11 | dialect: "postgresql", 12 | schema: "./lib/server/db/schema.ts", 13 | dbCredentials: { url: DATABASE_URL }, 14 | }); 15 | -------------------------------------------------------------------------------- /drizzle/0001_overrated_nick_fury.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "question1" text;--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD COLUMN "question2" text;--> statement-breakpoint 3 | ALTER TABLE "tenants" ADD COLUMN "question3" text; -------------------------------------------------------------------------------- /drizzle/0002_brainy_thing.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "profiles" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "created_at" timestamp with time zone DEFAULT now() NOT NULL, 4 | "updated_at" timestamp with time zone DEFAULT now() NOT NULL, 5 | "tenant_id" uuid NOT NULL, 6 | "user_id" uuid NOT NULL 7 | ); 8 | --> statement-breakpoint 9 | DO $$ BEGIN 10 | ALTER TABLE "profiles" ADD CONSTRAINT "profiles_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; 11 | EXCEPTION 12 | WHEN duplicate_object THEN null; 13 | END $$; 14 | --> statement-breakpoint 15 | DO $$ BEGIN 16 | ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; 17 | EXCEPTION 18 | WHEN duplicate_object THEN null; 19 | END $$; 20 | -------------------------------------------------------------------------------- /drizzle/0003_plain_taskmaster.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "invites" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "created_at" timestamp with time zone DEFAULT now() NOT NULL, 4 | "updated_at" timestamp with time zone DEFAULT now() NOT NULL, 5 | "tenant_id" uuid NOT NULL, 6 | "invited_by_id" uuid NOT NULL, 7 | "email" text 8 | ); 9 | --> statement-breakpoint 10 | DO $$ BEGIN 11 | ALTER TABLE "invites" ADD CONSTRAINT "invites_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; 12 | EXCEPTION 13 | WHEN duplicate_object THEN null; 14 | END $$; 15 | --> statement-breakpoint 16 | DO $$ BEGIN 17 | ALTER TABLE "invites" ADD CONSTRAINT "invites_invited_by_id_profiles_id_fk" FOREIGN KEY ("invited_by_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action; 18 | EXCEPTION 19 | WHEN duplicate_object THEN null; 20 | END $$; 21 | -------------------------------------------------------------------------------- /drizzle/0004_even_the_leader.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invites" ADD CONSTRAINT "invites_tenant_id_email_unique" UNIQUE("tenant_id","email"); -------------------------------------------------------------------------------- /drizzle/0005_wooden_black_bird.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "profiles" ADD CONSTRAINT "profiles_tenant_id_user_id_unique" UNIQUE("tenant_id","user_id"); -------------------------------------------------------------------------------- /drizzle/0006_classy_leopardon.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invites" ALTER COLUMN "email" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/0007_greedy_squadron_sinister.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "current_profile_id" uuid; -------------------------------------------------------------------------------- /drizzle/0008_late_klaw.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "conversations" ADD COLUMN "profile_id" uuid NOT NULL;--> statement-breakpoint 2 | DO $$ BEGIN 3 | ALTER TABLE "conversations" ADD CONSTRAINT "conversations_profile_id_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action; 4 | EXCEPTION 5 | WHEN duplicate_object THEN null; 6 | END $$; 7 | -------------------------------------------------------------------------------- /drizzle/0009_sparkling_odin.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | ALTER TABLE "users" ADD CONSTRAINT "users_current_profile_id_profiles_id_fk" FOREIGN KEY ("current_profile_id") REFERENCES "public"."profiles"("id") ON DELETE set null ON UPDATE no action; 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | -------------------------------------------------------------------------------- /drizzle/0010_messy_martin_li.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "password" text; -------------------------------------------------------------------------------- /drizzle/0011_flat_husk.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE "public"."roles" RENAME TO "message_roles"; -------------------------------------------------------------------------------- /drizzle/0012_mature_anita_blake.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."roles" AS ENUM('admin', 'user');--> statement-breakpoint 2 | ALTER TABLE "profiles" ADD COLUMN "role" "roles" DEFAULT 'admin'; -------------------------------------------------------------------------------- /drizzle/0013_yellow_agent_zero.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "profiles" ALTER COLUMN "role" DROP DEFAULT;--> statement-breakpoint 2 | ALTER TABLE "profiles" ALTER COLUMN "role" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/0014_mighty_sersi.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invites" ADD COLUMN "role" "roles" DEFAULT 'admin';--> statement-breakpoint 2 | ALTER TABLE "public"."invites" ALTER COLUMN "role" SET DATA TYPE text;--> statement-breakpoint 3 | ALTER TABLE "public"."invites" ALTER COLUMN "role" SET DATA TYPE "public"."roles" USING "role"::"public"."roles";--> statement-breakpoint 4 | -------------------------------------------------------------------------------- /drizzle/0015_left_lady_mastermind.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "invites" ALTER COLUMN "role" DROP DEFAULT;--> statement-breakpoint 2 | ALTER TABLE "invites" ALTER COLUMN "role" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/0016_loving_captain_universe.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" DROP CONSTRAINT "tenants_owner_id_users_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "tenants" DROP COLUMN IF EXISTS "owner_id"; -------------------------------------------------------------------------------- /drizzle/0017_concerned_viper.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "grounding_prompt" text;--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD COLUMN "system_prompt" text;--> statement-breakpoint -------------------------------------------------------------------------------- /drizzle/0018_sturdy_captain_america.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "logo_file_name" text;--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD COLUMN "logo_object_name" text;--> statement-breakpoint 3 | ALTER TABLE "tenants" ADD COLUMN "logo_url" text; -------------------------------------------------------------------------------- /drizzle/0019_lively_tarantula.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "slug" text;--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD CONSTRAINT "tenants_slug_unique" UNIQUE("slug"); 3 | UPDATE "tenants" SET "slug" = "id"; -------------------------------------------------------------------------------- /drizzle/0020_perfect_synch.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ALTER COLUMN "slug" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/0021_organic_madame_hydra.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "is_public" boolean DEFAULT false NOT NULL; -------------------------------------------------------------------------------- /drizzle/0022_dapper_squadron_sinister.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE "public"."roles" ADD VALUE 'guest'; -------------------------------------------------------------------------------- /drizzle/0023_majestic_veda.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "is_anonymous" boolean DEFAULT false NOT NULL; -------------------------------------------------------------------------------- /drizzle/0024_wandering_speed_demon.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "messages" ADD COLUMN "model" text DEFAULT 'gpt-4o' NOT NULL; -------------------------------------------------------------------------------- /drizzle/0025_organic_thunderbird.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "messages" ADD COLUMN "is_breadth" boolean DEFAULT false NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "messages" ADD COLUMN "rerank_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint 3 | ALTER TABLE "messages" ADD COLUMN "prioritize_recent" boolean DEFAULT false NOT NULL; -------------------------------------------------------------------------------- /drizzle/0026_colorful_the_executioner.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "messages" ALTER COLUMN "model" SET DEFAULT 'claude-3-7-sonnet-latest'; -------------------------------------------------------------------------------- /drizzle/0027_lowly_tana_nile.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "welcome_message" text; -------------------------------------------------------------------------------- /drizzle/0028_petite_killmonger.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "conversations_profile_idx" ON "conversations" USING btree ("profile_id");--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "conversations_tenant_profile_idx" ON "conversations" USING btree ("tenant_id","profile_id");--> statement-breakpoint 3 | CREATE INDEX IF NOT EXISTS "messages_conversation_idx" ON "messages" USING btree ("conversation_id");--> statement-breakpoint 4 | CREATE INDEX IF NOT EXISTS "messages_tenant_conversation_idx" ON "messages" USING btree ("tenant_id","conversation_id");--> statement-breakpoint 5 | CREATE INDEX IF NOT EXISTS "profiles_role_idx" ON "profiles" USING btree ("role"); -------------------------------------------------------------------------------- /drizzle/0029_icy_james_howlett.sql: -------------------------------------------------------------------------------- 1 | -- Verifications 2 | CREATE TABLE IF NOT EXISTS "verifications" ( 3 | "created_at" timestamp with time zone DEFAULT now() NOT NULL, 4 | "updated_at" timestamp with time zone DEFAULT now() NOT NULL, 5 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 6 | "identifier" text NOT NULL, 7 | "value" text NOT NULL, 8 | "expires_at" timestamp NOT NULL 9 | ); 10 | 11 | --> statement-breakpoint 12 | DROP TABLE "verification_tokens" CASCADE;--> statement-breakpoint 13 | ALTER TABLE "accounts" DROP COLUMN IF EXISTS "type";--> statement-breakpoint 14 | ALTER TABLE "accounts" DROP COLUMN IF EXISTS "token_type";--> statement-breakpoint 15 | ALTER TABLE "accounts" DROP COLUMN IF EXISTS "session_state"; 16 | 17 | 18 | -- Sessions 19 | ALTER TABLE "sessions" RENAME COLUMN "expires" TO "expires_at";--> statement-breakpoint 20 | ALTER TABLE "sessions" DROP CONSTRAINT "sessions_pkey";--> statement-breakpoint 21 | ALTER TABLE "sessions" ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL;--> statement-breakpoint 22 | ALTER TABLE "sessions" ADD COLUMN "ip_address" text;--> statement-breakpoint 23 | ALTER TABLE "sessions" ADD COLUMN "user_agent" text;--> statement-breakpoint 24 | ALTER TABLE "sessions" RENAME COLUMN "session_token" TO "token";--> statement-breakpoint 25 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_token_unique" UNIQUE("token"); 26 | 27 | 28 | -- Accounts 29 | ALTER TABLE "accounts" RENAME COLUMN "provider" TO "provider_id";--> statement-breakpoint 30 | ALTER TABLE "accounts" RENAME COLUMN "provider_account_id" TO "account_id";--> statement-breakpoint 31 | ALTER TABLE "accounts" RENAME COLUMN "expires_at" TO "access_token_expires_at";--> statement-breakpoint 32 | ALTER TABLE "accounts" ADD COLUMN "refresh_token_expires_at" timestamp;--> statement-breakpoint 33 | ALTER TABLE "accounts" ADD COLUMN "password" text; 34 | ALTER TABLE "accounts" ADD COLUMN _access_token_expires_at timestamp; 35 | UPDATE "accounts" SET _access_token_expires_at = TO_TIMESTAMP(access_token_expires_at); 36 | ALTER TABLE "accounts" DROP COLUMN access_token_expires_at; 37 | ALTER TABLE "accounts" RENAME COLUMN _access_token_expires_at TO access_token_expires_at; 38 | INSERT INTO "accounts" (user_id, provider_id, account_id, password) 39 | SELECT 40 | id, 41 | 'credential', 42 | id, 43 | password 44 | FROM "users" 45 | WHERE password IS NOT NULL; 46 | ALTER TABLE "users" DROP COLUMN IF EXISTS "password"; 47 | 48 | 49 | -- Users 50 | ALTER TABLE "users" ADD COLUMN _email_verified boolean NOT NULL DEFAULT false; 51 | UPDATE "users" SET _email_verified = (email_verified is not null); 52 | ALTER TABLE "users" DROP COLUMN email_verified; 53 | ALTER TABLE "users" RENAME COLUMN _email_verified TO email_verified; 54 | -------------------------------------------------------------------------------- /drizzle/0030_legal_namor.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "enabled_models" text[] DEFAULT '{"gpt-4o","gpt-3.5-turbo","gemini-2.0-flash","gemini-1.5-pro","claude-3-7-sonnet-latest","claude-3-5-haiku-latest"}'; -------------------------------------------------------------------------------- /drizzle/0031_tricky_blue_marvel.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "search_settings" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "created_at" timestamp with time zone DEFAULT now() NOT NULL, 4 | "updated_at" timestamp with time zone DEFAULT now() NOT NULL, 5 | "tenant_id" uuid NOT NULL, 6 | "is_breadth" boolean DEFAULT false NOT NULL, 7 | "rerank_enabled" boolean DEFAULT false NOT NULL, 8 | "prioritize_recent" boolean DEFAULT false NOT NULL, 9 | "override_breadth" boolean DEFAULT true NOT NULL, 10 | "override_rerank" boolean DEFAULT true NOT NULL, 11 | "override_prioritize_recent" boolean DEFAULT true NOT NULL, 12 | CONSTRAINT "search_settings_tenant_id_unique" UNIQUE("tenant_id") 13 | ); 14 | --> statement-breakpoint 15 | ALTER TABLE "tenants" ADD COLUMN "default_model" text DEFAULT 'claude-3-7-sonnet-latest';--> statement-breakpoint 16 | DO $$ BEGIN 17 | ALTER TABLE "search_settings" ADD CONSTRAINT "search_settings_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; 18 | EXCEPTION 19 | WHEN duplicate_object THEN null; 20 | END $$; 21 | -------------------------------------------------------------------------------- /drizzle/0032_dusty_morg.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ALTER COLUMN "enabled_models" SET DEFAULT '{"gpt-4o","gpt-3.5-turbo","gemini-2.0-flash","gemini-1.5-pro","claude-3-7-sonnet-latest","claude-3-5-haiku-latest","meta-llama/llama-4-scout-17b-16e-instruct"}'; -------------------------------------------------------------------------------- /drizzle/0033_fresh_baron_zemo.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "search_settings" CASCADE;--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD COLUMN "is_breadth" boolean DEFAULT false;--> statement-breakpoint 3 | ALTER TABLE "tenants" ADD COLUMN "rerank_enabled" boolean DEFAULT false;--> statement-breakpoint 4 | ALTER TABLE "tenants" ADD COLUMN "prioritize_recent" boolean DEFAULT false;--> statement-breakpoint 5 | ALTER TABLE "tenants" ADD COLUMN "override_breadth" boolean DEFAULT true;--> statement-breakpoint 6 | ALTER TABLE "tenants" ADD COLUMN "override_rerank" boolean DEFAULT true;--> statement-breakpoint 7 | ALTER TABLE "tenants" ADD COLUMN "override_prioritize_recent" boolean DEFAULT true; -------------------------------------------------------------------------------- /drizzle/0034_abandoned_mindworm.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "ragie_api_key" text;--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD COLUMN "ragie_partition" text; -------------------------------------------------------------------------------- /drizzle/0035_chilly_warpath.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "connections" ADD COLUMN "last_synced_at" timestamp with time zone; -------------------------------------------------------------------------------- /drizzle/0036_amused_rhino.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "connections" ADD COLUMN "addedBy" text; -------------------------------------------------------------------------------- /drizzle/0037_icy_excalibur.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."paid_status" AS ENUM('trial', 'active', 'expired', 'legacy');--> statement-breakpoint 2 | ALTER TABLE "tenants" ADD COLUMN "trial_expires_at" timestamp with time zone DEFAULT '2025-06-05T12:00:00.000Z' NOT NULL;--> statement-breakpoint 3 | ALTER TABLE "tenants" ADD COLUMN "paid_status" "paid_status" DEFAULT 'legacy' NOT NULL;--> statement-breakpoint 4 | CREATE INDEX IF NOT EXISTS "tenants_paid_status_idx" ON "tenants" USING btree ("paid_status"); -------------------------------------------------------------------------------- /drizzle/0038_smooth_darkstar.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ALTER COLUMN "trial_expires_at" DROP DEFAULT;--> statement-breakpoint 2 | ALTER TABLE "tenants" ALTER COLUMN "paid_status" SET DEFAULT 'trial'; -------------------------------------------------------------------------------- /drizzle/0039_worthless_wendigo.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "tenants" ADD COLUMN "partition_limit_exceeded_at" timestamp with time zone; -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # The public base url of this application 2 | BASE_URL=http://localhost:3000 3 | 4 | # The postgres database url 5 | DATABASE_URL=postgresql://localhost:5432/basechat 6 | 7 | # Your Ragie API key. You can get one by creating an account at https://ragie.ai 8 | RAGIE_API_KEY= 9 | 10 | RAGIE_API_BASE_URL=https://api.ragie.ai 11 | 12 | # Your Ragie webhook secret. You can get this by creating a webhook endpoint in your Ragie account. 13 | RAGIE_WEBHOOK_SECRET= 14 | 15 | # Your OpenAI API key 16 | OPENAI_API_KEY= 17 | 18 | # Your Google API key 19 | GOOGLE_GENERATIVE_AI_API_KEY= 20 | 21 | # Your Anthropic API key 22 | ANTHROPIC_API_KEY= 23 | 24 | # Your Groq API key 25 | GROQ_API_KEY= 26 | 27 | # Auth.js config 28 | AUTH_DRIZZLE_URL=postgresql://localhost:5432/basechat 29 | 30 | # https://www.better-auth.com/docs/installation 31 | BETTER_AUTH_SECRET= 32 | BETTER_AUTH_URL=http://localhost:3000 33 | 34 | # Configure auth provider secrets. Learn more by reading the documentation at https://authjs.dev/ 35 | AUTH_GOOGLE_ID= 36 | AUTH_GOOGLE_SECRET= 37 | 38 | # SMTP settings - These are default development settings for MailHog (https://github.com/mailhog/MailHog) 39 | SMTP_FROM=noreply@example.com 40 | SMTP_HOST=localhost 41 | SMTP_PORT=1025 42 | SMTP_SECURE=0 43 | SMTP_USER= # Not required 44 | SMTP_PASSWORD= # Not required 45 | 46 | # Your encryption key 47 | # Generate with: openssl rand -hex 32 48 | ENCRYPTION_KEY= 49 | 50 | # Whether to enable billing 51 | BILLING_ENABLED=false 52 | 53 | # The default pages processed limit for tenants 54 | DEFAULT_PARTITION_LIMIT= 55 | 56 | # Bucket Storage settings. Used for Logo 57 | # If STORAGE_ENDPOINT is empty, the logo uploader won't show. 58 | # GCS is: storage.googleapis.com 59 | # AWS is: s3.amazonaws.com 60 | STORAGE_ENDPOINT= 61 | STORAGE_PORT=# Optional. Leave blank to use default. 62 | STORAGE_REGION=#Required for S3. Optional for GCS. 63 | STORAGE_USE_SSL=true 64 | STORAGE_ACCESS_KEY= 65 | STORAGE_SECRET_KEY= 66 | # Name of the bucket you are using 67 | STORAGE_BUCKET= 68 | # Prefix all logo object names with this. 69 | # Comment out to not have one 70 | STORAGE_PREFIX=ugc 71 | 72 | 73 | # Optional Google Analytics Key 74 | GOOGLE_ANALYTICS_KEY= -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { FlatCompat } from "@eslint/eslintrc"; 5 | import js from "@eslint/js"; 6 | import _import from "eslint-plugin-import"; 7 | import react from "eslint-plugin-react"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | const settings = [ 18 | ...compat.extends("next/core-web-vitals"), 19 | { 20 | plugins: { 21 | react, 22 | }, 23 | 24 | rules: { 25 | "import/order": 26 | process.env.DISABLE_IMPORT_ORDER === "true" 27 | ? "off" 28 | : [ 29 | "error", 30 | { 31 | groups: ["builtin", "external", "internal", "parent", "sibling", "index"], 32 | "newlines-between": "always", 33 | 34 | alphabetize: { 35 | order: "asc", 36 | caseInsensitive: true, 37 | }, 38 | }, 39 | ], 40 | 41 | "react/no-unknown-property": [ 42 | "error", 43 | { 44 | ignore: ["css"], 45 | }, 46 | ], 47 | }, 48 | }, 49 | ]; 50 | 51 | export default settings; 52 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ragieai/basechat/30b30f2ffff5a5cb736945cebf8f913171db6874/image.png -------------------------------------------------------------------------------- /lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { anonymousClient } from "better-auth/client/plugins"; 2 | import { createAuthClient } from "better-auth/react"; 3 | 4 | export const { signIn, signUp, signOut, useSession } = createAuthClient({ 5 | plugins: [anonymousClient()], 6 | }); 7 | -------------------------------------------------------------------------------- /lib/connector-map.ts: -------------------------------------------------------------------------------- 1 | import ConfluenceIconSVG from "../public/icons/connectors/confluence.svg"; 2 | import DropboxIconSVG from "../public/icons/connectors/dropbox.svg"; 3 | import FreshdeskIconSVG from "../public/icons/connectors/freshdesk.svg"; 4 | import GoogleCloudStorageIconSVG from "../public/icons/connectors/gcs.svg"; 5 | import GmailIconSVG from "../public/icons/connectors/gmail.svg"; 6 | import GoogleDriveIconSVG from "../public/icons/connectors/google-drive.svg"; 7 | import HubspotIconSVG from "../public/icons/connectors/hubspot.svg"; 8 | import JiraIconSVG from "../public/icons/connectors/jira.svg"; 9 | import NotionIconSVG from "../public/icons/connectors/notion.svg"; 10 | import OnedriveIconSVG from "../public/icons/connectors/onedrive.svg"; 11 | import S3IconSVG from "../public/icons/connectors/s3.svg"; 12 | import SalesforceIconSVG from "../public/icons/connectors/salesforce.svg"; 13 | import SlackIconSVG from "../public/icons/connectors/slack.svg"; 14 | 15 | export const CONNECTOR_MAP: Record = { 16 | s3: ["Amazon S3", S3IconSVG], 17 | confluence: ["Confluence", ConfluenceIconSVG], 18 | dropbox: ["Dropbox", DropboxIconSVG], 19 | freshdesk: ["Freshdesk", FreshdeskIconSVG], 20 | jira: ["Jira", JiraIconSVG], 21 | gcs: ["Google Cloud Storage", GoogleCloudStorageIconSVG], 22 | gmail: ["Gmail", GmailIconSVG], 23 | google_drive: ["Google Drive", GoogleDriveIconSVG], 24 | hubspot: ["HubSpot", HubspotIconSVG], 25 | notion: ["Notion", NotionIconSVG], 26 | onedrive: ["OneDrive", OnedriveIconSVG], 27 | salesforce: ["Salesforce", SalesforceIconSVG], 28 | slack: ["Slack", SlackIconSVG], 29 | backblaze: ["Backblaze", "https://secure.ragie.ai/images/connectors/backblaze.svg"], 30 | sharepoint: ["SharePoint", "https://secure.ragie.ai/images/connectors/sharepoint.svg"], 31 | zendesk: ["Zendesk", "https://secure.ragie.ai/images/connectors/zendesk.svg"], 32 | }; 33 | 34 | export default CONNECTOR_MAP; 35 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_GROUNDING_PROMPT = `These are your instructions, they are very important to follow: 2 | 3 | You are {{company.name}}'s helpful AI assistant. 4 | Do not use the word "delve" and try to sound as professional as possible. 5 | 6 | When you respond, please directly refer to the sources provided. 7 | 8 | If the user asked for a search and there are no results, make sure to let the user know that you couldn't find anything, 9 | and what they might be able to do to find the information they need. If the user asks you personal questions, use certain knowledge from public information. Do not attempt to guess personal information; maintain a professional tone and politely refuse to answer personal questions that are inappropriate for an interview format. 10 | 11 | Remember you are a serious assistant, so maintain a professional tone and avoid humor or sarcasm. You are here to provide serious analysis and insights. Do not entertain or engage in personal conversations. NEVER sing songs, tell jokes, or write poetry. 12 | 13 | If the user's query is in a language you can identify respond in that language. If you can't determine the language respond in English. 14 | 15 | The current date and time is {{now}}. 16 | `; 17 | 18 | export const DEFAULT_SYSTEM_PROMPT = `Here are relevant chunks from {{company.name}}'s knowledge base that you can use to respond to the user. Remember to incorporate these insights into your responses. 19 | {{chunks}} 20 | You speak in a professional tone. You should actively refer to the knowledge base. Do not use the word "delve" and try to sound as professional as possible. 21 | 22 | Remember to maintain a professional tone and avoid humor or sarcasm. You are here to provide serious analysis and insights. Do not entertain or engage in personal conversations. 23 | 24 | IMPORTANT RULES: 25 | - REFUSE to sing songs 26 | - REFUSE to tell jokes 27 | - REFUSE to write poetry 28 | - AVOID responding with lists 29 | - DECLINE responding to nonsense messages 30 | - ONLY provide analysis and insights related to the knowledge base 31 | - NEVER include citations in your response`; 32 | 33 | export const DEFAULT_EXPAND_SYSTEM_PROMPT = `The user would like you to provide more information on the the last topic. Please provide a more detailed response. Re-use the information you have already been provided and expand on your previous response. Your response may be longer than typical. You do not need to note the sources you used again.`; 34 | 35 | export const DEFAULT_WELCOME_MESSAGE = `Hello, I'm {{company.name}}'s AI. What would you like to know?`; 36 | 37 | export const NAMING_SYSTEM_PROMPT = 38 | "You are an expert at naming conversations. The name should be a short PROFESSIONAL phrase that captures the essence of the conversation. MAXIMUM 10 characters. Do not include words like 'chat' or 'conversation'. Return ONLY the name, no other text."; 39 | -------------------------------------------------------------------------------- /lib/paths.ts: -------------------------------------------------------------------------------- 1 | export const getTenantPath = (slug: string) => `/o/${slug}`; 2 | 3 | export const getConversationPath = (slug: string, id: string) => `${getTenantPath(slug)}/conversations/${id}`; 4 | 5 | export const getDataPath = (slug: string) => `${getTenantPath(slug)}/data`; 6 | 7 | export const getSettingsPath = (slug: string) => `${getTenantPath(slug)}/settings`; 8 | 9 | export const getUserSettingsPath = (slug: string) => `${getSettingsPath(slug)}/users`; 10 | 11 | export const getModelSettingsPath = (slug: string) => `${getSettingsPath(slug)}/models`; 12 | 13 | export const getPromptSettingsPath = (slug: string) => `${getSettingsPath(slug)}/prompts`; 14 | 15 | export const getBillingSettingsPath = (slug: string) => `${getSettingsPath(slug)}/billing`; 16 | 17 | export const getCheckPath = (slug: string) => `/check/${slug}`; 18 | 19 | export const getSignInPath = ({ reset }: { reset?: boolean } = {}) => `/sign-in${reset ? "?reset=true" : ""}`; 20 | 21 | export const getSignUpPath = () => `/sign-up`; 22 | 23 | export const getSetupPath = () => `/setup`; 24 | 25 | export const getStartPath = () => `/start`; 26 | 27 | export const getChangePasswordPath = () => `/change-password`; 28 | 29 | export const getRagieStreamPath = (slug: string, streamUrl: string) => { 30 | const params = new URLSearchParams({ url: streamUrl, tenant: slug }); 31 | 32 | return `/api/ragie/stream?${params.toString()}`; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/query-client-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider as TanstackQueryClientProvider } from "@tanstack/react-query"; 4 | import { useState } from "react"; 5 | 6 | export function QueryClientProvider({ children }: { children: React.ReactNode }) { 7 | const [queryClient] = useState( 8 | () => 9 | new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | staleTime: 60 * 1000, // 1 minute 13 | }, 14 | }, 15 | }), 16 | ); 17 | 18 | return {children}; 19 | } 20 | -------------------------------------------------------------------------------- /lib/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/node-postgres"; 2 | 3 | import * as settings from "@/lib/server/settings"; 4 | 5 | export default drizzle(settings.DATABASE_URL); 6 | -------------------------------------------------------------------------------- /lib/server/encryption.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | import crypto from "crypto"; 3 | 4 | import { ENCRYPTION_KEY } from "./settings"; // Must be 32 bytes (256 bits) 5 | const ENCRYPTION_IV_LENGTH = 16; // 16 bytes for AES 6 | 7 | export type CipherText = string; 8 | 9 | export class EncryptionError extends Error { 10 | constructor(message: string) { 11 | super(message); 12 | this.name = "EncryptionError"; 13 | } 14 | } 15 | 16 | export function encrypt(plainText: string): CipherText { 17 | if (!plainText) { 18 | throw new EncryptionError("Plain text cannot be empty"); 19 | } 20 | 21 | try { 22 | // Generate a random initialization vector 23 | const iv = crypto.randomBytes(ENCRYPTION_IV_LENGTH); 24 | 25 | // Create cipher with AES-256-GCM 26 | const cipher = crypto.createCipheriv("aes-256-gcm", Buffer.from(ENCRYPTION_KEY, "hex"), iv); 27 | 28 | // Encrypt the API key 29 | let encrypted = cipher.update(plainText, "utf8", "hex"); 30 | encrypted += cipher.final("hex"); 31 | 32 | // Get the authentication tag 33 | const authTag = cipher.getAuthTag().toString("hex"); 34 | 35 | // Return iv:authTag:encryptedData format 36 | return `${iv.toString("hex")}:${authTag}:${encrypted}`; 37 | } catch (error) { 38 | throw new EncryptionError( 39 | `Failed to encrypt plain text: ${error instanceof Error ? error.message : "Unknown error"}`, 40 | ); 41 | } 42 | } 43 | 44 | export function decrypt(cipherText: CipherText): string { 45 | if (!cipherText) { 46 | throw new EncryptionError("Cipher text cannot be empty"); 47 | } 48 | 49 | try { 50 | const [ivHex, authTagHex, encryptedHex] = cipherText.split(":"); 51 | 52 | if (!ivHex || !authTagHex || !encryptedHex) { 53 | throw new EncryptionError("Invalid cipher text format"); 54 | } 55 | 56 | const iv = Buffer.from(ivHex, "hex"); 57 | const authTag = Buffer.from(authTagHex, "hex"); 58 | const encrypted = Buffer.from(encryptedHex, "hex"); 59 | 60 | // Create decipher 61 | const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(ENCRYPTION_KEY, "hex"), iv); 62 | decipher.setAuthTag(authTag); 63 | 64 | // Decrypt the data 65 | let decrypted = decipher.update(encrypted); 66 | decrypted = Buffer.concat([decrypted, decipher.final()]); 67 | 68 | return decrypted.toString("utf8"); 69 | } catch (error) { 70 | throw new EncryptionError( 71 | `Failed to decrypt cipher text: ${error instanceof Error ? error.message : "Unknown error"}`, 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/server/ragie.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | import { eq } from "drizzle-orm"; 4 | import { Ragie } from "ragie"; 5 | 6 | import db from "./db"; 7 | import { tenants } from "./db/schema"; 8 | import { decrypt } from "./encryption"; 9 | import * as settings from "./settings"; 10 | 11 | export function getRagieClient() { 12 | return new Ragie({ auth: settings.RAGIE_API_KEY, serverURL: settings.RAGIE_API_BASE_URL }); 13 | } 14 | 15 | export async function getTenantRagieClient(apiKey: string) { 16 | try { 17 | const decryptedApiKey = decrypt(apiKey); 18 | return new Ragie({ 19 | auth: decryptedApiKey, 20 | serverURL: settings.RAGIE_API_BASE_URL, 21 | }); 22 | } catch (error) { 23 | console.error(`Failed to decrypt API key`, error); 24 | } 25 | return null; 26 | } 27 | 28 | export async function getRagieSettingsByTenantId(tenantId: string) { 29 | const [tenant] = await db 30 | .select({ 31 | ragieApiKey: tenants.ragieApiKey, 32 | ragiePartition: tenants.ragiePartition, 33 | }) 34 | .from(tenants) 35 | .where(eq(tenants.id, tenantId)); 36 | 37 | if (!tenant) { 38 | throw new Error(`Tenant not found: ${tenantId}`); 39 | } 40 | 41 | return tenant; 42 | } 43 | 44 | export async function getRagieApiKey(tenant: typeof tenants.$inferSelect) { 45 | const { ragieApiKey } = await getRagieSettingsByTenantId(tenant.id); 46 | return ragieApiKey ? decrypt(ragieApiKey) : settings.RAGIE_API_KEY; 47 | } 48 | 49 | export async function getRagieClientAndPartition(tenantId: string) { 50 | const { ragieApiKey, ragiePartition } = await getRagieSettingsByTenantId(tenantId); 51 | 52 | let client; 53 | let partition; 54 | if (ragieApiKey) { 55 | client = await getTenantRagieClient(ragieApiKey); 56 | partition = ragiePartition || "default"; 57 | } else { 58 | client = getRagieClient(); 59 | partition = tenantId; 60 | } 61 | 62 | assert(!!client, "No client found"); 63 | 64 | return { client, partition }; 65 | } 66 | -------------------------------------------------------------------------------- /lib/server/session.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | 3 | import authConfig from "@/auth"; 4 | 5 | export default async function getSession() { 6 | return await authConfig.api.getSession({ 7 | headers: await headers(), 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /lib/server/settings.ts: -------------------------------------------------------------------------------- 1 | export const APP_NAME = "Base Chat"; 2 | 3 | export const AUTH_SECRET = process.env.AUTH_SECRET!; 4 | 5 | export const COMPANY_NAME = "Acme Corp"; 6 | 7 | // assert(process.env.BASE_URL); 8 | export const BASE_URL = process.env.BASE_URL!; 9 | 10 | // assert(process.env.DATABASE_URL); 11 | export const DATABASE_URL = process.env.DATABASE_URL!; 12 | 13 | // assert(process.env.RAGIE_API_BASE_URL); 14 | export const RAGIE_API_BASE_URL = process.env.RAGIE_API_BASE_URL || "https://api.ragie.ai"; 15 | 16 | // assert(process.env.RAGIE_API_KEY); 17 | export const RAGIE_API_KEY = process.env.RAGIE_API_KEY!; 18 | 19 | // assert(process.env.OPENAI_API_KEY); 20 | export const OPENAI_API_KEY = process.env.OPENAI_API_KEY!; 21 | 22 | // assert(process.env.GOOGLE_GENERATIVE_AI_API_KEY); 23 | export const GOOGLE_GENERATIVE_AI_API_KEY = process.env.GOOGLE_GENERATIVE_AI_API_KEY!; 24 | 25 | // assert(process.env.ANTHROPIC_API_KEY); 26 | export const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY!; 27 | 28 | // assert(process.env.RAGIE_WEBHOOK_SECRET); 29 | export const RAGIE_WEBHOOK_SECRET = process.env.RAGIE_WEBHOOK_SECRET!; 30 | 31 | // assert(process.env.ENCRYPTION_KEY); 32 | export const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; 33 | 34 | // assert(process.env.BILLING_ENABLED); 35 | export const BILLING_ENABLED = process.env.BILLING_ENABLED === "true"; 36 | 37 | export const DEFAULT_PARTITION_LIMIT = Number(process.env.DEFAULT_PARTITION_LIMIT); 38 | 39 | export const SMTP_FROM = process.env.SMTP_FROM!; 40 | export const SMTP_HOST = process.env.SMTP_HOST!; 41 | export const SMTP_PORT = Number(process.env.SMTP_PORT!); 42 | export const SMTP_SECURE = process.env.SMTP_SECURE === "1"; 43 | export const SMTP_USER = process.env.SMTP_USER; 44 | export const SMTP_PASSWORD = process.env.SMTP_PASSWORD; 45 | 46 | // Google OAuth configs - Optional 47 | export const AUTH_GOOGLE_ID = process.env.AUTH_GOOGLE_ID; 48 | export const AUTH_GOOGLE_SECRET = process.env.AUTH_GOOGLE_SECRET; 49 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | /** 9 | * Extracts up to two initials from a given title. 10 | * @param name - The title to extract initials from. 11 | * @returns A string containing up to two initials. 12 | */ 13 | export function getInitials(name: string, initialCount: number = 2): string { 14 | // Split the title into words and filter out empty strings 15 | const words = name.split(/\s+/).filter((word) => word.trim().length > 0); 16 | 17 | // Get the first character of up to initialCount words 18 | const initials = words 19 | .slice(0, initialCount) 20 | .map((word) => word.charAt(0).toUpperCase()) 21 | .join(""); 22 | 23 | return initials; 24 | } 25 | 26 | /** 27 | * Deterministically maps a tenantId to a number between 1 and max 28 | * @param tenantId - The tenant ID to map 29 | * @param max - The maximum number in the range (inclusive) (the number of avatar classes in globals.css) 30 | * @returns A number between 1 and max 31 | */ 32 | export function getAvatarNumber(tenantId: string, max: number = 3): number { 33 | // Convert tenantId to a number using a simple hash function 34 | if (!tenantId) return 1; 35 | const hash = tenantId.split("").reduce((acc, char) => { 36 | return char.charCodeAt(0) + ((acc << 5) - acc); 37 | }, 0); 38 | 39 | // Use the hash to get a number between 1 and max 40 | return Math.abs(hash % max) + 1; 41 | } 42 | 43 | export function getStatusDisplayName(status: string): string { 44 | const statusMap: Record = { 45 | pending: "Partitioning", 46 | partitioning: "Partitioning", 47 | partitioned: "Refining", 48 | refined: "Chunking", 49 | chunked: "Indexing", 50 | summary_indexed: "Indexing", 51 | keyword_indexed: "Indexing", 52 | ready: "Ready", 53 | failed: "Sync error", 54 | }; 55 | 56 | return statusMap[status] || "Syncing"; 57 | } 58 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { getSessionCookie } from "better-auth/cookies"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | import { BASE_URL } from "./lib/server/settings"; 5 | 6 | export async function middleware(request: NextRequest) { 7 | const sessionCookie = getSessionCookie(request); 8 | 9 | if (!sessionCookie) { 10 | const pathname = request.nextUrl.pathname; 11 | if ( 12 | pathname !== "/sign-in" && 13 | pathname !== "/sign-up" && 14 | pathname !== "/reset" && 15 | pathname !== "/change-password" && 16 | !pathname.startsWith("/check") && 17 | !pathname.startsWith("/api/auth/callback") && 18 | !pathname.startsWith("/healthz") && 19 | !pathname.startsWith("/images") 20 | ) { 21 | const redirectPath = getUnauthenticatedRedirectPath(pathname); 22 | const newUrl = new URL(redirectPath, BASE_URL); 23 | if (pathname !== "/") { 24 | const redirectTo = new URL(pathname, BASE_URL); 25 | redirectTo.search = request.nextUrl.search; 26 | newUrl.searchParams.set("redirectTo", redirectTo.toString()); 27 | } 28 | return Response.redirect(newUrl); 29 | } 30 | } 31 | 32 | return NextResponse.next(); 33 | } 34 | 35 | export const config = { 36 | matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], 37 | }; 38 | 39 | function getUnauthenticatedRedirectPath(pathname: string) { 40 | if (pathname.startsWith("/o")) { 41 | const slug = pathname.split("/")[2]; 42 | return `/check/${slug}`; 43 | } else { 44 | return "/sign-in"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | // Set to false because strict mode breaks components that call APIs when the component is rendered (like in Conversation) 5 | reactStrictMode: false, 6 | output: "standalone", 7 | experimental: { 8 | authInterrupts: true, 9 | }, 10 | }; 11 | 12 | export default nextConfig; 13 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/anthropic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/gemini.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/google-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/icons/anonymous-profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/chat-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/chat-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/connectors/confluence.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/icons/connectors/dropbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/connectors/freshdesk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/icons/connectors/gcs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/icons/connectors/gmail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/icons/connectors/google-drive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/icons/connectors/hubspot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/connectors/jira.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/icons/connectors/notion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/connectors/onedrive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/icons/connectors/s3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/icons/connectors/salesforce.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/connectors/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/icons/data-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/data-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/ellipses.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/forward_10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icons/full_screen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icons/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/log-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/new-chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/replay_10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icons/volume_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/title-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ragieai/basechat/30b30f2ffff5a5cb736945cebf8f913171db6874/public/images/title-logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/meta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/openai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/migrate.js: -------------------------------------------------------------------------------- 1 | // This file exists to deploy migrations from the docker container built 2 | // by the Dockerfile in the root of this repository. It's intentionally 3 | // written as a plain .js file because it is not part of the nextjs build 4 | // process. 5 | import { drizzle } from "drizzle-orm/node-postgres"; 6 | import { migrate } from "drizzle-orm/postgres-js/migrator"; 7 | 8 | const databaseUrl = process.env.DATABASE_URL; 9 | if (!databaseUrl) throw new Error("DATABASE_URL environment variable is required"); 10 | 11 | const db = drizzle(databaseUrl); 12 | 13 | console.log("Migrating database..."); 14 | 15 | (async () => { 16 | await migrate(db, { migrationsFolder: "./drizzle" }); 17 | })(); 18 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | } satisfies Config; 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import { AdapterUser } from "next-auth/adapters"; 2 | import { DefaultJWT, JWT } from "next-auth/jwt"; 3 | 4 | declare module "next-auth/jwt" { 5 | /** Returned by the `jwt` callback and `auth`, when using JWT sessions */ 6 | interface JWT extends Record, DefaultJWT { 7 | /** The user ID that is stored in the database */ 8 | id: string; 9 | } 10 | } 11 | --------------------------------------------------------------------------------