├── .vscode └── settings.json ├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── chat │ │ ├── schema.ts │ │ └── route.ts │ ├── uploadthing │ │ ├── route.ts │ │ └── core.ts │ ├── chats │ │ └── route.ts │ └── file │ │ └── download │ │ └── route.ts ├── auth │ ├── login │ │ └── page.tsx │ └── error │ │ └── page.tsx ├── (home) │ ├── page.tsx │ ├── history │ │ └── page.tsx │ ├── layout.tsx │ └── error.tsx ├── robot.ts ├── chat │ ├── layout.tsx │ └── [id] │ │ ├── page.tsx │ │ ├── error.tsx │ │ └── not-found.tsx ├── sitemap.ts ├── auth.ts ├── layout.tsx └── globals.css ├── public ├── Google.png └── unsplash.png ├── lib ├── hooks │ ├── index.ts │ ├── use-clipboard.ts │ ├── use-local-storage.ts │ ├── use-mobile.ts │ ├── use-animated-text.ts │ ├── use-scroll.ts │ └── use-search.ts ├── uploadthing.ts ├── errors.ts ├── drizzle │ ├── migrations │ │ ├── meta │ │ │ └── _journal.json │ │ └── 0000_yielding_leader.sql │ ├── index.ts │ └── schema.ts ├── ai │ ├── utis.ts │ ├── prompt.ts │ └── models.tsx ├── types │ ├── index.ts │ └── schema.ts ├── data.tsx ├── server │ ├── helpers.ts │ ├── index.ts │ └── actions.ts ├── helpers.tsx └── utils.ts ├── postcss.config.mjs ├── .eslintrc.json ├── components ├── navbar │ ├── index.tsx │ ├── toggle-mode.tsx │ ├── nav-item.tsx │ ├── nav-links.tsx │ ├── nav-content.tsx │ ├── user.tsx │ └── nav-items.tsx ├── auth │ ├── providers │ │ ├── index.tsx │ │ └── provider.tsx │ ├── alert.tsx │ └── login-form.tsx ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── input.tsx │ ├── separator.tsx │ ├── sonner.tsx │ ├── badge.tsx │ ├── tooltip.tsx │ ├── popover.tsx │ ├── switch.tsx │ ├── avatar.tsx │ ├── alert.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── card.tsx │ ├── table.tsx │ ├── dialog.tsx │ ├── sheet.tsx │ ├── alert-dialog.tsx │ ├── command.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── providers │ └── theme-provider.tsx ├── ai │ ├── user-message.tsx │ ├── message.tsx │ ├── spinner.tsx │ ├── spinner-message.tsx │ ├── bot-message.tsx │ ├── button-row.tsx │ ├── code.tsx │ └── markdown.tsx ├── chat │ ├── empty-messages.tsx │ ├── auto-scoller.tsx │ ├── scroll-anchor.tsx │ ├── message.tsx │ ├── messages.tsx │ ├── view-attachement.tsx │ ├── reasoning.tsx │ ├── attachment-preview.tsx │ ├── model-select.tsx │ ├── index.tsx │ └── input.tsx ├── search.tsx ├── chat-history.tsx ├── skeletons.tsx ├── chat-item.tsx └── dialogs.tsx ├── .github └── renovate.json ├── drizzle.config.ts ├── .env.example ├── components.json ├── .gitignore ├── middleware.ts ├── tsconfig.json ├── next.config.ts ├── LICENSE ├── README.md ├── GEMINI.md ├── tailwind.config.ts └── package.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ikuzweshema/code_copilot/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/Google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ikuzweshema/code_copilot/HEAD/public/Google.png -------------------------------------------------------------------------------- /public/unsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ikuzweshema/code_copilot/HEAD/public/unsplash.png -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ikuzweshema/code_copilot/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ikuzweshema/code_copilot/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "~/app/auth"; 2 | 3 | export const { GET, POST } = handlers; 4 | -------------------------------------------------------------------------------- /lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-clipboard" 2 | export * from "./use-scroll" 3 | export * from "./use-local-storage" 4 | export * from "./use-animated-text" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/api/chat/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { models } from "~/lib/ai/models"; 3 | 4 | export const chatSchema = z.object({ 5 | id: z.string(), 6 | messages: z.array(z.any()), 7 | }); 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules":{ 4 | "@typescript-eslint/no-unused-vars":"off", 5 | "@typescript-eslint/no-explicit-any":"off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from "uploadthing/next"; 2 | import { ourFileRouter } from "./core"; 3 | export const { GET, POST } = createRouteHandler({ 4 | router: ourFileRouter, 5 | }); 6 | -------------------------------------------------------------------------------- /components/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import NavContent from "./nav-content"; 2 | import { auth } from "~/app/auth"; 3 | 4 | export default function Navbar() { 5 | const session = auth(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { generateReactHelpers } from "@uploadthing/react"; 2 | import { type OurFileRouter } from "~/app/api/uploadthing/core"; 3 | 4 | export const { useUploadThing, uploadFiles } = 5 | generateReactHelpers(); 6 | -------------------------------------------------------------------------------- /lib/errors.ts: -------------------------------------------------------------------------------- 1 | class ChatError extends Error { 2 | constructor(message: string, public code: string,{ 3 | 4 | }) { 5 | super(message); 6 | this.name = "ChatError"; 7 | this.code = code; 8 | } 9 | } 10 | 11 | export { ChatError }; -------------------------------------------------------------------------------- /lib/drizzle/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1735802918472, 9 | "tag": "0000_yielding_leader", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /components/auth/providers/index.tsx: -------------------------------------------------------------------------------- 1 | import Provider from "~/components/auth/providers/provider"; 2 | 3 | export default function Providers() { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", ":disableDependencyDashboard"], 4 | "packageRules": [ 5 | { 6 | "matchUpdateTypes": ["minor"], 7 | "automerge": true, 8 | "automergeType": "pr" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | export default defineConfig({ 3 | dbCredentials: { 4 | url: process.env.DATABASE_URL!, 5 | }, 6 | dialect: "postgresql", 7 | out: "./lib/drizzle/migrations", 8 | schema: "./lib/drizzle/schema.ts", 9 | strict: true, 10 | verbose: true, 11 | }); 12 | -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "~/components/auth/login-form"; 2 | import { Metadata } from "next"; 3 | 4 | export const metadata: Metadata = { 5 | title: "ChatBot-Login ", 6 | description: "Login page for the AI ChatBot", 7 | }; 8 | export default async function Page() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import Chat from "~/components/chat"; 2 | import { generateChatId } from "~/lib/ai/utis"; 3 | 4 | export default function Home() { 5 | const chatId = generateChatId(); 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/api/chats/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "~/app/auth"; 2 | import { getChats } from "~/lib/server"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET() { 6 | const session = await auth(); 7 | const id = session?.user?.id; 8 | const chats = await getChats(id); 9 | return NextResponse.json(chats, { status: 200 }); 10 | } -------------------------------------------------------------------------------- /app/robot.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function robot(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: ["/", "/history", "/auth/login"], 8 | disallow: ["/chat","/api"], 9 | }, 10 | sitemap: `${process.env.NEXT_PUBLIC_BASE_URL}/sitemap.xml`, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 3 | import { type ThemeProviderProps } from "next-themes/dist/types"; 4 | 5 | export default function ThemeProvider({ 6 | children, 7 | ...props 8 | }: ThemeProviderProps) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /lib/drizzle/index.ts: -------------------------------------------------------------------------------- 1 | import * as schema from "./schema"; 2 | import { drizzle } from "drizzle-orm/neon-http"; 3 | 4 | const db = drizzle(process.env.DATABASE_URL!, { 5 | schema, 6 | }); 7 | 8 | type Chat = typeof schema.chats.$inferSelect; 9 | type User = typeof schema.users.$inferSelect; 10 | type Account = typeof schema.accounts.$inferSelect; 11 | 12 | export { db, type Chat, type User, type Account }; 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GOOGLE_GENERATIVE_AI_API_KEY=xxxxx 2 | DATABASE_URL=xxxxxxxxxxxxxxxxxxxxxxx 3 | AUTH_GOOGLE_ID=xxxxxxxxxxxxxxxxxxxxxxx 4 | AUTH_GOOGLE_SECRET=xxxxxxxxxxxxxxxxxxxxxx 5 | AUTH_SECRET=xxxxxxxxxxxxxxxxxxxx 6 | NEXT_PUBLIC_BASE_URL=http://localhost:3000 7 | UPLOADTHING_TOKEN=xxxxxxxxxxxxxxxxxxxxxx 8 | UPSTASH_REDIS_REST_URL=xxxxxxxxxxxxxxxx 9 | UPSTASH_REDIS_REST_TOKEN=xxxxxxxxxxxxxxxxxxxx 10 | AUTH_GITHUB_SECRET=xxxxxxxxxxxx 11 | AUTH_GITHUB_SECRET=xxxxxxxxxxxx -------------------------------------------------------------------------------- /lib/ai/utis.ts: -------------------------------------------------------------------------------- 1 | import { createIdGenerator } from "ai"; 2 | import { customAlphabet } from "nanoid"; 3 | 4 | const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; 5 | export const idGenerator = createIdGenerator({ 6 | prefix: "msg_", 7 | size: 14, 8 | alphabet: alphabet, 9 | }); 10 | 11 | export function generateMessageId() { 12 | return idGenerator(); 13 | } 14 | 15 | export const generateChatId = () => { 16 | return customAlphabet(alphabet, 7)(); 17 | }; 18 | -------------------------------------------------------------------------------- /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": "~/lib/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | .yarn/install-state.gz 6 | 7 | # testing 8 | /coverage 9 | 10 | # next.js 11 | /.next/ 12 | /out/ 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | .gitpod.yml 36 | next-env.d.ts 37 | assets/ 38 | .idea/ 39 | 40 | -------------------------------------------------------------------------------- /lib/hooks/use-clipboard.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | function useClipBoard() { 3 | const [isCopied, setIsCopied] = useState(false); 4 | function copyText(text: string|undefined) { 5 | if (!text || typeof window === undefined) return; 6 | navigator.clipboard.writeText(text).then(() => { 7 | setIsCopied(true); 8 | setTimeout(() => { 9 | setIsCopied(false); 10 | }, 2000); 11 | }); 12 | } 13 | 14 | return [isCopied, copyText] as const; 15 | } 16 | export { useClipBoard}; 17 | -------------------------------------------------------------------------------- /app/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "~/components/navbar"; 2 | import { SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar"; 3 | import React from "react"; 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 |
7 | 8 | 9 |
10 | 11 | {children} 12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/ai/user-message.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IconUser } from "~/components/ui/icons"; 3 | 4 | export function UserMessage({ children }: { children: React.ReactNode }) { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | {children} 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /lib/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ChatRequestOptions } from "ai"; 2 | import { Chat } from "../drizzle"; 3 | 4 | type Status = "success" | "error"; 5 | 6 | export type AuthStatus = { 7 | status: Status; 8 | message: string; 9 | }; 10 | export type GroupedChats = { 11 | today: Chat[]; 12 | yesterday: Chat[]; 13 | lastWeek: Chat[]; 14 | lastMonth: Chat[]; 15 | older: Chat[]; 16 | }; 17 | 18 | export type RegenerateFunc = ({ 19 | messageId, 20 | ...options 21 | }?: { 22 | messageId?: string | undefined; 23 | } & ChatRequestOptions) => Promise; 24 | -------------------------------------------------------------------------------- /app/(home)/history/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatHistory from "~/components/chat-history"; 2 | import { ChatHistorySkeleton } from "~/components/skeletons"; 3 | import { getUserChats } from "../../../lib/server"; 4 | import { Suspense } from "react"; 5 | 6 | export const metadata = { 7 | title: "Chats History", 8 | description: "Recent chats", 9 | }; 10 | 11 | export default function Page() { 12 | const chats = getUserChats(); 13 | 14 | return ( 15 | }> 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Navbar from "~/components/navbar"; 3 | import { SidebarProvider, SidebarTrigger } from "~/components/ui/sidebar"; 4 | 5 | export default function Layout({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
8 | 9 | 10 |
11 | 12 | {children} 13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | function useLocalStorage(key: string, initialValue: T) { 3 | const [item, setItem] = useState(() => { 4 | if (typeof window === "undefined") { 5 | return initialValue; 6 | } 7 | const item = window.localStorage.getItem(key); 8 | return item ? (JSON.parse(item) as T) : initialValue; 9 | }); 10 | useEffect(() => { 11 | window.localStorage.setItem(key, JSON.stringify(item)); 12 | }, [key, item]); 13 | 14 | return [item, setItem] as const; 15 | } 16 | export { useLocalStorage }; 17 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "~/app/auth"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export default auth((request) => { 5 | const { nextUrl } = request; 6 | const isLoggedIn = !!request.auth?.user; 7 | const isAuthRoute = nextUrl.pathname.startsWith("/auth"); 8 | if (isAuthRoute) { 9 | if (isLoggedIn) { 10 | return NextResponse.redirect(new URL("/", nextUrl)); 11 | } 12 | return NextResponse.next(); 13 | } 14 | 15 | return NextResponse.next(); 16 | }); 17 | export const config = { 18 | matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], 19 | }; 20 | -------------------------------------------------------------------------------- /lib/types/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | const fileSchema = z 3 | .instanceof(File, { message: "File is Required" }) 4 | .refine((file) => file.type.startsWith("application/pdf"), { 5 | message: "Only PDF files supported.", 6 | }); 7 | 8 | const editChatSchema = z.object({ 9 | chatId: z.string().min(10, { 10 | message: "Chat does not exist", 11 | }), 12 | title: z 13 | .string() 14 | .min(3, { 15 | message: "Chat title too small", 16 | }) 17 | .max(40, { 18 | message: "Please use ashort title", 19 | }), 20 | }); 21 | 22 | export { fileSchema, editChatSchema }; 23 | -------------------------------------------------------------------------------- /app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | return [ 5 | { 6 | url: `${process.env.NEXT_PUBLIC_BASE_URL}/`, 7 | lastModified: new Date().toDateString(), 8 | priority: 1, 9 | }, 10 | { 11 | url: `${process.env.NEXT_PUBLIC_BASE_URL}/history`, 12 | lastModified: new Date().toDateString(), 13 | priority: 0.8, 14 | }, 15 | { 16 | url: `${process.env.NEXT_PUBLIC_BASE_URL}/auth/login`, 17 | lastModified: new Date().toDateString(), 18 | priority: 0.8, 19 | }, 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /lib/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /components/navbar/toggle-mode.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTheme } from "next-themes"; 3 | import { Switch } from "../ui/switch"; 4 | import { useState } from "react"; 5 | 6 | export default function ModeToggle() { 7 | const { setTheme, theme } = useTheme(); 8 | const [checked, setChecked] = useState(theme === "dark"); 9 | function toggleMode() { 10 | setTheme(theme === "dark" ? "light" : "dark"); 11 | setChecked(!checked); 12 | return; 13 | } 14 | return ( 15 |
16 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /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 | "~/*": ["./*"] 22 | }, 23 | "target": "ES2018" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/ai/message.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { FileText } from "lucide-react"; 3 | import { UIMessage } from "ai"; 4 | 5 | interface MessageProps { 6 | message: UIMessage; 7 | attachment?: File; 8 | text?: string; 9 | } 10 | export default function MessageText({ attachment, text }: MessageProps) { 11 | return ( 12 |
13 | {attachment && ( 14 |
15 | 16 |
{attachment.name}
17 |
18 | )} 19 |
20 | {text} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /lib/data.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | NextJsIcon, 3 | PrismaIcon, 4 | ReactIcon, 5 | NodejsIcon, 6 | DrizzleORM, 7 | } from "~/components/ui/icons"; 8 | import React from "react"; 9 | 10 | const exampleMessages: Array<{ heading: string; icon: React.ReactNode }> = [ 11 | { 12 | heading: "Create a basic HTTP server using Node.js", 13 | icon: , 14 | }, 15 | { 16 | heading: "Setup Drizzle ORM", 17 | icon: , 18 | }, 19 | { 20 | heading: "Implement server-side rendering with Next.js App Router?", 21 | icon: , 22 | }, 23 | { 24 | heading: "What are React hooks ?", 25 | icon: , 26 | }, 27 | ]; 28 | 29 | export { exampleMessages }; 30 | -------------------------------------------------------------------------------- /components/ai/spinner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export const spinner = ( 4 | 14 | 15 | 16 | ); 17 | 18 | export default function Spinner() { 19 | return ( 20 |
21 |
22 | {spinner} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/auth/alert.tsx: -------------------------------------------------------------------------------- 1 | import { AuthStatus } from "~/lib/types"; 2 | import { Alert, AlertDescription } from "~/components/ui/alert"; 3 | import { CheckCheck, ShieldAlert, TriangleAlert } from "lucide-react"; 4 | 5 | export default function AlertMessage(status: AuthStatus) { 6 | return ( 7 | 11 | 12 | 13 | {status.status === "success" ? ( 14 | 15 | ) : ( 16 | 17 | )} 18 | {status.message} 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | import { cn } from "~/lib/utils"; 7 | 8 | const labelVariants = cva( 9 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 10 | ); 11 | 12 | const Label = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef & 15 | VariantProps 16 | >(({ className, ...props }, ref) => ( 17 | 22 | )); 23 | Label.displayName = LabelPrimitive.Root.displayName; 24 | 25 | export { Label }; -------------------------------------------------------------------------------- /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 | 24 | 25 | -------------------------------------------------------------------------------- /components/chat/empty-messages.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent } from "~/components/ui/card"; 2 | import { cn } from "~/lib/utils"; 3 | import { useAnimatedText } from "~/lib/hooks"; 4 | import { useSession } from "next-auth/react"; 5 | 6 | interface Props { 7 | onSubmit: (message: string) => void; 8 | } 9 | 10 | export default function EmptyScreen({ onSubmit }: Props) { 11 | const session = useSession(); 12 | const name = session.data?.user?.name?.split(" ")[0]; 13 | const [text] = useAnimatedText( 14 | `${name ? name + " !" : ""} How can I Assist you ?`, 15 | { 16 | duration: 4, 17 | shouldAnimate: true, 18 | }, 19 | ); 20 | return ( 21 |
22 | 23 | {text} 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "~/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "lh3.googleusercontent.com", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "avatars.githubusercontent.com", 13 | }, 14 | { 15 | protocol: "https", 16 | hostname: "utfs.io", 17 | }, 18 | { 19 | protocol: "http", 20 | hostname: "localhost", 21 | port: "3000", 22 | }, 23 | { 24 | protocol: "https", 25 | hostname: "1f1pkbmpee.ufs.sh", 26 | }, 27 | ], 28 | }, 29 | typescript: { 30 | ignoreBuildErrors: true, 31 | }, 32 | eslint: { 33 | ignoreDuringBuilds: true, 34 | }, 35 | experimental: { 36 | reactCompiler: true, 37 | }, 38 | }; 39 | 40 | export default nextConfig; 41 | -------------------------------------------------------------------------------- /app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Chat from "~/components/chat"; 2 | import { getChatById } from "~/lib/server"; 3 | import { capitalize } from "~/lib/utils"; 4 | import { Metadata } from "next"; 5 | import { notFound } from "next/navigation"; 6 | 7 | interface PageParams { 8 | params: Promise<{ id: string }>; 9 | } 10 | 11 | export async function generateMetadata({ 12 | params, 13 | }: PageParams): Promise { 14 | const { id } = await params; 15 | 16 | const chat = await getChatById(id); 17 | if (!chat) notFound(); 18 | return { 19 | title: capitalize(chat?.title || "Untitled"), 20 | description: chat?.title, 21 | }; 22 | } 23 | export default async function Page({ params }: PageParams) { 24 | const { id } = await params; 25 | const chat = await getChatById(id); 26 | if (!chat) notFound(); 27 | const messages = chat.messages; 28 | return ; 29 | } 30 | -------------------------------------------------------------------------------- /lib/ai/prompt.ts: -------------------------------------------------------------------------------- 1 | export const systemPrompt = ` 2 | You are CodeAssist, a focused programming assistant today's date is ${new Date().toISOString()}. Your core behaviors: 3 | 4 | 1. PROGRAMMING TASKS 5 | - Explain code clearly with examples 6 | - Help with debugging and troubleshooting 7 | - Suggest optimizations and best practices 8 | - Assist with software architecture 9 | - Guide testing and documentation 10 | 11 | 2. RESPONSES 12 | - Use clear explanations with code examples 13 | - Include relevant comments 14 | - Highlight potential issues 15 | - Consider performance and security 16 | 17 | 3. LIMITATIONS 18 | For non-programming queries: 19 | "I'm a programming assistant focused on software development. I can help you with coding, debugging, testing, and software design instead." 20 | 21 | For unsupported features: 22 | "This feature isn't currently supported, but I can help by explaining the concept or suggesting alternatives." 23 | 24 | Always prioritize code quality, security, and maintainability in all responses.`; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { systemPrompt } from "~/lib/ai/prompt"; 3 | import { chatSchema } from "./schema"; 4 | import { models } from "~/lib/ai/models"; 5 | import { convertToModelMessages, streamText } from "ai"; 6 | import { saveChatData } from "~/lib/server"; 7 | import { cookies } from "next/headers"; 8 | 9 | export async function POST(request: NextRequest) { 10 | const parsedBody = chatSchema.parse(await request.json()); 11 | const { id, messages } = parsedBody; 12 | const cookieStore = await cookies(); 13 | const modelId = cookieStore.get("model.id")?.value; 14 | const model = models.find((m) => m.id === Number(modelId)) ?? models[0]; 15 | const coreMessage = convertToModelMessages(messages); 16 | const result = streamText({ 17 | model: model.model, 18 | messages: coreMessage, 19 | system: systemPrompt, 20 | }); 21 | return result.toUIMessageStreamResponse({ 22 | async onFinish({ messages, responseMessage }) { 23 | await saveChatData(id, [...messages, responseMessage]); 24 | }, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /components/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardHeader, 6 | CardTitle, 7 | } from "~/components/ui/card"; 8 | import Providers from "~/components/auth/providers"; 9 | import { AssitantIcon } from "~/components/ui/icons"; 10 | 11 | export function LoginForm() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 | Welcome back, 21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/chat/auto-scoller.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, forwardRef } from "react"; 2 | 3 | interface AutoScrollerProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | const AutoScroller = forwardRef( 9 | ({ children, className }, ref) => { 10 | useEffect(() => { 11 | if (!ref || typeof ref === 'function' || !ref.current) return; 12 | 13 | const observer = new MutationObserver(async () => { 14 | if (ref.current) { 15 | ref.current.scroll({ 16 | top: ref.current.scrollHeight, 17 | behavior: "smooth", 18 | }); 19 | } 20 | }); 21 | 22 | observer.observe(ref.current, { 23 | childList: true, 24 | subtree: true, 25 | }); 26 | 27 | return () => { 28 | observer.disconnect(); 29 | }; 30 | }, [ref]); 31 | 32 | return ( 33 |
34 | {children} 35 |
36 | ); 37 | } 38 | ); 39 | 40 | AutoScroller.displayName = "AutoScroller"; 41 | export { AutoScroller }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 elisa 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 | -------------------------------------------------------------------------------- /components/search.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from "lucide-react"; 2 | import { Input } from "~/components/ui/input"; 3 | import React from "react"; 4 | import Form from "next/form"; 5 | import { cn } from "~/lib/utils"; 6 | 7 | interface SearchProps { 8 | searchTerm: string; 9 | setSearchTerm: React.Dispatch>; 10 | placeholder?: string; 11 | searchParams?: string; 12 | className?: string; 13 | } 14 | 15 | export default function SearchInput({ 16 | searchTerm, 17 | setSearchTerm, 18 | placeholder, 19 | searchParams, 20 | className, 21 | }: SearchProps) { 22 | return ( 23 |
24 |
25 | 26 | setSearchTerm(e.target.value)} 31 | className={cn("pl-8", className)} 32 | /> 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/api/file/download/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getFileType } from "~/lib/server/helpers"; 3 | 4 | export async function GET(request: NextRequest) { 5 | const searchParams = request.nextUrl.searchParams; 6 | const url = searchParams.get("url"); 7 | if (!url) { 8 | return NextResponse.json( 9 | { message: "file url is required" }, 10 | { status: 404 }, 11 | ); 12 | } 13 | const res = await fetch(url); 14 | 15 | if (!res.ok) { 16 | return NextResponse.json({ message: "File not found" }, { status: 404 }); 17 | } 18 | 19 | const fileBuffer = Buffer.from(await res.arrayBuffer()); 20 | const fileType = await getFileType(fileBuffer); 21 | const fileSize = fileBuffer.byteLength; 22 | if (!fileType) { 23 | return NextResponse.json({ message: "Invalid file type" }, { status: 400 }); 24 | } 25 | return new NextResponse(fileBuffer, { 26 | headers: { 27 | "Content-Disposition": `attachment; filename="attachment.${fileType.ext}"`, 28 | "Content-Type": fileType.mime, 29 | "Content-Length": fileSize.toString(), 30 | }, 31 | status: 200, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /app/chat/[id]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; 3 | import { Button } from "~/components/ui/button"; 4 | import { AlertTriangle } from "lucide-react"; 5 | 6 | export default function Error({ 7 | error, 8 | reset, 9 | }: { 10 | error: Error & { digest?: string }; 11 | reset: () => void; 12 | }) { 13 | return ( 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | {"something went wrong our team is already notified"} 24 | 25 | 26 | 27 | 30 |
31 |
32 |
33 | ); 34 | } -------------------------------------------------------------------------------- /app/(home)/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; 3 | import { Button } from "~/components/ui/button"; 4 | import { TriangleAlert } from "lucide-react"; 5 | 6 | export default function Error({ 7 | error, 8 | reset, 9 | }: { 10 | error: Error & { digest?: string }; 11 | reset: () => void; 12 | }) { 13 | return ( 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | {"something went wrong our team is already notified"} 24 | 25 | 26 | 27 | 28 | 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Google from "next-auth/providers/google"; 3 | import Github from "next-auth/providers/github"; 4 | import { DrizzleAdapter } from "@auth/drizzle-adapter"; 5 | import { db } from "~/lib/drizzle"; 6 | import { accounts, users } from "~/lib/drizzle/schema"; 7 | 8 | const { signIn, signOut, handlers, auth } = NextAuth({ 9 | adapter: DrizzleAdapter(db, { 10 | usersTable: users, 11 | accountsTable: accounts, 12 | }), 13 | providers: [ 14 | Google({ 15 | clientId: process.env.AUTH_GOOGLE_ID, 16 | clientSecret: process.env.AUTH_GOOGLE_SECRET, 17 | }), 18 | Github({ 19 | clientSecret: process.env.AUTH_GITHUB_SECRET, 20 | clientId: process.env.AUTH_GITHUB_ID, 21 | }), 22 | ], 23 | session: { 24 | strategy: "jwt", 25 | }, 26 | pages: { 27 | error: "/auth/error", 28 | signIn: "/auth/login", 29 | }, 30 | trustHost: true, 31 | callbacks: { 32 | jwt: async ({ token, user }) => { 33 | if (user) { 34 | token.sub = user.id; 35 | } 36 | return token; 37 | }, 38 | session: async ({ session, token }) => { 39 | session.user.id = token.sub as string; 40 | return session; 41 | }, 42 | }, 43 | }); 44 | 45 | export { signIn, signOut, handlers, auth }; 46 | -------------------------------------------------------------------------------- /components/ai/spinner-message.tsx: -------------------------------------------------------------------------------- 1 | import { AssitantIcon } from "~/components/ui/icons"; 2 | import { Loader2 } from "lucide-react"; 3 | import { Button } from "../ui/button"; 4 | export function SpinnerMessage() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 |
12 | Copilot is thinking... 13 |
14 |
15 |
16 | ); 17 | } 18 | 19 | interface LoadingButtonProps { 20 | stop: () => void; 21 | } 22 | 23 | export function LoadingButton({ stop }: LoadingButtonProps) { 24 | return ( 25 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; -------------------------------------------------------------------------------- /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 | 19 | 28 | 29 | )) 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Code Copilot 2 | 3 | Code Copilot Chat bot 4 | 5 | > Some Features may not work or Contain bugs. 6 | > 7 | > Contributions are welcome 8 | 9 | ## Features 10 | 11 | - [x] Generate Response 12 | - [x] Copy messages Response 13 | - [x] Regenerate message 14 | - [x] Save Chat into database 15 | - [x] Retrieve Chat from database 16 | - [x] Upload Attachments 17 | - [ ] Edit Messages 18 | - [x] Delete Chat from database 19 | - [x] Update Chat from database 20 | - [x] Show Chat History 21 | - [ ] Canvas Feature 22 | - [ ] Run code using web container 23 | 24 | ## Technologies 25 | 26 | - [Nextjs App Router](https://nextjs.org) - Framework for React 27 | - [React](https://reactjs.org) - A JavaScript library for building user interfaces 28 | - [Nodejs](https://nodejs.org) - A JavaScript runtime 29 | - [ai](https://sdk.vercel.ai) - A Library for AI building Applications 30 | - [Auth js](https://authjs.dev/) - Authentication Library 31 | 32 | ## Installation 33 | 34 | ```bash 35 | pnpm install 36 | ``` 37 | 38 | ```bash 39 | pnpm run dev 40 | ``` 41 | 42 | ## License 43 | 44 | MIT License 45 | 46 | ## Contributing 47 | 48 | 1. Fork it! 49 | 2. Create your feature branch: `git checkout -b my-new-feature` 50 | 3. Commit your changes: `git commit -am 'Add some feature'` 51 | 4. Push to the branch: `git push origin my-new-feature` 52 | 5. Submit a pull request :D 53 | -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 2 | import { UploadThingError } from "uploadthing/server"; 3 | import { auth as authUser } from "~/app/auth"; 4 | import { ratelimit } from "~/lib/server/helpers"; 5 | const f = createUploadthing({ 6 | errorFormatter: (error) => { 7 | throw new UploadThingError(error.message); 8 | }, 9 | }); 10 | const auth = (req: Request) => { 11 | const user = authUser(); 12 | return user; 13 | }; 14 | 15 | export const ourFileRouter = { 16 | imageUploader: f({ 17 | image: { 18 | maxFileSize: "4MB", 19 | maxFileCount: 3, 20 | }, 21 | text: { 22 | maxFileSize: "2MB", 23 | maxFileCount: 3, 24 | }, 25 | }) 26 | .middleware(async ({ req }) => { 27 | const user = await auth(req); 28 | const userId = user?.user?.id; 29 | if (!userId) 30 | throw new UploadThingError("Please login to upload attachments."); 31 | const { success } = await ratelimit.limit(userId); 32 | if (!success) { 33 | throw new UploadThingError("rate limited"); 34 | } 35 | return { userId: user.user?.id }; 36 | }) 37 | .onUploadComplete(async ({ metadata, file }) => { 38 | return { uploadedBy: metadata.userId }; 39 | }), 40 | } satisfies FileRouter; 41 | 42 | export type OurFileRouter = typeof ourFileRouter; 43 | -------------------------------------------------------------------------------- /lib/server/helpers.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | import { google } from "@ai-sdk/google"; 3 | import { Ratelimit } from "@upstash/ratelimit"; 4 | import { Redis } from "@upstash/redis"; 5 | import { fileTypeFromBuffer } from "file-type"; 6 | import { generateObject, UIMessage, convertToModelMessages } from "ai"; 7 | import { z } from "zod"; 8 | import { UTApi } from "uploadthing/server"; 9 | 10 | export const utpapi = new UTApi({ 11 | token: process.env.UPLOADTHING_TOKEN, 12 | }); 13 | export const ratelimit = new Ratelimit({ 14 | redis: Redis.fromEnv(), 15 | limiter: Ratelimit.fixedWindow(5, "5h"), 16 | }); 17 | 18 | async function getChatTitle(messages: UIMessage[]) { 19 | const modelMessages = convertToModelMessages(messages); 20 | const title = await generateObject({ 21 | model: google("gemini-2.0-flash-exp"), 22 | system: `you are a chat title generator assistant based The main context in chat messages about programming concepts. 23 | if you are given achat message generate a small title for it`, 24 | messages: modelMessages, 25 | schema: z.object({ 26 | title: z.string().describe("chat title"), 27 | }), 28 | }); 29 | 30 | return title.object.title; 31 | } 32 | async function getFileType(buffer: ArrayBuffer) { 33 | const fileType = await fileTypeFromBuffer(buffer); 34 | return fileType; 35 | } 36 | 37 | export { getChatTitle, getFileType }; 38 | -------------------------------------------------------------------------------- /lib/hooks/use-animated-text.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { animate, MotionValue, useMotionValue } from "motion/react"; 3 | interface AnimatedTextOptions { 4 | duration?: number; 5 | shouldAnimate?: boolean; 6 | onComplete?: () => void; 7 | } 8 | function useAnimatedText(text: string, options: AnimatedTextOptions) { 9 | const { duration = 3, shouldAnimate = true, onComplete = () => {} } = options; 10 | const [textIndex, setTextIndex] = useState(() => { 11 | return options.shouldAnimate ? 0 : text.length; 12 | }); 13 | const animatedIndex = useMotionValue(0); 14 | const [isAnimating, setIsAnimating] = useState(shouldAnimate); 15 | useEffect(() => { 16 | if (!shouldAnimate) { 17 | return; 18 | } 19 | const controls = animate( 20 | animatedIndex, 21 | text.trim().length as unknown as MotionValue, 22 | { 23 | duration: duration, 24 | ease: "linear", 25 | onUpdate: (latest: number) => setTextIndex(Math.floor(latest)), 26 | onComplete: () => { 27 | setIsAnimating(false); 28 | onComplete(); 29 | }, 30 | } 31 | ); 32 | return () => controls.stop(); 33 | }, [text.length, options.duration, animatedIndex]); 34 | return [text.split("").slice(0, textIndex).join(""), isAnimating] as const; 35 | } 36 | 37 | export { useAnimatedText }; 38 | -------------------------------------------------------------------------------- /lib/hooks/use-scroll.ts: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useCallback, useEffect } from "react"; 2 | import { useInView } from "react-intersection-observer"; 3 | 4 | function useScroll() { 5 | const [isAtBottom, setIsAtBottom] = useState(true); 6 | const messagesRef = useRef(null); 7 | 8 | const { ref: visibilityRef, inView: isVisible } = useInView({ 9 | triggerOnce: false, 10 | delay: 100, 11 | rootMargin: "0px 0px 0px 0px", 12 | }); 13 | const handleScroll = (e: React.UIEvent) => { 14 | const target = e.target as T; 15 | const offset = 25; 16 | const isAtBottom = 17 | target.scrollTop + target.clientHeight >= target.scrollHeight - offset; 18 | setIsAtBottom(isAtBottom); 19 | }; 20 | const scrollToBottom = useCallback(() => { 21 | if (messagesRef.current) { 22 | messagesRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); 23 | } 24 | }, []); 25 | useEffect(() => { 26 | if (messagesRef.current) { 27 | if (isAtBottom && !isVisible) { 28 | messagesRef.current.scrollIntoView({ 29 | block: "end", 30 | behavior: "instant", 31 | }); 32 | } 33 | } 34 | }, [isAtBottom, isVisible]); 35 | 36 | return { 37 | messagesRef, 38 | visibilityRef, 39 | scrollToBottom, 40 | isAtBottom, 41 | handleScroll, 42 | }; 43 | } 44 | 45 | export { useScroll }; 46 | -------------------------------------------------------------------------------- /lib/helpers.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CIcon, 3 | CSharpIcon, 4 | CssIcon, 5 | Go, 6 | HtmlIcon, 7 | JavaIcon, 8 | JavaScriptIcon, 9 | PHPIcon, 10 | PrismaIcon, 11 | PythonIcon, 12 | ReactIcon, 13 | TypeScriptIcon, 14 | } from "~/components/ui/icons"; 15 | import { CpuIcon } from "lucide-react"; 16 | import React from "react"; 17 | 18 | function getLanguageIcon(language: string): React.ReactNode { 19 | switch (language) { 20 | case "jsx": 21 | return ; 22 | case "typescript": 23 | return ; 24 | case "javascript": 25 | return ; 26 | case "php": 27 | return ; 28 | case "c": 29 | return ; 30 | case "java": 31 | return ; 32 | case "python": 33 | return ; 34 | case "cpp": 35 | return ; 36 | case "csharp": 37 | return ; 38 | case "css": 39 | return ; 40 | case "html": 41 | return ; 42 | case "prisma": 43 | return ; 44 | case "go": 45 | case "golang": 46 | return ; 47 | default: 48 | return {language}; 49 | } 50 | } 51 | 52 | export { getLanguageIcon }; 53 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "~/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/chat/scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | import { Button } from "../ui/button"; 3 | import { ChevronDown } from "lucide-react"; 4 | import { motion, AnimatePresence } from "motion/react"; 5 | 6 | interface Props { 7 | isAtBottom: boolean; 8 | scrollToBottom: () => void; 9 | } 10 | function ScrollAnchor({ isAtBottom, scrollToBottom }: Props) { 11 | return ( 12 | 13 | 19 | 29 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default ScrollAnchor; 48 | -------------------------------------------------------------------------------- /lib/hooks/use-search.ts: -------------------------------------------------------------------------------- 1 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 2 | import { useEffect, useState } from "react"; 3 | import { useDebouncedCallback } from "use-debounce"; 4 | 5 | function useSearch( 6 | items: T[], 7 | options: { 8 | predicate: (item: T, query: string) => boolean; 9 | debounce?: number; 10 | searchParams?: string; 11 | } 12 | ) { 13 | const params = useSearchParams(); 14 | const [filtered, setFiltered] = useState(items); 15 | const [query, setQuery] = useState(() => { 16 | return params.get(options?.searchParams || "query") || ""; 17 | }); 18 | const pathname = usePathname(); 19 | const router = useRouter(); 20 | const search = useDebouncedCallback(() => { 21 | const searchParams = new URLSearchParams(params); 22 | if (query.trim() === "") { 23 | searchParams.delete("query"); 24 | setFiltered(items); 25 | router.replace(`${pathname}`); 26 | return; 27 | } 28 | const newItems: T[] = []; 29 | for (const item of items) { 30 | if (options.predicate(item, query.toLocaleLowerCase())) { 31 | newItems.push(item); 32 | } 33 | } 34 | setFiltered(newItems); 35 | searchParams.set(options?.searchParams || "query", query); 36 | router.replace(`${pathname}?${searchParams.toString()}`); 37 | }, options?.debounce || 200); 38 | 39 | useEffect(() => { 40 | search(); 41 | }, [query, search]); 42 | 43 | return [query, setQuery, filtered] as const; 44 | } 45 | 46 | export default useSearch; 47 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 | import { cn } from "~/lib/utils"; 6 | 7 | import { Sun, Moon } from "lucide-react"; 8 | 9 | const Switch = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 26 | {props.checked ? ( 27 | 28 | ) : ( 29 | 30 | )} 31 | 32 | 33 | )); 34 | Switch.displayName = SwitchPrimitives.Root.displayName; 35 | 36 | export { Switch }; 37 | -------------------------------------------------------------------------------- /components/chat/message.tsx: -------------------------------------------------------------------------------- 1 | import { UserMessage } from "~/components/ai/user-message"; 2 | import ViewAttachment from "~/components/chat/view-attachement"; 3 | import { UIMessage } from "ai"; 4 | import { BotMessage } from "~/components/ai/bot-message"; 5 | import { useMemo } from "react"; 6 | import { RegenerateFunc } from "~/lib/types"; 7 | 8 | interface MessageProps { 9 | message: UIMessage; 10 | regenerate: RegenerateFunc; 11 | loading: boolean; 12 | } 13 | export default function Message({ 14 | message, 15 | regenerate, 16 | loading, 17 | }: MessageProps) { 18 | const { text, files } = useMemo(() => { 19 | const parts = message?.parts || []; 20 | let text = parts.find((part) => part?.type === "text")?.text ?? ""; 21 | const files = parts.filter((part) => part?.type === "file"); 22 | return { 23 | files, 24 | text, 25 | }; 26 | }, [message]); 27 | 28 | return ( 29 |
30 | {message.role === "user" ? ( 31 | 32 |
33 | {files.map((part, index) => ( 34 | 35 | ))} 36 | {text} 37 |
38 |
39 | ) : ( 40 | <> 41 | 46 | 47 | )} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/auth/providers/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useActionState } from "react"; 3 | import { Button } from "~/components/ui/button"; 4 | import signInWithProvider from "~/lib/server/actions"; 5 | import { GitHubLogoIcon, ReloadIcon } from "@radix-ui/react-icons"; 6 | import AlertMessage from "~/components/auth/alert"; 7 | import Image from "next/image"; 8 | import { BuiltInProviderType } from "@auth/core/providers"; 9 | import { Card } from "~/components/ui/card"; 10 | import { Github } from "lucide-react"; 11 | 12 | type ProviderProps = { 13 | name: BuiltInProviderType; 14 | }; 15 | export default function Provider({ name }: ProviderProps) { 16 | const [status, dispatch, isPending] = useActionState( 17 | signInWithProvider, 18 | undefined 19 | ); 20 | return ( 21 | 22 |
23 | 24 | 39 |
40 | {status?.status && } 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/chat/[id]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import Link from "next/link"; 3 | import { MessageSquarePlus } from "lucide-react"; 4 | import { Button } from "~/components/ui/button"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardFooter, 10 | CardHeader, 11 | CardTitle, 12 | } from "~/components/ui/card"; 13 | 14 | export const metadata: Metadata = { 15 | title: "Page Not Found", 16 | description: "The page you're looking for doesn't exist.", 17 | }; 18 | 19 | export default function NotFound() { 20 | return ( 21 |
22 | 23 | 24 | 25 | 404 26 | 27 | 28 | Not Found 29 | 30 | 31 | 32 |

33 | Oops! The chat you're looking for doesn't exist or has 34 | been removed. 35 |

36 |
37 | 38 | 44 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import ThemeProvider from "~/components/providers/theme-provider"; 3 | import { TooltipProvider } from "~/components/ui/tooltip"; 4 | import { SessionProvider } from "next-auth/react"; 5 | import { Toaster } from "~/components/ui/sonner"; 6 | import { cn } from "~/lib/utils"; 7 | import { Geist } from "next/font/google"; 8 | 9 | import React from "react"; 10 | export const metadata = { 11 | title: { 12 | default: "Code Copilot", 13 | template: "%s | Code Copilot", 14 | }, 15 | description: "A Programming AI Assistant", 16 | icons: { 17 | icon: "/favicon.ico", 18 | }, 19 | metadataBase: new URL("https://code-copilot.vercel.app"), 20 | keywords: [ 21 | "Programming assistant", 22 | "Code analysis", 23 | "AI-powered coding", 24 | "Code debugging", 25 | ], 26 | }; 27 | 28 | const geist = Geist({ 29 | display: "swap", 30 | subsets: ["latin"], 31 | weight: "400", 32 | }); 33 | 34 | export default function RootLayout({ 35 | children, 36 | }: Readonly<{ 37 | children: React.ReactNode; 38 | }>) { 39 | return ( 40 | 41 | 45 | 46 | 47 | 53 | {children} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /components/navbar/nav-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { MessageSquareText } from "lucide-react"; 3 | import Link from "next/link"; 4 | import { SidebarMenuButton, SidebarMenuItem } from "~/components/ui/sidebar"; 5 | import { cn } from "~/lib/utils"; 6 | import { usePathname } from "next/navigation"; 7 | import { Chat } from "~/lib/drizzle"; 8 | import { useAnimatedText, useLocalStorage } from "~/lib/hooks"; 9 | 10 | interface NavItemProps { 11 | chat: Chat; 12 | } 13 | 14 | export default function NavItem({ chat }: NavItemProps) { 15 | const pathname = usePathname(); 16 | const path = `/chat/${chat.id}`; 17 | const isActive = pathname === path; 18 | const [newChat, setNewChat] = useLocalStorage("chatId", null); 19 | const animate = chat.id === newChat; 20 | 21 | const [text] = useAnimatedText(chat.title, { 22 | shouldAnimate: animate, 23 | duration: 2, 24 | onComplete() { 25 | setNewChat(null); 26 | }, 27 | }); 28 | return ( 29 | 30 | 37 | 38 |
44 | 45 |
46 | {text} 47 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import { GroupedChats } from "~/lib/types"; 4 | import { Chat } from "~/lib/drizzle"; 5 | import { 6 | isToday, 7 | isYesterday, 8 | subMonths, 9 | subWeeks, 10 | formatDistanceToNow, 11 | } from "date-fns"; 12 | 13 | export function cn(...inputs: ClassValue[]) { 14 | return twMerge(clsx(inputs)); 15 | } 16 | export const sleep = (ms: number) => { 17 | return new Promise((resolve) => setTimeout(resolve, ms)); 18 | }; 19 | 20 | export function capitalize(text: string) { 21 | return text.charAt(0).toUpperCase() + text.slice(1).toLocaleLowerCase(); 22 | } 23 | export async function fetcher(url: string) { 24 | const res = await fetch(url); 25 | return await res.json(); 26 | } 27 | 28 | export function groupChats(chats: Chat[]): GroupedChats { 29 | return chats.reduce( 30 | (acc, chat) => { 31 | const chatDate = new Date(chat.updatedAt); 32 | if (isToday(chatDate)) { 33 | acc.today.push(chat); 34 | } else if (isYesterday(chatDate)) { 35 | acc.yesterday.push(chat); 36 | } else if (chatDate > subWeeks(new Date(), 1)) { 37 | acc.lastWeek.push(chat); 38 | } else if (chatDate > subMonths(new Date(), 1)) { 39 | acc.lastMonth.push(chat); 40 | } else { 41 | acc.older.push(chat); 42 | } 43 | return acc; 44 | }, 45 | { 46 | today: [], 47 | yesterday: [], 48 | lastWeek: [], 49 | lastMonth: [], 50 | older: [], 51 | } as GroupedChats 52 | ); 53 | } 54 | 55 | export function formatTime(chatDate: Date): string { 56 | const date = new Date(chatDate); 57 | 58 | const formated = formatDistanceToNow(date); 59 | return `${formated} ago`; 60 | } 61 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "~/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }, 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = "AlertTitle"; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = "AlertDescription"; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "~/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /components/chat/messages.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { forwardRef } from "react"; 3 | import { UIMessage } from "ai"; 4 | import { BotMessage } from "~/components/ai/bot-message"; 5 | import { RegenerateFunc } from "~/lib/types"; 6 | import Message from "./message"; 7 | import { SpinnerMessage } from "../ai/spinner-message"; 8 | import ButtonRow from "../ai/button-row"; 9 | interface MessageProps { 10 | messages: UIMessage[]; 11 | error: Error | undefined; 12 | isLoading: boolean; 13 | regenerate: RegenerateFunc; 14 | } 15 | 16 | const Messages = forwardRef(function Messages( 17 | { messages, error, isLoading, regenerate }: MessageProps, 18 | ref, 19 | ) { 20 | return ( 21 |
27 | {messages.map((message) => ( 28 |
29 | 34 |
35 | ))} 36 | {isLoading && messages[messages.length - 1].role === "user" && ( 37 |
38 | 39 |
40 | )} 41 | {error && ( 42 |
43 | 48 | Unable to generate response. Please try again 49 | 50 | 51 | {!isLoading ? : null} 52 |
53 | )} 54 |
55 | ); 56 | }); 57 | Messages.displayName = "Messages"; 58 | 59 | export default Messages; 60 | -------------------------------------------------------------------------------- /components/navbar/nav-links.tsx: -------------------------------------------------------------------------------- 1 | import { BookOpen } from "lucide-react"; 2 | import { 3 | SidebarGroup, 4 | SidebarContent, 5 | SidebarMenuItem, 6 | SidebarMenuButton, 7 | } from "../ui/sidebar"; 8 | import Link from "next/link"; 9 | 10 | import { AssitantIcon } from "../ui/icons"; 11 | import { usePathname } from "next/navigation"; 12 | import { cn } from "~/lib/utils"; 13 | 14 | export default function NavLinks() { 15 | const links = [ 16 | { 17 | label: "New chat", 18 | href: "/", 19 | icon: , 20 | }, 21 | { 22 | label: "History", 23 | href: "/history", 24 | icon: , 25 | }, 26 | ]; 27 | const pathname = usePathname(); 28 | const isActive = (href: string) => { 29 | return pathname === href; 30 | }; 31 | return ( 32 | 33 | 34 | 35 | {links.map((link) => ( 36 | 37 | 44 |
52 | {link.icon} 53 |
54 | 55 | 56 | {link.label} 57 | 58 | 59 |
60 | ))} 61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/chat-history.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import ChatItem from "~/components/chat-item"; 3 | import SearchInput from "./search"; 4 | import useSearch from "~/lib/hooks/use-search"; 5 | import { MoveRight, Search } from "lucide-react"; 6 | import {Chat,User} from "~/lib/drizzle" 7 | import { Button } from "./ui/button"; 8 | import Link from "next/link"; 9 | 10 | import { use } from "react"; 11 | interface Props { 12 | chatsPromise: Promise>; 13 | } 14 | export default function ChatHistory({ chatsPromise }: Props) { 15 | const initialChats = use(chatsPromise); 16 | 17 | const [searchText, setSearchText, chats] = useSearch(initialChats, { 18 | predicate: (item, query) => { 19 | return item.title.toLocaleLowerCase().includes(query); 20 | }, 21 | debounce: 400, 22 | searchParams: "chat", 23 | }); 24 | 25 | return ( 26 |
27 |
28 |
29 | 36 |
37 |
38 | {chats && chats.length > 0 ? ( 39 | chats.map((chat) => ) 40 | ) : ( 41 |
42 | 43 | 44 | No Recent Chats Found{" "} 45 | 46 | 51 |
52 | )} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /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 | import { cn } from "~/lib/utils" 5 | 6 | const buttonVariants = cva( 7 | "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", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-9 px-4 py-2", 24 | sm: "h-8 rounded-md px-3 text-xs", 25 | lg: "h-10 rounded-md px-8", 26 | icon: "h-9 w-9", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /lib/drizzle/migrations/0000_yielding_leader.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "accounts" ( 2 | "user_id" uuid NOT NULL, 3 | "type" text NOT NULL, 4 | "provider" text NOT NULL, 5 | "providerAccountId" text NOT NULL, 6 | "refresh_token" text, 7 | "access_token" text, 8 | "expires_at" integer, 9 | "token_type" text, 10 | "scope" text, 11 | "id_token" text, 12 | "session_state" text, 13 | "createdAt" timestamp DEFAULT now() NOT NULL, 14 | "updatedAt" timestamp DEFAULT now() NOT NULL, 15 | CONSTRAINT "accounts_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId") 16 | ); 17 | --> statement-breakpoint 18 | CREATE TABLE "attachments" ( 19 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 20 | "name" varchar NOT NULL, 21 | "chatId" uuid NOT NULL, 22 | "url" varchar NOT NULL, 23 | "type" varchar NOT NULL, 24 | "createdAt" timestamp DEFAULT now() NOT NULL, 25 | "updatedAt" timestamp DEFAULT now() NOT NULL 26 | ); 27 | --> statement-breakpoint 28 | CREATE TABLE "chats" ( 29 | "id" uuid PRIMARY KEY NOT NULL, 30 | "title" varchar NOT NULL, 31 | "messages" json DEFAULT '[]'::json NOT NULL, 32 | "userId" uuid NOT NULL, 33 | "createdAt" timestamp DEFAULT now() NOT NULL, 34 | "updatedAt" timestamp DEFAULT now() NOT NULL 35 | ); 36 | --> statement-breakpoint 37 | CREATE TABLE "users" ( 38 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 39 | "name" varchar, 40 | "email" varchar, 41 | "emailVerified" timestamp, 42 | "image" varchar, 43 | "createdAt" timestamp DEFAULT now() NOT NULL, 44 | "updatedAt" timestamp DEFAULT now() NOT NULL 45 | ); 46 | --> statement-breakpoint 47 | ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 48 | ALTER TABLE "attachments" ADD CONSTRAINT "attachments_chatId_chats_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."chats"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 49 | ALTER TABLE "chats" ADD CONSTRAINT "chats_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; -------------------------------------------------------------------------------- /components/ai/bot-message.tsx: -------------------------------------------------------------------------------- 1 | import { AssitantIcon } from "~/components/ui/icons"; 2 | import ButtonRow from "~/components/ai/button-row"; 3 | import { cn } from "~/lib/utils"; 4 | import { Markdown } from "./markdown"; 5 | import { RegenerateFunc } from "~/lib/types"; 6 | import { ReasoningMessage } from "~/components/chat/reasoning"; 7 | import { UIMessage } from "ai"; 8 | import { Fragment } from "react"; 9 | 10 | export function BotMessage({ 11 | className, 12 | reload, 13 | isLoading, 14 | message, 15 | children = null, 16 | }: { 17 | className?: string; 18 | isLoading?: boolean; 19 | children?: React.ReactNode; 20 | reload: RegenerateFunc; 21 | message?: UIMessage; 22 | }) { 23 | return ( 24 |
25 |
31 | 32 |
33 |
39 | {message 40 | ? message.parts.map((msg, index) => { 41 | switch (msg.type) { 42 | case "reasoning": 43 | return ( 44 | 45 | 46 | {msg.text} 47 | 48 | 49 | ); 50 | case "text": 51 | return ( 52 | 53 | {msg.text} 54 | {!isLoading ? ( 55 | 56 | ) : null} 57 | 58 | ); 59 | } 60 | }) 61 | : children} 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/skeletons.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Fragment } from "react"; 3 | import { SidebarMenuSkeleton } from "./ui/sidebar"; 4 | import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card"; 5 | import { Skeleton } from "~/components/ui/skeleton"; 6 | import { Separator } from "~/components/ui/separator"; 7 | import SearchInput from "./search"; 8 | export function ChatsSkeleton() { 9 | return ( 10 | 11 | {Array.from({ length: 5 }).map((_, index) => ( 12 | 13 | ))} 14 | 15 | ); 16 | } 17 | function ChatItemSkeleton() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | export function ChatHistorySkeleton() { 43 | return ( 44 |
45 |
46 |
47 | { 50 | return; 51 | }} 52 | placeholder="Search Chat..." 53 | searchParams="chat" 54 | className="w-full" 55 | /> 56 |
57 |
58 | {Array.from({ length: 3 }).map((_, index) => ( 59 | 60 | ))} 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /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 |

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

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

61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; -------------------------------------------------------------------------------- /lib/server/index.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | import { db } from "~/lib/drizzle"; 3 | import { cache } from "react"; 4 | import { UIMessage } from "ai"; 5 | import { auth } from "~/app/auth"; 6 | import { getChatTitle } from "~/lib/server/helpers"; 7 | import { chats } from "~/lib/drizzle/schema"; 8 | 9 | export const getChats = cache(async (userId: string | undefined) => { 10 | if (!userId) return []; 11 | try { 12 | const chats = await db.query.chats.findMany({ 13 | where: (chat, { eq }) => eq(chat.userId, userId), 14 | orderBy: (chat, { desc }) => desc(chat.updatedAt), 15 | }); 16 | return chats; 17 | } catch (e) { 18 | return []; 19 | } 20 | }); 21 | 22 | export const getChatById = async (id: string | undefined) => { 23 | if (!id) return null; 24 | const chat = await db.query.chats.findFirst({ 25 | where: (chat, { eq }) => eq(chat.id, id), 26 | }); 27 | return chat; 28 | }; 29 | 30 | export async function saveChatData(id: string, messages: UIMessage[]) { 31 | try { 32 | const session = await auth(); 33 | if (!session || !session?.user?.id) return; 34 | const existing = await getChatById(id); 35 | const userId = existing ? existing.userId : session.user.id; 36 | const title = existing ? existing.title : await getChatTitle(messages); 37 | if (!userId) return null; 38 | await db 39 | .insert(chats) 40 | .values({ 41 | id, 42 | title, 43 | userId, 44 | messages: messages, 45 | }) 46 | .onConflictDoUpdate({ 47 | target: chats.id, 48 | set: { 49 | messages: messages, 50 | }, 51 | }); 52 | } catch (e) { 53 | console.error("Error saving chat data:"); 54 | return null; 55 | } 56 | } 57 | 58 | export const getUserChats = async () => { 59 | try { 60 | const session = await auth(); 61 | const userId = session?.user?.id; 62 | if (!userId) { 63 | return []; 64 | } 65 | const chats = await db.query.chats.findMany({ 66 | where: (chats, { eq }) => eq(chats.userId, userId), 67 | orderBy: (chat, { desc }) => desc(chat.updatedAt), 68 | with: { 69 | user: true, 70 | }, 71 | }); 72 | return chats; 73 | } catch (e) { 74 | return []; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /app/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; 2 | import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; 3 | import { TriangleAlert } from "lucide-react"; 4 | import { Button } from "~/components/ui/button"; 5 | import Link from "next/link"; 6 | import { AssitantIcon } from "~/components/ui/icons"; 7 | import { Metadata } from "next"; 8 | export const metadata: Metadata = { 9 | title: "ChatBot-Error", 10 | description: "error page", 11 | }; 12 | export default async function Page(props: { 13 | searchParams: Promise<{ error: string }>; 14 | }) { 15 | const searchParams = await props.searchParams; 16 | const { error } = searchParams; 17 | return ( 18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | {error || "something went wrong"} Error 38 | 39 | 40 | 41 |
42 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /GEMINI.md: -------------------------------------------------------------------------------- 1 | # GEMINI.md 2 | 3 | ## Project Overview 4 | 5 | This is a Next.js application that provides a chat interface for interacting with an AI. It uses a variety of modern web technologies to provide a feature-rich experience. 6 | 7 | The project is a "Code Copilot" chat bot. It allows users to generate responses, copy messages, regenerate messages, save and retrieve chats from a database, and upload attachments. 8 | 9 | ## Building and Running 10 | 11 | To get started with this project, you'll need to have Node.js and pnpm installed. 12 | 13 | 1. **Install dependencies:** 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | 2. **Run the development server:** 20 | 21 | ```bash 22 | pnpm run dev 23 | ``` 24 | 25 | This will start the development server on [http://localhost:3000](http://localhost:3000). 26 | 27 | 3. **Build for production:** 28 | 29 | ```bash 30 | pnpm run build 31 | ``` 32 | 33 | 4. **Start the production server:** 34 | 35 | ```bash 36 | pnpm run start 37 | ``` 38 | 39 | ### Other useful commands: 40 | 41 | * `pnpm run lint`: Lint the code. 42 | * `pnpm run typecheck`: Run the TypeScript compiler to check for type errors. 43 | * `pnpm run db:push`: Push database schema changes. 44 | * `pnpm run db:generate`: Generate database migration files. 45 | * `pnpm run db:migrate`: Apply database migrations. 46 | * `pnpm run db:studio`: Open the Drizzle Studio to view and manage your database. 47 | 48 | ## Development Conventions 49 | 50 | * **Framework:** The project is built with [Next.js](https://nextjs.org/) and [React](https://reactjs.org/). 51 | * **Authentication:** Authentication is handled by [NextAuth.js](https://next-auth.js.org/), with providers for Google and Github. 52 | * **Database:** The project uses a PostgreSQL database with [Drizzle ORM](https://orm.drizzle.team/). The database schema is defined in `lib/drizzle/schema.ts`. 53 | * **Styling:** The project uses [Tailwind CSS](https://tailwindcss.com/) for styling. 54 | * **UI Components:** The project uses [Radix UI](https://www.radix-ui.com/) and custom components for the user interface. 55 | * **Linting and Formatting:** The project uses [ESLint](https://eslint.org/) for linting and [Prettier](https://prettier.io/) for code formatting. 56 | * **Type Checking:** The project uses [TypeScript](https://www.typescriptlang.org/) for static type checking. 57 | -------------------------------------------------------------------------------- /components/ai/button-row.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Check, 3 | Copy, 4 | LucideIcon, 5 | Repeat, 6 | ThumbsDown, 7 | ThumbsUp, 8 | } from "lucide-react"; 9 | import { Button } from "~/components/ui/button"; 10 | import { 11 | Tooltip, 12 | TooltipContent, 13 | TooltipTrigger, 14 | } from "~/components/ui/tooltip"; 15 | import { toast } from "sonner"; 16 | import { useClipBoard } from "~/lib/hooks"; 17 | import { RegenerateFunc } from "~/lib/types"; 18 | 19 | interface Props { 20 | content: string; 21 | reload:RegenerateFunc 22 | } 23 | export default function ButtonRow({ content, reload }: Props) { 24 | const [isCopied, copyText] = useClipBoard(); 25 | const buttons: Array<{ 26 | icon: LucideIcon; 27 | tooltip: string; 28 | label?: string; 29 | onClick: () => void; 30 | }> = [ 31 | { 32 | icon: isCopied ? Check : Copy, 33 | tooltip: isCopied ? "Copied!" : "Copy", 34 | onClick: copy, 35 | label: isCopied ? "Copied!" : "Copy", 36 | }, 37 | { 38 | icon: Repeat, 39 | tooltip: "Regenerate", 40 | onClick: async () => { 41 | await reload({ 42 | 43 | }); 44 | }, 45 | }, 46 | { icon: ThumbsUp, tooltip: "Like", onClick: like }, 47 | { icon: ThumbsDown, tooltip: "Dislike", onClick: dislike }, 48 | ]; 49 | 50 | function like() { 51 | toast("Thanks for your feedback!", { 52 | position: "top-center", 53 | }); 54 | } 55 | function dislike() { 56 | toast("Thanks for your feedback! We will try to improve", { 57 | position: "top-center", 58 | }); 59 | } 60 | function copy() { 61 | copyText(content); 62 | } 63 | return ( 64 |
65 | {buttons.map(({ icon: Icon, onClick, tooltip, label }, index) => ( 66 | 67 | 68 | 79 | 80 | 81 |

{tooltip}

82 |
83 |
84 | ))} 85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 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 | sidebar: { 54 | DEFAULT: "hsl(var(--sidebar-background))", 55 | foreground: "hsl(var(--sidebar-foreground))", 56 | primary: "hsl(var(--sidebar-primary))", 57 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))", 58 | accent: "hsl(var(--sidebar-accent))", 59 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))", 60 | border: "hsl(var(--sidebar-border))", 61 | ring: "hsl(var(--sidebar-ring))", 62 | }, 63 | }, 64 | borderRadius: { 65 | lg: "var(--radius)", 66 | md: "calc(var(--radius) - 2px)", 67 | sm: "calc(var(--radius) - 4px)", 68 | }, 69 | }, 70 | }, 71 | plugins: [require("tailwindcss-animate")], 72 | }; 73 | export default config; 74 | -------------------------------------------------------------------------------- /components/chat/view-attachement.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FileIcon, Download, Eye } from "lucide-react"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | import { Button } from "~/components/ui/button"; 7 | import { Card, CardContent, CardFooter } from "~/components/ui/card"; 8 | 9 | export default function ViewAttachment({ 10 | attachment, 11 | }: { 12 | attachment: Attachment; 13 | }) { 14 | const isImage = attachment.contentType?.startsWith("image/"); 15 | 16 | return ( 17 | 18 | 19 | {isImage ? ( 20 |
21 | {attachment.name 28 |
29 | ) : ( 30 |
31 | 32 |
33 | )} 34 |
35 | 36 |
37 |

{attachment.name}

38 |

39 | {isImage ? "Image" : attachment.contentType} 40 |

41 |
42 |
45 | 55 | 65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /components/chat/reasoning.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "motion/react"; 2 | import { cn } from "~/lib/utils"; 3 | import { Markdown } from "~/components/ai/markdown"; 4 | import { useEffect, useState } from "react"; 5 | import { Loader2 } from "lucide-react"; 6 | import { ChevronDown } from "lucide-react"; 7 | 8 | interface ReasoningProps { 9 | isLoading?: boolean; 10 | children: string; 11 | } 12 | 13 | const variants = { 14 | collapsed: { 15 | height: 0, 16 | opacity: 0, 17 | marginTop: 0, 18 | marginBottom: 0, 19 | }, 20 | expanded: { 21 | height: "auto", 22 | opacity: 1, 23 | marginTop: "1rem", 24 | marginBottom: "0.5rem", 25 | }, 26 | }; 27 | 28 | export function ReasoningMessage({ 29 | isLoading = false, 30 | children, 31 | }: ReasoningProps) { 32 | const [isExpanded, setIsExpanded] = useState(false); 33 | useEffect(() => { 34 | setIsExpanded(isLoading); 35 | }, [isLoading]); 36 | 37 | return ( 38 |
39 | {isLoading ? ( 40 |
41 |
Reasoning
42 |
43 | 44 |
45 |
46 | ) : ( 47 |
48 |
Reasoned for a few seconds
49 | 64 |
65 | )} 66 | 67 | 68 | {isExpanded && ( 69 | 80 | {children} 81 | 82 | )} 83 | 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /lib/drizzle/schema.ts: -------------------------------------------------------------------------------- 1 | import { AdapterAccountType } from "@auth/core/adapters"; 2 | import { UIMessage } from "ai"; 3 | import { relations } from "drizzle-orm"; 4 | import { 5 | index, 6 | integer, 7 | json, 8 | pgTable, 9 | primaryKey, 10 | text, 11 | timestamp, 12 | uuid, 13 | varchar, 14 | } from "drizzle-orm/pg-core"; 15 | 16 | const timestamps = { 17 | createdAt: timestamp("createdAt", { mode: "date" }).notNull().defaultNow(), 18 | updatedAt: timestamp("updatedAt", { mode: "date" }) 19 | .notNull() 20 | .defaultNow() 21 | .$onUpdate(() => new Date()), 22 | }; 23 | 24 | export const users = pgTable("users", { 25 | id: uuid("id").defaultRandom().primaryKey(), 26 | name: varchar("name"), 27 | email: varchar("email").unique(), 28 | emailVerified: timestamp("emailVerified", { mode: "date" }), 29 | image: text("image"), 30 | ...timestamps, 31 | }); 32 | 33 | export const chats = pgTable( 34 | "chats", 35 | { 36 | id: varchar("id").primaryKey(), 37 | title: varchar("title").notNull(), 38 | messages: json("messages").$type().notNull().default([]), 39 | userId: uuid("userId") 40 | .notNull() 41 | .references(() => users.id, { onDelete: "cascade" }), 42 | ...timestamps, 43 | }, 44 | (chats) => [ 45 | { 46 | userIndex: index("user_index").on(chats.userId), 47 | }, 48 | ] 49 | ); 50 | 51 | export const accounts = pgTable( 52 | "accounts", 53 | { 54 | userId: uuid("user_id") 55 | .notNull() 56 | .references(() => users.id, { onDelete: "cascade" }), 57 | type: text("type").$type().notNull(), 58 | provider: text("provider").notNull(), 59 | providerAccountId: text("providerAccountId").notNull(), 60 | refresh_token: text("refresh_token"), 61 | access_token: text("access_token"), 62 | expires_at: integer("expires_at"), 63 | token_type: text("token_type"), 64 | scope: text("scope"), 65 | id_token: text("id_token"), 66 | session_state: text("session_state"), 67 | }, 68 | (account) => [ 69 | { 70 | compoundKey: primaryKey({ 71 | columns: [account.provider, account.providerAccountId], 72 | }), 73 | }, 74 | ] 75 | ); 76 | 77 | export const userRelations = relations(users, ({ many }) => { 78 | return { 79 | accounts: many(accounts), 80 | chats: many(chats), 81 | }; 82 | }); 83 | 84 | export const chatRelations = relations(chats, ({ one }) => ({ 85 | user: one(users, { fields: [chats.userId], references: [users.id] }), 86 | })); 87 | 88 | export const accountRelations = relations(accounts, ({ one }) => ({ 89 | user: one(users, { fields: [accounts.userId], references: [users.id] }), 90 | })); 91 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5.9% 10%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | --sidebar-background: 0 0% 100%; 33 | --sidebar-foreground: 240 5.3% 26.1%; 34 | --sidebar-primary: 240 5.9% 10%; 35 | --sidebar-primary-foreground: 0 0% 98%; 36 | --sidebar-accent: 240 4.8% 95.9%; 37 | --sidebar-accent-foreground: 240 5.9% 10%; 38 | --sidebar-border: 220 13% 91%; 39 | --sidebar-ring: 217.2 91.2% 59.8%; 40 | } 41 | 42 | .dark { 43 | --background: 240 10% 3.9%; 44 | --foreground: 0 0% 98%; 45 | --card: 240 10% 3.9%; 46 | --card-foreground: 0 0% 98%; 47 | --popover: 240 10% 3.9%; 48 | --popover-foreground: 0 0% 98%; 49 | --primary: 0 0% 98%; 50 | --primary-foreground: 240 5.9% 10%; 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | --muted: 240 3.7% 15.9%; 54 | --muted-foreground: 240 5% 64.9%; 55 | --accent: 240 3.7% 15.9%; 56 | --accent-foreground: 0 0% 98%; 57 | --destructive: 0 62.8% 30.6%; 58 | --destructive-foreground: 0 0% 98%; 59 | --border: 240 3.7% 15.9%; 60 | --input: 240 3.7% 15.9%; 61 | --ring: 240 4.9% 83.9%; 62 | --chart-1: 220 70% 50%; 63 | --chart-2: 160 60% 45%; 64 | --chart-3: 30 80% 55%; 65 | --chart-4: 280 65% 60%; 66 | --chart-5: 340 75% 55%; 67 | --sidebar-background: 240 10% 3.9%; 68 | --sidebar-foreground: 240 4.8% 95.9%; 69 | --sidebar-primary: 224.3 76.3% 48%; 70 | --sidebar-primary-foreground: 0 0% 100%; 71 | --sidebar-accent: 240 3.7% 15.9%; 72 | --sidebar-accent-foreground: 240 4.8% 95.9%; 73 | --sidebar-border: 240 3.7% 15.9%; 74 | --sidebar-ring: 217.2 91.2% 59.8%; 75 | } 76 | } 77 | 78 | @layer base { 79 | * { 80 | @apply border-border; 81 | } 82 | body { 83 | @apply bg-background text-foreground; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /components/chat-item.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card"; 3 | import { IconUser } from "~/components/ui/icons"; 4 | import { Separator } from "~/components/ui/separator"; 5 | import { formatTime } from "~/lib/utils"; 6 | import { Button } from "~/components/ui/button"; 7 | import { Ellipsis } from "lucide-react"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "./ui/dropdown-menu"; 14 | import { DeleteDialog, RenameDialog, ShareDialog } from "./dialogs"; 15 | import { useRouter } from "next/navigation"; 16 | import {Chat,User} from "~/lib/drizzle" 17 | 18 | interface Props { 19 | chat: Chat & { user: User }; 20 | } 21 | export default function ChatItem({ chat }: Props) { 22 | const formatedDate = formatTime(new Date(chat.updatedAt)); 23 | const firstMessage = chat.messages[0].parts.map((part) => part.type === "text" && part.text).join("").slice(0, 200); 24 | const content = typeof firstMessage === "string" ? firstMessage : chat.title; 25 | const router = useRouter(); 26 | return ( 27 | 28 | {chat.title} 29 | router.push(`chat/${chat.id}`)} 32 | > 33 | {content} 34 | 35 | 36 | 37 |
38 | {chat.user.name} 39 |
40 |
41 | 42 | Last Updated {formatedDate} 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /lib/server/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { AuthStatus } from "~/lib/types"; 3 | import { AuthError } from "next-auth"; 4 | import { BuiltInProviderType } from "@auth/core/providers"; 5 | import { signIn } from "~/app/auth"; 6 | import { db } from "~/lib/drizzle"; 7 | import { editChatSchema } from "~/lib/types/schema"; 8 | import { z } from "zod"; 9 | import { revalidatePath } from "next/cache"; 10 | import { chats } from "~/lib/drizzle/schema"; 11 | import { eq } from "drizzle-orm"; 12 | import { redirect } from "next/navigation"; 13 | import { utpapi } from "./helpers"; 14 | 15 | export default async function signInWithProvider( 16 | prevState: AuthStatus | undefined, 17 | formData: FormData 18 | ): Promise { 19 | try { 20 | const provider = formData.get("provider") as BuiltInProviderType; 21 | await signIn(provider); 22 | return { 23 | status: "success", 24 | message: "login successfully", 25 | }; 26 | } catch (e) { 27 | if (e instanceof AuthError) { 28 | switch (e.type) { 29 | case "OAuthCallbackError": 30 | return { 31 | status: "error", 32 | message: e.message, 33 | }; 34 | default: 35 | return { 36 | status: "error", 37 | message: "something went wrong", 38 | }; 39 | } 40 | } 41 | throw e; 42 | } 43 | } 44 | 45 | export async function deleteAttachment(attachemnt: string) { 46 | const status = await utpapi.deleteFiles(attachemnt); 47 | return status.success; 48 | } 49 | export async function deleteChat( 50 | prevState: AuthStatus | undefined, 51 | formData: FormData 52 | ): Promise { 53 | const validate = z 54 | .object({ 55 | chatId: z.string().min(10, { 56 | message: "Chat not found", 57 | }), 58 | }) 59 | .safeParse(Object.fromEntries(formData.entries())); 60 | if (!validate.success) { 61 | return { 62 | status: "error", 63 | message: validate.error.message, 64 | }; 65 | } 66 | const { chatId } = validate.data; 67 | await db.delete(chats).where(eq(chats.id, chatId)); 68 | 69 | revalidatePath("/history", "page"); 70 | redirect("/history"); 71 | } 72 | 73 | export async function editChat( 74 | prevState: AuthStatus | undefined, 75 | formData: FormData 76 | ): Promise { 77 | const validate = editChatSchema.safeParse( 78 | Object.fromEntries(formData.entries()) 79 | ); 80 | if (!validate.success) { 81 | return { 82 | status: "error", 83 | message: validate.error.message, 84 | }; 85 | } 86 | const { chatId, title } = validate.data; 87 | await db.update(chats).set({ title: title }).where(eq(chats.id, chatId)); 88 | revalidatePath("/history", "page"); 89 | redirect("/history"); 90 | } 91 | -------------------------------------------------------------------------------- /components/navbar/nav-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { Suspense, use } from "react"; 3 | import { AssitantIcon } from "../ui/icons"; 4 | import { ScrollArea } from "../ui/scroll-area"; 5 | import { 6 | Sidebar, 7 | SidebarContent, 8 | SidebarFooter, 9 | SidebarHeader, 10 | SidebarMenu, 11 | SidebarMenuItem, 12 | useSidebar, 13 | } from "../ui/sidebar"; 14 | import UserButton from "./user"; 15 | import Link from "next/link"; 16 | import NavLinks from "./nav-links"; 17 | import { Session } from "next-auth"; 18 | import NavItems from "./nav-items"; 19 | import { Button } from "../ui/button"; 20 | 21 | interface Props { 22 | sessionPromise: Promise; 23 | } 24 | 25 | export default function NavContent({ sessionPromise }: Props) { 26 | const { state } = useSidebar(); 27 | const collapsed = state === "collapsed"; 28 | const session = use(sessionPromise); 29 | const isLoggedIn = !!session?.user; 30 | return ( 31 | 37 | 38 | 39 |
40 | 41 |
42 | 43 | 44 | Code Copilot 45 | 46 | 47 |
48 | {isLoggedIn ? ( 49 | <> 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ) : ( 63 | <> 64 | 65 | 66 | 67 | Login to save chats 68 | 69 | 70 | 71 | 72 |
73 | 76 | 79 |
80 |
81 | 82 | )} 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-copilot", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build --turbopack", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "typecheck": "tsc --noEmit", 11 | "preview": "next build && next start", 12 | "db:push": "drizzle-kit push", 13 | "db:generate": "drizzle-kit generate", 14 | "db:migrate": "drizzle-kit migrate", 15 | "db:pull": "drizzle-kit pull", 16 | "db:studio": "drizzle-kit studio" 17 | }, 18 | "dependencies": { 19 | "@ai-sdk/google": "^2.0.13", 20 | "@ai-sdk/groq": "^2.0.17", 21 | "@ai-sdk/react": "^2.0.38", 22 | "@auth/core": "0.35.3", 23 | "@auth/drizzle-adapter": "1.7.4", 24 | "@neondatabase/serverless": "0.10.4", 25 | "@openrouter/ai-sdk-provider": "1.2.0", 26 | "@radix-ui/react-alert-dialog": "1.1.4", 27 | "@radix-ui/react-avatar": "1.1.1", 28 | "@radix-ui/react-dialog": "1.1.2", 29 | "@radix-ui/react-dropdown-menu": "2.1.2", 30 | "@radix-ui/react-icons": "1.3.1", 31 | "@radix-ui/react-label": "2.1.0", 32 | "@radix-ui/react-popover": "1.1.2", 33 | "@radix-ui/react-scroll-area": "1.2.0", 34 | "@radix-ui/react-select": "2.1.2", 35 | "@radix-ui/react-separator": "1.1.0", 36 | "@radix-ui/react-slot": "1.1.0", 37 | "@radix-ui/react-switch": "1.1.1", 38 | "@radix-ui/react-tooltip": "1.1.3", 39 | "@uploadthing/react": "^7.3.3", 40 | "@upstash/ratelimit": "^2.0.6", 41 | "@upstash/redis": "^1.35.3", 42 | "ai": "^5.0.38", 43 | "babel-plugin-react-compiler": "19.0.0-beta-201e55d-20241215", 44 | "class-variance-authority": "0.7.0", 45 | "clsx": "2.1.1", 46 | "cmdk": "^1.1.1", 47 | "date-fns": "4.1.0", 48 | "drizzle-orm": "0.38.3", 49 | "file-type": "^19.6.0", 50 | "js-cookie": "^3.0.5", 51 | "lucide-react": "0.441.0", 52 | "marked": "^15.0.12", 53 | "motion": "11.12.0", 54 | "nanoid": "^5.1.5", 55 | "next": "^15.5.2", 56 | "next-auth": "5.0.0-beta.25", 57 | "next-themes": "0.3.0", 58 | "react": "19.1.1", 59 | "react-dom": "19.1.1", 60 | "react-intersection-observer": "9.13.1", 61 | "react-markdown": "9.0.1", 62 | "react-syntax-highlighter": "15.6.1", 63 | "react-textarea-autosize": "8.5.4", 64 | "remark-gfm": "4.0.0", 65 | "server-only": "0.0.1", 66 | "sonner": "1.7.0", 67 | "swr": "2.2.5", 68 | "tailwind-merge": "2.5.4", 69 | "tailwindcss-animate": "1.0.7", 70 | "uploadthing": "^7.7.4", 71 | "use-debounce": "10.0.4", 72 | "zod": "4.1.5" 73 | }, 74 | "devDependencies": { 75 | "@types/js-cookie": "^3.0.6", 76 | "@types/node": "24.3.1", 77 | "@types/react": "19.0.1", 78 | "@types/react-dom": "19.0.1", 79 | "@types/react-syntax-highlighter": "15.5.13", 80 | "drizzle-kit": "0.30.1", 81 | "eslint": "9.14.0", 82 | "eslint-config-next": "15.0.4", 83 | "postcss": "8.4.48", 84 | "prettier": "3.4.2", 85 | "tailwindcss": "3.4.14", 86 | "typescript": "5.6.3" 87 | }, 88 | "pnpm": { 89 | "overrides": { 90 | "react": "19.1.1", 91 | "react-dom": "19.1.1", 92 | "@types/react": "19.0.1", 93 | "@types/react-dom": "19.0.1" 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /components/ai/code.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "~/components/ui/button"; 4 | import { CheckIcon, CopyIcon } from "lucide-react"; 5 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 6 | import { 7 | materialDark, 8 | materialLight, 9 | } from "react-syntax-highlighter/dist/cjs/styles/prism"; 10 | import { useClipBoard } from "~/lib/hooks"; 11 | import { useTheme } from "next-themes"; 12 | import { Card } from "../ui/card"; 13 | import { getLanguageIcon } from "~/lib/helpers"; 14 | 15 | interface CodeProps { 16 | language: string; 17 | codes: string; 18 | } 19 | 20 | export default function Code({ codes, language }: CodeProps) { 21 | const [isCopied, copyText] = useClipBoard(); 22 | const { theme } = useTheme(); 23 | const languageIcon = getLanguageIcon(language); 24 | return ( 25 | 26 |
27 | 28 | {languageIcon} 29 | 30 |
31 | 51 |
52 |
53 | 54 |
55 | 86 | {codes} 87 | 88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /components/chat/attachment-preview.tsx: -------------------------------------------------------------------------------- 1 | import { FileUIPart } from "ai"; 2 | import { FileIcon, X, Loader2 } from "lucide-react"; 3 | import { Button } from "~/components/ui/button"; 4 | import { Card, CardContent } from "~/components/ui/card"; 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipProvider, 9 | TooltipTrigger, 10 | } from "~/components/ui/tooltip"; 11 | 12 | interface AttachmentPreviewProps { 13 | attachment: FileUIPart; 14 | handleRemove: (name: string) => void; 15 | } 16 | 17 | export default function AttachmentPreview({ 18 | attachment, 19 | handleRemove, 20 | }: AttachmentPreviewProps) { 21 | const isImage = attachment.mediaType?.startsWith("image/"); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {isImage ? ( 30 |
31 | {attachment.filename 36 |
37 | ) : ( 38 |
39 | 40 |
41 | )} 42 |
43 |

44 | {attachment.filename} 45 |

46 |

47 | {isImage ? "Image" : attachment.mediaType} 48 |

49 |
50 | 58 |
59 |
60 |
61 | 62 |

{attachment.filename}

63 |

{attachment.mediaType}

64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | export function Loading({ attachment }: { attachment: FileUIPart }) { 71 | return ( 72 | 73 | 74 |
75 | 76 |
77 |
78 |

{attachment.filename}

79 |

80 | {attachment.mediaType} 81 |

82 |
83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /components/navbar/user.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { Settings, LogOut, SquareChevronUp } from "lucide-react"; 4 | import { signOut } from "next-auth/react"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from "~/components/ui/dropdown-menu"; 13 | import { Button } from "~/components/ui/button"; 14 | import ModeToggle from "~/components/navbar/toggle-mode"; 15 | import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; 16 | import { Session } from "next-auth"; 17 | import { use } from "react"; 18 | 19 | interface Props { 20 | sessionPromise: Promise; 21 | } 22 | 23 | export default function UserButton({ sessionPromise }: Props) { 24 | const session = use(sessionPromise); 25 | return ( 26 | 27 | 28 |
29 | 41 | 42 |
43 |
44 | 45 | {session?.user?.name} 46 | 47 | 48 | {session?.user?.email} 49 | 50 |
51 | 52 | 53 |
54 |
55 |
56 | 57 | 58 |
59 |

60 | {session?.user?.name} 61 |

62 |

63 | {session?.user?.email} 64 |

65 |
66 |
67 | 68 | 69 | 70 | 71 | Settings 72 | 73 | 74 | 75 | Preferences 76 | 77 | Toggle theme 78 | 79 | 80 | 81 | 82 | 91 | 92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/navbar/nav-items.tsx: -------------------------------------------------------------------------------- 1 | import NavItem from "~/components/navbar/nav-item"; 2 | import { 3 | SidebarGroup, 4 | SidebarGroupContent, 5 | SidebarGroupLabel, 6 | SidebarMenuButton, 7 | SidebarMenuItem, 8 | } from "../ui/sidebar"; 9 | import useSwr from "swr"; 10 | import { Chat } from "~/lib/drizzle"; 11 | import { fetcher, groupChats } from "~/lib/utils"; 12 | import Spinner from "../ai/spinner"; 13 | 14 | import { MessageSquarePlus } from "lucide-react"; 15 | import { ChatsSkeleton } from "../skeletons"; 16 | export default function NavItems() { 17 | const { 18 | data: chats, 19 | isLoading, 20 | isValidating, 21 | } = useSwr>("/api/chats", fetcher, { 22 | suspense: true, 23 | fallbackData: [], 24 | revalidateOnFocus: false, 25 | }); 26 | const groupedChats = groupChats(chats || []); 27 | 28 | return ( 29 | <> 30 | {isLoading || isValidating ? ( 31 | 32 | 33 | Loading chats 34 | 35 | 36 | 37 | 38 | 39 | ) : chats && chats.length > 0 ? ( 40 | <> 41 | {groupedChats.today.length > 0 && ( 42 | 43 | Today 44 | 45 | {groupedChats.today.map((chat) => ( 46 | 47 | ))} 48 | 49 | 50 | )} 51 | {groupedChats.yesterday.length > 0 && ( 52 | 53 | Yesterday 54 | 55 | {groupedChats.yesterday.map((chat) => ( 56 | 57 | ))} 58 | 59 | 60 | )} 61 | {groupedChats.lastWeek.length > 0 && ( 62 | 63 | Previous 7 Days 64 | 65 | {groupedChats.lastWeek.map((chat) => ( 66 | 67 | ))} 68 | 69 | 70 | )} 71 | {groupedChats.lastMonth.length > 0 && ( 72 | 73 | Last Month 74 | 75 | {groupedChats.lastMonth.map((chat) => ( 76 | 77 | ))} 78 | 79 | 80 | )} 81 | {groupedChats.older.length > 0 && ( 82 | 83 | Older Chats 84 | 85 | {groupedChats.older.map((chat) => ( 86 | 87 | ))} 88 | 89 | 90 | )} 91 | 92 | ) : ( 93 | 94 | 95 | 96 | 97 | 98 | No recent chats 99 | 100 | 101 | 102 | 103 | )} 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "~/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SheetPrimitive from "@radix-ui/react-dialog"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | import { cn } from "~/lib/utils"; 7 | import { Cross2Icon } from "@radix-ui/react-icons"; 8 | 9 | const Sheet = SheetPrimitive.Root; 10 | 11 | const SheetTrigger = SheetPrimitive.Trigger; 12 | 13 | const SheetClose = SheetPrimitive.Close; 14 | 15 | const SheetPortal = SheetPrimitive.Portal; 16 | 17 | const SheetOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; 31 | 32 | const sheetVariants = cva( 33 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", 34 | { 35 | variants: { 36 | side: { 37 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 38 | bottom: 39 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 40 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 41 | right: 42 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 43 | }, 44 | }, 45 | defaultVariants: { 46 | side: "right", 47 | }, 48 | } 49 | ); 50 | 51 | interface SheetContentProps 52 | extends React.ComponentPropsWithoutRef, 53 | VariantProps {} 54 | 55 | const SheetContent = React.forwardRef< 56 | React.ElementRef, 57 | SheetContentProps 58 | >(({ side = "right", className, children, ...props }, ref) => ( 59 | 60 | 61 | 66 | 67 | 68 | Close 69 | 70 | {children} 71 | 72 | 73 | )); 74 | SheetContent.displayName = SheetPrimitive.Content.displayName; 75 | 76 | const SheetHeader = ({ 77 | className, 78 | ...props 79 | }: React.HTMLAttributes) => ( 80 |
87 | ); 88 | SheetHeader.displayName = "SheetHeader"; 89 | 90 | const SheetFooter = ({ 91 | className, 92 | ...props 93 | }: React.HTMLAttributes) => ( 94 |
101 | ); 102 | SheetFooter.displayName = "SheetFooter"; 103 | 104 | const SheetTitle = React.forwardRef< 105 | React.ElementRef, 106 | React.ComponentPropsWithoutRef 107 | >(({ className, ...props }, ref) => ( 108 | 113 | )); 114 | SheetTitle.displayName = SheetPrimitive.Title.displayName; 115 | 116 | const SheetDescription = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, ...props }, ref) => ( 120 | 125 | )); 126 | SheetDescription.displayName = SheetPrimitive.Description.displayName; 127 | 128 | export { 129 | Sheet, 130 | SheetPortal, 131 | SheetOverlay, 132 | SheetTrigger, 133 | SheetClose, 134 | SheetContent, 135 | SheetHeader, 136 | SheetFooter, 137 | SheetTitle, 138 | SheetDescription, 139 | }; 140 | -------------------------------------------------------------------------------- /components/chat/model-select.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { models, type Model } from "~/lib/ai/models"; 3 | import React, { useState, useMemo } from "react"; 4 | import { Search } from "lucide-react"; 5 | import { 6 | Popover, 7 | PopoverContent, 8 | PopoverTrigger, 9 | } from "~/components/ui/popover"; 10 | import { Button } from "~/components/ui/button"; 11 | import { ScrollArea } from "~/components/ui/scroll-area"; 12 | 13 | interface ModelSelectorProps { 14 | selectedModel: Model | null; 15 | onModelSelect: React.Dispatch>; 16 | } 17 | 18 | export function ModelSelector({ 19 | selectedModel, 20 | onModelSelect, 21 | }: ModelSelectorProps) { 22 | const [searchQuery, setSearchQuery] = useState(""); 23 | const [open, setOpen] = useState(false); 24 | 25 | const filteredModels = useMemo(() => { 26 | return models.filter((model) => { 27 | const matchesSearch = 28 | model.name.toLowerCase().includes(searchQuery.toLowerCase()) || 29 | model.provider.name.toLowerCase().includes(searchQuery.toLowerCase()); 30 | return matchesSearch; 31 | }); 32 | }, [searchQuery]); 33 | 34 | const handleModelSelect = (model: Model) => { 35 | onModelSelect(model); 36 | setOpen(false); 37 | setSearchQuery(""); 38 | }; 39 | 40 | return ( 41 | 42 | 43 | 64 | 65 | 66 | 67 |
68 |
69 | 70 | setSearchQuery(e.target.value)} 74 | className="pl-10 h-10 outline-0 border-0 shadow-none focus:border-0 focus:outline-0 focus-within:border-0 focus-within:outline-none " 75 | /> 76 |
77 |
78 | 79 | 80 |
81 | {filteredModels.length > 0 ? ( 82 | filteredModels.map((model) => ( 83 |
handleModelSelect(model)} 89 | role="button" 90 | tabIndex={0} 91 | aria-label={`Select ${model.name} from ${model.provider.name}`} 92 | onKeyDown={(e) => { 93 | if (e.key === "Enter" || e.key === " ") { 94 | handleModelSelect(model); 95 | } 96 | }} 97 | > 98 |
99 | 100 |
101 |
102 |
{model.name}
103 |
104 | 105 |

{model.provider.name}

106 |
107 |
108 |
109 | )) 110 | ) : ( 111 |
112 |

No models found matching your search.

113 |
114 | )} 115 |
116 |
117 |
118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "~/lib/utils" 7 | import { buttonVariants } from "~/components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /components/ai/markdown.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { memo, useMemo } from "react"; 3 | import ReactMarkdown, { type Components } from "react-markdown"; 4 | import remarkGfm from "remark-gfm"; 5 | import Link from "next/link"; 6 | import { 7 | Table, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from "../ui/table"; 13 | import Code from "./code"; 14 | import { cn } from "~/lib/utils"; 15 | import { marked } from "marked"; 16 | 17 | function parseMarkdown(markdown: string): string[] { 18 | const content = marked.lexer(markdown); 19 | return content.map((c) => c.raw); 20 | } 21 | function MarkdownComponent({ children }: { children: string }) { 22 | const components = useMemo( 23 | () => 24 | ({ 25 | table: ({ node, className, ...props }) => ( 26 |
27 | 34 | 35 | ), 36 | thead: ({ node, className, ...props }) => ( 37 | 38 | ), 39 | th: ({ node, ...props }: any) => ( 40 | 44 | ), 45 | tr: ({ node, ...props }: any) => ( 46 | 47 | ), 48 | td: ({ node, ...props }: any) => ( 49 | 50 | ), 51 | ul: ({ children, className, ...props }) => ( 52 |
    53 | {children} 54 |
55 | ), 56 | ol: ({ children, className, ...props }) => ( 57 |
    58 | {children} 59 |
60 | ), 61 | li: ({ children, className, ...props }) => ( 62 |
  • 63 | {children} 64 |
  • 65 | ), 66 | strong: ({ children, className, ...props }) => ( 67 | 68 | {children} 69 | 70 | ), 71 | a: ({ node, children, className, ...props }) => { 72 | return ( 73 | 79 | {children} 80 | 81 | ); 82 | }, 83 | code: ({ node, inline, className, children, ...props }) => { 84 | const match = /language-(\w+)/.exec(className || ""); 85 | return !inline && match ? ( 86 |
    87 |
     88 |                 
     89 |               
    90 |
    91 | ) : ( 92 | 99 | {children} 100 | 101 | ); 102 | }, 103 | p: ({ children, className, ...props }) => ( 104 |

    105 | {children} 106 |

    107 | ), 108 | h1: ({ children, className, ...props }) => ( 109 |

    110 | {children} 111 |

    112 | ), 113 | h2: ({ children, className, ...props }) => ( 114 |

    115 | {children} 116 |

    117 | ), 118 | h3: ({ children, className, ...props }) => ( 119 |

    120 | {children} 121 |

    122 | ), 123 | }) satisfies Components, 124 | [], 125 | ); 126 | 127 | return ( 128 |
    129 | 134 | {children} 135 | 136 |
    137 | ); 138 | } 139 | 140 | const MarkdownBlock = memo( 141 | MarkdownComponent, 142 | (prev, next) => prev.children === next.children, 143 | ); 144 | 145 | export const Markdown = memo(({ children }: { children: string }) => { 146 | const blocks = useMemo(() => parseMarkdown(children), [children]); 147 | return blocks.map((block, index) => ( 148 | {block} 149 | )); 150 | }); 151 | 152 | Markdown.displayName = "Markdown"; 153 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { cn } from "~/lib/utils" 7 | import { Dialog, DialogContent } from "~/components/ui/dialog" 8 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons" 9 | 10 | const Command = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | Command.displayName = CommandPrimitive.displayName 24 | 25 | const CommandDialog = ({ children, ...props }: DialogProps) => { 26 | return ( 27 | 28 | 29 | 30 | {children} 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | const CommandInput = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, ...props }, ref) => ( 41 |
    42 | 43 | 51 |
    52 | )) 53 | 54 | CommandInput.displayName = CommandPrimitive.Input.displayName 55 | 56 | const CommandList = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, ...props }, ref) => ( 60 | 65 | )) 66 | 67 | CommandList.displayName = CommandPrimitive.List.displayName 68 | 69 | const CommandEmpty = React.forwardRef< 70 | React.ElementRef, 71 | React.ComponentPropsWithoutRef 72 | >((props, ref) => ( 73 | 78 | )) 79 | 80 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 81 | 82 | const CommandGroup = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | 96 | CommandGroup.displayName = CommandPrimitive.Group.displayName 97 | 98 | const CommandSeparator = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 107 | )) 108 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 109 | 110 | const CommandItem = React.forwardRef< 111 | React.ElementRef, 112 | React.ComponentPropsWithoutRef 113 | >(({ className, ...props }, ref) => ( 114 | 122 | )) 123 | 124 | CommandItem.displayName = CommandPrimitive.Item.displayName 125 | 126 | const CommandShortcut = ({ 127 | className, 128 | ...props 129 | }: React.HTMLAttributes) => { 130 | return ( 131 | 138 | ) 139 | } 140 | CommandShortcut.displayName = "CommandShortcut" 141 | 142 | export { 143 | Command, 144 | CommandDialog, 145 | CommandInput, 146 | CommandList, 147 | CommandEmpty, 148 | CommandGroup, 149 | CommandItem, 150 | CommandShortcut, 151 | CommandSeparator, 152 | } 153 | -------------------------------------------------------------------------------- /components/dialogs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useActionState } from "react"; 3 | import { 4 | Dialog, 5 | DialogClose, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | DialogTrigger, 12 | } from "~/components/ui/dialog"; 13 | import { deleteChat, editChat } from "~/lib/server/actions"; 14 | import { Input } from "~/components/ui/input"; 15 | import { Button } from "~/components/ui/button"; 16 | import { useClipBoard } from "~/lib/hooks"; 17 | import { 18 | AlertCircle, 19 | Check, 20 | Copy, 21 | Edit3, 22 | Link, 23 | Loader2, 24 | Save, 25 | Share2, 26 | Trash2, 27 | } from "lucide-react"; 28 | import AlertMessage from "~/components/auth/alert"; 29 | import { Chat } from "~/lib/drizzle"; 30 | 31 | interface Props { 32 | chat: Chat; 33 | } 34 | 35 | export function DeleteDialog({ chat }: Props) { 36 | const [state, action, isPending] = useActionState(deleteChat, undefined); 37 | 38 | return ( 39 | 40 | 41 | 48 | 49 | 50 | 51 | 52 | 53 | Confirm Deletion 54 | 55 | 56 | Are you sure you want to delete this chat? This action cannot be 57 | undone. 58 | 59 | 60 |
    61 | 62 | {state && } 63 | 64 | 65 | 68 | 69 | 70 | 83 | 84 | 85 |
    86 |
    87 | ); 88 | } 89 | 90 | export function RenameDialog({ chat }: Props) { 91 | const [state, action, isPending] = useActionState(editChat, undefined); 92 | 93 | return ( 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 105 | Rename Chat 106 | 107 | Enter a new name for your chat. 108 | 109 |
    110 | 116 | 122 | {state && } 123 | 124 | 125 | 128 | 129 | 130 | 143 | 144 | 145 |
    146 |
    147 | ); 148 | } 149 | 150 | export function ShareDialog({ chat }: Props) { 151 | const [isCopied, copyText] = useClipBoard(); 152 | const link = `${process.env.NEXT_PUBLIC_BASE_URL}/chat/${chat.id}`; 153 | 154 | return ( 155 | 156 | 157 | 161 | 162 | 163 | 164 | 165 | 166 | Share Chat 167 | 168 | 169 | Copy the link below to share this chat with others. 170 | 171 | 172 |
    173 | 174 | 187 |
    188 |
    189 |
    190 | ); 191 | } 192 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from "@radix-ui/react-icons" 10 | import * as SelectPrimitive from "@radix-ui/react-select" 11 | 12 | import { cn } from "~/lib/utils" 13 | 14 | const Select = SelectPrimitive.Root 15 | 16 | const SelectGroup = SelectPrimitive.Group 17 | 18 | const SelectValue = SelectPrimitive.Value 19 | 20 | const SelectTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, children, ...props }, ref) => ( 24 | span]:line-clamp-1", 28 | className 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | 37 | )) 38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 39 | 40 | const SelectScrollUpButton = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | 53 | 54 | )) 55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 56 | 57 | const SelectScrollDownButton = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 69 | 70 | 71 | )) 72 | SelectScrollDownButton.displayName = 73 | SelectPrimitive.ScrollDownButton.displayName 74 | 75 | const SelectContent = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, position = "popper", ...props }, ref) => ( 79 | 80 | 91 | 92 | 99 | {children} 100 | 101 | 102 | 103 | 104 | )) 105 | SelectContent.displayName = SelectPrimitive.Content.displayName 106 | 107 | const SelectLabel = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, ...props }, ref) => ( 111 | 116 | )) 117 | SelectLabel.displayName = SelectPrimitive.Label.displayName 118 | 119 | const SelectItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | SelectItem.displayName = SelectPrimitive.Item.displayName 140 | 141 | const SelectSeparator = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef 144 | >(({ className, ...props }, ref) => ( 145 | 150 | )) 151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 152 | 153 | export { 154 | Select, 155 | SelectGroup, 156 | SelectValue, 157 | SelectTrigger, 158 | SelectContent, 159 | SelectLabel, 160 | SelectItem, 161 | SelectSeparator, 162 | SelectScrollUpButton, 163 | SelectScrollDownButton, 164 | } 165 | -------------------------------------------------------------------------------- /components/chat/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useMemo, useOptimistic, useState } from "react"; 3 | import { ScrollArea } from "~/components/ui/scroll-area"; 4 | import { UIMessage, useChat } from "@ai-sdk/react"; 5 | import InputField from "~/components/chat/input"; 6 | import Messages from "~/components/chat/messages"; 7 | import ScrollAnchor from "~/components/chat/scroll-anchor"; 8 | import EmptyScreen from "~/components/chat/empty-messages"; 9 | import { useLocalStorage, useScroll } from "~/lib/hooks"; 10 | import { cn } from "~/lib/utils"; 11 | import { useSWRConfig } from "swr"; 12 | import { usePathname } from "next/navigation"; 13 | import { Button } from "~/components/ui/button"; 14 | import Link from "next/link"; 15 | import { useIsMobile } from "~/lib/hooks/use-mobile"; 16 | import { useSession } from "next-auth/react"; 17 | import { Github } from "lucide-react"; 18 | import { AutoScroller } from "./auto-scoller"; 19 | import { Model, models } from "~/lib/ai/models"; 20 | import { DefaultChatTransport, ChatTransport, FileUIPart } from "ai"; 21 | import { generateMessageId } from "~/lib/ai/utis"; 22 | import cookies from "js-cookie"; 23 | 24 | interface ChatProps { 25 | initialMessages: UIMessage[]; 26 | chatId: string; 27 | chatTitle?: string; 28 | } 29 | export default function Chat({ 30 | chatId, 31 | initialMessages, 32 | chatTitle, 33 | }: ChatProps) { 34 | const [_new, setChatId] = useLocalStorage("chatId", null); 35 | const [input, setInput] = useState(""); 36 | const session = useSession(); 37 | const isLoggedIn = session.status === "loading" ? true : !!session.data?.user; 38 | const { mutate } = useSWRConfig(); 39 | const path = usePathname(); 40 | const [selectedModel, setSelectedModel] = useState(() => { 41 | return models.find((model) => model.isDefault) || models[0]; 42 | }); 43 | const [attachments, setAttachments] = useState>([]); 44 | const [optimisticAttachments, setOptimisticAttachments] = 45 | useOptimistic>(attachments); 46 | 47 | const { messages, status, error, sendMessage, regenerate, stop } = useChat({ 48 | messages: initialMessages, 49 | id: chatId, 50 | transport: new DefaultChatTransport({ 51 | api: "/api/chat", 52 | }), 53 | generateId: generateMessageId, 54 | onFinish: (data) => { 55 | setChatId(chatId); 56 | mutate("/api/chats"); 57 | }, 58 | }); 59 | 60 | async function handleSubmit(e: React.FormEvent) { 61 | e.preventDefault(); 62 | if (!input) return; 63 | sendMessage({ text: input }); 64 | setInput(""); 65 | } 66 | const loading = ["streaming", "submitted"].includes(status); 67 | const isEmpty = messages.length === 0; 68 | const { 69 | isAtBottom, 70 | scrollToBottom, 71 | messagesRef, 72 | visibilityRef, 73 | handleScroll, 74 | } = useScroll(); 75 | const isMobile = useIsMobile(); 76 | 77 | useEffect(() => { 78 | cookies.set("model.id", selectedModel.id.toString(), { 79 | expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), 80 | }); 81 | }, [selectedModel]); 82 | 83 | return ( 84 |
    85 | {!isLoggedIn ? ( 86 |
    87 | 90 | 93 |
    94 | ) : ( 95 | isMobile && 96 | !isEmpty && 97 | !path.includes(chatId) && ( 98 |
    99 | 100 | {chatTitle 101 | ? chatTitle?.length > 35 102 | ? chatTitle?.slice(0, 30) + "..." 103 | : chatTitle 104 | : "Unititled Chat"} 105 | 106 | 109 |
    110 | ) 111 | )} 112 | {isEmpty ? ( 113 | sendMessage({ text: msg })} /> 114 | ) : ( 115 | <> 116 | 120 | 124 | 131 | 132 | 133 |
    134 | 138 |
    139 | 140 | )} 141 |
    142 |
    143 |
    144 | setInput(e.target.value)} 155 | /> 156 |
    157 |
    158 |
    159 |
    160 | 165 | view Project On Github 166 | 167 |
    168 |
    169 |
    170 |
    171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /components/chat/input.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "~/components/ui/button"; 2 | import Textarea from "react-textarea-autosize"; 3 | import { 4 | CloudUpload, 5 | MoveUp, 6 | Paperclip, 7 | Send, 8 | TriangleAlert, 9 | } from "lucide-react"; 10 | import React, { ChangeEvent, useRef, useTransition } from "react"; 11 | import { cn, sleep } from "~/lib/utils"; 12 | import { LoadingButton } from "~/components/ai/spinner-message"; 13 | import AttachmentPreview, { 14 | Loading, 15 | } from "~/components/chat/attachment-preview"; 16 | import { FileUIPart } from "ai"; 17 | import { useUploadThing } from "~/lib/uploadthing"; 18 | import { toast } from "sonner"; 19 | import { deleteAttachment } from "~/lib/server/actions"; 20 | import { Separator } from "../ui/separator"; 21 | import { ModelSelector } from "./model-select"; 22 | import { Model } from "~/lib/ai/models"; 23 | 24 | interface InputFieldProps { 25 | handleSubmit: (e: React.FormEvent) => void; 26 | handleChange: (e: ChangeEvent) => void; 27 | input: string; 28 | isLoading: boolean; 29 | stop: () => void; 30 | setAttachments: React.Dispatch>; 31 | setOPtimisticAttachments: React.Dispatch>; 32 | optimisticAttachments: Array; 33 | selectedModel: Model; 34 | setSelectedModel: React.Dispatch>; 35 | } 36 | function InputField({ 37 | handleChange, 38 | handleSubmit, 39 | input, 40 | isLoading, 41 | stop, 42 | setAttachments, 43 | setOPtimisticAttachments, 44 | optimisticAttachments, 45 | selectedModel, 46 | setSelectedModel, 47 | }: InputFieldProps) { 48 | const inputRef = useRef(null); 49 | const attachementRef = useRef(null); 50 | const [isPending, startTransition] = useTransition(); 51 | function onKeyDown(e: React.KeyboardEvent) { 52 | if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { 53 | e.currentTarget.form?.requestSubmit(); 54 | e.preventDefault(); 55 | } 56 | } 57 | 58 | const { startUpload } = useUploadThing("imageUploader", { 59 | onUploadError: (error) => { 60 | toast.error("Error", { 61 | description: "Attachment upload failed", 62 | icon: , 63 | position: "top-center", 64 | action: { 65 | label: "Retry", 66 | onClick: () => handleOnClick(), 67 | }, 68 | }); 69 | }, 70 | onClientUploadComplete: (files) => { 71 | files.forEach((file) => { 72 | setAttachments((prev) => [ 73 | ...prev, 74 | { 75 | url: file.ufsUrl, 76 | contentType: file.type, 77 | name: file.name, 78 | type: "file", 79 | mediaType: file.type, 80 | }, 81 | ]); 82 | }); 83 | }, 84 | }); 85 | async function removeAttachement(key: string | undefined) { 86 | if (!key) return; 87 | const deleted = await deleteAttachment(key); 88 | if (!deleted) return; 89 | setAttachments((current) => { 90 | return current.filter((a) => a.filename != key); 91 | }); 92 | } 93 | function handleOnClick() { 94 | if (!attachementRef.current) return; 95 | attachementRef.current?.click(); 96 | } 97 | async function handleFileChange(e: ChangeEvent) { 98 | const files = Array.from(e.target.files || []); 99 | if (!files) return; 100 | startTransition(async () => { 101 | files.forEach((file) => { 102 | setOPtimisticAttachments((prev) => [ 103 | ...prev, 104 | { 105 | name: file.name, 106 | contentType: file.type, 107 | url: URL.createObjectURL(file), 108 | isUploading: true, 109 | key: file.name, 110 | type: "file", 111 | mediaType: file.type, 112 | }, 113 | ]); 114 | }); 115 | await sleep(2000); 116 | await startUpload(files); 117 | }); 118 | setAttachments([]); 119 | } 120 | 121 | return ( 122 |
    126 | {optimisticAttachments.length > 0 && ( 127 | <> 128 |
    129 |
    130 | {optimisticAttachments.map((a, index) => ( 131 |
    132 | {a.isUploading ? ( 133 | 134 | ) : ( 135 | 140 | )} 141 |
    142 | ))} 143 |
    144 |
    145 | 146 | 147 | )} 148 | 149 |
    150 |
    151 |