├── .eslintrc.json ├── src ├── config │ ├── infinite-query.ts │ ├── message.ts │ ├── links.ts │ └── stripe.ts ├── app │ ├── favicon.ico │ ├── api │ │ ├── auth │ │ │ └── [kindeAuth] │ │ │ │ └── route.ts │ │ ├── trpc │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ ├── uploadthing │ │ │ ├── route.ts │ │ │ └── core.ts │ │ ├── webhooks │ │ │ └── stripe │ │ │ │ └── route.ts │ │ └── message │ │ │ └── route.ts │ ├── _trpc │ │ └── client.ts │ ├── dashboard │ │ ├── billing │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── [fileId] │ │ │ └── page.tsx │ ├── layout.tsx │ ├── auth-callback │ │ └── page.tsx │ ├── globals.css │ ├── page.tsx │ └── pricing │ │ └── page.tsx ├── lib │ ├── openai.ts │ ├── validators │ │ └── send-message-validator.ts │ ├── uploadthing.ts │ ├── pinecone.ts │ ├── stripe.ts │ └── utils.ts ├── middleware.ts ├── types │ └── message.ts ├── db │ └── index.ts ├── components │ ├── max-width-wrapper.tsx │ ├── icons.tsx │ ├── ui │ │ ├── input.tsx │ │ ├── textarea.tsx │ │ ├── sonner.tsx │ │ ├── progress.tsx │ │ ├── tooltip.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ └── dropdown-menu.tsx │ ├── providers.tsx │ ├── upgrade-button.tsx │ ├── pdf-fullscreen.tsx │ ├── chat │ │ ├── chat-input.tsx │ │ ├── message.tsx │ │ ├── messages.tsx │ │ ├── chat-wrapper.tsx │ │ └── chat-context.tsx │ ├── billing-form.tsx │ ├── delete-file-modal.tsx │ ├── user-account-nav.tsx │ ├── navbar.tsx │ ├── mobile-nav.tsx │ ├── dashboard.tsx │ ├── upload-button.tsx │ └── pdf-renderer.tsx └── trpc │ ├── trpc.ts │ └── index.ts ├── public ├── thumbnail.png ├── dashboard-preview.jpg ├── file-upload-preview.jpg └── github.svg ├── .github ├── images │ ├── img1.png │ ├── img2.png │ ├── img3.png │ ├── img4.png │ ├── img_main.png │ └── stats.svg ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── postcss.config.js ├── components.json ├── .gitignore ├── tsconfig.json ├── SECURITY.md ├── environment.d.ts ├── next.config.mjs ├── LICENSE ├── .env.example ├── prisma └── schema.prisma ├── tailwind.config.ts ├── package.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/config/infinite-query.ts: -------------------------------------------------------------------------------- 1 | export const INFINITE_QUERY_LIMIT = 10; 2 | -------------------------------------------------------------------------------- /src/config/message.ts: -------------------------------------------------------------------------------- 1 | export const AI_RESPONSE_MESSAGE_ID = "ai-response"; 2 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/config/links.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_CODE = "https://github.com/sanidhyy/quill"; 2 | -------------------------------------------------------------------------------- /public/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/public/thumbnail.png -------------------------------------------------------------------------------- /.github/images/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/.github/images/img1.png -------------------------------------------------------------------------------- /.github/images/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/.github/images/img2.png -------------------------------------------------------------------------------- /.github/images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/.github/images/img3.png -------------------------------------------------------------------------------- /.github/images/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/.github/images/img4.png -------------------------------------------------------------------------------- /.github/images/img_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/.github/images/img_main.png -------------------------------------------------------------------------------- /public/dashboard-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/public/dashboard-preview.jpg -------------------------------------------------------------------------------- /public/file-upload-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanidhyy/quill/HEAD/public/file-upload-preview.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | export const openai = new OpenAI({ 4 | apiKey: process.env.OPENAI_API_KEY, 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/api/auth/[kindeAuth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server"; 2 | 3 | export const GET = handleAuth(); 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sanidhyy] 4 | patreon: sanidhy 5 | custom: https://www.buymeacoffee.com/sanidhy 6 | -------------------------------------------------------------------------------- /src/app/_trpc/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | 3 | import type { AppRouter } from "@/trpc"; 4 | 5 | export const trpc = createTRPCReact({}); 6 | -------------------------------------------------------------------------------- /src/lib/validators/send-message-validator.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const sendMessageValidator = z.object({ 4 | fileId: z.string(), 5 | message: z.string(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { generateReactHelpers } from "@uploadthing/react"; 2 | 3 | import type { OurFileRouter } from "@/app/api/uploadthing/core"; 4 | 5 | export const { useUploadThing } = generateReactHelpers(); 6 | -------------------------------------------------------------------------------- /src/lib/pinecone.ts: -------------------------------------------------------------------------------- 1 | import { Pinecone } from "@pinecone-database/pinecone"; 2 | 3 | export const getPineconeClient = () => { 4 | const client = new Pinecone({ 5 | apiKey: process.env.PINECONE_API_KEY!, 6 | }); 7 | 8 | return client; 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/dashboard/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import { BillingForm } from "@/components/billing-form"; 2 | import { getUserSubscriptionPlan } from "@/lib/stripe"; 3 | 4 | const BillingPage = async () => { 5 | const subscriptionPlan = await getUserSubscriptionPlan(); 6 | 7 | return ; 8 | }; 9 | 10 | export default BillingPage; 11 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware"; 2 | import { type NextRequest } from "next/server"; 3 | 4 | export default function middleware(req: NextRequest) { 5 | return withAuth(req, { 6 | isReturnToCurrentPage: true, 7 | }); 8 | } 9 | export const config = { 10 | matcher: ["/dashboard/:path*", "/auth-callback"], 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | 3 | import { appRouter } from "@/trpc"; 4 | 5 | const handler = (req: Request) => 6 | fetchRequestHandler({ 7 | endpoint: "/api/trpc", 8 | req, 9 | router: appRouter, 10 | createContext: () => ({}), 11 | }); 12 | 13 | export { handler as GET, handler as POST }; 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types/message.ts: -------------------------------------------------------------------------------- 1 | import { AppRouter } from "@/trpc"; 2 | import { inferRouterOutputs } from "@trpc/server"; 3 | 4 | type RouterOutput = inferRouterOutputs; 5 | 6 | export type Messages = RouterOutput["getFileMessages"]["messages"]; 7 | 8 | type OmitText = Omit; 9 | 10 | type ExtendedText = { 11 | text: string | JSX.Element; 12 | }; 13 | 14 | export type ExtendedMessage = OmitText & ExtendedText; 15 | -------------------------------------------------------------------------------- /src/app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | export const { GET, POST } = createRouteHandler({ 6 | router: ourFileRouter, 7 | config: { 8 | uploadthingId: process.env.UPLOADTHING_APP_ID!, 9 | uploadthingSecret: process.env.UPLOADTHING_SECRET!, 10 | callbackUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/uploadthing`, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | // eslint-disable-next-line no-var 5 | var cachedPrisma: PrismaClient; 6 | } 7 | 8 | let prisma: PrismaClient; 9 | if (process.env.NODE_ENV === "production") { 10 | prisma = new PrismaClient(); 11 | } else { 12 | if (!global.cachedPrisma) { 13 | global.cachedPrisma = new PrismaClient(); 14 | } 15 | prisma = global.cachedPrisma; 16 | } 17 | 18 | export const db = prisma; 19 | -------------------------------------------------------------------------------- /src/components/max-width-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import type { PropsWithChildren } from "react"; 3 | 4 | export const MaxWidthWrapper = ({ 5 | className, 6 | children, 7 | }: PropsWithChildren<{ 8 | className?: string; 9 | }>) => { 10 | return ( 11 |
17 | {children} 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/config/stripe.ts: -------------------------------------------------------------------------------- 1 | export const PLANS = [ 2 | { 3 | name: "Free", 4 | slug: "free", 5 | quota: 10, 6 | pagesPerPdf: 5, 7 | price: { 8 | amount: 0.0, 9 | priceIds: { 10 | test: "", 11 | production: "", 12 | }, 13 | }, 14 | }, 15 | { 16 | name: "Pro", 17 | slug: "pro", 18 | quota: 50, 19 | pagesPerPdf: 25, 20 | price: { 21 | amount: 14.0, 22 | priceIds: { 23 | test: process.env.STRIPE_PRICE_ID!, 24 | production: "", 25 | }, 26 | }, 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { type LucideProps, User } from "lucide-react"; 2 | 3 | export const Icons = { 4 | user: User, 5 | logo: (props: LucideProps) => ( 6 | 7 | 8 | 9 | ), 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # testing tools 40 | /ngrok 41 | -------------------------------------------------------------------------------- /src/trpc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; 2 | import { TRPCError, initTRPC } from "@trpc/server"; 3 | 4 | const t = initTRPC.create(); 5 | const isAuth = t.middleware(async (opts) => { 6 | const { getUser } = getKindeServerSession(); 7 | const user = await getUser(); 8 | 9 | if (!user || !user.id) throw new TRPCError({ code: "UNAUTHORIZED" }); 10 | 11 | return opts.next({ 12 | ctx: { 13 | userId: user.id, 14 | user, 15 | }, 16 | }); 17 | }); 18 | 19 | export const router = t.router; 20 | export const procedure = t.procedure; 21 | export const privateProcedure = t.procedure.use(isAuth); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | // This file is needed to support autocomplete for process.env 2 | export {}; 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface ProcessEnv { 7 | // neon db uri 8 | DATABASE_URL: string; 9 | 10 | // uploadthing api key and app id 11 | UPLOADTHING_SECRET: string; 12 | UPLOADTHING_APP_ID: string; 13 | 14 | // app base url 15 | NEXT_PUBLIC_BASE_URL: string; 16 | 17 | // pinecone api key 18 | PINECONE_API_KEY: string; 19 | 20 | // openai api key 21 | OPENAI_API_KEY: string; 22 | 23 | // stripe secret key, price id and webhook secret 24 | STRIPE_SECRET_KEY: string; 25 | STRIPE_PRICE_ID: string; 26 | STRIPE_WEBHOOK_SECRET: string; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | async redirects() { 4 | return [ 5 | { 6 | source: "/sign-in", 7 | destination: "/api/auth/login", 8 | permanent: true, 9 | }, 10 | { 11 | source: "/sign-up", 12 | destination: "/api/auth/register", 13 | permanent: true, 14 | }, 15 | ]; 16 | }, 17 | webpack: (config) => { 18 | config.resolve.alias.canvas = false; 19 | config.resolve.alias.encoding = false; 20 | 21 | return config; 22 | }, 23 | images: { 24 | remotePatterns: [ 25 | { 26 | protocol: "https", 27 | hostname: "lh3.googleusercontent.com", 28 | }, 29 | ], 30 | }, 31 | }; 32 | 33 | export default nextConfig; 34 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { Dashboard } from "@/components/dashboard"; 5 | import { db } from "@/db"; 6 | import { getUserSubscriptionPlan } from "@/lib/stripe"; 7 | 8 | const DashboardPage = async () => { 9 | const { getUser } = getKindeServerSession(); 10 | const user = await getUser(); 11 | 12 | if (!user || !user.id) redirect("/auth-callback?origin=dashboard"); 13 | 14 | const dbUser = await db.user.findUnique({ 15 | where: { 16 | id: user.id, 17 | }, 18 | }); 19 | 20 | if (!dbUser) redirect("/auth-callback?origin=dashboard"); 21 | 22 | const subscriptionPlan = await getUserSubscriptionPlan(); 23 | 24 | return ; 25 | }; 26 | 27 | export default DashboardPage; 28 | -------------------------------------------------------------------------------- /public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import TextareaAutosize, { 3 | type TextareaAutosizeProps, 4 | } from "react-textarea-autosize"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | export interface TextareaProps 9 | extends React.TextareaHTMLAttributes {} 10 | 11 | const Textarea = React.forwardRef( 12 | ({ className, ...props }, ref) => { 13 | return ( 14 | 22 | ); 23 | }, 24 | ); 25 | Textarea.displayName = "Textarea"; 26 | 27 | export { Textarea }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/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 | 28 | ); 29 | }; 30 | 31 | export { Toaster }; 32 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | type ProgressProps = React.ComponentPropsWithoutRef< 9 | typeof ProgressPrimitive.Root 10 | > & { 11 | indicatorColor?: string; 12 | }; 13 | 14 | const Progress = React.forwardRef< 15 | React.ElementRef, 16 | ProgressProps 17 | >(({ className, value, indicatorColor, ...props }, ref) => ( 18 | 26 | 33 | 34 | )); 35 | Progress.displayName = ProgressPrimitive.Root.displayName; 36 | 37 | export { Progress }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sanidhya Kumar Verma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | QueryCache, 5 | QueryClient, 6 | QueryClientProvider, 7 | } from "@tanstack/react-query"; 8 | import { httpBatchLink } from "@trpc/client"; 9 | import { useRouter } from "next/navigation"; 10 | import { type PropsWithChildren, useState } from "react"; 11 | 12 | import { trpc } from "@/app/_trpc/client"; 13 | 14 | export const Providers = ({ children }: PropsWithChildren) => { 15 | const router = useRouter(); 16 | const [queryClient] = useState( 17 | () => 18 | new QueryClient({ 19 | queryCache: new QueryCache({ 20 | onError: (_error: unknown, query) => { 21 | if (query.meta?.errCode === "UNAUTHORIZED") router.push("/sign-in"); 22 | }, 23 | }), 24 | }), 25 | ); 26 | 27 | const [trpcClient] = useState(() => 28 | trpc.createClient({ 29 | links: [ 30 | httpBatchLink({ 31 | url: "/api/trpc", 32 | }), 33 | ], 34 | }), 35 | ); 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # .env 2 | 3 | # disabled next.js telemetry 4 | NEXT_TELEMETRY_DISABLED=1 5 | 6 | # kinde keys and urls 7 | KINDE_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXX 8 | KINDE_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 9 | KINDE_ISSUER_URL=https://example.kinde.com 10 | KINDE_SITE_URL=http://localhost:3000 11 | KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000 12 | KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard 13 | 14 | # neon db uri 15 | DATABASE_URL="postgresql://:@:/quill?sslmode=require" 16 | 17 | # uploadthing api key and app id 18 | UPLOADTHING_SECRET=sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 19 | UPLOADTHING_APP_ID=xxxxxxxxxxx 20 | 21 | # app base url 22 | NEXT_PUBLIC_BASE_URL=http://localhost:3000 23 | 24 | # pinecone api key 25 | PINECONE_API_KEY=xxxxxxxxxx-xxxxx-xxxx-xxxxxx-xxxxxxxxxxx 26 | 27 | # openai api key 28 | OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 29 | 30 | # stripe secret key, price id and webhook secret 31 | STRIPE_SECRET_KEY=sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 32 | STRIPE_PRICE_ID=price_XXXXXXXXXXXXXXXXXXXXXXXXX 33 | STRIPE_WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 34 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 31 | -------------------------------------------------------------------------------- /src/components/upgrade-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ArrowRight } from "lucide-react"; 4 | import { useEffect, useState } from "react"; 5 | import { toast } from "sonner"; 6 | 7 | import { trpc } from "@/app/_trpc/client"; 8 | import { Button } from "@/components/ui/button"; 9 | 10 | const CHECKOUT_TOAST_ID = "checkout-toast"; 11 | 12 | export const UpgradeButton = () => { 13 | const [isLoading, setIsLoading] = useState(false); 14 | const { mutate: createStripeSession } = trpc.createStripeSession.useMutation({ 15 | onSuccess: ({ url }) => { 16 | window.location.href = url ?? "/dashboard/billing"; 17 | }, 18 | onSettled: () => setIsLoading(false), 19 | }); 20 | 21 | const handleClick = () => { 22 | setIsLoading(true); 23 | toast.loading("Redirecting to checkout...", { 24 | description: "Please wait while we redirect you to checkout page.", 25 | id: CHECKOUT_TOAST_ID, 26 | }); 27 | 28 | createStripeSession(); 29 | }; 30 | 31 | useEffect(() => { 32 | if (!isLoading) toast.dismiss(CHECKOUT_TOAST_ID); 33 | }, [isLoading]); 34 | 35 | return ( 36 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin"; 2 | import type { Viewport } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import type { PropsWithChildren } from "react"; 5 | import { Toaster } from "sonner"; 6 | import { extractRouterConfig } from "uploadthing/server"; 7 | 8 | import "react-loading-skeleton/dist/skeleton.css"; 9 | import "simplebar-react/dist/simplebar.min.css"; 10 | 11 | import { ourFileRouter } from "@/app/api/uploadthing/core"; 12 | import { Navbar } from "@/components/navbar"; 13 | import { Providers } from "@/components/providers"; 14 | import { cn, constructMetadata } from "@/lib/utils"; 15 | 16 | import "./globals.css"; 17 | 18 | const inter = Inter({ subsets: ["latin"] }); 19 | 20 | export const metadata = constructMetadata(); 21 | 22 | export const viewport: Viewport = { 23 | themeColor: "#FFF", 24 | }; 25 | 26 | export default function RootLayout({ children }: Readonly) { 27 | return ( 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | {children} 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id @unique // matches kinde user id 12 | email String @unique 13 | 14 | File File[] 15 | Message Message[] 16 | 17 | stripeCustomerId String? @unique @map(name: "stripe_customer_id") 18 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id") 19 | stripePriceId String? @map(name: "stripe_price_id") 20 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end") 21 | } 22 | 23 | enum UploadStatus { 24 | PENDING 25 | PROCESSING 26 | FAILED 27 | SUCCESS 28 | } 29 | 30 | model File { 31 | id String @id @default(cuid()) 32 | name String 33 | 34 | uploadStatus UploadStatus @default(PENDING) 35 | url String @unique 36 | key String @unique 37 | 38 | messages Message[] 39 | 40 | User User? @relation(fields: [userId], references: [id]) 41 | userId String? 42 | 43 | createdAt DateTime @default(now()) 44 | updatedAt DateTime @updatedAt 45 | } 46 | 47 | model Message { 48 | id String @id @default(cuid()) 49 | text String @db.Text 50 | 51 | isUserMessage Boolean 52 | 53 | User User? @relation(fields: [userId], references: [id]) 54 | userId String? 55 | 56 | File File? @relation(fields: [fileId], references: [id]) 57 | fileId String? 58 | 59 | createdAt DateTime @default(now()) 60 | updatedAt DateTime @updatedAt 61 | } 62 | -------------------------------------------------------------------------------- /src/app/auth-callback/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader2 } from "lucide-react"; 4 | import React, { useEffect } from "react"; 5 | import { useRouter, useSearchParams } from "next/navigation"; 6 | 7 | import { trpc } from "@/app/_trpc/client"; 8 | 9 | enum TErrCodes { 10 | UNAUTHORIZED, 11 | } 12 | 13 | const AuthCallbackLoading = () => { 14 | const router = useRouter(); 15 | const searchParams = useSearchParams(); 16 | 17 | const origin = searchParams.get("origin"); 18 | 19 | const { data, isLoading, error } = trpc.authCallback.useQuery(undefined, { 20 | meta: { errCode: TErrCodes.UNAUTHORIZED }, 21 | }); 22 | 23 | useEffect(() => { 24 | if (error?.data?.code === "UNAUTHORIZED") router.push("/sign-in"); 25 | else if (!isLoading) 26 | router.push( 27 | data !== undefined && data.success && origin 28 | ? `${origin}` 29 | : "/dashboard" 30 | ); 31 | 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, [data, isLoading]); 34 | 35 | return ( 36 |
37 |
38 | 39 |

Setting up your account...

40 |

You will be redirected automatically.

41 |
42 |
43 | ); 44 | }; 45 | 46 | const AuthCallbackPage = () => ( 47 | 48 | 49 | 50 | ); 51 | 52 | export default AuthCallbackPage; 53 | -------------------------------------------------------------------------------- /src/app/dashboard/[fileId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; 2 | import { notFound, redirect } from "next/navigation"; 3 | 4 | import { ChatWrapper } from "@/components/chat/chat-wrapper"; 5 | import { PDFRenderer } from "@/components/pdf-renderer"; 6 | import { db } from "@/db"; 7 | 8 | const FileIdPage = async ({ params }: { params: { fileId: string } }) => { 9 | // retrieve file id 10 | const { fileId } = params; 11 | 12 | // authenticate user 13 | const { getUser } = getKindeServerSession(); 14 | const user = await getUser(); 15 | 16 | if (!user || !user.id) redirect(`/auth-callback?origin=dashboard/${fileId}`); 17 | 18 | // get file details 19 | const file = await db.file.findFirst({ 20 | where: { 21 | id: fileId, 22 | userId: user.id, 23 | }, 24 | }); 25 | 26 | if (!file) notFound(); 27 | 28 | return ( 29 |
30 |
31 | {/* left side */} 32 |
33 |
34 | 35 |
36 |
37 | 38 | {/* right side */} 39 |
40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | export default FileIdPage; 48 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /src/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; 2 | import Stripe from "stripe"; 3 | 4 | import { PLANS } from "@/config/stripe"; 5 | import { db } from "@/db"; 6 | 7 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", { 8 | apiVersion: "2024-04-10", 9 | typescript: true, 10 | }); 11 | 12 | export async function getUserSubscriptionPlan() { 13 | const { getUser } = getKindeServerSession(); 14 | const user = await getUser(); 15 | 16 | if (!user?.id) { 17 | return { 18 | ...PLANS[0], 19 | isSubscribed: false, 20 | isCanceled: false, 21 | stripeCurrentPeriodEnd: null, 22 | }; 23 | } 24 | 25 | const dbUser = await db.user.findFirst({ 26 | where: { 27 | id: user.id, 28 | }, 29 | }); 30 | 31 | if (!dbUser) { 32 | return { 33 | ...PLANS[0], 34 | isSubscribed: false, 35 | isCanceled: false, 36 | stripeCurrentPeriodEnd: null, 37 | }; 38 | } 39 | 40 | const isSubscribed = Boolean( 41 | dbUser.stripePriceId && 42 | dbUser.stripeCurrentPeriodEnd && // 86400000 = 1 day 43 | dbUser.stripeCurrentPeriodEnd.getTime() + 86_400_000 > Date.now(), 44 | ); 45 | 46 | const plan = isSubscribed 47 | ? PLANS.find((plan) => plan.price.priceIds.test === dbUser.stripePriceId) 48 | : null; 49 | 50 | let isCanceled = false; 51 | if (isSubscribed && dbUser.stripeSubscriptionId) { 52 | const stripePlan = await stripe.subscriptions.retrieve( 53 | dbUser.stripeSubscriptionId, 54 | ); 55 | isCanceled = stripePlan.cancel_at_period_end; 56 | } 57 | 58 | return { 59 | ...plan, 60 | stripeSubscriptionId: dbUser.stripeSubscriptionId, 61 | stripeCurrentPeriodEnd: dbUser.stripeCurrentPeriodEnd, 62 | stripeCustomerId: dbUser.stripeCustomerId, 63 | isSubscribed, 64 | isCanceled, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | 22 | // custom 23 | danger: "bg-rose-500 text-white hover:bg-rose-500/90", 24 | }, 25 | size: { 26 | default: "h-10 px-4 py-2", 27 | sm: "h-9 rounded-md px-3", 28 | lg: "h-11 rounded-md px-8", 29 | icon: "h-10 w-10", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | }, 37 | ); 38 | 39 | export interface ButtonProps 40 | extends React.ButtonHTMLAttributes, 41 | VariantProps { 42 | asChild?: boolean; 43 | } 44 | 45 | const Button = React.forwardRef( 46 | ({ className, variant, size, asChild = false, ...props }, ref) => { 47 | const Comp = asChild ? Slot : "button"; 48 | return ( 49 | 54 | ); 55 | }, 56 | ); 57 | Button.displayName = "Button"; 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /src/app/api/webhooks/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import type Stripe from "stripe"; 3 | 4 | import { db } from "@/db"; 5 | import { stripe } from "@/lib/stripe"; 6 | 7 | export async function POST(request: Request) { 8 | const body = await request.text(); 9 | const signature = headers().get("Stripe-Signature") ?? ""; 10 | 11 | let event: Stripe.Event; 12 | 13 | try { 14 | event = stripe.webhooks.constructEvent( 15 | body, 16 | signature, 17 | process.env.STRIPE_WEBHOOK_SECRET || "", 18 | ); 19 | } catch (err) { 20 | return new Response( 21 | `Webhook Error: ${err instanceof Error ? err.message : "Unknown Error"}`, 22 | { status: 400 }, 23 | ); 24 | } 25 | 26 | const session = event.data.object as Stripe.Checkout.Session; 27 | 28 | if (!session?.metadata?.userId) { 29 | return new Response(null, { 30 | status: 200, 31 | }); 32 | } 33 | 34 | if (event.type === "checkout.session.completed") { 35 | const subscription = await stripe.subscriptions.retrieve( 36 | session.subscription as string, 37 | ); 38 | 39 | await db.user.update({ 40 | where: { 41 | id: session.metadata?.userId, 42 | }, 43 | data: { 44 | stripeSubscriptionId: subscription.id, 45 | stripeCustomerId: subscription.customer as string, 46 | stripePriceId: subscription.items.data[0]?.price.id, 47 | stripeCurrentPeriodEnd: new Date( 48 | subscription.current_period_end * 1000, 49 | ), 50 | }, 51 | }); 52 | } 53 | 54 | if (event.type === "invoice.payment_succeeded") { 55 | // Retrieve the subscription details from Stripe. 56 | const subscription = await stripe.subscriptions.retrieve( 57 | session.subscription as string, 58 | ); 59 | 60 | await db.user.update({ 61 | where: { 62 | stripeSubscriptionId: subscription.id, 63 | }, 64 | data: { 65 | stripePriceId: subscription.items.data[0]?.price.id, 66 | stripeCurrentPeriodEnd: new Date( 67 | subscription.current_period_end * 1000, 68 | ), 69 | }, 70 | }); 71 | } 72 | 73 | return new Response(null, { status: 200 }); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/pdf-fullscreen.tsx: -------------------------------------------------------------------------------- 1 | import { Expand, Loader2 } from "lucide-react"; 2 | import { useState } from "react"; 3 | import { Document, Page } from "react-pdf"; 4 | import { useResizeDetector } from "react-resize-detector"; 5 | import SimpleBar from "simplebar-react"; 6 | import { toast } from "sonner"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; 10 | 11 | type PdfFullscreenProps = { 12 | fileUrl: string; 13 | }; 14 | 15 | export const PdfFullscreen = ({ fileUrl }: PdfFullscreenProps) => { 16 | const { width, ref } = useResizeDetector(); 17 | 18 | const [numPages, setNumPages] = useState(undefined); 19 | const [isOpen, setIsOpen] = useState(false); 20 | const [currPage, setCurrPage] = useState(1); 21 | 22 | return ( 23 | { 26 | if (!v) setIsOpen(v); 27 | }} 28 | > 29 | setIsOpen(true)} asChild> 30 | 38 | 39 | 40 | 41 | 42 |
43 | 46 | 47 |
48 | } 49 | onLoadSuccess={({ numPages }) => setNumPages(numPages)} 50 | onLoadError={() => 51 | toast.error("Error loading PDF.", { 52 | description: "Please try again.", 53 | }) 54 | } 55 | file={fileUrl} 56 | className="max-h-full" 57 | > 58 | {new Array(numPages).fill(0).map((_, i) => ( 59 | 60 | ))} 61 | 62 |
63 | 64 | 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { type Metadata } from "next"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | export function absoluteUrl(path: string) { 10 | return `${process.env.NEXT_PUBLIC_BASE_URL}${path}`; 11 | } 12 | 13 | export function constructMetadata({ 14 | title = "Quill - The SaaS for Students", 15 | description = "Quill is an open-source software to make chatting to your PDF files easy.", 16 | image = "/thumbnail.png", 17 | noIndex = false, 18 | }: { 19 | title?: string; 20 | description?: string; 21 | image?: string; 22 | noIndex?: boolean; 23 | } = {}): Metadata { 24 | return { 25 | title, 26 | description, 27 | keywords: [ 28 | "reactjs", 29 | "nextjs", 30 | "vercel", 31 | "react", 32 | "ai", 33 | "artificial-intelligence", 34 | "machine-learning", 35 | "ai-chat", 36 | "shadcn", 37 | "shadcn-ui", 38 | "radix-ui", 39 | "cn", 40 | "clsx", 41 | "quill", 42 | "realtime-chat", 43 | "summarize-pdf", 44 | "pdf-ai", 45 | "langchain", 46 | "openai", 47 | "neon-db", 48 | "sonner", 49 | "zustand", 50 | "zod", 51 | "sql", 52 | "postgresql", 53 | "aiven", 54 | "lucide-react", 55 | "next-themes", 56 | "postcss", 57 | "prettier", 58 | "react-dom", 59 | "tailwindcss", 60 | "tailwindcss-animate", 61 | "ui/ux", 62 | "js", 63 | "javascript", 64 | "typescript", 65 | "eslint", 66 | "html", 67 | "css", 68 | ] as Array, 69 | authors: { 70 | name: "Sanidhya Kumar Verma", 71 | url: "https://github.com/sanidhyy", 72 | }, 73 | openGraph: { 74 | title, 75 | description, 76 | images: [ 77 | { 78 | url: image, 79 | }, 80 | ], 81 | }, 82 | twitter: { 83 | card: "summary_large_image", 84 | title, 85 | description, 86 | images: [image], 87 | creator: "@TechnicalShubam", 88 | }, 89 | metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL), 90 | ...(noIndex && { 91 | robots: { 92 | index: false, 93 | follow: false, 94 | }, 95 | }), 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{ts,tsx}", 7 | "./components/**/*.{ts,tsx}", 8 | "./app/**/*.{ts,tsx}", 9 | "./src/**/*.{ts,tsx}", 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], 78 | } satisfies Config; 79 | 80 | export default config; 81 | -------------------------------------------------------------------------------- /src/components/chat/chat-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Send } from "lucide-react"; 4 | import { useContext, useRef } from "react"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Textarea } from "@/components/ui/textarea"; 8 | 9 | import { ChatContext } from "./chat-context"; 10 | 11 | type ChatInputProps = { 12 | isDisabled?: boolean; 13 | }; 14 | 15 | export const ChatInput = ({ isDisabled }: ChatInputProps) => { 16 | const textareaRef = useRef(null); 17 | const { addMessage, handleInputChange, isLoading, message } = 18 | useContext(ChatContext); 19 | 20 | return ( 21 |
22 |
e.preventDefault()} 24 | autoCapitalize="off" 25 | autoComplete="off" 26 | className="mx-2 flex flex-row gap-3 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl" 27 | > 28 |
29 |
30 |
31 |