├── .gitignore ├── README.md ├── expo-ai-chatbot ├── .env.local.example ├── .gitignore ├── .prettierrc ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── README.md ├── app.json ├── babel.config.js ├── bun.lockb ├── components.json ├── eas.json ├── global.d.ts ├── metro.config.js ├── nativewind-env.d.ts ├── package.json ├── src │ ├── ThemeProvider.tsx │ ├── actions │ │ └── schema.ts │ ├── app │ │ ├── (app) │ │ │ └── index.tsx │ │ └── _layout.tsx │ ├── assets │ │ ├── expo-logo.png │ │ ├── icon.png │ │ ├── loader-three-dots.json │ │ └── splash.png │ ├── components │ │ ├── chat-interface.tsx │ │ ├── lottie-loader.tsx │ │ ├── scroll-adapt.tsx │ │ ├── sonner │ │ │ ├── index.ts │ │ │ └── index.web.ts │ │ ├── suggested-actions.tsx │ │ ├── ui │ │ │ ├── avatar.tsx │ │ │ ├── avatar.web.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── chat-input.tsx │ │ │ ├── chat-text-input.tsx │ │ │ ├── dialog.tsx │ │ │ ├── form.tsx │ │ │ ├── h1.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── markdown.tsx │ │ │ ├── pressable-scale.tsx │ │ │ └── text.tsx │ │ ├── weather.tsx │ │ └── welcome-message.tsx │ ├── design-system │ │ └── color-scheme │ │ │ ├── context.tsx │ │ │ ├── hook.tsx │ │ │ ├── index.ts │ │ │ ├── provider.tsx │ │ │ ├── provider.web.tsx │ │ │ ├── store.tsx │ │ │ └── store.web.tsx │ ├── global.css │ ├── hooks │ │ └── useImagePicker.ts │ ├── lib │ │ ├── api-client.ts │ │ ├── constants.ts │ │ ├── getOpenGraphDataQuery.ts │ │ ├── globalStore.ts │ │ ├── icons │ │ │ ├── Cloud.ts │ │ │ ├── Droplets.ts │ │ │ ├── Sun.ts │ │ │ ├── Wind.ts │ │ │ ├── iconWithClassName.ts │ │ │ └── index.ts │ │ ├── supabase.ts │ │ ├── useColorScheme.tsx │ │ └── utils.ts │ ├── providers.tsx │ └── utils │ │ └── useMediaQueries.ts ├── tailwind.config.js └── tsconfig.json └── nextjs-ai-chatbot ├── .env.local.sample ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── actions.ts │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── token │ │ │ └── route.ts │ ├── auth.config.ts │ ├── auth.ts │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── (chat) │ ├── actions.ts │ ├── api │ │ ├── chat-open │ │ │ └── route.ts │ │ ├── chat │ │ │ └── route.ts │ │ ├── document │ │ │ └── route.ts │ │ ├── files │ │ │ └── upload │ │ │ │ └── route.ts │ │ ├── history │ │ │ └── route.ts │ │ ├── suggestions │ │ │ └── route.ts │ │ └── vote │ │ │ └── route.ts │ ├── chat │ │ └── [id] │ │ │ └── page.tsx │ ├── layout.tsx │ ├── opengraph-image.png │ ├── page.tsx │ └── twitter-image.png ├── api │ ├── auth │ │ └── register │ │ │ └── route.ts │ └── chat │ │ └── [id] │ │ └── route.ts ├── favicon.ico ├── globals.css └── layout.tsx ├── biome.jsonc ├── components.json ├── components ├── app-sidebar.tsx ├── auth-form.tsx ├── block-actions.tsx ├── block-close-button.tsx ├── block-messages.tsx ├── block.tsx ├── chat-header.tsx ├── chat.tsx ├── code-block.tsx ├── code-editor.tsx ├── console.tsx ├── data-stream-handler.tsx ├── diffview.tsx ├── document-preview.tsx ├── document-skeleton.tsx ├── document.tsx ├── editor.tsx ├── icons.tsx ├── image-editor.tsx ├── markdown.tsx ├── message-actions.tsx ├── message-editor.tsx ├── message.tsx ├── messages.tsx ├── model-selector.tsx ├── multimodal-input.tsx ├── overview.tsx ├── preview-attachment.tsx ├── run-code-button.tsx ├── sidebar-history.tsx ├── sidebar-toggle.tsx ├── sidebar-user-nav.tsx ├── sign-out-form.tsx ├── submit-button.tsx ├── suggested-actions.tsx ├── suggestion.tsx ├── theme-provider.tsx ├── toolbar.tsx ├── ui │ ├── alert-dialog.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── textarea.tsx │ └── tooltip.tsx ├── use-scroll-to-bottom.ts ├── version-footer.tsx ├── visibility-selector.tsx └── weather.tsx ├── drizzle.config.ts ├── hooks ├── use-block.ts ├── use-chat-visibility.ts ├── use-mobile.tsx ├── use-multimodal-copy-to-clipboard.ts └── use-user-message-id.ts ├── lib ├── ai │ ├── custom-middleware.ts │ ├── index.ts │ ├── models.ts │ └── prompts.ts ├── auth │ └── token.ts ├── db │ ├── migrate.ts │ ├── migrations │ │ ├── 0000_keen_devos.sql │ │ ├── 0001_sparkling_blue_marvel.sql │ │ ├── 0002_wandering_riptide.sql │ │ ├── 0003_cloudy_glorian.sql │ │ ├── 0004_odd_slayback.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ └── _journal.json │ ├── queries.ts │ └── schema.ts ├── editor │ ├── config.ts │ ├── diff.js │ ├── functions.tsx │ ├── react-renderer.tsx │ └── suggestions.tsx └── utils.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── fonts │ ├── geist-mono.woff2 │ └── geist.woff2 └── images │ └── demo-thumbnail.png ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /ai-chatbot/ 2 | /.idea/ 3 | /.vscode/ 4 | .DS_Store 5 | aisdk llm.txt 6 | /expo-ai-chatbot/node_modules/ 7 | /nextjs-ai-chatbot/node_modules/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expo AI Chatbot Lite 2 | 3 | ℹ️ This is a lite version of Expo AI Chatbot. It is a simplified version of the pro chatbot that is designed to be ready to use for commercial apps. It has Authentication, Suggested Actions / Tooling, Image Attachments, History drawer, Markdown support, new chat navigation, and best of all Next.js AI Chatbot Compatible. 4 | 5 | **Go get the pro version here** → [expoaichatbot.com](https://www.expoaichatbot.com) 6 | 7 | --- 8 | ## Next.js AI Chatbot reference codebase 9 | 10 | Reference nextjs-ai-chatbot [README.md](./nextjs-ai-chatbot/README.md) 11 | 12 | ## Expo AI Chatbot setup 13 | 14 | Reference expo-ai-chatbot [README.md](./expo-ai-chatbot/README.md) 15 | 16 | -------------------------------------------------------------------------------- /expo-ai-chatbot/.env.local.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_API_URL="http://localhost:3000" 2 | -------------------------------------------------------------------------------- /expo-ai-chatbot/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | .idea 16 | 17 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 18 | # The following patterns were generated by expo-cli 19 | 20 | expo-env.d.ts 21 | # @end expo-cli 22 | /.env.local 23 | 24 | /ios/ 25 | /android/ 26 | -------------------------------------------------------------------------------- /expo-ai-chatbot/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } -------------------------------------------------------------------------------- /expo-ai-chatbot/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /expo-ai-chatbot/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "radon-ide", 5 | "request": "launch", 6 | "name": "Radon IDE panel", 7 | "ios": {} 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /expo-ai-chatbot/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enablePaths": [ 3 | "supabase/functions" 4 | ], 5 | "deno.lint": true, 6 | "deno.unstable": true, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "denoland.vscode-deno" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /expo-ai-chatbot/README.md: -------------------------------------------------------------------------------- 1 | # Expo AI Chatbot 2 | 3 | A mobile chat interface built with Expo and React Native. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js 8 | - npm, yarn, or bun 9 | - Expo CLI (`npm install -g expo-cli`) 10 | 11 | ## Setup & Installation 12 | 13 | 1. Install dependencies: 14 | 15 | ``` 16 | npm install 17 | # or 18 | yarn install 19 | # or 20 | bun install 21 | ``` 22 | 23 | 2. Start the Next.js API server: 24 | 25 | ``` 26 | cd ../nextjs-ai-chatbot 27 | npm run dev 28 | ``` 29 | 30 | 3. Start Expo development server: 31 | 32 | ``` 33 | bun start 34 | # or 35 | npm start 36 | # or 37 | yarn start 38 | ``` 39 | 40 | 4. Run on device/simulator: 41 | 42 | - Scan QR code with Expo Go app (iOS/Android) 43 | - Press 'i' for iOS simulator 44 | - Press 'a' for Android emulator 45 | 46 | ## Development 47 | 48 | The app requires both the Expo frontend and Next.js backend to be running: 49 | 50 | - Expo frontend runs on default Expo port (8081) 51 | - Next.js API runs on http://localhost:3000 52 | 53 | ## Project Structure 54 | 55 | - `/components` - React Native components 56 | - `/screens` - App screens/pages 57 | - `/design-system` - Design system components based on Tailwind CSS 58 | - `/services/auth` - Authentication service 59 | 60 | ## API 61 | 62 | ### `useChatFromHistory` 63 | 64 | This hook is used to fetch the chat history from the server. It is used in the `index.tsx` file and also on the `DrawerContent` component. It is used to fetch the chat history from the server and return the messages in a format that can be used by the `Chat` component when the user clicks on the chat history button. Also it's used when a new chat is created. 65 | 66 | ### ChatInput 67 | 68 | This component is used to input the chat messages. It is used in the `index.tsx` file. 69 | 70 | ℹ️ There is a `ChatInputAnimated` component that is a alpha version of the `ChatInput` component. It is used to input the chat messages too but it replicates the OpenAI chat interface interaction on new messages where the last message is placed on the top of the vissible chat body screen. 71 | 72 | ### ChatInterface 73 | 74 | This component is used to display the chat messages. It is used in the `index.tsx` file. It also handles Tool Invocations. 75 | 76 | ### CustomMarkdown 77 | 78 | It has custom Nativewind components and custom rules to better display markdown content in the React Native chat context. It is using the `react-native-markdown-display` library and the `@expo/html-elements` library on top. Per the docs: "It a 100% compatible CommonMark renderer, a react-native markdown renderer done right. This is not a web-view markdown renderer but a renderer that uses native components for all its elements.". It has plugins and extensions too to further enhance the markdown rendering to your needs. 79 | -------------------------------------------------------------------------------- /expo-ai-chatbot/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "expo-ai-chatbot-lite", 4 | "slug": "expo-ai-chatbot-lite", 5 | "scheme": "lite.expoaichatbot.com", 6 | "userInterfaceStyle": "automatic", 7 | "orientation": "portrait", 8 | "version": "0.0.1", 9 | "web": { 10 | "output": "server", 11 | "bundler": "metro" 12 | }, 13 | "assetBundlePatterns": ["src/assets/**/*"], 14 | "plugins": [ 15 | [ 16 | "expo-router", 17 | { 18 | "origin": "http://localhost:8081" 19 | } 20 | ], 21 | "expo-secure-store", 22 | "expo-font", 23 | [ 24 | "expo-build-properties", 25 | { 26 | "ios": { 27 | "deploymentTarget": "15.1" 28 | }, 29 | "android": { 30 | "kotlinVersion": "1.6.21" 31 | } 32 | } 33 | ], 34 | [ 35 | "expo-image-picker", 36 | { 37 | "photosPermission": "This app needs access to your photos to let you share them.", 38 | "cameraPermission": "This app needs access to your camera to let you take photos." 39 | } 40 | ], 41 | [ 42 | "expo-splash-screen", 43 | { 44 | "backgroundColor": "#ffffff", 45 | "image": "./src/assets/splash.png", 46 | "imageWidth": 200 47 | } 48 | ] 49 | ], 50 | "experiments": { 51 | "typedRoutes": true 52 | }, 53 | "android": { 54 | "package": "com.expoaichatbot.lite" 55 | }, 56 | "ios": { 57 | "bundleIdentifier": "com.expoaichatbot.lite", 58 | "supportsTablet": true 59 | }, 60 | "runtimeVersion": { 61 | "policy": "appVersion" 62 | }, 63 | "owner": "bidah", 64 | "extra": { 65 | "eas": { 66 | "projectId": "b88444bb-597c-4abf-8322-686e6e525a07" 67 | } 68 | }, 69 | "icon": "./src/assets/icon.png", 70 | "splash": { 71 | "image": "./src/assets/splash.png", 72 | "resizeMode": "contain", 73 | "backgroundColor": "#ffffff" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /expo-ai-chatbot/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 6 | "nativewind/babel", 7 | ], 8 | plugins: [ 9 | "@babel/plugin-proposal-export-namespace-from", 10 | "react-native-reanimated/plugin", 11 | ], 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /expo-ai-chatbot/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo-ai-chatbot/expo-ai-chatbot-lite/adbb426407bf72143b5ee47f42c7345f9e07c01c/expo-ai-chatbot/bun.lockb -------------------------------------------------------------------------------- /expo-ai-chatbot/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "aliases": { 3 | "components": "@/components", 4 | "lib": "@/lib" 5 | } 6 | } -------------------------------------------------------------------------------- /expo-ai-chatbot/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 10.2.1" 4 | }, 5 | "build": { 6 | "development:emulator": { 7 | "distribution": "internal", 8 | "developmentClient": true, 9 | "channel": "development", 10 | "env": { 11 | "APP_ENV": "development" 12 | }, 13 | "android": { 14 | "buildType": "apk", 15 | "image": "latest" 16 | } 17 | }, 18 | "development:simulator": { 19 | "distribution": "internal", 20 | "developmentClient": true, 21 | "channel": "development", 22 | "env": { 23 | "APP_ENV": "development" 24 | }, 25 | "ios": { 26 | "simulator": true, 27 | "image": "latest" 28 | } 29 | }, 30 | "development:device": { 31 | "distribution": "internal", 32 | "developmentClient": true, 33 | "channel": "development", 34 | "env": { 35 | "APP_ENV": "development" 36 | }, 37 | "ios": { 38 | "image": "latest" 39 | } 40 | }, 41 | "development": { 42 | "distribution": "internal", 43 | "channel": "development", 44 | "env": { 45 | "APP_ENV": "development", 46 | "NODE_ENV": "development" 47 | }, 48 | "android": { 49 | "buildType": "apk" 50 | }, 51 | "ios": { 52 | "image": "latest" 53 | } 54 | }, 55 | "staging": { 56 | "distribution": "store", 57 | "channel": "staging", 58 | "android": { 59 | "buildType": "apk" 60 | }, 61 | "env": { 62 | "APP_ENV": "staging" 63 | }, 64 | "ios": { 65 | "image": "latest" 66 | }, 67 | "autoIncrement": true 68 | }, 69 | "production": { 70 | "env": { 71 | "APP_ENV": "production", 72 | "NODE_ENV": "production" 73 | }, 74 | "channel": "production" 75 | } 76 | }, 77 | "submit": { 78 | "production": {} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /expo-ai-chatbot/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /expo-ai-chatbot/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | const { withNativeWind } = require("nativewind/metro"); 3 | 4 | const config = getDefaultConfig(__dirname); 5 | 6 | config.resolver = { 7 | ...config.resolver, 8 | // unstable_enablePackageExports: tue, 9 | }; 10 | 11 | module.exports = withNativeWind(config, { input: "./src/global.css" }); 12 | -------------------------------------------------------------------------------- /expo-ai-chatbot/nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. -------------------------------------------------------------------------------- /expo-ai-chatbot/src/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import "@/global.css"; 2 | 3 | import AsyncStorage from "@react-native-async-storage/async-storage"; 4 | import { 5 | DarkTheme, 6 | DefaultTheme, 7 | Theme, 8 | ThemeProvider, 9 | } from "@react-navigation/native"; 10 | import { SplashScreen, Stack } from "expo-router"; 11 | import { StatusBar } from "expo-status-bar"; 12 | import * as React from "react"; 13 | import { Platform } from "react-native"; 14 | import { NAV_THEME } from "@/lib/constants"; 15 | import { useColorScheme } from "@/lib/useColorScheme"; 16 | // import { PortalHost } from "@rn-primitives/portal"; 17 | // import { ThemeToggle } from "@/components/ThemeToggle"; 18 | // import { setAndroidNavigationBar } from "@/lib/android-navigation-bar"; 19 | 20 | const LIGHT_THEME: Theme = { 21 | ...DefaultTheme, 22 | colors: NAV_THEME.light, 23 | }; 24 | const DARK_THEME: Theme = { 25 | ...DarkTheme, 26 | colors: NAV_THEME.dark, 27 | }; 28 | 29 | export { 30 | // Catch any errors thrown by the Layout component. 31 | ErrorBoundary, 32 | } from "expo-router"; 33 | 34 | // Prevent the splash screen from auto-hiding before getting the color scheme. 35 | SplashScreen.preventAutoHideAsync(); 36 | 37 | export default function RootLayout({ 38 | children, 39 | }: { 40 | children: React.ReactNode; 41 | }) { 42 | const { colorScheme, setColorScheme, isDarkColorScheme } = useColorScheme(); 43 | const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false); 44 | 45 | React.useEffect(() => { 46 | (async () => { 47 | const theme = await AsyncStorage.getItem("theme"); 48 | if (Platform.OS === "web") { 49 | // Adds the background color to the html element to prevent white background on overscroll. 50 | document.documentElement.classList.add("bg-background"); 51 | } 52 | if (!theme) { 53 | AsyncStorage.setItem("theme", colorScheme); 54 | setIsColorSchemeLoaded(true); 55 | return; 56 | } 57 | const colorTheme = theme === "dark" ? "dark" : "light"; 58 | if (colorTheme !== colorScheme) { 59 | setColorScheme(colorTheme); 60 | // setAndroidNavigationBar(colorTheme); 61 | setIsColorSchemeLoaded(true); 62 | return; 63 | } 64 | // setAndroidNavigationBar(colorTheme); 65 | setIsColorSchemeLoaded(true); 66 | })().finally(() => { 67 | SplashScreen.hideAsync(); 68 | }); 69 | }, []); 70 | 71 | if (!isColorSchemeLoaded) { 72 | return null; 73 | } 74 | 75 | return ( 76 | 77 | 78 | {children} 79 | {/* */} 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/actions/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const loginSchema = z.object({ 4 | email: z.string().email(), 5 | password: z.string().min(8), 6 | }); 7 | 8 | export type LoginFormValues = z.infer; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import Providers from "@/providers"; 2 | import { Stack } from "expo-router"; 3 | 4 | export default function Layout() { 5 | return ( 6 | 7 | 8 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/assets/expo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo-ai-chatbot/expo-ai-chatbot-lite/adbb426407bf72143b5ee47f42c7345f9e07c01c/expo-ai-chatbot/src/assets/expo-logo.png -------------------------------------------------------------------------------- /expo-ai-chatbot/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo-ai-chatbot/expo-ai-chatbot-lite/adbb426407bf72143b5ee47f42c7345f9e07c01c/expo-ai-chatbot/src/assets/icon.png -------------------------------------------------------------------------------- /expo-ai-chatbot/src/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expo-ai-chatbot/expo-ai-chatbot-lite/adbb426407bf72143b5ee47f42c7345f9e07c01c/expo-ai-chatbot/src/assets/splash.png -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/lottie-loader.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { View } from "react-native"; 3 | import LottieView from "lottie-react-native"; 4 | import loaderAnimation from "../assets/loader-three-dots.json"; 5 | 6 | export const LottieLoader = ({ width = 60, height = 60 }) => { 7 | const animationRef = useRef(null); 8 | 9 | return ( 10 | 19 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/scroll-adapt.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollView } from "react-native"; 2 | import type React from "react"; 3 | import { forwardRef, useImperativeHandle, useRef } from "react"; 4 | import { 5 | useWindowDimensions, 6 | type NativeSyntheticEvent, 7 | type NativeScrollEvent, 8 | } from "react-native"; 9 | 10 | export interface ScrollAdaptRef { 11 | scrollRight: (index: number) => void; 12 | } 13 | 14 | interface ScrollAdaptProps { 15 | children: React.ReactNode; 16 | withSnap?: boolean; 17 | itemWidth?: number; 18 | isStudy?: boolean; 19 | } 20 | 21 | export const ScrollAdapt = forwardRef( 22 | ({ children, withSnap = false, itemWidth, isStudy = false }, ref) => { 23 | const { width } = useWindowDimensions(); 24 | const scrollViewRef = useRef(null); 25 | 26 | useImperativeHandle(ref, () => ({ 27 | scrollRight: (index: number) => { 28 | if (itemWidth) { 29 | scrollViewRef.current?.scrollTo({ 30 | x: itemWidth * (index + 1), 31 | animated: true, 32 | }); 33 | } 34 | }, 35 | })); 36 | 37 | const handleScroll = (event: NativeSyntheticEvent) => { 38 | if (isStudy) { 39 | const currentOffset = event.nativeEvent.contentOffset.x; 40 | scrollViewRef.current?.scrollTo({ x: currentOffset, animated: false }); 41 | } 42 | }; 43 | 44 | return ( 45 | 56 | {children} 57 | 58 | ); 59 | }, 60 | ); 61 | 62 | ScrollAdapt.displayName = "ScrollAdapt"; 63 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'sonner-native'; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/sonner/index.web.ts: -------------------------------------------------------------------------------- 1 | export * from 'sonner/dist'; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/suggested-actions.tsx: -------------------------------------------------------------------------------- 1 | import { View, Pressable } from "react-native"; 2 | import { Text } from "@/components/ui/text"; 3 | import { cn } from "@/lib/utils"; 4 | import { ScrollAdapt } from "@/components/scroll-adapt"; 5 | import { useWindowDimensions } from "react-native"; 6 | import { useState, useEffect } from "react"; 7 | import Animated, { 8 | useAnimatedStyle, 9 | useSharedValue, 10 | withTiming, 11 | } from "react-native-reanimated"; 12 | import { useStore } from "@/lib/globalStore"; 13 | import { generateUUID } from "@/lib/utils"; 14 | import type { Message, CreateMessage } from "ai"; 15 | 16 | interface SuggestedActionsProps { 17 | hasInput?: boolean; 18 | append: ( 19 | message: Message | CreateMessage, 20 | chatRequestOptions?: { body?: object }, 21 | ) => Promise; 22 | } 23 | 24 | export function SuggestedActions({ 25 | hasInput = false, 26 | append, 27 | }: SuggestedActionsProps) { 28 | const { selectedImageUris, setChatId } = useStore(); 29 | const { width } = useWindowDimensions(); 30 | const [cardWidth, setCardWidth] = useState(0); 31 | 32 | const opacity = useSharedValue(1); 33 | 34 | useEffect(() => { 35 | opacity.value = withTiming( 36 | hasInput || selectedImageUris.length > 0 ? 0 : 1, 37 | { 38 | duration: 200, 39 | }, 40 | ); 41 | }, [hasInput, selectedImageUris]); 42 | 43 | const animatedStyle = useAnimatedStyle(() => ({ 44 | opacity: opacity.value, 45 | })); 46 | 47 | const handlePress = async (action: string) => { 48 | const newChatId = generateUUID(); 49 | setChatId({ id: newChatId, from: "newChat" }); 50 | 51 | // Send the initial message using append 52 | await append( 53 | { 54 | role: "user", 55 | content: action, 56 | }, 57 | { 58 | body: { id: newChatId }, 59 | }, 60 | ); 61 | }; 62 | 63 | const actions = [ 64 | { 65 | title: "What's the weather forecast", 66 | label: 67 | "Get detailed weather information for San Francisco, including temperature and wind speed.", 68 | action: "What is the weather in San Francisco today?", 69 | }, 70 | { 71 | title: "Help me write an essay", 72 | label: 73 | "Create a well-researched essay exploring Silicon Valley's history, tech culture, innovation ecosystem and global impact", 74 | action: "Help me draft a short essay about Silicon Valley", 75 | }, 76 | { 77 | title: "Get stock market analysis", 78 | label: 79 | "Check current stock prices, market trends, trading volume and key financial metrics for any publicly traded company", 80 | action: "What is the current stock price of Apple (AAPL)?", 81 | }, 82 | ]; 83 | 84 | return ( 85 | 86 | 87 | {actions.map((item, i) => ( 88 | handlePress(item.action)}> 89 | setCardWidth(e.nativeEvent.layout.width)} 91 | className={cn( 92 | "mb-3 mr-2.5 h-32 w-[280px] rounded-lg border border-gray-200 bg-white p-4 dark:bg-black", 93 | )} 94 | style={{ 95 | // borderWidth: StyleSheet.hairlineWidth, 96 | // borderColor: "red", 97 | ...(i === actions.length - 1 && { 98 | marginRight: width - cardWidth, 99 | }), 100 | }} 101 | > 102 | {item.title} 103 | 104 | {item.label} 105 | 106 | 107 | 108 | ))} 109 | 110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as AvatarPrimitive from '@rn-primitives/avatar'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | const AvatarPrimitiveRoot = AvatarPrimitive.Root; 6 | const AvatarPrimitiveImage = AvatarPrimitive.Image; 7 | const AvatarPrimitiveFallback = AvatarPrimitive.Fallback; 8 | 9 | const Avatar = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )); 19 | Avatar.displayName = AvatarPrimitiveRoot.displayName; 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )); 31 | AvatarImage.displayName = AvatarPrimitiveImage.displayName; 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )); 46 | AvatarFallback.displayName = AvatarPrimitiveFallback.displayName; 47 | 48 | export { Avatar, AvatarFallback, AvatarImage }; 49 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/avatar.web.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Avatar = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )) 20 | Avatar.displayName = AvatarPrimitive.Root.displayName 21 | 22 | const AvatarImage = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => ( 26 | 31 | )) 32 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 33 | 34 | const AvatarFallback = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | )) 47 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 48 | 49 | export { Avatar, AvatarImage, AvatarFallback } 50 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import type { TextRef, ViewRef } from "@rn-primitives/types"; 2 | import * as React from "react"; 3 | import { Text, type TextProps, View, type ViewProps } from "react-native"; 4 | import { cn } from "@/lib/utils"; 5 | import { TextClassContext } from "@/components/ui/text"; 6 | 7 | const Card = React.forwardRef( 8 | ({ className, ...props }, ref) => ( 9 | 17 | ), 18 | ); 19 | Card.displayName = "Card"; 20 | 21 | const CardHeader = React.forwardRef( 22 | ({ className, ...props }, ref) => ( 23 | 28 | ), 29 | ); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef( 33 | ({ className, ...props }, ref) => ( 34 | 44 | ), 45 | ); 46 | CardTitle.displayName = "CardTitle"; 47 | 48 | const CardDescription = React.forwardRef( 49 | ({ className, ...props }, ref) => ( 50 | 55 | ), 56 | ); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef( 60 | ({ className, ...props }, ref) => ( 61 | 62 | 63 | 64 | ), 65 | ); 66 | CardContent.displayName = "CardContent"; 67 | 68 | const CardFooter = React.forwardRef( 69 | ({ className, ...props }, ref) => ( 70 | 75 | ), 76 | ); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardContent, 82 | CardDescription, 83 | CardFooter, 84 | CardHeader, 85 | CardTitle, 86 | }; 87 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/chat-text-input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { TextInput } from "react-native"; 3 | import { cn } from "@/lib/utils"; 4 | import { Platform } from "react-native"; 5 | import { Slot } from "@radix-ui/react-slot"; 6 | 7 | const ChatTextInput = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef & { 10 | asChild?: boolean; 11 | noFocus?: boolean; 12 | autoFocus?: boolean; 13 | } 14 | >( 15 | ( 16 | { 17 | className, 18 | placeholderClassName, 19 | asChild = false, 20 | noFocus = false, 21 | autoFocus = false, 22 | ...props 23 | }, 24 | ref, 25 | ) => { 26 | const Comp = asChild ? Slot : TextInput; 27 | 28 | return ( 29 | 42 | ); 43 | }, 44 | ); 45 | 46 | ChatTextInput.displayName = "ChatTextInput"; 47 | 48 | export { ChatTextInput }; 49 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/h1.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import type { PropsWithChildren } from "react"; 3 | import { Text } from "react-native"; 4 | 5 | const H1: React.FC = ({ children }) => { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | 13 | export default H1; 14 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { TextInput } from "react-native"; 3 | import { cn } from "@/lib/utils"; 4 | import { Platform } from "react-native"; 5 | import { Slot } from "@radix-ui/react-slot"; 6 | 7 | // global default or per component override 8 | // you get shadcn 9 | const Input = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef & { 12 | asChild?: boolean; 13 | noFocus?: boolean; 14 | } 15 | >( 16 | ( 17 | { 18 | className, 19 | placeholderClassName, 20 | asChild = false, 21 | noFocus = false, 22 | ...props 23 | }, 24 | ref, 25 | ) => { 26 | const Comp = asChild ? Slot : TextInput; 27 | 28 | return ( 29 | 43 | ); 44 | }, 45 | ); 46 | 47 | Input.displayName = "Input"; 48 | 49 | export { Input }; 50 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/markdown.tsx: -------------------------------------------------------------------------------- 1 | import Markdown from "react-native-markdown-display"; 2 | import { 3 | H1 as ExpoH1, 4 | H2 as ExpoH2, 5 | H3 as ExpoH3, 6 | H4 as ExpoH4, 7 | H5 as ExpoH5, 8 | H6 as ExpoH6, 9 | Code as ExpoCode, 10 | Pre as ExpoPre, 11 | UL as ExpoUl, 12 | LI as ExpoLI, 13 | Strong as ExpoStrong, 14 | A as ExpoA, 15 | P as ExpoP, 16 | Div as ExpoDiv, 17 | } from "@expo/html-elements"; 18 | import { cssInterop } from "nativewind"; 19 | 20 | const H1 = cssInterop(ExpoH1, { className: "style" }); 21 | const H2 = cssInterop(ExpoH2, { className: "style" }); 22 | const H3 = cssInterop(ExpoH3, { className: "style" }); 23 | const H4 = cssInterop(ExpoH4, { className: "style" }); 24 | const H5 = cssInterop(ExpoH5, { className: "style" }); 25 | const H6 = cssInterop(ExpoH6, { className: "style" }); 26 | const Code = cssInterop(ExpoCode, { className: "style" }); 27 | const Pre = cssInterop(ExpoPre, { className: "style" }); 28 | const Ol = cssInterop(ExpoUl, { className: "style" }); 29 | const Ul = cssInterop(ExpoUl, { className: "style" }); 30 | const Li = cssInterop(ExpoLI, { className: "style" }); 31 | const Strong = cssInterop(ExpoStrong, { className: "style" }); 32 | const A = cssInterop(ExpoA, { className: "style" }); 33 | const P = cssInterop(ExpoP, { className: "style" }); 34 | const Div = cssInterop(ExpoDiv, { className: "style" }); 35 | 36 | const rules = { 37 | heading1: (node, children) => ( 38 |

{children}

39 | ), 40 | heading2: (node, children) => ( 41 |

{children}

42 | ), 43 | heading3: (node, children) => ( 44 |

{children}

45 | ), 46 | heading4: (node, children) => ( 47 |

{children}

48 | ), 49 | heading5: (node, children) => ( 50 |

{children}

51 | ), 52 | heading6: (node, children) => ( 53 |

{children}

54 | ), 55 | code: (node, children, parent) => { 56 | return parent.length > 1 ? ( 57 |
58 |         {children}
59 |       
60 | ) : ( 61 | 62 | {children} 63 | 64 | ); 65 | }, 66 | list_item: (node, children) =>
  • {children}
  • , 67 | ordered_list: (node, children) => ( 68 |
      {children}
    69 | ), 70 | unordered_list: (node, children) => ( 71 |
      {children}
    72 | ), 73 | strong: (node, children) => ( 74 | {children} 75 | ), 76 | link: (node, children) => ( 77 | 83 | {children} 84 | 85 | ), 86 | text: (node) => { 87 | return

    {node.content}

    ; 88 | }, 89 | body: (node, children) => { 90 | return
    {children}
    ; 91 | }, 92 | }; 93 | 94 | export function CustomMarkdown({ content }) { 95 | return {content}; 96 | } 97 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/pressable-scale.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentProps, useMemo } from "react"; 2 | import { MotiPressable } from "moti/interactions"; 3 | import { cssInterop } from "nativewind"; 4 | 5 | export type PressableScaleProps = ComponentProps & { 6 | scaleTo?: number; 7 | role?: string; 8 | className?: string; 9 | }; 10 | 11 | const StyledMotiPressable = cssInterop(MotiPressable, { className: "style" }); 12 | 13 | export function PressableScale({ 14 | scaleTo = 0.95, 15 | className, 16 | disabled, 17 | ...props 18 | }: PressableScaleProps) { 19 | return ( 20 | 26 | ({ pressed }) => { 27 | "worklet"; 28 | if (disabled) { 29 | return {}; 30 | } 31 | return { 32 | scale: pressed ? scaleTo : 1, 33 | }; 34 | }, 35 | [disabled, scaleTo] 36 | )} 37 | /> 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/ui/text.tsx: -------------------------------------------------------------------------------- 1 | import * as Slot from "@rn-primitives/slot"; 2 | import type { SlottableTextProps, TextRef } from "@rn-primitives/types"; 3 | import * as React from "react"; 4 | import { Text as RNText } from "react-native"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | const TextClassContext = React.createContext(undefined); 8 | 9 | const Text = React.forwardRef( 10 | ({ className, asChild = false, ...props }, ref) => { 11 | const textClass = React.useContext(TextClassContext); 12 | const Component = asChild ? Slot.Text : RNText; 13 | return ( 14 | 23 | ); 24 | } 25 | ); 26 | Text.displayName = "Text"; 27 | 28 | export { Text, TextClassContext }; 29 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/weather.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from "react-native"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { Wind, Droplets, Sun, Cloud } from "@/lib/icons"; 4 | 5 | interface WeatherCardProps { 6 | city: string; 7 | temperature: number; 8 | weatherCode: number; 9 | humidity: number; 10 | wind: number; 11 | } 12 | 13 | export default function WeatherCard({ 14 | city, 15 | temperature, 16 | weatherCode, 17 | humidity, 18 | wind, 19 | }: WeatherCardProps) { 20 | const getWeatherIcon = (code: number) => { 21 | // WMO Weather interpretation codes 22 | // https://open-meteo.com/en/docs 23 | if (code === 0) { 24 | // Clear sky 25 | return ( 26 | 30 | ); 31 | } else if (code >= 1 && code <= 3) { 32 | // Partly cloudy 33 | return ( 34 | 39 | ); 40 | } 41 | return ( 42 | 47 | ); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | {city} 54 | {getWeatherIcon(weatherCode)} 55 | 56 | 57 | {temperature}°F 58 | 59 | 60 | 61 | {humidity}% Humidity 62 | 63 | 64 | 65 | {wind} mph 66 | 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/components/welcome-message.tsx: -------------------------------------------------------------------------------- 1 | import { View, Pressable, Linking } from "react-native"; 2 | import { Image } from "expo-image"; 3 | import { Text } from "@/components/ui/text"; 4 | import { MessageCircle } from "@/lib/icons"; 5 | 6 | export const WelcomeMessage = () => { 7 | return ( 8 | 9 | 10 | 15 | + 16 | 17 | + 18 | 19 | 20 | 21 | 22 | 23 | This is a chatbot template built with React Native / Expo and the AI 24 | SDK by Vercel that complements Next.js AI Chatbot. It uses native 25 | components with modern UX patterns with the{" "} 26 | streamText{" "} 27 | function in the server and the{" "} 28 | useChat hook 29 | on the client to create a seamless chat experience. 30 | 31 | 32 | 33 | Learn more about the AI SDK by visiting the{" "} 34 | Linking.openURL("https://sdk.vercel.ai/docs")} 36 | > 37 | 38 | docs 39 | 40 | 41 | . 42 | 43 | 44 | Learn more about the Expo SDK by visiting the{" "} 45 | Linking.openURL("https://docs.expo.dev")}> 46 | 47 | docs 48 | 49 | 50 | . 51 | 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import type { ColorSchemeName } from "react-native"; 3 | 4 | type SetColorScheme = (colorScheme: ColorSchemeName) => void; 5 | 6 | export const ColorSchemeContext = createContext( 7 | null as unknown as { 8 | colorScheme: ColorSchemeName; 9 | setColorScheme: SetColorScheme; 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/hook.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { ColorSchemeContext } from "./context"; 4 | 5 | export const useColorScheme = () => { 6 | const colorSchemeContext = useContext(ColorSchemeContext); 7 | 8 | if (!colorSchemeContext) { 9 | console.error( 10 | "Please wrap your app with from @showtime-xyz/universal.color-scheme" 11 | ); 12 | } 13 | // return colorSchemeContext; 14 | // TODO: fix 15 | return {colorScheme: "light"} 16 | }; 17 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/index.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from "./hook"; 2 | export { ColorSchemeProvider, toggleColorScheme } from "./provider"; 3 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/provider.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect } from "react"; 2 | import { 3 | useColorScheme as useDeviceColorScheme, 4 | Appearance, 5 | Platform, 6 | } from "react-native"; 7 | import type { ColorSchemeName } from "react-native"; 8 | 9 | import * as NavigationBar from "expo-navigation-bar"; 10 | import * as SystemUI from "expo-system-ui"; 11 | import { useColorScheme as useTailwindColorScheme } from "nativewind"; 12 | 13 | import { ColorSchemeContext } from "./context"; 14 | import { 15 | deleteDisabledSystemTheme, 16 | getColorScheme as getPersistedColorScheme, 17 | getDisabledSystemTheme, 18 | setColorScheme as persistColorScheme, 19 | setDisabledSystemTheme, 20 | } from "./store"; 21 | 22 | export const toggleColorScheme = (isDark?: boolean) => { 23 | if (Platform.OS !== "android") return; 24 | NavigationBar.setBackgroundColorAsync(isDark ? "#000" : "#FFF"); 25 | NavigationBar.setButtonStyleAsync(isDark ? "light" : "dark"); 26 | }; 27 | 28 | export function ColorSchemeProvider({ 29 | children, 30 | }: { 31 | children: React.ReactNode; 32 | }): JSX.Element { 33 | const deviceColorScheme = useDeviceColorScheme(); 34 | const nativewind = useTailwindColorScheme(); 35 | const [colorScheme, setColorScheme] = useState<"dark" | "light">( 36 | deviceColorScheme 37 | ); 38 | 39 | useEffect(() => { 40 | getPersistedColorScheme().then((persistedScheme) => { 41 | if (persistedScheme) setColorScheme(persistedScheme); 42 | }); 43 | }, []); 44 | 45 | const changeTheme = useCallback( 46 | (newColorScheme: ColorSchemeName) => { 47 | if (!newColorScheme) return; 48 | persistColorScheme(newColorScheme); 49 | setColorScheme(newColorScheme); 50 | const isDark = newColorScheme === "dark"; 51 | // TODO: build error: comment for now 52 | // if (isDark) { 53 | // toggleColorScheme(isDark); 54 | // SystemUI.setBackgroundColorAsync("black"); 55 | // nativewind.setColorScheme("dark"); 56 | // } else { 57 | // toggleColorScheme(); 58 | // SystemUI.setBackgroundColorAsync("white"); 59 | // nativewind.setColorScheme("light"); 60 | // } 61 | }, 62 | [nativewind] 63 | ); 64 | 65 | useEffect(() => { 66 | const themeChangeListener = () => { 67 | const theme = Appearance.getColorScheme(); 68 | changeTheme(theme && !getDisabledSystemTheme() ? theme : colorScheme); 69 | }; 70 | themeChangeListener(); 71 | const appearanceListener = 72 | Appearance.addChangeListener(themeChangeListener); 73 | return () => { 74 | // @ts-ignore 75 | appearanceListener.remove(); 76 | }; 77 | }, [changeTheme, colorScheme]); 78 | 79 | const handleColorSchemeChange = (newColorScheme: ColorSchemeName) => { 80 | if (newColorScheme) { 81 | changeTheme(newColorScheme); 82 | setDisabledSystemTheme(); 83 | } else { 84 | deleteDisabledSystemTheme(); 85 | const theme = Appearance.getColorScheme(); 86 | if (theme) { 87 | changeTheme(theme); 88 | } 89 | } 90 | }; 91 | 92 | return ( 93 | 96 | {children} 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/provider.web.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from "react"; 2 | import { 3 | useColorScheme as useDeviceColorScheme, 4 | Appearance, 5 | } from "react-native"; 6 | import type { ColorSchemeName } from "react-native"; 7 | 8 | import { ColorSchemeContext } from "./context"; 9 | import { 10 | deleteDisabledSystemTheme, 11 | getColorScheme as getPersistedColorScheme, 12 | setColorScheme as persistColorScheme, 13 | setDisabledSystemTheme, 14 | } from "./store"; 15 | import { getDisabledSystemTheme } from "./store"; 16 | 17 | // eslint-disable-next-line unused-imports/no-unused-vars 18 | export const toggleColorScheme = (isDark?: boolean) => {}; 19 | 20 | export function ColorSchemeProvider({ 21 | children, 22 | }: { 23 | children: React.ReactNode; 24 | }): JSX.Element { 25 | const deviceColorScheme = useDeviceColorScheme(); 26 | const [colorScheme, setColorScheme] = useState<"dark" | "light">( 27 | getPersistedColorScheme() ?? deviceColorScheme 28 | ); 29 | const changeTheme = useCallback((newColorScheme: ColorSchemeName) => { 30 | if (!newColorScheme) return; 31 | persistColorScheme(newColorScheme); 32 | setColorScheme(newColorScheme); 33 | const isDark = newColorScheme === "dark"; 34 | document.documentElement.setAttribute( 35 | "data-color-scheme", 36 | isDark ? "dark" : "light" 37 | ); 38 | if (isDark) { 39 | document.documentElement.classList.add("dark"); 40 | } else { 41 | document.documentElement.classList.remove("dark"); 42 | } 43 | 44 | const themeColor = isDark ? "#000000" : "#ffffff"; 45 | // Change the content attribute 46 | let metaThemeColor = document.querySelector("meta[name='theme-color']"); 47 | if (metaThemeColor) { 48 | metaThemeColor.setAttribute("content", themeColor); 49 | } else { 50 | // If the meta tag does not exist, you can create it and append it to the 51 | metaThemeColor = document.createElement("meta"); 52 | metaThemeColor.setAttribute("name", "theme-color"); 53 | metaThemeColor.setAttribute("content", themeColor); 54 | document.head.appendChild(metaThemeColor); 55 | } 56 | }, []); 57 | 58 | useEffect(() => { 59 | const themeChangeListener = () => { 60 | const theme = Appearance.getColorScheme(); 61 | changeTheme(theme && !getDisabledSystemTheme() ? theme : colorScheme); 62 | }; 63 | 64 | themeChangeListener(); 65 | const appearanceListener = 66 | Appearance.addChangeListener(themeChangeListener); 67 | return () => { 68 | // @ts-ignore 69 | appearanceListener.remove(); 70 | }; 71 | }, [changeTheme, colorScheme]); 72 | 73 | const handleColorSchemeChange = (newColorScheme: ColorSchemeName) => { 74 | if (newColorScheme) { 75 | changeTheme(newColorScheme); 76 | setDisabledSystemTheme(); 77 | } else { 78 | deleteDisabledSystemTheme(); 79 | const theme = Appearance.getColorScheme(); 80 | if (theme) { 81 | changeTheme(theme); 82 | } 83 | } 84 | }; 85 | 86 | return ( 87 | 90 | {children} 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/store.tsx: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | 3 | const COLOR_SCHEME_STRING = "color-scheme"; 4 | const DISABLED_SYSTEM_THEME = "disabled-system-theme"; 5 | 6 | export async function setColorScheme(colorScheme: "light" | "dark") { 7 | await AsyncStorage.setItem(COLOR_SCHEME_STRING, colorScheme); 8 | } 9 | 10 | export async function getColorScheme() { 11 | const value = await AsyncStorage.getItem(COLOR_SCHEME_STRING); 12 | return value as "light" | "dark"; 13 | } 14 | 15 | export async function deleteColorScheme() { 16 | await AsyncStorage.removeItem(COLOR_SCHEME_STRING); 17 | } 18 | 19 | export async function getDisabledSystemTheme() { 20 | return await AsyncStorage.getItem(DISABLED_SYSTEM_THEME); 21 | } 22 | 23 | export async function setDisabledSystemTheme() { 24 | await AsyncStorage.setItem(DISABLED_SYSTEM_THEME, "true"); 25 | } 26 | 27 | export async function deleteDisabledSystemTheme() { 28 | await AsyncStorage.removeItem(DISABLED_SYSTEM_THEME); 29 | } 30 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/design-system/color-scheme/store.web.tsx: -------------------------------------------------------------------------------- 1 | const COLOR_SCHEME_STRING = "color-scheme"; 2 | const DISABLED_SYSTEM_THEME = "disabled-system-theme"; 3 | 4 | export function setColorScheme(colorScheme: "light" | "dark") { 5 | if (typeof window !== "undefined") { 6 | localStorage.setItem(COLOR_SCHEME_STRING, colorScheme); 7 | } 8 | } 9 | 10 | export function getColorScheme() { 11 | if (typeof window !== "undefined") { 12 | return localStorage.getItem(COLOR_SCHEME_STRING) as "light" | "dark"; 13 | } 14 | } 15 | 16 | export function deleteColorScheme() { 17 | if (typeof window !== "undefined") { 18 | localStorage.removeItem(COLOR_SCHEME_STRING); 19 | } 20 | } 21 | 22 | export function getDisabledSystemTheme() { 23 | return localStorage.getItem(DISABLED_SYSTEM_THEME); 24 | } 25 | 26 | export function setDisabledSystemTheme() { 27 | localStorage.setItem(DISABLED_SYSTEM_THEME, "true"); 28 | } 29 | 30 | export function deleteDisabledSystemTheme() { 31 | localStorage.removeItem(DISABLED_SYSTEM_THEME); 32 | } 33 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/global.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 | } 27 | 28 | .dark:root { 29 | --background: 240 10% 3.9%; 30 | --foreground: 0 0% 98%; 31 | --card: 240 10% 3.9%; 32 | --card-foreground: 0 0% 98%; 33 | --popover: 240 10% 3.9%; 34 | --popover-foreground: 0 0% 98%; 35 | --primary: 0 0% 98%; 36 | --primary-foreground: 240 5.9% 10%; 37 | --secondary: 240 3.7% 15.9%; 38 | --secondary-foreground: 0 0% 98%; 39 | --muted: 240 3.7% 15.9%; 40 | --muted-foreground: 240 5% 64.9%; 41 | --accent: 240 3.7% 15.9%; 42 | --accent-foreground: 0 0% 98%; 43 | --destructive: 0 72% 51%; 44 | --destructive-foreground: 0 0% 98%; 45 | --border: 240 3.7% 15.9%; 46 | --input: 240 3.7% 15.9%; 47 | --ring: 240 4.9% 83.9%; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/hooks/useImagePicker.ts: -------------------------------------------------------------------------------- 1 | import * as ImagePicker from 'expo-image-picker'; 2 | import { Alert } from 'react-native'; 3 | 4 | type ImagePickerResult = { 5 | pickImage: () => Promise; 6 | }; 7 | 8 | export function useImagePicker(): ImagePickerResult { 9 | const pickImage = async (): Promise => { 10 | try { 11 | const result = await ImagePicker.launchImageLibraryAsync({ 12 | allowsMultipleSelection: true, 13 | mediaTypes: ImagePicker.MediaTypeOptions.Images, 14 | aspect: [4, 3], 15 | quality: 1, 16 | }); 17 | 18 | if (!result.canceled && result.assets.length > 0) { 19 | return result.assets.map(asset => asset.uri); 20 | } 21 | } catch (error) { 22 | Alert.alert( 23 | 'Error', 24 | 'Failed to pick image. Please try again.', 25 | [{ text: 'OK' }] 26 | ); 27 | } 28 | }; 29 | 30 | return { pickImage }; 31 | } -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/api-client.ts: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | 3 | export async function fetchApi( 4 | endpoint: string, 5 | options: { token: string; chatId?: string }, 6 | ) { 7 | const token = await AsyncStorage.getItem("session"); 8 | 9 | const response = await fetch( 10 | `${process.env.EXPO_PUBLIC_API_URL}/api/${endpoint}`, 11 | { 12 | credentials: "include", 13 | headers: { 14 | "Content-Type": "application/json", 15 | ...(token && { Authorization: `Bearer ${token}` }), 16 | }, 17 | ...(options.chatId && { 18 | body: JSON.stringify({ chatId: options.chatId }), 19 | }), 20 | }, 21 | ); 22 | 23 | if (!response.ok) { 24 | throw new Error(`API error: ${response.statusText}`); 25 | } 26 | 27 | return response.json(); 28 | } 29 | 30 | export async function getChatsByUserId({ token }: { token: string }) { 31 | try { 32 | console.log("getChatsByUserId called"); 33 | const response = await fetchApi("history", { 34 | token, 35 | }); 36 | console.log("getChatsByUserId response", response); 37 | return response; 38 | } catch (error) { 39 | console.error("Error fetching chats.", error); 40 | throw new Error("Failed to fetch chats"); 41 | } 42 | } 43 | 44 | export async function getChatById({ 45 | chatId, 46 | token, 47 | }: { 48 | chatId: string; 49 | token: string; 50 | }) { 51 | try { 52 | const response = await fetchApi("/api/chat", { chatId, token }); 53 | return response; 54 | } catch (error) { 55 | console.error("Error fetching chat.", error); 56 | throw new Error("Failed to fetch chat"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const NAV_THEME = { 2 | light: { 3 | background: 'hsl(0 0% 100%)', // background 4 | border: 'hsl(240 5.9% 90%)', // border 5 | card: 'hsl(0 0% 100%)', // card 6 | notification: 'hsl(0 84.2% 60.2%)', // destructive 7 | primary: 'hsl(240 5.9% 10%)', // primary 8 | text: 'hsl(240 10% 3.9%)', // foreground 9 | }, 10 | dark: { 11 | background: 'hsl(240 10% 3.9%)', // background 12 | border: 'hsl(240 3.7% 15.9%)', // border 13 | card: 'hsl(240 10% 3.9%)', // card 14 | notification: 'hsl(0 72% 51%)', // destructive 15 | primary: 'hsl(0 0% 98%)', // primary 16 | text: 'hsl(0 0% 98%)', // foreground 17 | }, 18 | }; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/getOpenGraphDataQuery.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | 3 | interface OpenGraphData { 4 | title: string; 5 | description: string; 6 | image: string; 7 | url: string; 8 | siteName: string; 9 | } 10 | 11 | export async function getOpenGraphDataQuery( 12 | url: string, 13 | ): Promise { 14 | const userAgent = "Mozilla/5.0 (compatible; MySuperTool)"; 15 | 16 | try { 17 | const response = await fetch(url, { 18 | headers: { 19 | "User-Agent": userAgent, 20 | }, 21 | // mode: 'no-cors' 22 | }); 23 | 24 | const html = await response.text(); 25 | const $ = cheerio.load(html); 26 | 27 | const ogData: OpenGraphData = { 28 | title: $('meta[property="og:title"]').attr("content") || 29 | $("title").text() || "", 30 | description: $('meta[property="og:description"]').attr("content") || 31 | $('meta[name="description"]').attr("content") || "", 32 | image: $('meta[property="og:image"]').attr("content") || "", 33 | url: $('meta[property="og:url"]').attr("content") || url, 34 | siteName: $('meta[property="og:site_name"]').attr("content") || "", 35 | }; 36 | 37 | console.log("ogData", ogData); 38 | return ogData; 39 | } catch (error) { 40 | console.error("Error fetching Open Graph data:", error); 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/globalStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | type ChatIdState = { 4 | id: string; 5 | from: "history" | "newChat"; 6 | } | null; 7 | 8 | interface StoreState { 9 | scrollY: number; 10 | setScrollY: (value: number) => void; 11 | selectedImageUris: string[]; 12 | addImageUri: (uri: string) => void; 13 | removeImageUri: (uri: string) => void; 14 | clearImageUris: () => void; 15 | setBottomChatHeightHandler: (value: boolean) => void; 16 | bottomChatHeightHandler: boolean; 17 | chatId: ChatIdState; 18 | setChatId: (value: { id: string; from: "history" | "newChat" }) => void; 19 | setFocusKeyboard: (value: boolean) => void; 20 | focusKeyboard: boolean; 21 | } 22 | 23 | export const useStore = create((set) => ({ 24 | scrollY: 0, 25 | setScrollY: (value: number) => set({ scrollY: value }), 26 | selectedImageUris: [], 27 | addImageUri: (uri: string) => 28 | set((state) => ({ 29 | selectedImageUris: [...state.selectedImageUris, uri], 30 | })), 31 | removeImageUri: (uri: string) => 32 | set((state) => ({ 33 | selectedImageUris: state.selectedImageUris.filter( 34 | (imageUri) => imageUri !== uri, 35 | ), 36 | })), 37 | clearImageUris: () => set({ selectedImageUris: [] }), 38 | bottomChatHeightHandler: false, 39 | setBottomChatHeightHandler: (value: boolean) => 40 | set({ bottomChatHeightHandler: value }), 41 | chatId: null, 42 | setChatId: (value) => set({ chatId: value }), 43 | focusKeyboard: false, 44 | setFocusKeyboard: (value: boolean) => set({ focusKeyboard: value }), 45 | })); 46 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/icons/Cloud.ts: -------------------------------------------------------------------------------- 1 | import { Cloud } from 'lucide-react-native'; 2 | import { iconWithClassName } from './iconWithClassName'; 3 | iconWithClassName(Cloud); 4 | export { Cloud }; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/icons/Droplets.ts: -------------------------------------------------------------------------------- 1 | import { Droplets } from 'lucide-react-native'; 2 | import { iconWithClassName } from './iconWithClassName'; 3 | iconWithClassName(Droplets); 4 | export { Droplets }; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/icons/Sun.ts: -------------------------------------------------------------------------------- 1 | import { Sun } from 'lucide-react-native'; 2 | import { iconWithClassName } from './iconWithClassName'; 3 | iconWithClassName(Sun); 4 | export { Sun }; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/icons/Wind.ts: -------------------------------------------------------------------------------- 1 | import { Wind } from 'lucide-react-native'; 2 | import { iconWithClassName } from './iconWithClassName'; 3 | iconWithClassName(Wind) 4 | export { Wind }; -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/icons/iconWithClassName.ts: -------------------------------------------------------------------------------- 1 | import type { LucideIcon } from 'lucide-react-native'; 2 | import { cssInterop } from 'nativewind'; 3 | 4 | export function iconWithClassName(icon: LucideIcon) { 5 | cssInterop(icon, { 6 | className: { 7 | target: 'style', 8 | nativeStyleToProp: { 9 | color: true, 10 | opacity: true, 11 | }, 12 | }, 13 | }); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/icons/index.ts: -------------------------------------------------------------------------------- 1 | import * as LucideIcons from 'lucide-react-native'; 2 | import { iconWithClassName } from './iconWithClassName'; 3 | 4 | for (const icon of Object.values(LucideIcons)) { 5 | if (typeof icon === 'function' && 'displayName' in icon) { 6 | iconWithClassName(icon); 7 | } 8 | } 9 | 10 | export * from 'lucide-react-native'; 11 | 12 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | import { AppState, Platform } from "react-native"; 4 | import type { Session } from "@/lib/supabase"; 5 | 6 | const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL; 7 | const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY; 8 | 9 | export const supabase = createClient(supabaseUrl, supabaseAnonKey, { 10 | auth: { 11 | // fix ref: https://github.com/supabase/supabase-js/issues/786#issuecomment-1871436625 12 | ...(Platform.OS !== "web" ? { storage: AsyncStorage } : {}), 13 | autoRefreshToken: true, 14 | persistSession: true, 15 | detectSessionInUrl: false, 16 | }, 17 | }); 18 | 19 | export { 20 | type AuthChangeEvent, 21 | type AuthError, 22 | type Session, 23 | } from "@supabase/supabase-js"; 24 | 25 | /** 26 | * Tells Supabase to autorefresh the session while the application 27 | * is in the foreground. (Docs: https://supabase.com/docs/reference/javascript/auth-startautorefresh) 28 | */ 29 | AppState.addEventListener("change", (nextAppState) => { 30 | if (nextAppState === "active") { 31 | supabase.auth.startAutoRefresh(); 32 | } else { 33 | supabase.auth.stopAutoRefresh(); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/lib/useColorScheme.tsx: -------------------------------------------------------------------------------- 1 | // import { useColorScheme as useNativewindColorScheme } from "nativewind"; 2 | 3 | export function useColorScheme() { 4 | const { colorScheme, setColorScheme, toggleColorScheme } = {colorScheme: "light", setColorScheme: "", toggleColorScheme: "" } 5 | // useNativewindColorScheme(); 6 | return { 7 | colorScheme: colorScheme ?? "dark", 8 | isDarkColorScheme: colorScheme === "dark", 9 | setColorScheme, 10 | toggleColorScheme, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /expo-ai-chatbot/src/providers.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 3 | import { ColorSchemeProvider } from "@/design-system/color-scheme/provider"; 4 | import { Toaster } from "@/components/sonner"; 5 | import NativewindThemeProvider from "./ThemeProvider"; 6 | import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; 7 | 8 | function Providers({ children }: { children: React.ReactNode }) { 9 | return ( 10 | 11 | 12 | 13 | 14 | {children} 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default Providers; 22 | -------------------------------------------------------------------------------- /expo-ai-chatbot/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { hairlineWidth } = require("nativewind/theme"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: "class", 6 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 7 | // content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'], 8 | presets: [require("nativewind/preset")], 9 | theme: { 10 | screens: { 11 | xs: { max: "639px" }, 12 | sm: "640px", 13 | md: "768px", 14 | lg: "1024px", 15 | xl: "1280px", 16 | "2xl": "1536px", 17 | }, 18 | extend: { 19 | fontFamily: { 20 | // 'inter-900': ["Inter_900Black", "sans-serif"], 21 | 'inter-600': ["Inter_600SemiBold", "sans-serif"], 22 | 'inter-700': ["Inter_700Bold", "sans-serif"], 23 | }, 24 | colors: { 25 | border: "hsl(var(--border))", 26 | input: "hsl(var(--input))", 27 | ring: "hsl(var(--ring))", 28 | background: "hsl(var(--background))", 29 | foreground: "hsl(var(--foreground))", 30 | primary: { 31 | DEFAULT: "hsl(var(--primary))", 32 | foreground: "hsl(var(--primary-foreground))", 33 | }, 34 | secondary: { 35 | DEFAULT: "hsl(var(--secondary))", 36 | foreground: "hsl(var(--secondary-foreground))", 37 | }, 38 | destructive: { 39 | DEFAULT: "hsl(var(--destructive))", 40 | foreground: "hsl(var(--destructive-foreground))", 41 | }, 42 | muted: { 43 | DEFAULT: "hsl(var(--muted))", 44 | foreground: "hsl(var(--muted-foreground))", 45 | }, 46 | accent: { 47 | DEFAULT: "hsl(var(--accent))", 48 | foreground: "hsl(var(--accent-foreground))", 49 | }, 50 | popover: { 51 | DEFAULT: "hsl(var(--popover))", 52 | foreground: "hsl(var(--popover-foreground))", 53 | }, 54 | card: { 55 | DEFAULT: "hsl(var(--card))", 56 | foreground: "hsl(var(--card-foreground))", 57 | }, 58 | }, 59 | borderWidth: { 60 | hairline: hairlineWidth(), 61 | }, 62 | }, 63 | }, 64 | plugins: [], 65 | }; 66 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/.env.local.sample: -------------------------------------------------------------------------------- 1 | # Get your OpenAI API Key here: https://platform.openai.com/account/api-keys 2 | OPENAI_API_KEY= 3 | 4 | # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` 5 | AUTH_SECRET= 6 | 7 | # The following keys below are automatically created and 8 | # added to your environment when you deploy on vercel 9 | 10 | # Instructions to create a Vercel Blob Store here: https://vercel.com/docs/storage/vercel-blob 11 | BLOB_READ_WRITE_TOKEN= 12 | 13 | # Instructions to create a database here: https://vercel.com/docs/storage/vercel-postgres/quickstart 14 | POSTGRES_URL= 15 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:import/recommended", 5 | "plugin:import/typescript", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "tailwindcss/no-custom-classname": "off", 12 | "tailwindcss/classnames-order": "off" 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "typescript": { 17 | "alwaysTryTypes": true 18 | } 19 | } 20 | }, 21 | "ignorePatterns": ["**/components/ui/**"] 22 | } 23 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | *.idea 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .pnpm-debug.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | .env 37 | .vercel 38 | .vscode 39 | .env*.local 40 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /nextjs-ai-chatbot/README.md: -------------------------------------------------------------------------------- 1 | 2 | Next.js 14 and App Router-ready AI chatbot. 3 |

    Next.js AI Chatbot

    4 |
    5 | 6 |

    7 | An Open-Source AI Chatbot Template Built With Next.js and the AI SDK by Vercel. 8 |

    9 | 10 |

    11 | Features · 12 | Model Providers · 13 | Deploy Your Own · 14 | Running locally 15 |

    16 |
    17 | 18 | ## Features 19 | 20 | - [Next.js](https://nextjs.org) App Router 21 | - Advanced routing for seamless navigation and performance 22 | - React Server Components (RSCs) and Server Actions for server-side rendering and increased performance 23 | - [AI SDK](https://sdk.vercel.ai/docs) 24 | - Unified API for generating text, structured objects, and tool calls with LLMs 25 | - Hooks for building dynamic chat and generative user interfaces 26 | - Supports OpenAI (default), Anthropic, Cohere, and other model providers 27 | - [shadcn/ui](https://ui.shadcn.com) 28 | - Styling with [Tailwind CSS](https://tailwindcss.com) 29 | - Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility 30 | - Data Persistence 31 | - [Vercel Postgres powered by Neon](https://vercel.com/storage/postgres) for saving chat history and user data 32 | - [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage 33 | - [NextAuth.js](https://github.com/nextauthjs/next-auth) 34 | - Simple and secure authentication 35 | 36 | ## Model Providers 37 | 38 | This template ships with OpenAI `gpt-4o` as the default. However, with the [AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://sdk.vercel.ai/providers/ai-sdk-providers) with just a few lines of code. 39 | 40 | ## Deploy Your Own 41 | 42 | You can deploy your own version of the Next.js AI Chatbot to Vercel with one click: 43 | 44 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot&env=AUTH_SECRET,OPENAI_API_KEY&envDescription=Learn%20more%20about%20how%20to%20get%20the%20API%20Keys%20for%20the%20application&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&demo-title=AI%20Chatbot&demo-description=An%20Open-Source%20AI%20Chatbot%20Template%20Built%20With%20Next.js%20and%20the%20AI%20SDK%20by%20Vercel.&demo-url=https%3A%2F%2Fchat.vercel.ai&stores=[{%22type%22:%22postgres%22},{%22type%22:%22blob%22}]) 45 | 46 | ## Running locally 47 | 48 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. 49 | 50 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts. 51 | 52 | 1. Install Vercel CLI: `npm i -g vercel` 53 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` 54 | 3. Download your environment variables: `vercel env pull` 55 | 56 | ```bash 57 | pnpm install 58 | pnpm dev 59 | ``` 60 | 61 | Your app template should now be running on [localhost:3000](http://localhost:3000/). 62 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { z } from 'zod'; 4 | 5 | import { createUser, getUser } from '@/lib/db/queries'; 6 | 7 | import { signIn } from './auth'; 8 | 9 | const authFormSchema = z.object({ 10 | email: z.string().email(), 11 | password: z.string().min(6), 12 | }); 13 | 14 | export interface LoginActionState { 15 | status: 'idle' | 'in_progress' | 'success' | 'failed' | 'invalid_data'; 16 | } 17 | 18 | export const login = async ( 19 | _: LoginActionState, 20 | formData: FormData, 21 | ): Promise => { 22 | try { 23 | const validatedData = authFormSchema.parse({ 24 | email: formData.get('email'), 25 | password: formData.get('password'), 26 | }); 27 | 28 | await signIn('credentials', { 29 | email: validatedData.email, 30 | password: validatedData.password, 31 | redirect: false, 32 | }); 33 | 34 | return { status: 'success' }; 35 | } catch (error) { 36 | if (error instanceof z.ZodError) { 37 | return { status: 'invalid_data' }; 38 | } 39 | 40 | return { status: 'failed' }; 41 | } 42 | }; 43 | 44 | export interface RegisterActionState { 45 | status: 46 | | 'idle' 47 | | 'in_progress' 48 | | 'success' 49 | | 'failed' 50 | | 'user_exists' 51 | | 'invalid_data'; 52 | } 53 | 54 | export const register = async ( 55 | _: RegisterActionState, 56 | formData: FormData, 57 | ): Promise => { 58 | try { 59 | const validatedData = authFormSchema.parse({ 60 | email: formData.get('email'), 61 | password: formData.get('password'), 62 | }); 63 | 64 | const [user] = await getUser(validatedData.email); 65 | 66 | if (user) { 67 | return { status: 'user_exists' } as RegisterActionState; 68 | } 69 | await createUser(validatedData.email, validatedData.password); 70 | await signIn('credentials', { 71 | email: validatedData.email, 72 | password: validatedData.password, 73 | redirect: false, 74 | }); 75 | 76 | return { status: 'success' }; 77 | } catch (error) { 78 | if (error instanceof z.ZodError) { 79 | return { status: 'invalid_data' }; 80 | } 81 | 82 | return { status: 'failed' }; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/app/(auth)/auth'; 2 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/api/token/route.ts: -------------------------------------------------------------------------------- 1 | import { compare } from 'bcrypt-ts'; 2 | import { getUser } from '@/lib/db/queries'; 3 | import { NextResponse } from 'next/server'; 4 | import { createToken } from '@/lib/auth/token'; 5 | 6 | export async function POST(request: Request) { 7 | try { 8 | const { email, password } = await request.json(); 9 | 10 | const users = await getUser(email); 11 | if (users.length === 0) { 12 | return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); 13 | } 14 | 15 | const passwordsMatch = await compare(password, users[0].password!); 16 | if (!passwordsMatch) { 17 | return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }); 18 | } 19 | 20 | const user = users[0]; 21 | 22 | console.log('User:', user); 23 | const token = await createToken({ 24 | id: user.id, 25 | email: user.email, 26 | }); 27 | 28 | console.log('Token:', token); 29 | return NextResponse.json({ 30 | token, 31 | user: { 32 | id: user.id, 33 | email: user.email, 34 | } 35 | }); 36 | 37 | } catch (error) { 38 | console.error('Token auth error:', error); 39 | return NextResponse.json( 40 | { error: 'Authentication failed' }, 41 | { status: 500 } 42 | ); 43 | } 44 | } -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from 'next-auth'; 2 | 3 | export const authConfig = { 4 | pages: { 5 | signIn: '/login', 6 | newUser: '/', 7 | }, 8 | providers: [ 9 | // added later in auth.ts since it requires bcrypt which is only compatible with Node.js 10 | // while this file is also used in non-Node.js environments 11 | ], 12 | callbacks: { 13 | authorized({ auth, request: { nextUrl } }) { 14 | const isLoggedIn = !!auth?.user; 15 | const isOnChat = nextUrl.pathname.startsWith('/'); 16 | const isOnRegister = nextUrl.pathname.startsWith('/register'); 17 | const isOnLogin = nextUrl.pathname.startsWith('/login'); 18 | 19 | if (isLoggedIn && (isOnLogin || isOnRegister)) { 20 | return Response.redirect(new URL('/', nextUrl as unknown as URL)); 21 | } 22 | 23 | if (isOnRegister || isOnLogin) { 24 | return true; // Always allow access to register and login pages 25 | } 26 | 27 | if (isOnChat) { 28 | if (isLoggedIn) return true; 29 | return false; // Redirect unauthenticated users to login page 30 | } 31 | 32 | if (isLoggedIn) { 33 | return Response.redirect(new URL('/', nextUrl as unknown as URL)); 34 | } 35 | 36 | return true; 37 | }, 38 | }, 39 | } satisfies NextAuthConfig; 40 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/auth.ts: -------------------------------------------------------------------------------- 1 | import { compare } from 'bcrypt-ts'; 2 | import NextAuth, { type User, type Session } from 'next-auth'; 3 | import Credentials from 'next-auth/providers/credentials'; 4 | 5 | import { getUser } from '@/lib/db/queries'; 6 | 7 | import { authConfig } from './auth.config'; 8 | 9 | interface ExtendedSession extends Session { 10 | user: User; 11 | } 12 | 13 | export const { 14 | handlers: { GET, POST }, 15 | auth, 16 | signIn, 17 | signOut, 18 | } = NextAuth({ 19 | ...authConfig, 20 | providers: [ 21 | Credentials({ 22 | credentials: {}, 23 | async authorize({ email, password }: any) { 24 | const users = await getUser(email); 25 | if (users.length === 0) return null; 26 | // biome-ignore lint: Forbidden non-null assertion. 27 | const passwordsMatch = await compare(password, users[0].password!); 28 | if (!passwordsMatch) return null; 29 | return users[0] as any; 30 | }, 31 | }), 32 | ], 33 | callbacks: { 34 | async jwt({ token, user }) { 35 | if (user) { 36 | token.id = user.id; 37 | } 38 | 39 | return token; 40 | }, 41 | async session({ 42 | session, 43 | token, 44 | }: { 45 | session: ExtendedSession; 46 | token: any; 47 | }) { 48 | if (session.user) { 49 | session.user.id = token.id as string; 50 | } 51 | 52 | return session; 53 | }, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useActionState, useEffect, useState } from 'react'; 6 | import { toast } from 'sonner'; 7 | 8 | import { AuthForm } from '@/components/auth-form'; 9 | import { SubmitButton } from '@/components/submit-button'; 10 | 11 | import { login, type LoginActionState } from '../actions'; 12 | 13 | export default function Page() { 14 | const router = useRouter(); 15 | 16 | const [email, setEmail] = useState(''); 17 | const [isSuccessful, setIsSuccessful] = useState(false); 18 | 19 | const [state, formAction] = useActionState( 20 | login, 21 | { 22 | status: 'idle', 23 | }, 24 | ); 25 | 26 | useEffect(() => { 27 | if (state.status === 'failed') { 28 | toast.error('Invalid credentials!'); 29 | } else if (state.status === 'invalid_data') { 30 | toast.error('Failed validating your submission!'); 31 | } else if (state.status === 'success') { 32 | setIsSuccessful(true); 33 | router.refresh(); 34 | } 35 | }, [state.status, router]); 36 | 37 | const handleSubmit = (formData: FormData) => { 38 | setEmail(formData.get('email') as string); 39 | formAction(formData); 40 | }; 41 | 42 | return ( 43 |
    44 |
    45 |
    46 |

    Sign In

    47 |

    48 | Use your email and password to sign in 49 |

    50 |
    51 | 52 | Sign in 53 |

    54 | {"Don't have an account? "} 55 | 59 | Sign up 60 | 61 | {' for free.'} 62 |

    63 |
    64 |
    65 |
    66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useActionState, useEffect, useState } from 'react'; 6 | import { toast } from 'sonner'; 7 | 8 | import { AuthForm } from '@/components/auth-form'; 9 | import { SubmitButton } from '@/components/submit-button'; 10 | 11 | import { register, type RegisterActionState } from '../actions'; 12 | 13 | export default function Page() { 14 | const router = useRouter(); 15 | 16 | const [email, setEmail] = useState(''); 17 | const [isSuccessful, setIsSuccessful] = useState(false); 18 | 19 | const [state, formAction] = useActionState( 20 | register, 21 | { 22 | status: 'idle', 23 | }, 24 | ); 25 | 26 | useEffect(() => { 27 | if (state.status === 'user_exists') { 28 | toast.error('Account already exists'); 29 | } else if (state.status === 'failed') { 30 | console.log(state); 31 | toast.error('Failed to create account'); 32 | } else if (state.status === 'invalid_data') { 33 | toast.error('Failed validating your submission!'); 34 | } else if (state.status === 'success') { 35 | toast.success('Account created successfully'); 36 | setIsSuccessful(true); 37 | router.refresh(); 38 | } 39 | }, [state, router]); 40 | 41 | const handleSubmit = (formData: FormData) => { 42 | setEmail(formData.get('email') as string); 43 | formAction(formData); 44 | }; 45 | 46 | return ( 47 |
    48 |
    49 |
    50 |

    Sign Up

    51 |

    52 | Create an account with your email and password 53 |

    54 |
    55 | 56 | Sign Up 57 |

    58 | {'Already have an account? '} 59 | 63 | Sign in 64 | 65 | {' instead.'} 66 |

    67 |
    68 |
    69 |
    70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { type CoreUserMessage, generateText } from 'ai'; 4 | import { cookies } from 'next/headers'; 5 | 6 | import { customModel } from '@/lib/ai'; 7 | import { 8 | deleteMessagesByChatIdAfterTimestamp, 9 | getMessageById, 10 | updateChatVisiblityById, 11 | } from '@/lib/db/queries'; 12 | import { VisibilityType } from '@/components/visibility-selector'; 13 | 14 | export async function saveModelId(model: string) { 15 | const cookieStore = await cookies(); 16 | cookieStore.set('model-id', model); 17 | } 18 | 19 | export async function generateTitleFromUserMessage({ 20 | message, 21 | }: { 22 | message: CoreUserMessage; 23 | }) { 24 | const { text: title } = await generateText({ 25 | model: customModel('gpt-4o-mini'), 26 | system: `\n 27 | - you will generate a short title based on the first message a user begins a conversation with 28 | - ensure it is not more than 80 characters long 29 | - the title should be a summary of the user's message 30 | - do not use quotes or colons`, 31 | prompt: JSON.stringify(message), 32 | }); 33 | 34 | return title; 35 | } 36 | 37 | export async function deleteTrailingMessages({ id }: { id: string }) { 38 | const [message] = await getMessageById({ id }); 39 | 40 | await deleteMessagesByChatIdAfterTimestamp({ 41 | chatId: message.chatId, 42 | timestamp: message.createdAt, 43 | }); 44 | } 45 | 46 | export async function updateChatVisibility({ 47 | chatId, 48 | visibility, 49 | }: { 50 | chatId: string; 51 | visibility: VisibilityType; 52 | }) { 53 | await updateChatVisiblityById({ chatId, visibility }); 54 | } 55 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/api/chat-open/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Message, 3 | convertToCoreMessages, 4 | createDataStreamResponse, 5 | streamText, 6 | } from 'ai'; 7 | import { customModel } from '@/lib/ai'; 8 | import { models } from '@/lib/ai/models'; 9 | import { systemPrompt } from '@/lib/ai/prompts'; 10 | import { generateUUID } from '@/lib/utils'; 11 | 12 | export async function POST(request: Request) { 13 | try { 14 | console.log(">> Request received"); 15 | const { messages, modelId = 'gpt-4' } = await request.json(); 16 | console.log(">> Messages:", messages); 17 | 18 | const model = models.find((m) => m.id === modelId) || models[0]; 19 | const coreMessages = convertToCoreMessages(messages); 20 | const userMessageId = generateUUID(); 21 | 22 | return createDataStreamResponse({ 23 | execute: (dataStream) => { 24 | dataStream.writeData({ 25 | type: 'user-message-id', 26 | content: userMessageId, 27 | }); 28 | 29 | const result = streamText({ 30 | model: customModel(model.apiIdentifier), 31 | system: systemPrompt, 32 | messages: coreMessages, 33 | maxSteps: 5, 34 | }); 35 | 36 | result.mergeIntoDataStream(dataStream); 37 | }, 38 | }); 39 | 40 | } catch (error) { 41 | console.error("API Error:", error); 42 | return new Response( 43 | JSON.stringify({ error: "Failed to generate response" }), 44 | { 45 | status: 500, 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | } 49 | } 50 | ); 51 | } 52 | } 53 | 54 | export async function GET() { 55 | return new Response('Ready', { status: 200 }); 56 | } 57 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/api/files/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { put } from '@vercel/blob'; 2 | import { NextResponse } from 'next/server'; 3 | import { z } from 'zod'; 4 | 5 | import { auth } from '@/app/(auth)/auth'; 6 | 7 | // Use Blob instead of File since File is not available in Node.js environment 8 | const FileSchema = z.object({ 9 | file: z 10 | .instanceof(Blob) 11 | .refine((file) => file.size <= 5 * 1024 * 1024, { 12 | message: 'File size should be less than 5MB', 13 | }) 14 | // Update the file type based on the kind of files you want to accept 15 | .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), { 16 | message: 'File type should be JPEG or PNG', 17 | }), 18 | }); 19 | 20 | export async function POST(request: Request) { 21 | const session = await auth(); 22 | 23 | if (!session) { 24 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 25 | } 26 | 27 | if (request.body === null) { 28 | return new Response('Request body is empty', { status: 400 }); 29 | } 30 | 31 | try { 32 | const formData = await request.formData(); 33 | const file = formData.get('file') as Blob; 34 | 35 | if (!file) { 36 | return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); 37 | } 38 | 39 | const validatedFile = FileSchema.safeParse({ file }); 40 | 41 | if (!validatedFile.success) { 42 | const errorMessage = validatedFile.error.errors 43 | .map((error) => error.message) 44 | .join(', '); 45 | 46 | return NextResponse.json({ error: errorMessage }, { status: 400 }); 47 | } 48 | 49 | // Get filename from formData since Blob doesn't have name property 50 | const filename = (formData.get('file') as File).name; 51 | const fileBuffer = await file.arrayBuffer(); 52 | 53 | try { 54 | const data = await put(`${filename}`, fileBuffer, { 55 | access: 'public', 56 | }); 57 | 58 | return NextResponse.json(data); 59 | } catch (error) { 60 | return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); 61 | } 62 | } catch (error) { 63 | return NextResponse.json( 64 | { error: 'Failed to process request' }, 65 | { status: 500 }, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/api/history/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/app/(auth)/auth'; 2 | import { getChatsByUserId } from '@/lib/db/queries'; 3 | 4 | export async function GET() { 5 | const session = await auth(); 6 | 7 | if (!session || !session.user) { 8 | return Response.json('Unauthorized!', { status: 401 }); 9 | } 10 | 11 | // biome-ignore lint: Forbidden non-null assertion. 12 | const chats = await getChatsByUserId({ id: session.user.id! }); 13 | return Response.json(chats); 14 | } -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/api/suggestions/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/app/(auth)/auth'; 2 | import { getSuggestionsByDocumentId } from '@/lib/db/queries'; 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams } = new URL(request.url); 6 | const documentId = searchParams.get('documentId'); 7 | 8 | if (!documentId) { 9 | return new Response('Not Found', { status: 404 }); 10 | } 11 | 12 | const session = await auth(); 13 | 14 | if (!session || !session.user) { 15 | return new Response('Unauthorized', { status: 401 }); 16 | } 17 | 18 | const suggestions = await getSuggestionsByDocumentId({ 19 | documentId, 20 | }); 21 | 22 | const [suggestion] = suggestions; 23 | 24 | if (!suggestion) { 25 | return Response.json([], { status: 200 }); 26 | } 27 | 28 | if (suggestion.userId !== session.user.id) { 29 | return new Response('Unauthorized', { status: 401 }); 30 | } 31 | 32 | return Response.json(suggestions, { status: 200 }); 33 | } 34 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/api/vote/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/app/(auth)/auth'; 2 | import { getVotesByChatId, voteMessage } from '@/lib/db/queries'; 3 | import { verifyToken, extractTokenFromHeader } from '@/lib/auth/token'; 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams } = new URL(request.url); 7 | const chatId = searchParams.get('chatId'); 8 | 9 | if (!chatId) { 10 | return new Response('chatId is required', { status: 400 }); 11 | } 12 | 13 | // Try token auth first 14 | let userId: string | undefined; 15 | const authHeader = request.headers.get('authorization'); 16 | if (authHeader) { 17 | const token = extractTokenFromHeader(authHeader); 18 | if (token) { 19 | const payload = await verifyToken(token); 20 | if (payload) { 21 | userId = payload.id; 22 | } 23 | } 24 | } 25 | 26 | // If no token or invalid token, try session auth 27 | if (!userId) { 28 | const session = await auth(); 29 | if (session?.user?.id) { 30 | userId = session.user.id; 31 | } 32 | } 33 | 34 | // If no valid auth found 35 | if (!userId) { 36 | return new Response('Unauthorized', { status: 401 }); 37 | } 38 | 39 | try { 40 | const votes = await getVotesByChatId({ id: chatId }); 41 | return Response.json(votes, { status: 200 }); 42 | } catch (error) { 43 | return Response.json([], { status: 200 }); 44 | } 45 | } 46 | 47 | export async function PATCH(request: Request) { 48 | const { 49 | chatId, 50 | messageId, 51 | type, 52 | }: { chatId: string; messageId: string; type: 'up' | 'down' } = 53 | await request.json(); 54 | 55 | if (!chatId || !messageId || !type) { 56 | return new Response('messageId and type are required', { status: 400 }); 57 | } 58 | 59 | // Try token auth first 60 | let userId: string | undefined; 61 | const authHeader = request.headers.get('authorization'); 62 | if (authHeader) { 63 | const token = extractTokenFromHeader(authHeader); 64 | if (token) { 65 | const payload = await verifyToken(token); 66 | if (payload) { 67 | userId = payload.id; 68 | } 69 | } 70 | } 71 | 72 | // If no token or invalid token, try session auth 73 | if (!userId) { 74 | const session = await auth(); 75 | if (session?.user?.id) { 76 | userId = session.user.id; 77 | } 78 | } 79 | 80 | // If no valid auth found 81 | if (!userId) { 82 | return new Response('Unauthorized', { status: 401 }); 83 | } 84 | 85 | try { 86 | await voteMessage({ 87 | chatId, 88 | messageId, 89 | type: type, 90 | }); 91 | return new Response('Message voted', { status: 200 }); 92 | } catch (error) { 93 | return new Response('Vote failed', { status: 200 }); // Return 200 even if vote fails 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | import { notFound } from 'next/navigation'; 3 | 4 | import { auth } from '@/app/(auth)/auth'; 5 | import { Chat } from '@/components/chat'; 6 | import { DEFAULT_MODEL_NAME, models } from '@/lib/ai/models'; 7 | import { getChatById, getMessagesByChatId } from '@/lib/db/queries'; 8 | import { convertToUIMessages } from '@/lib/utils'; 9 | import { DataStreamHandler } from '@/components/data-stream-handler'; 10 | 11 | export default async function Page(props: { params: Promise<{ id: string }> }) { 12 | const params = await props.params; 13 | const { id } = params; 14 | const chat = await getChatById({ id }); 15 | 16 | if (!chat) { 17 | notFound(); 18 | } 19 | 20 | const session = await auth(); 21 | 22 | if (chat.visibility === 'private') { 23 | if (!session || !session.user) { 24 | return notFound(); 25 | } 26 | 27 | if (session.user.id !== chat.userId) { 28 | return notFound(); 29 | } 30 | } 31 | 32 | const messagesFromDb = await getMessagesByChatId({ 33 | id, 34 | }); 35 | 36 | const cookieStore = await cookies(); 37 | const modelIdFromCookie = cookieStore.get('model-id')?.value; 38 | const selectedModelId = 39 | models.find((model) => model.id === modelIdFromCookie)?.id || 40 | DEFAULT_MODEL_NAME; 41 | 42 | return ( 43 | <> 44 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /nextjs-ai-chatbot/app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | 3 | import { AppSidebar } from '@/components/app-sidebar'; 4 | import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; 5 | 6 | import { auth } from '../(auth)/auth'; 7 | import Script from 'next/script'; 8 | 9 | export const experimental_ppr = true; 10 | 11 | export default async function Layout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | const [session, cookieStore] = await Promise.all([auth(), cookies()]); 17 | const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true'; 18 | 19 | return ( 20 | <> 21 |