├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── app
├── (chat)
│ ├── chat
│ │ └── [id]
│ │ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── actions.ts
├── analyticsInstance.ts
├── globals.css
├── layout.tsx
├── login
│ ├── actions.ts
│ └── page.tsx
├── opengraph-image.png
├── share
│ └── [id]
│ │ └── page.tsx
├── signup
│ ├── actions.ts
│ └── page.tsx
└── twitter-image.png
├── auth.config.ts
├── auth.ts
├── components.json
├── components
├── button-scroll-to-bottom.tsx
├── chat-history.tsx
├── chat-list.tsx
├── chat-message-actions.tsx
├── chat-message.tsx
├── chat-panel.tsx
├── chat-scroll-anchor.tsx
├── chat-share-dialog.tsx
├── chat.tsx
├── clear-history.tsx
├── empty-screen.tsx
├── external-link.tsx
├── footer.tsx
├── header.tsx
├── login-button.tsx
├── login-form.tsx
├── markdown.tsx
├── prompt-form.tsx
├── providers.tsx
├── sidebar-actions.tsx
├── sidebar-desktop.tsx
├── sidebar-footer.tsx
├── sidebar-item.tsx
├── sidebar-items.tsx
├── sidebar-list.tsx
├── sidebar-mobile.tsx
├── sidebar-toggle.tsx
├── sidebar.tsx
├── signup-form.tsx
├── stocks
│ ├── event.tsx
│ ├── events-skeleton.tsx
│ ├── index.tsx
│ ├── message.tsx
│ ├── spinner.tsx
│ ├── stock-purchase.tsx
│ ├── stock-skeleton.tsx
│ ├── stock.tsx
│ ├── stocks-skeleton.tsx
│ └── stocks.tsx
├── tailwind-indicator.tsx
├── theme-toggle.tsx
├── ui
│ ├── alert-dialog.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── codeblock.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── icons.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
└── user-menu.tsx
├── example-env-vars
├── lib
├── chat
│ └── actions.tsx
├── hooks
│ ├── use-at-bottom.tsx
│ ├── use-copy-to-clipboard.tsx
│ ├── use-enter-submit.tsx
│ ├── use-local-storage.ts
│ ├── use-sidebar.tsx
│ └── use-streamable-text.ts
├── types.ts
└── utils.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.cjs
├── public
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon.ico
├── next.svg
├── repo_img.png
├── thirteen.svg
└── vercel.svg
├── scripts
└── seed.mjs
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
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 | "tailwindcss": {
16 | "callees": ["cn", "cva"],
17 | "config": "tailwind.config.js"
18 | }
19 | },
20 | "overrides": [
21 | {
22 | "files": ["*.ts", "*.tsx"],
23 | "parser": "@typescript-eslint/parser"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/.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 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 | .env
32 | .env.*
33 |
34 | # turbo
35 | .turbo
36 |
37 | .env
38 | .vercel
39 | .vscode
40 | .env*.local
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Next.js AI Chatbot with Twilio Segment analytics instrumentation
4 |
5 |
6 | An open-source AI copilot with built-in Segment analytics built with Next.js, the Vercel AI SDK, OpenAI, Vercel KV and Twilio Segment.
7 |
8 |
9 |
10 | Features ·
11 | Model Providers ·
12 | Deploy Your Own ·
13 | Running locally ·
14 | Authors
15 |
16 |
17 |
18 | ## Features
19 |
20 | - [Next.js](https://nextjs.org) App Router
21 | - React Server Components (RSCs), Suspense, and Server Actions
22 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
23 | - Support for OpenAI (default), Anthropic, Cohere, Hugging Face, or custom AI chat models and/or LangChain
24 | - [shadcn/ui](https://ui.shadcn.com)
25 | - Styling with [Tailwind CSS](https://tailwindcss.com)
26 | - [Radix UI](https://radix-ui.com) for headless component primitives
27 | - Icons from [Phosphor Icons](https://phosphoricons.com)
28 | - Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv) and [Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres)
29 | - [analytics-node](https://github.com/segmentio/analytics-node) for model observability with [Twilio Segment](https://segment.com/)
30 |
31 | ## Model Providers
32 | This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.
33 |
34 | ## Deploy Your Own
35 |
36 | You can deploy your own version of the Next.js AI Chatbot with Twilio Segment observability to Vercel with one click:
37 |
38 | [](https://vercel.com/new/clone?demo-description=An%20open-source%20AI%20copilot%20with%20built-in%20segment%20analytics%20using%20Next.js%2C%20Vercel%20AI%20SDK%2C%20OpenAI%2C%20Vercel%20KV%2C%20and%20Twilio%20Segment.&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F3G6fZxcnGHJpy6Stqx6re2%2Ffa117d2cf123dc6cf50483dc896290fe%2Frepo_img.png&demo-title=Next.js%20AI%20Chatbot%20with%20Twilio%20Segment%20Analytics&demo-url=https%3A%2F%2Fsegment-ai-copilot.vercel.app&env=OPENAI_API_KEY%2CNEXT_PUBLIC_SEGMENT_WRITE_KEY%2CAUTH_SECRET%2CKV_URL%2CKV_REST_API_URL%2CKV_REST_API_TOKEN%2CKV_REST_API_READ_ONLY_TOKEN%2CPOSTGRES_DATABASE%2CPOSTGRES_HOST%2CPOSTGRES_PASSWORD%2CPOSTGRES_PRISMA_URL%2CPOSTGRES_URL%2CPOSTGRES_URL_NON_POOLING%2CPOSTGRES_URL_NO_SSL%2CPOSTGRES_USER&envDescription=Learn%20how%20to%20configure%20the%20environment%20variables&envLink=https%3A%2F%2Fgithub.com%2Fvaithschmitz%2Fsegment-ai-copilot%3Ftab%3Dreadme-ov-file%23creating-a-kv-database-instance&repository-name=nextjs-ai-chatbot-with-twilio-segment&repository-url=https%3A%2F%2Fgithub.com%2Fvaithschmitz%2Fsegment-ai-copilot)
39 |
40 | ## Creating a KV Database Instance
41 |
42 | Follow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) provided by Vercel. This guide will assist you in creating and configuring your KV database instance on Vercel, enabling your application to interact with it.
43 |
44 | Remember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup.
45 |
46 | ## Creating a Postgres Database Instance
47 |
48 | Similarly, follow the steps outline in the [quick start guide](https://vercel.com/docs/storage/vercel-postgres/quickstart) provided by Vercel. This guide will assist you in creating and configuring your Postgres database instance on Vercel, enabling your application to interact with it.
49 |
50 | Remember to update your environment variables (`POSTGRES_URL`, `POSTGRES_PRISMA_URL`, `POSTGRES_URL_NO_SSL`, `POSTGRES_URL_NON_POOLING`, `POSTGRES_USER`, `POSTGRES_HOST`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`) in the `.env` file with the appropriate credentials provided during the Postgres database setup.
51 |
52 | ## Enabling Twilio Segment model tracking
53 |
54 | For the full guided walkthrough, follow the blog post [here.](https://segment.com/blog/instrumenting-user-insights-for-your-ai-copilot)
55 |
56 | Use your existing Twilio Segment account or [create a free one here](https://segment.com/signup/). Create a new `node.js` source by following the instructions [here](https://segment.com/docs/connections/sources/) and record your write key.
57 |
58 | Remember to update the segment write key in your environment `.env` file (`NEXT_PUBLIC_SEGMENT_WRITE_KEY`) with the write key to your node.js Segment source.
59 |
60 |
61 | ## Running locally
62 |
63 | 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.
64 |
65 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI, Twilio Segment and authentication provider accounts.
66 |
67 | 1. Install Vercel CLI: `npm i -g vercel`
68 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
69 | 3. Download your environment variables: `vercel env pull`
70 |
71 | ```bash
72 | pnpm install
73 | pnpm seed
74 | pnpm dev
75 | ```
76 |
77 | Your app template should now be running on [localhost:3000](http://localhost:3000/).
78 |
79 | ## Authors
80 |
81 | This template is created by [Vercel](https://vercel.com), [Next.js](https://nextjs.org) and [Twilio Segment](https://segment.com/) team members, with contributions from:
82 |
83 | - Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)
84 | - Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)
85 | - shadcn ([@shadcn](https://twitter.com/shadcn)) - [Vercel](https://vercel.com)
86 | - Vaith Schmitz [(@vaith)](https://www.linkedin.com/in/vaithschmitz/) - [Twilio Segment](https://segment.com/)
87 |
--------------------------------------------------------------------------------
/app/(chat)/chat/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next'
2 | import { notFound, redirect } from 'next/navigation'
3 |
4 | import { auth } from '@/auth'
5 | import { getChat, getMissingKeys } from '@/app/actions'
6 | import { Chat } from '@/components/chat'
7 | import { AI } from '@/lib/chat/actions'
8 | import { Session } from '@/lib/types'
9 |
10 | export interface ChatPageProps {
11 | params: {
12 | id: string
13 | }
14 | }
15 |
16 | export async function generateMetadata({
17 | params
18 | }: ChatPageProps): Promise {
19 | const session = await auth()
20 |
21 | if (!session?.user) {
22 | return {}
23 | }
24 |
25 | const chat = await getChat(params.id, session.user.id)
26 | return {
27 | title: chat?.title.toString().slice(0, 50) ?? 'Chat'
28 | }
29 | }
30 |
31 | export default async function ChatPage({ params }: ChatPageProps) {
32 | const session = (await auth()) as Session
33 | const missingKeys = await getMissingKeys()
34 |
35 | if (!session?.user) {
36 | redirect(`/login?next=/chat/${params.id}`)
37 | }
38 |
39 | const userId = session.user.id as string
40 | const chat = await getChat(params.id, userId)
41 |
42 | if (!chat) {
43 | redirect('/')
44 | }
45 |
46 | if (chat?.userId !== session?.user?.id) {
47 | notFound()
48 | }
49 |
50 | return (
51 |
52 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/(chat)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarDesktop } from '@/components/sidebar-desktop'
2 |
3 | interface ChatLayoutProps {
4 | children: React.ReactNode
5 | }
6 |
7 | export default async function ChatLayout({ children }: ChatLayoutProps) {
8 | return (
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/app/(chat)/page.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from '@/lib/utils'
2 | import { Chat } from '@/components/chat'
3 | import { AI } from '@/lib/chat/actions'
4 | import { auth } from '@/auth'
5 | import { Session } from '@/lib/types'
6 | import { getMissingKeys } from '../actions'
7 |
8 |
9 | export const metadata = {
10 | title: 'Next.js AI Chatbot'
11 | }
12 |
13 | export default async function IndexPage() {
14 | const id = nanoid()
15 | const session = (await auth()) as Session
16 | const missingKeys = await getMissingKeys()
17 | return (
18 |
19 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/app/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { revalidatePath } from 'next/cache'
4 | import { redirect } from 'next/navigation'
5 | import { kv } from '@vercel/kv'
6 |
7 | import { auth } from '@/auth'
8 | import { type Chat } from '@/lib/types'
9 |
10 | export async function getChats(userId?: string | null) {
11 | if (!userId) {
12 | return []
13 | }
14 |
15 | try {
16 | const pipeline = kv.pipeline()
17 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
18 | rev: true
19 | })
20 |
21 | for (const chat of chats) {
22 | pipeline.hgetall(chat)
23 | }
24 |
25 | const results = await pipeline.exec()
26 |
27 | return results as Chat[]
28 | } catch (error) {
29 | return []
30 | }
31 | }
32 |
33 | export async function getChat(id: string, userId: string) {
34 | const chat = await kv.hgetall(`chat:${id}`)
35 |
36 | if (!chat || (userId && chat.userId !== userId)) {
37 | return null
38 | }
39 |
40 | return chat
41 | }
42 |
43 | export async function removeChat({ id, path }: { id: string; path: string }) {
44 | const session = await auth()
45 |
46 | if (!session) {
47 | return {
48 | error: 'Unauthorized'
49 | }
50 | }
51 |
52 | //Convert uid to string for consistent comparison with session.user.id
53 | const uid = String(await kv.hget(`chat:${id}`, 'userId'))
54 |
55 | if (uid !== session?.user?.id) {
56 | return {
57 | error: 'Unauthorized'
58 | }
59 | }
60 |
61 | await kv.del(`chat:${id}`)
62 | await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)
63 |
64 | revalidatePath('/')
65 | return revalidatePath(path)
66 | }
67 |
68 | export async function clearChats() {
69 | const session = await auth()
70 |
71 | if (!session?.user?.id) {
72 | return {
73 | error: 'Unauthorized'
74 | }
75 | }
76 |
77 | const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
78 | if (!chats.length) {
79 | return redirect('/')
80 | }
81 | const pipeline = kv.pipeline()
82 |
83 | for (const chat of chats) {
84 | pipeline.del(chat)
85 | pipeline.zrem(`user:chat:${session.user.id}`, chat)
86 | }
87 |
88 | await pipeline.exec()
89 |
90 | revalidatePath('/')
91 | return redirect('/')
92 | }
93 |
94 | export async function getSharedChat(id: string) {
95 | const chat = await kv.hgetall(`chat:${id}`)
96 |
97 | if (!chat || !chat.sharePath) {
98 | return null
99 | }
100 |
101 | return chat
102 | }
103 |
104 | export async function shareChat(id: string) {
105 | const session = await auth()
106 |
107 | if (!session?.user?.id) {
108 | return {
109 | error: 'Unauthorized'
110 | }
111 | }
112 |
113 | const chat = await kv.hgetall(`chat:${id}`)
114 |
115 | if (!chat || chat.userId !== session.user.id) {
116 | return {
117 | error: 'Something went wrong'
118 | }
119 | }
120 |
121 | const payload = {
122 | ...chat,
123 | sharePath: `/share/${chat.id}`
124 | }
125 |
126 | await kv.hmset(`chat:${chat.id}`, payload)
127 |
128 | return payload
129 | }
130 |
131 | export async function saveChat(chat: Chat) {
132 | const session = await auth()
133 |
134 | if (session && session.user) {
135 | const pipeline = kv.pipeline()
136 | pipeline.hmset(`chat:${chat.id}`, chat)
137 | pipeline.zadd(`user:chat:${chat.userId}`, {
138 | score: Date.now(),
139 | member: `chat:${chat.id}`
140 | })
141 | await pipeline.exec()
142 | } else {
143 | return
144 | }
145 | }
146 |
147 | export async function refreshHistory(path: string) {
148 | redirect(path)
149 | }
150 |
151 | export async function getMissingKeys() {
152 | const keysRequired = ['OPENAI_API_KEY']
153 | return keysRequired
154 | .map(key => (process.env[key] ? '' : key))
155 | .filter(key => key !== '')
156 | }
157 |
--------------------------------------------------------------------------------
/app/analyticsInstance.ts:
--------------------------------------------------------------------------------
1 | import { Analytics } from '@segment/analytics-node'
2 |
3 | const analyticsSingleton = () => {
4 | return new Analytics({ writeKey: process.env.NEXT_PUBLIC_SEGMENT_WRITE_KEY as string})
5 | }
6 |
7 | declare global {
8 | var analyticsGlobal: undefined | ReturnType
9 | }
10 |
11 | const analytics = globalThis.analyticsGlobal ?? analyticsSingleton()
12 |
13 | export default analytics
14 |
15 | if (process.env.NODE_ENV !== 'production') globalThis.analyticsGlobal = analytics
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 240 10% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 |
22 | --muted: 240 4.8% 95.9%;
23 | --muted-foreground: 240 3.8% 46.1%;
24 |
25 | --accent: 240 4.8% 95.9%;
26 | --accent-foreground: 240 5.9% 10%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 5.9% 90%;
32 | --input: 240 5.9% 90%;
33 | --ring: 240 10% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 240 10% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 240 10% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 240 10% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 240 5.9% 10%;
50 |
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 240 3.7% 15.9%;
55 | --muted-foreground: 240 5% 64.9%;
56 |
57 | --accent: 240 3.7% 15.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { GeistSans } from 'geist/font/sans'
2 | import { GeistMono } from 'geist/font/mono'
3 |
4 | import '@/app/globals.css'
5 | import { cn } from '@/lib/utils'
6 | import { TailwindIndicator } from '@/components/tailwind-indicator'
7 | import { Providers } from '@/components/providers'
8 | import { Header } from '@/components/header'
9 | import { Toaster } from '@/components/ui/sonner'
10 |
11 | export const metadata = {
12 | metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
13 | title: {
14 | default: 'Next.js AI Chatbot',
15 | template: `%s - Next.js AI Chatbot`
16 | },
17 | description: 'An AI-powered chatbot template built with Next.js and Vercel.',
18 | icons: {
19 | icon: '/favicon.ico',
20 | shortcut: '/favicon-16x16.png',
21 | apple: '/apple-touch-icon.png'
22 | }
23 | }
24 |
25 | export const viewport = {
26 | themeColor: [
27 | { media: '(prefers-color-scheme: light)', color: 'white' },
28 | { media: '(prefers-color-scheme: dark)', color: 'black' }
29 | ]
30 | }
31 |
32 | interface RootLayoutProps {
33 | children: React.ReactNode
34 | }
35 |
36 | export default function RootLayout({ children }: RootLayoutProps) {
37 | return (
38 |
39 |
46 |
47 |
53 |
54 |
55 | {children}
56 |
57 |
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/app/login/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { signIn } from '@/auth'
4 | import { AuthResult } from '@/lib/types'
5 | import { AuthError } from 'next-auth'
6 | import { z } from 'zod'
7 |
8 |
9 |
10 | export async function authenticate(
11 | _prevState: AuthResult | undefined,
12 | formData: FormData
13 | ) {
14 | try {
15 | const email = formData.get('email')
16 | const password = formData.get('password')
17 |
18 | const parsedCredentials = z
19 | .object({
20 | email: z.string().email(),
21 | password: z.string().min(6)
22 | })
23 | .safeParse({
24 | email,
25 | password
26 | })
27 |
28 | if (parsedCredentials.success) {
29 | await signIn('credentials', {
30 | email,
31 | password,
32 | redirectTo: '/'
33 | })
34 |
35 | } else {
36 | return { type: 'error', message: 'Invalid credentials!' }
37 | }
38 | } catch (error) {
39 | if (error instanceof AuthError) {
40 | switch (error.type) {
41 | case 'CredentialsSignin':
42 | return { type: 'error', message: 'Invalid credentials!' }
43 | default:
44 | return {
45 | type: 'error',
46 | message: 'Something went wrong, please try again!'
47 | }
48 | }
49 | }
50 |
51 | throw error
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth'
2 | import LoginForm from '@/components/login-form'
3 | import { Session } from '@/lib/types'
4 | import { redirect } from 'next/navigation'
5 |
6 | export default async function LoginPage() {
7 | const session = (await auth()) as Session
8 |
9 | if (session) {
10 | redirect('/')
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaithschmitz/segment-ai-copilot/b285cecb633814a627e6af7ce336240a2dce662c/app/opengraph-image.png
--------------------------------------------------------------------------------
/app/share/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next'
2 | import { notFound, redirect } from 'next/navigation'
3 |
4 | import { formatDate } from '@/lib/utils'
5 | import { getSharedChat } from '@/app/actions'
6 | import { ChatList } from '@/components/chat-list'
7 | import { FooterText } from '@/components/footer'
8 | import { AI, UIState, getUIStateFromAIState } from '@/lib/chat/actions'
9 |
10 | export const runtime = 'edge'
11 | export const preferredRegion = 'home'
12 |
13 | interface SharePageProps {
14 | params: {
15 | id: string
16 | }
17 | }
18 |
19 | export async function generateMetadata({
20 | params
21 | }: SharePageProps): Promise {
22 | const chat = await getSharedChat(params.id)
23 |
24 | return {
25 | title: chat?.title.slice(0, 50) ?? 'Chat'
26 | }
27 | }
28 |
29 | export default async function SharePage({ params }: SharePageProps) {
30 | const chat = await getSharedChat(params.id)
31 |
32 | if (!chat || !chat?.sharePath) {
33 | notFound()
34 | }
35 |
36 | const uiState: UIState = getUIStateFromAIState(chat)
37 |
38 | return (
39 | <>
40 |
41 |
42 |
43 |
44 |
{chat.title}
45 |
46 | {formatDate(chat.createdAt)} · {chat.messages.length} messages
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | >
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/app/signup/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { signIn } from '@/auth'
4 | import { db } from '@vercel/postgres'
5 | import { getStringFromBuffer } from '@/lib/utils'
6 | import { z } from 'zod'
7 | import { AuthResult } from '@/lib/types'
8 |
9 |
10 | export async function signup(
11 | _prevState: AuthResult | undefined,
12 | formData: FormData
13 | ) {
14 | const email = formData.get('email') as string
15 | const password = formData.get('password') as string
16 |
17 | const parsedCredentials = z
18 | .object({
19 | email: z.string().email(),
20 | password: z.string().min(6)
21 | })
22 | .safeParse({
23 | email,
24 | password
25 | })
26 |
27 | if (parsedCredentials.success) {
28 | const salt = crypto.randomUUID()
29 |
30 | const encoder = new TextEncoder()
31 | const saltedPassword = encoder.encode(password + salt)
32 | const hashedPasswordBuffer = await crypto.subtle.digest(
33 | 'SHA-256',
34 | saltedPassword
35 | )
36 | const hashedPassword = getStringFromBuffer(hashedPasswordBuffer)
37 |
38 | const client = await db.connect()
39 |
40 | try {
41 | await client.sql`
42 | INSERT INTO users (email, password, salt)
43 | VALUES (${email}, ${hashedPassword}, ${salt})
44 | ON CONFLICT (id) DO NOTHING;
45 | `
46 |
47 | await signIn('credentials', {
48 | email,
49 | password,
50 | redirect: false
51 | })
52 | return { type: 'success', message: 'Account created!' }
53 | } catch (error) {
54 | const { message } = error as Error
55 |
56 | if (
57 | message.startsWith('duplicate key value violates unique constraint')
58 | ) {
59 | return { type: 'error', message: 'User already exists! Please log in.' }
60 | } else {
61 | return {
62 | type: 'error',
63 | message: 'Something went wrong! Please try again.'
64 | }
65 | }
66 | } finally {
67 | client.release()
68 | }
69 | } else {
70 | return {
71 | type: 'error',
72 | message: 'Invalid entries, please try again!'
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth'
2 | import SignupForm from '@/components/signup-form'
3 | import { Session } from '@/lib/types'
4 | import { redirect } from 'next/navigation'
5 |
6 | export default async function SignupPage() {
7 | const session = (await auth()) as Session
8 |
9 | if (session) {
10 | redirect('/')
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaithschmitz/segment-ai-copilot/b285cecb633814a627e6af7ce336240a2dce662c/app/twitter-image.png
--------------------------------------------------------------------------------
/auth.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextAuthConfig } from 'next-auth'
2 |
3 | export const authConfig = {
4 | secret: process.env.AUTH_SECRET,
5 | pages: {
6 | signIn: '/login',
7 | newUser: '/signup'
8 | },
9 | callbacks: {
10 | async authorized({ auth, request: { nextUrl } }) {
11 | const isLoggedIn = !!auth?.user
12 | const isOnLoginPage = nextUrl.pathname.startsWith('/login')
13 | const isOnSignupPage = nextUrl.pathname.startsWith('/signup')
14 |
15 | if (isLoggedIn) {
16 | if (isOnLoginPage || isOnSignupPage) {
17 | return Response.redirect(new URL('/', nextUrl))
18 | }
19 | }
20 |
21 | return true
22 | },
23 | async jwt({ token, user }) {
24 | if (user) {
25 | token = { ...token, id: user.id }
26 | }
27 |
28 | return token
29 | },
30 | async session({ session, token }) {
31 | if (token) {
32 | const { id } = token as { id: string }
33 | const { user } = session
34 |
35 | session = { ...session, user: { ...user, id } }
36 | }
37 |
38 | return session
39 | }
40 | },
41 | providers: []
42 | } satisfies NextAuthConfig
43 |
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth'
2 | import Credentials from 'next-auth/providers/credentials'
3 | import { authConfig } from './auth.config'
4 | import { z } from 'zod'
5 | import { sql } from '@vercel/postgres'
6 | import { getStringFromBuffer } from './lib/utils'
7 |
8 | interface User {
9 | id: string
10 | name: string
11 | email: string
12 | password: string
13 | salt: string
14 | }
15 |
16 | async function getUser(email: string): Promise {
17 | try {
18 | const user = await sql`SELECT * FROM users WHERE email=${email}`
19 | return user.rows[0]
20 | } catch (error) {
21 | throw new Error('Failed to fetch user.')
22 | }
23 | }
24 |
25 | export const { auth, signIn, signOut } = NextAuth({
26 | ...authConfig,
27 | providers: [
28 | Credentials({
29 | async authorize(credentials) {
30 | const parsedCredentials = z
31 | .object({
32 | email: z.string().email(),
33 | password: z.string().min(6)
34 | })
35 | .safeParse(credentials)
36 |
37 | if (parsedCredentials.success) {
38 | const { email, password } = parsedCredentials.data
39 | const user = await getUser(email)
40 |
41 | if (!user) return null
42 |
43 | const encoder = new TextEncoder()
44 | const saltedPassword = encoder.encode(password + user.salt)
45 | const hashedPasswordBuffer = await crypto.subtle.digest(
46 | 'SHA-256',
47 | saltedPassword
48 | )
49 | const hashedPassword = getStringFromBuffer(hashedPasswordBuffer)
50 |
51 | if (hashedPassword === user.password) {
52 | return user
53 | } else {
54 | return null
55 | }
56 | }
57 |
58 | return null
59 | }
60 | })
61 | ]
62 | })
63 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/button-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 | import { Button, type ButtonProps } from '@/components/ui/button'
8 | import { IconArrowDown } from '@/components/ui/icons'
9 |
10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11 | const isAtBottom = useAtBottom()
12 |
13 | return (
14 |
23 | window.scrollTo({
24 | top: document.body.offsetHeight,
25 | behavior: 'smooth'
26 | })
27 | }
28 | {...props}
29 | >
30 |
31 | Scroll to bottom
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/chat-history.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import Link from 'next/link'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { SidebarList } from '@/components/sidebar-list'
7 | import { buttonVariants } from '@/components/ui/button'
8 | import { IconPlus } from '@/components/ui/icons'
9 |
10 | interface ChatHistoryProps {
11 | userId?: string
12 | }
13 |
14 | export async function ChatHistory({ userId }: ChatHistoryProps) {
15 | return (
16 |
17 |
18 |
Chat History
19 |
20 |
21 |
28 |
29 | New Chat
30 |
31 |
32 |
35 | {Array.from({ length: 10 }).map((_, i) => (
36 |
40 | ))}
41 |
42 | }
43 | >
44 | {/* @ts-ignore */}
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/components/chat-list.tsx:
--------------------------------------------------------------------------------
1 | import { Separator } from '@/components/ui/separator'
2 | import { UIState } from '@/lib/chat/actions'
3 |
4 | export interface ChatList {
5 | messages: UIState
6 | }
7 |
8 | export function ChatList({ messages }: ChatList) {
9 | if (!messages.length) {
10 | return null
11 | }
12 |
13 | return (
14 |
15 | {messages.map((message, index) => (
16 |
17 | {message.display}
18 | {index < messages.length - 1 && }
19 |
20 | ))}
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/components/chat-message-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { type Message } from 'ai'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import { IconCheck, IconCopy } from '@/components/ui/icons'
7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8 | import { cn } from '@/lib/utils'
9 |
10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
11 | message: Message
12 | }
13 |
14 | export function ChatMessageActions({
15 | message,
16 | className,
17 | ...props
18 | }: ChatMessageActionsProps) {
19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
20 |
21 | const onCopy = () => {
22 | if (isCopied) return
23 | copyToClipboard(message.content)
24 | }
25 |
26 | return (
27 |
34 |
35 | {isCopied ? : }
36 | Copy message
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3 |
4 | import { Message } from 'ai'
5 | import remarkGfm from 'remark-gfm'
6 | import remarkMath from 'remark-math'
7 |
8 | import { cn } from '@/lib/utils'
9 | import { CodeBlock } from '@/components/ui/codeblock'
10 | import { MemoizedReactMarkdown } from '@/components/markdown'
11 | import { IconOpenAI, IconUser } from '@/components/ui/icons'
12 | import { ChatMessageActions } from '@/components/chat-message-actions'
13 |
14 | export interface ChatMessageProps {
15 | message: Message
16 | }
17 |
18 | export function ChatMessage({ message, ...props }: ChatMessageProps) {
19 | return (
20 |
24 |
32 | {message.role === 'user' ? : }
33 |
34 |
35 | {children}
41 | },
42 | code({ node, inline, className, children, ...props }) {
43 | if (children.length) {
44 | if (children[0] == '▍') {
45 | return (
46 | ▍
47 | )
48 | }
49 |
50 | children[0] = (children[0] as string).replace('`▍`', '▍')
51 | }
52 |
53 | const match = /language-(\w+)/.exec(className || '')
54 |
55 | if (inline) {
56 | return (
57 |
58 | {children}
59 |
60 | )
61 | }
62 |
63 | return (
64 |
70 | )
71 | }
72 | }}
73 | >
74 | {message.content}
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/components/chat-panel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { shareChat } from '@/app/actions'
4 | import { Button } from '@/components/ui/button'
5 | import { PromptForm } from '@/components/prompt-form'
6 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
7 | import { IconShare } from '@/components/ui/icons'
8 | import { FooterText } from '@/components/footer'
9 | import { ChatShareDialog } from '@/components/chat-share-dialog'
10 | import { useAIState, useActions, useUIState } from 'ai/rsc'
11 | import type { AI } from '@/lib/chat/actions'
12 | import { nanoid } from 'nanoid'
13 | import { UserMessage } from './stocks/message'
14 |
15 | export interface ChatPanelProps {
16 | id?: string
17 | title?: string
18 | input: string
19 | setInput: (value: string) => void
20 | }
21 |
22 | export function ChatPanel({ id, title, input, setInput }: ChatPanelProps) {
23 | const [aiState] = useAIState()
24 | const [messages, setMessages] = useUIState()
25 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
26 |
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {messages?.length >= 2 ? (
35 |
36 |
37 | {id && title ? (
38 | <>
39 | setShareDialogOpen(true)}
42 | >
43 |
44 | Share
45 |
46 | setShareDialogOpen(false)}
50 | shareChat={shareChat}
51 | chat={{
52 | id,
53 | title,
54 | messages: aiState.messages
55 | }}
56 | />
57 | >
58 | ) : null}
59 |
60 |
61 | ) : null}
62 |
63 |
67 |
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/components/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useInView } from 'react-intersection-observer'
5 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
6 |
7 | interface ChatScrollAnchorProps {
8 | trackVisibility?: boolean
9 | }
10 |
11 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
12 | const isAtBottom = useAtBottom()
13 | const { ref, entry, inView } = useInView({
14 | trackVisibility,
15 | delay: 100,
16 | rootMargin: '0px 0px -125px 0px'
17 | })
18 |
19 | React.useEffect(() => {
20 | if (isAtBottom && trackVisibility && !inView) {
21 | entry?.target.scrollIntoView({
22 | block: 'start'
23 | })
24 | }
25 | }, [inView, entry, isAtBottom, trackVisibility])
26 |
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/components/chat-share-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { type DialogProps } from '@radix-ui/react-dialog'
5 | import { toast } from 'sonner'
6 |
7 | import { ServerActionResult, type Chat } from '@/lib/types'
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | Dialog,
11 | DialogContent,
12 | DialogDescription,
13 | DialogFooter,
14 | DialogHeader,
15 | DialogTitle
16 | } from '@/components/ui/dialog'
17 | import { IconSpinner } from '@/components/ui/icons'
18 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
19 |
20 | interface ChatShareDialogProps extends DialogProps {
21 | chat: Pick
22 | shareChat: (id: string) => ServerActionResult
23 | onCopy: () => void
24 | }
25 |
26 | export function ChatShareDialog({
27 | chat,
28 | shareChat,
29 | onCopy,
30 | ...props
31 | }: ChatShareDialogProps) {
32 | const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 })
33 | const [isSharePending, startShareTransition] = React.useTransition()
34 |
35 | const copyShareLink = React.useCallback(
36 | async (chat: Chat) => {
37 | if (!chat.sharePath) {
38 | return toast.error('Could not copy share link to clipboard')
39 | }
40 |
41 | const url = new URL(window.location.href)
42 | url.pathname = chat.sharePath
43 | copyToClipboard(url.toString())
44 | onCopy()
45 | toast.success('Share link copied to clipboard')
46 | },
47 | [copyToClipboard, onCopy]
48 | )
49 |
50 | return (
51 |
52 |
53 |
54 | Share link to chat
55 |
56 | Anyone with the URL will be able to view the shared chat.
57 |
58 |
59 |
60 |
{chat.title}
61 |
62 | {chat.messages.length} messages
63 |
64 |
65 |
66 | {
69 | // @ts-ignore
70 | startShareTransition(async () => {
71 | const result = await shareChat(chat.id)
72 |
73 | if (result && 'error' in result) {
74 | toast.error(result.error)
75 | return
76 | }
77 |
78 | copyShareLink(result)
79 | })
80 | }}
81 | >
82 | {isSharePending ? (
83 | <>
84 |
85 | Copying...
86 | >
87 | ) : (
88 | <>Copy link>
89 | )}
90 |
91 |
92 |
93 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/components/chat.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import { ChatList } from '@/components/chat-list'
5 | import { ChatPanel } from '@/components/chat-panel'
6 | import { EmptyScreen } from '@/components/empty-screen'
7 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
8 | import { useLocalStorage } from '@/lib/hooks/use-local-storage'
9 | import { useEffect, useState } from 'react'
10 | import { useUIState, useAIState } from 'ai/rsc'
11 | import { Session } from '@/lib/types'
12 | import { usePathname, useRouter } from 'next/navigation'
13 | import { Message } from '@/lib/chat/actions'
14 | import { toast } from 'sonner'
15 | import analytics from '@/app/analyticsInstance'
16 |
17 |
18 | export interface ChatProps extends React.ComponentProps<'div'> {
19 | initialMessages?: Message[]
20 | id?: string
21 | session?: Session
22 | missingKeys: string[]
23 | }
24 |
25 | // changed hooks to only fire on load
26 | export function Chat({ id, className, session, missingKeys }: ChatProps) {
27 | const router = useRouter()
28 | const path = usePathname()
29 | const [input, setInput] = useState('')
30 | const [messages] = useUIState()
31 | const [aiState] = useAIState()
32 | const isLoading = true
33 |
34 | const [_, setNewChatId] = useLocalStorage('newChatId', id)
35 |
36 | useEffect(() => {
37 | if (session?.user) {
38 | if (!path.includes('chat') && messages.length === 1) {
39 | window.history.replaceState({}, '', `/chat/${id}`)
40 | }
41 | }
42 | }, [id, path, session?.user, messages])
43 |
44 | useEffect(() => {
45 | const messagesLength = aiState.messages?.length
46 | if (messagesLength === 2) {
47 | router.refresh()
48 | }
49 | }, [aiState.messages, router])
50 |
51 | useEffect(() => {
52 | setNewChatId(id)
53 | if (messages.length === 0) {
54 | analytics.track({
55 | userId: '123',
56 | event: 'Conversation Started',
57 | properties: {
58 | conversationId: id
59 | }
60 | })
61 | }
62 | }, [])
63 |
64 | useEffect(() => {
65 | missingKeys.map(key => {
66 | toast.error(`Missing ${key} environment variable!`)
67 | })
68 | }, [missingKeys])
69 |
70 | return (
71 | <>
72 |
73 | {messages.length ? (
74 | <>
75 |
76 |
77 | >
78 | ) : (
79 |
80 | )}
81 |
82 |
83 | >
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/components/clear-history.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'sonner'
6 |
7 | import { ServerActionResult } from '@/lib/types'
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle,
18 | AlertDialogTrigger
19 | } from '@/components/ui/alert-dialog'
20 | import { IconSpinner } from '@/components/ui/icons'
21 |
22 | interface ClearHistoryProps {
23 | isEnabled: boolean
24 | clearChats: () => ServerActionResult
25 | }
26 |
27 | export function ClearHistory({
28 | isEnabled = false,
29 | clearChats
30 | }: ClearHistoryProps) {
31 | const [open, setOpen] = React.useState(false)
32 | const [isPending, startTransition] = React.useTransition()
33 | const router = useRouter()
34 |
35 | return (
36 |
37 |
38 |
39 | {isPending && }
40 | Clear history
41 |
42 |
43 |
44 |
45 | Are you absolutely sure?
46 |
47 | This will permanently delete your chat history and remove your data
48 | from our servers.
49 |
50 |
51 |
52 | Cancel
53 | {
56 | event.preventDefault()
57 | startTransition(async () => {
58 | const result = await clearChats()
59 | if (result && 'error' in result) {
60 | toast.error(result.error)
61 | return
62 | }
63 |
64 | setOpen(false)
65 | })
66 | }}
67 | >
68 | {isPending && }
69 | Delete
70 |
71 |
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/components/empty-screen.tsx:
--------------------------------------------------------------------------------
1 | import { UseChatHelpers } from 'ai/react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { ExternalLink } from '@/components/external-link'
5 | import { IconArrowRight } from '@/components/ui/icons'
6 |
7 | export function EmptyScreen({ setInput }: Pick) {
8 | return (
9 |
10 |
11 |
12 | Welcome to the Twilio Segment Finance Copilot
13 |
14 |
15 | This is an open source AI chatbot app template with baked in model observability built with:{' '}
16 | Twilio Segment , {' '}
17 | Next.js , the{' '}
18 |
19 | Vercel AI SDK
20 |
21 | , and{' '}
22 |
23 | Vercel KV
24 |
25 | .
26 |
27 |
28 | `You can ask me things like: “What’s the price of a certain stock?” “What are five great stocks?” “Buy 300 stocks”`
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/external-link.tsx:
--------------------------------------------------------------------------------
1 | export function ExternalLink({
2 | href,
3 | children
4 | }: {
5 | href: string
6 | children: React.ReactNode
7 | }) {
8 | return (
9 |
14 | {children}
15 |
22 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 | import { ExternalLink } from '@/components/external-link'
5 |
6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
7 | return (
8 |
15 | Open source AI chatbot built with{' '}
16 | Twilio Segment,
17 | Next.js and{' '}
18 |
19 | Vercel KV
20 |
21 | .
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Link from 'next/link'
3 |
4 | import { cn } from '@/lib/utils'
5 | import { auth } from '@/auth'
6 | import { Button, buttonVariants } from '@/components/ui/button'
7 | import {
8 | IconGitHub,
9 | IconNextChat,
10 | IconSeparator,
11 | IconVercel,
12 | } from '@/components/ui/icons'
13 | import { UserMenu } from '@/components/user-menu'
14 | import { SidebarMobile } from './sidebar-mobile'
15 | import { SidebarToggle } from './sidebar-toggle'
16 | import { ChatHistory } from './chat-history'
17 | import { Session } from '@/lib/types'
18 |
19 | async function UserOrLogin() {
20 | const session = (await auth()) as Session
21 | return (
22 | <>
23 | {session?.user ? (
24 | <>
25 |
26 |
27 |
28 |
29 | >
30 | ) : (
31 |
32 |
33 |
34 |
35 | )}
36 |
37 |
38 |
39 | {session?.user ? (
40 |
41 | ) : (
42 |
43 | Login
44 |
45 | )}
46 |
47 | >
48 | )
49 | }
50 |
51 | export function Header() {
52 | return (
53 |
54 |
55 | }>
56 |
57 |
58 |
59 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/components/login-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { signIn } from 'next-auth/react'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { Button, type ButtonProps } from '@/components/ui/button'
8 | import { IconGitHub, IconSpinner } from '@/components/ui/icons'
9 |
10 | interface LoginButtonProps extends ButtonProps {
11 | showGithubIcon?: boolean
12 | text?: string
13 | }
14 |
15 | export function LoginButton({
16 | text = 'Login with GitHub',
17 | showGithubIcon = true,
18 | className,
19 | ...props
20 | }: LoginButtonProps) {
21 | const [isLoading, setIsLoading] = React.useState(false)
22 | return (
23 | {
26 | setIsLoading(true)
27 | // next-auth signIn() function doesn't work yet at Edge Runtime due to usage of BroadcastChannel
28 | signIn('github', { callbackUrl: `/` })
29 | }}
30 | disabled={isLoading}
31 | className={cn(className)}
32 | {...props}
33 | >
34 | {isLoading ? (
35 |
36 | ) : showGithubIcon ? (
37 |
38 | ) : null}
39 | {text}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/components/login-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useFormState, useFormStatus } from 'react-dom'
4 | import { authenticate } from '@/app/login/actions'
5 | import Link from 'next/link'
6 | import { useEffect } from 'react'
7 | import { toast } from 'sonner'
8 | import { IconSpinner } from './ui/icons'
9 |
10 | export default function LoginForm() {
11 | const [result, dispatch] = useFormState(authenticate, undefined)
12 |
13 | useEffect(() => {
14 | if (result) {
15 | if (result.type === 'error') {
16 | toast.error(result.message)
17 | } else {
18 | toast.success(result.message)
19 | }
20 | }
21 | }, [result])
22 |
23 | return (
24 |
79 | )
80 | }
81 |
82 | function LoginButton() {
83 | const { pending } = useFormStatus()
84 |
85 | return (
86 |
90 | {pending ? : 'Log in'}
91 |
92 | )
93 | }
94 |
--------------------------------------------------------------------------------
/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from 'react'
2 | import ReactMarkdown, { Options } from 'react-markdown'
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | )
10 |
--------------------------------------------------------------------------------
/components/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import Textarea from 'react-textarea-autosize'
5 | import analytics from '@/app/analyticsInstance'
6 |
7 | import { useActions, useUIState } from 'ai/rsc'
8 |
9 | import { UserMessage } from './stocks/message'
10 | import { type AI } from '@/lib/chat/actions'
11 | import { Button } from '@/components/ui/button'
12 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
13 | import {
14 | Tooltip,
15 | TooltipContent,
16 | TooltipTrigger
17 | } from '@/components/ui/tooltip'
18 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
19 | import { nanoid } from 'nanoid'
20 | import { useRouter } from 'next/navigation'
21 |
22 | export function PromptForm({
23 | input,
24 | setInput
25 | }: {
26 | input: string
27 | setInput: (value: string) => void
28 | }) {
29 | const router = useRouter()
30 | const { formRef, onKeyDown } = useEnterSubmit()
31 | const inputRef = React.useRef(null)
32 | const { submitUserMessage } = useActions()
33 | const [_, setMessages] = useUIState()
34 |
35 | React.useEffect(() => {
36 | if (inputRef.current) {
37 | inputRef.current.focus()
38 | }
39 | }, [])
40 |
41 | return (
42 |
125 | )
126 | }
127 |
--------------------------------------------------------------------------------
/components/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { ThemeProviderProps } from 'next-themes/dist/types'
6 | import { SidebarProvider } from '@/lib/hooks/use-sidebar'
7 | import { TooltipProvider } from '@/components/ui/tooltip'
8 |
9 | export function Providers({ children, ...props }: ThemeProviderProps) {
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/sidebar-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useRouter } from 'next/navigation'
4 | import * as React from 'react'
5 | import { toast } from 'sonner'
6 |
7 | import { ServerActionResult, type Chat } from '@/lib/types'
8 | import {
9 | AlertDialog,
10 | AlertDialogAction,
11 | AlertDialogCancel,
12 | AlertDialogContent,
13 | AlertDialogDescription,
14 | AlertDialogFooter,
15 | AlertDialogHeader,
16 | AlertDialogTitle
17 | } from '@/components/ui/alert-dialog'
18 | import { Button } from '@/components/ui/button'
19 | import { IconShare, IconSpinner, IconTrash } from '@/components/ui/icons'
20 | import { ChatShareDialog } from '@/components/chat-share-dialog'
21 | import {
22 | Tooltip,
23 | TooltipContent,
24 | TooltipTrigger
25 | } from '@/components/ui/tooltip'
26 |
27 | interface SidebarActionsProps {
28 | chat: Chat
29 | removeChat: (args: { id: string; path: string }) => ServerActionResult
30 | shareChat: (id: string) => ServerActionResult
31 | }
32 |
33 | export function SidebarActions({
34 | chat,
35 | removeChat,
36 | shareChat
37 | }: SidebarActionsProps) {
38 | const router = useRouter()
39 | const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
40 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
41 | const [isRemovePending, startRemoveTransition] = React.useTransition()
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 | setShareDialogOpen(true)}
52 | >
53 |
54 | Share
55 |
56 |
57 | Share chat
58 |
59 |
60 |
61 | setDeleteDialogOpen(true)}
66 | >
67 |
68 | Delete
69 |
70 |
71 | Delete chat
72 |
73 |
74 | setShareDialogOpen(false)}
80 | />
81 |
82 |
83 |
84 | Are you absolutely sure?
85 |
86 | This will permanently delete your chat message and remove your
87 | data from our servers.
88 |
89 |
90 |
91 |
92 | Cancel
93 |
94 | {
97 | event.preventDefault()
98 | // @ts-ignore
99 | startRemoveTransition(async () => {
100 | const result = await removeChat({
101 | id: chat.id,
102 | path: chat.path
103 | })
104 |
105 | if (result && 'error' in result) {
106 | toast.error(result.error)
107 | return
108 | }
109 |
110 | setDeleteDialogOpen(false)
111 | router.refresh()
112 | router.push('/')
113 | toast.success('Chat deleted')
114 | })
115 | }}
116 | >
117 | {isRemovePending && }
118 | Delete
119 |
120 |
121 |
122 |
123 | >
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/components/sidebar-desktop.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar } from '@/components/sidebar'
2 |
3 | import { auth } from '@/auth'
4 | import { ChatHistory } from '@/components/chat-history'
5 |
6 | export async function SidebarDesktop() {
7 | const session = await auth()
8 |
9 | if (!session?.user?.id) {
10 | return null
11 | }
12 |
13 | return (
14 |
15 | {/* @ts-ignore */}
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/components/sidebar-footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | export function SidebarFooter({
4 | children,
5 | className,
6 | ...props
7 | }: React.ComponentProps<'div'>) {
8 | return (
9 |
13 | {children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/components/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import Link from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | import { motion } from 'framer-motion'
9 |
10 | import { buttonVariants } from '@/components/ui/button'
11 | import { IconMessage, IconUsers } from '@/components/ui/icons'
12 | import {
13 | Tooltip,
14 | TooltipContent,
15 | TooltipTrigger
16 | } from '@/components/ui/tooltip'
17 | import { useLocalStorage } from '@/lib/hooks/use-local-storage'
18 | import { type Chat } from '@/lib/types'
19 | import { cn } from '@/lib/utils'
20 |
21 | interface SidebarItemProps {
22 | index: number
23 | chat: Chat
24 | children: React.ReactNode
25 | }
26 |
27 | export function SidebarItem({ index, chat, children }: SidebarItemProps) {
28 | const pathname = usePathname()
29 |
30 | const isActive = pathname === chat.path
31 | const [newChatId, setNewChatId] = useLocalStorage('newChatId', null)
32 | const shouldAnimate = index === 0 && isActive && newChatId
33 |
34 | if (!chat?.id) return null
35 |
36 | return (
37 |
56 |
57 | {chat.sharePath ? (
58 |
59 |
63 |
64 |
65 | This is a shared chat.
66 |
67 | ) : (
68 |
69 | )}
70 |
71 |
79 |
83 |
84 | {shouldAnimate ? (
85 | chat.title.split('').map((character, index) => (
86 | {
107 | if (index === chat.title.length - 1) {
108 | setNewChatId(null)
109 | }
110 | }}
111 | >
112 | {character}
113 |
114 | ))
115 | ) : (
116 | {chat.title}
117 | )}
118 |
119 |
120 |
121 | {isActive && {children}
}
122 |
123 | )
124 | }
125 |
--------------------------------------------------------------------------------
/components/sidebar-items.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Chat } from '@/lib/types'
4 | import { AnimatePresence, motion } from 'framer-motion'
5 |
6 | import { removeChat, shareChat } from '@/app/actions'
7 |
8 | import { SidebarActions } from '@/components/sidebar-actions'
9 | import { SidebarItem } from '@/components/sidebar-item'
10 |
11 | interface SidebarItemsProps {
12 | chats?: Chat[]
13 | }
14 |
15 | export function SidebarItems({ chats }: SidebarItemsProps) {
16 | if (!chats?.length) return null
17 |
18 | return (
19 |
20 | {chats.map(
21 | (chat, index) =>
22 | chat && (
23 |
30 |
31 |
36 |
37 |
38 | )
39 | )}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/components/sidebar-list.tsx:
--------------------------------------------------------------------------------
1 | import { clearChats, getChats } from '@/app/actions'
2 | import { ClearHistory } from '@/components/clear-history'
3 | import { SidebarItems } from '@/components/sidebar-items'
4 | import { ThemeToggle } from '@/components/theme-toggle'
5 | import { cache } from 'react'
6 |
7 | interface SidebarListProps {
8 | userId?: string
9 | children?: React.ReactNode
10 | }
11 |
12 | const loadChats = cache(async (userId?: string) => {
13 | return await getChats(userId)
14 | })
15 |
16 | export async function SidebarList({ userId }: SidebarListProps) {
17 | const chats = await loadChats(userId)
18 |
19 | return (
20 |
21 |
22 | {chats?.length ? (
23 |
24 |
25 |
26 | ) : (
27 |
28 |
No chat history
29 |
30 | )}
31 |
32 |
33 |
34 | 0} />
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/sidebar-mobile.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
4 |
5 | import { Sidebar } from '@/components/sidebar'
6 | import { Button } from '@/components/ui/button'
7 |
8 | import { IconSidebar } from '@/components/ui/icons'
9 |
10 | interface SidebarMobileProps {
11 | children: React.ReactNode
12 | }
13 |
14 | export function SidebarMobile({ children }: SidebarMobileProps) {
15 | return (
16 |
17 |
18 |
19 |
20 | Toggle Sidebar
21 |
22 |
23 |
27 | {children}
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/components/sidebar-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { useSidebar } from '@/lib/hooks/use-sidebar'
6 | import { Button } from '@/components/ui/button'
7 | import { IconSidebar } from '@/components/ui/icons'
8 |
9 | export function SidebarToggle() {
10 | const { toggleSidebar } = useSidebar()
11 |
12 | return (
13 | {
17 | toggleSidebar()
18 | }}
19 | >
20 |
21 | Toggle Sidebar
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { useSidebar } from '@/lib/hooks/use-sidebar'
6 | import { cn } from '@/lib/utils'
7 |
8 | export interface SidebarProps extends React.ComponentProps<'div'> {}
9 |
10 | export function Sidebar({ className, children }: SidebarProps) {
11 | const { isSidebarOpen, isLoading } = useSidebar()
12 |
13 | return (
14 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/signup-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useFormState, useFormStatus } from 'react-dom'
4 | import { signup } from '@/app/signup/actions'
5 | import Link from 'next/link'
6 | import { useEffect } from 'react'
7 | import { toast } from 'sonner'
8 | import { IconSpinner } from './ui/icons'
9 | import { useRouter } from 'next/navigation'
10 |
11 | export default function SignupForm() {
12 | const router = useRouter()
13 | const [result, dispatch] = useFormState(signup, undefined)
14 |
15 | useEffect(() => {
16 | if (result) {
17 | if (result.type === 'error') {
18 | toast.error(result.message)
19 | } else {
20 | router.refresh()
21 | toast.success(result.message)
22 | }
23 | }
24 | }, [result, router])
25 |
26 | return (
27 |
31 |
32 |
Sign up for an account!
33 |
34 |
35 |
39 | Email
40 |
41 |
42 |
50 |
51 |
52 |
53 |
57 | Password
58 |
59 |
60 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Already have an account?
77 | Log in
78 |
79 |
80 | )
81 | }
82 |
83 | function LoginButton() {
84 | const { pending } = useFormStatus()
85 |
86 | return (
87 |
91 | {pending ? : 'Create account'}
92 |
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/components/stocks/event.tsx:
--------------------------------------------------------------------------------
1 | import { format, parseISO } from 'date-fns'
2 |
3 | interface Event {
4 | date: string
5 | headline: string
6 | description: string
7 | }
8 |
9 | export function Events({ props: events }: { props: Event[] }) {
10 | return (
11 |
12 | {events.map(event => (
13 |
17 |
18 | {format(parseISO(event.date), 'dd LLL, yyyy')}
19 |
20 |
21 | {event.headline}
22 |
23 |
24 | {event.description.slice(0, 70)}...
25 |
26 |
27 | ))}
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/components/stocks/events-skeleton.tsx:
--------------------------------------------------------------------------------
1 | const placeholderEvents = [
2 | {
3 | date: '2022-10-01',
4 | headline: 'NVIDIA releases new AI-powered graphics card',
5 | description:
6 | 'NVIDIA unveils the latest graphics card infused with AI capabilities, revolutionizing gaming and rendering experiences.'
7 | }
8 | ]
9 |
10 | export const EventsSkeleton = ({ events = placeholderEvents }) => {
11 | return (
12 |
13 | {placeholderEvents.map(event => (
14 |
18 |
19 | {event.date}
20 |
21 |
22 | {event.headline}
23 |
24 |
25 | {event.description.slice(0, 70)}...
26 |
27 |
28 | ))}
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/components/stocks/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import dynamic from 'next/dynamic'
4 | import { StockSkeleton } from './stock-skeleton'
5 | import { StocksSkeleton } from './stocks-skeleton'
6 | import { EventsSkeleton } from './events-skeleton'
7 |
8 | export { spinner } from './spinner'
9 | export { BotCard, BotMessage, SystemMessage } from './message'
10 |
11 | const Stock = dynamic(() => import('./stock').then(mod => mod.Stock), {
12 | ssr: false,
13 | loading: () =>
14 | })
15 |
16 | const Purchase = dynamic(
17 | () => import('./stock-purchase').then(mod => mod.Purchase),
18 | {
19 | ssr: false,
20 | loading: () => (
21 |
22 | Loading stock info...
23 |
24 | )
25 | }
26 | )
27 |
28 | const Stocks = dynamic(() => import('./stocks').then(mod => mod.Stocks), {
29 | ssr: false,
30 | loading: () =>
31 | })
32 |
33 | const Events = dynamic(() => import('./event').then(mod => mod.Events), {
34 | ssr: false,
35 | loading: () =>
36 | })
37 |
38 | export { Stock, Purchase, Stocks, Events }
39 |
--------------------------------------------------------------------------------
/components/stocks/message.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useRef } from 'react';
4 | import { IconOpenAI, IconUser } from '@/components/ui/icons'
5 | import { cn } from '@/lib/utils'
6 | import { spinner } from './spinner'
7 | import { CodeBlock } from '../ui/codeblock'
8 | import { MemoizedReactMarkdown } from '../markdown'
9 | import remarkGfm from 'remark-gfm'
10 | import remarkMath from 'remark-math'
11 | import { StreamableValue } from 'ai/rsc'
12 | import { useStreamableText } from '@/lib/hooks/use-streamable-text'
13 |
14 | // Different types of message bubbles.
15 |
16 | export function UserMessage({ children }: { children: React.ReactNode }) {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | {children}
24 |
25 |
26 | )
27 | }
28 |
29 | export function BotMessage({
30 | content,
31 | className
32 | }: {
33 | content: string | StreamableValue
34 | className?: string
35 | }) {
36 | const text = useStreamableText(content);
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 | {children}
50 | },
51 | code({ node, inline, className, children, ...props }) {
52 | if (children.length) {
53 | if (children[0] == '▍') {
54 | return (
55 | ▍
56 | )
57 | }
58 |
59 | children[0] = (children[0] as string).replace('`▍`', '▍')
60 | }
61 |
62 | const match = /language-(\w+)/.exec(className || '')
63 |
64 | if (inline) {
65 | return (
66 |
67 | {children}
68 |
69 |
70 | )
71 | }
72 |
73 | return (
74 |
80 | )
81 | }
82 | }}
83 | >
84 | {text}
85 |
86 |
87 |
88 | )
89 | }
90 |
91 | export function BotCard({
92 | children,
93 | showAvatar = true
94 | }: {
95 | children: React.ReactNode
96 | showAvatar?: boolean
97 | }) {
98 | return (
99 |
100 |
106 |
107 |
108 |
{children}
109 |
110 | )
111 | }
112 |
113 | export function SystemMessage({ children }: { children: React.ReactNode }) {
114 | return (
115 |
122 | )
123 | }
124 |
125 | export function SpinnerMessage() {
126 | return (
127 |
128 |
129 |
130 |
131 |
132 | {spinner}
133 |
134 |
135 | )
136 | }
137 |
--------------------------------------------------------------------------------
/components/stocks/spinner.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export const spinner = (
4 |
14 |
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/components/stocks/stock-purchase.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useId, useState } from 'react'
4 | import { useActions, useAIState, useUIState } from 'ai/rsc'
5 | import { formatNumber } from '@/lib/utils'
6 |
7 | import type { AI } from '@/lib/chat/actions'
8 |
9 | interface Purchase {
10 | defaultAmount?: number
11 | name: string
12 | price: number
13 | status: 'requires_action' | 'completed' | 'expired'
14 | }
15 |
16 | export function Purchase({
17 | props: { defaultAmount, name, price, status = 'expired' }
18 | }: {
19 | props: Purchase
20 | }) {
21 | const [value, setValue] = useState(defaultAmount || 100)
22 | const [purchasingUI, setPurchasingUI] = useState(null)
23 | const [aiState, setAIState] = useAIState()
24 | const [, setMessages] = useUIState()
25 | const { confirmPurchase } = useActions()
26 |
27 | // Unique identifier for this UI component.
28 | const id = useId()
29 |
30 | // Whenever the slider changes, we need to update the local value state and the history
31 | // so LLM also knows what's going on.
32 | function onSliderChange(e: React.ChangeEvent) {
33 | const newValue = Number(e.target.value)
34 | setValue(newValue)
35 |
36 | // Insert a hidden history info to the list.
37 | const message = {
38 | role: 'system' as const,
39 | content: `[User has changed to purchase ${newValue} shares of ${name}. Total cost: $${(
40 | newValue * price
41 | ).toFixed(2)}]`,
42 |
43 | // Identifier of this UI component, so we don't insert it many times.
44 | id
45 | }
46 |
47 | // If last history state is already this info, update it. This is to avoid
48 | // adding every slider change to the history.
49 | if (aiState.messages[aiState.messages.length - 1]?.id === id) {
50 | setAIState({
51 | ...aiState,
52 | messages: [...aiState.messages.slice(0, -1), message]
53 | })
54 |
55 | return
56 | }
57 |
58 | // If it doesn't exist, append it to history.
59 | setAIState({ ...aiState, messages: [...aiState.messages, message] })
60 | }
61 |
62 | return (
63 |
64 |
65 | +1.23% ↑
66 |
67 |
{name}
68 |
${price}
69 | {purchasingUI ? (
70 |
{purchasingUI}
71 | ) : status === 'requires_action' ? (
72 | <>
73 |
74 |
Shares to purchase
75 |
84 |
85 | 10
86 |
87 |
88 | 100
89 |
90 |
91 | 500
92 |
93 |
94 | 1000
95 |
96 |
97 |
98 |
99 |
Total cost
100 |
101 |
102 | {value}
103 |
104 | shares
105 |
106 |
107 |
×
108 |
109 | ${price}
110 |
111 | per share
112 |
113 |
114 |
115 | = {formatNumber(value * price)}
116 |
117 |
118 |
119 |
120 |
{
123 | const response = await confirmPurchase(name, price, value)
124 | setPurchasingUI(response.purchasingUI)
125 |
126 | // Insert a new system message to the UI.
127 | setMessages((currentMessages: any) => [
128 | ...currentMessages,
129 | response.newMessage
130 | ])
131 | }}
132 | >
133 | Purchase
134 |
135 | >
136 | ) : status === 'completed' ? (
137 |
138 | You have successfully purchased {value} ${name}. Total cost:{' '}
139 | {formatNumber(value * price)}
140 |
141 | ) : status === 'expired' ? (
142 |
Your checkout session has expired!
143 | ) : null}
144 |
145 | )
146 | }
147 |
--------------------------------------------------------------------------------
/components/stocks/stock-skeleton.tsx:
--------------------------------------------------------------------------------
1 | export const StockSkeleton = () => {
2 | return (
3 |
4 |
5 | xxxxxxx
6 |
7 |
8 | xxxx
9 |
10 |
11 | xxxx
12 |
13 |
14 | xxxxxx xxx xx xxxx xx xxx
15 |
16 |
17 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/components/stocks/stock.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useRef, useEffect, useId } from 'react'
4 | import { scaleLinear } from 'd3-scale'
5 | import { subMonths, format } from 'date-fns'
6 | import { useResizeObserver } from 'usehooks-ts'
7 | import { useAIState } from 'ai/rsc'
8 |
9 | interface Stock {
10 | symbol: string
11 | price: number
12 | delta: number
13 | }
14 |
15 | export function Stock({ props: { symbol, price, delta } }: { props: Stock }) {
16 | const [aiState, setAIState] = useAIState()
17 | const id = useId()
18 |
19 | const [priceAtTime, setPriceAtTime] = useState({
20 | time: '00:00',
21 | value: price.toFixed(2),
22 | x: 0
23 | })
24 |
25 | const [startHighlight, setStartHighlight] = useState(0)
26 | const [endHighlight, setEndHighlight] = useState(0)
27 |
28 | const chartRef = useRef(null)
29 | const { width = 0 } = useResizeObserver({
30 | ref: chartRef,
31 | box: 'border-box'
32 | })
33 |
34 | const xToDate = scaleLinear(
35 | [0, width],
36 | [subMonths(new Date(), 6), new Date()]
37 | )
38 | const xToValue = scaleLinear(
39 | [0, width],
40 | [price - price / 2, price + price / 2]
41 | )
42 |
43 | useEffect(() => {
44 | if (startHighlight && endHighlight) {
45 | const message = {
46 | id,
47 | role: 'system' as const,
48 | content: `[User has highlighted dates between between ${format(
49 | xToDate(startHighlight),
50 | 'd LLL'
51 | )} and ${format(xToDate(endHighlight), 'd LLL, yyyy')}`
52 | }
53 |
54 | if (aiState.messages[aiState.messages.length - 1]?.id === id) {
55 | setAIState({
56 | ...aiState,
57 | messages: [...aiState.messages.slice(0, -1), message]
58 | })
59 | } else {
60 | setAIState({
61 | ...aiState,
62 | messages: [...aiState.messages, message]
63 | })
64 | }
65 | }
66 | }, [startHighlight, endHighlight])
67 |
68 | return (
69 |
70 |
71 | {`${delta > 0 ? '+' : ''}${((delta / price) * 100).toFixed(2)}% ${
72 | delta > 0 ? '↑' : '↓'
73 | }`}
74 |
75 |
{symbol}
76 |
${price}
77 |
78 | Closed: Feb 27, 4:59 PM EST
79 |
80 |
81 |
{
84 | if (chartRef.current) {
85 | const { clientX } = event
86 | const { left } = chartRef.current.getBoundingClientRect()
87 |
88 | setStartHighlight(clientX - left)
89 | setEndHighlight(0)
90 |
91 | setPriceAtTime({
92 | time: format(xToDate(clientX), 'dd LLL yy'),
93 | value: xToValue(clientX).toFixed(2),
94 | x: clientX - left
95 | })
96 | }
97 | }}
98 | onPointerUp={event => {
99 | if (chartRef.current) {
100 | const { clientX } = event
101 | const { left } = chartRef.current.getBoundingClientRect()
102 |
103 | setEndHighlight(clientX - left)
104 | }
105 | }}
106 | onPointerMove={event => {
107 | if (chartRef.current) {
108 | const { clientX } = event
109 | const { left } = chartRef.current.getBoundingClientRect()
110 |
111 | setPriceAtTime({
112 | time: format(xToDate(clientX), 'dd LLL yy'),
113 | value: xToValue(clientX).toFixed(2),
114 | x: clientX - left
115 | })
116 | }
117 | }}
118 | onPointerLeave={() => {
119 | setPriceAtTime({
120 | time: '00:00',
121 | value: price.toFixed(2),
122 | x: 0
123 | })
124 | }}
125 | ref={chartRef}
126 | >
127 | {priceAtTime.x > 0 ? (
128 |
135 |
${priceAtTime.value}
136 |
137 | {priceAtTime.time}
138 |
139 |
140 | ) : null}
141 |
142 | {startHighlight ? (
143 |
153 | ) : null}
154 |
155 |
161 |
162 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
199 |
206 |
207 |
208 |
209 | )
210 | }
211 |
--------------------------------------------------------------------------------
/components/stocks/stocks-skeleton.tsx:
--------------------------------------------------------------------------------
1 | export const StocksSkeleton = () => {
2 | return (
3 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/components/stocks/stocks.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useActions, useUIState } from 'ai/rsc'
4 |
5 | import type { AI } from '@/lib/chat/actions'
6 |
7 | interface Stock {
8 | symbol: string
9 | price: number
10 | delta: number
11 | }
12 |
13 | export function Stocks({ props: stocks }: { props: Stock[] }) {
14 | const [, setMessages] = useUIState()
15 | const { submitUserMessage } = useActions()
16 |
17 | return (
18 |
19 | {stocks.map(stock => (
20 |
{
24 | const response = await submitUserMessage(`View ${stock.symbol}`)
25 | setMessages(currentMessages => [...currentMessages, response])
26 | }}
27 | >
28 | 0 ? 'text-green-600' : 'text-red-600'
31 | } flex w-11 flex-row justify-center rounded-md bg-white/10 p-2`}
32 | >
33 | {stock.delta > 0 ? '↑' : '↓'}
34 |
35 |
36 |
{stock.symbol}
37 |
${stock.price}
38 |
39 |
40 |
0 ? 'text-green-600' : 'text-red-600'
43 | } bold text-right uppercase`}
44 | >
45 | {` ${((stock.delta / stock.price) * 100).toFixed(2)}%`}
46 |
47 |
0 ? 'text-green-700' : 'text-red-700'
50 | } text-right text-base`}
51 | >
52 | {stock.delta}
53 |
54 |
55 |
56 | ))}
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === 'production') return null
3 |
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useTheme } from 'next-themes'
5 |
6 | import { Button } from '@/components/ui/button'
7 | import { IconMoon, IconSun } from '@/components/ui/icons'
8 |
9 | export function ThemeToggle() {
10 | const { setTheme, theme } = useTheme()
11 | const [_, startTransition] = React.useTransition()
12 |
13 | return (
14 | {
18 | startTransition(() => {
19 | setTheme(theme === 'light' ? 'dark' : 'light')
20 | })
21 | }}
22 | >
23 | {!theme ? null : theme === 'dark' ? (
24 |
25 | ) : (
26 |
27 | )}
28 | Toggle theme
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
16 | outline:
17 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
20 | ghost: 'hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 hover:underline'
22 | },
23 | size: {
24 | default: 'h-9 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3 text-xs',
26 | lg: 'h-10 rounded-md px-8',
27 | icon: 'size-9'
28 | }
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default'
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/codeblock.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3 |
4 | 'use client'
5 |
6 | import { FC, memo } from 'react'
7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
9 |
10 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
11 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
12 | import { Button } from '@/components/ui/button'
13 |
14 | interface Props {
15 | language: string
16 | value: string
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: '.js',
25 | python: '.py',
26 | java: '.java',
27 | c: '.c',
28 | cpp: '.cpp',
29 | 'c++': '.cpp',
30 | 'c#': '.cs',
31 | ruby: '.rb',
32 | php: '.php',
33 | swift: '.swift',
34 | 'objective-c': '.m',
35 | kotlin: '.kt',
36 | typescript: '.ts',
37 | go: '.go',
38 | perl: '.pl',
39 | rust: '.rs',
40 | scala: '.scala',
41 | haskell: '.hs',
42 | lua: '.lua',
43 | shell: '.sh',
44 | sql: '.sql',
45 | html: '.html',
46 | css: '.css'
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | }
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = ''
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length))
55 | }
56 | return lowercase ? result.toLowerCase() : result
57 | }
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === 'undefined') {
64 | return
65 | }
66 | const fileExtension = programmingLanguages[language] || '.file'
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`
71 | const fileName = window.prompt('Enter file name' || '', suggestedFileName)
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return
76 | }
77 |
78 | const blob = new Blob([value], { type: 'text/plain' })
79 | const url = URL.createObjectURL(blob)
80 | const link = document.createElement('a')
81 | link.download = fileName
82 | link.href = url
83 | link.style.display = 'none'
84 | document.body.appendChild(link)
85 | link.click()
86 | document.body.removeChild(link)
87 | URL.revokeObjectURL(url)
88 | }
89 |
90 | const onCopy = () => {
91 | if (isCopied) return
92 | copyToClipboard(value)
93 | }
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
106 |
107 | Download
108 |
109 |
115 | {isCopied ? : }
116 | Copy code
117 |
118 |
119 |
120 |
141 | {value}
142 |
143 |
144 | )
145 | })
146 | CodeBlock.displayName = 'CodeBlock'
147 |
148 | export { CodeBlock }
149 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/user-menu.tsx:
--------------------------------------------------------------------------------
1 | import { type Session } from '@/lib/types'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuSeparator,
9 | DropdownMenuTrigger
10 | } from '@/components/ui/dropdown-menu'
11 | import { signOut } from '@/auth'
12 |
13 | export interface UserMenuProps {
14 | user: Session['user']
15 | }
16 |
17 | function getUserInitials(name: string) {
18 | const [firstName, lastName] = name.split(' ')
19 | return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
20 | }
21 |
22 | export function UserMenu({ user }: UserMenuProps) {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | {getUserInitials(user.email)}
30 |
31 | {user.email}
32 |
33 |
34 |
35 |
36 | {user.email}
37 |
38 |
39 | {
41 | 'use server'
42 | await signOut()
43 | }}
44 | >
45 |
46 | Sign Out
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/example-env-vars:
--------------------------------------------------------------------------------
1 | # You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview
2 | # Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys
3 | OPENAI_API_KEY=""
4 |
5 | # You must first create a Twilio Segment account here: https://segment.com/signup/
6 | # Then get your node.js source key here: https://segment.com/docs/connections/find-writekey/
7 | NEXT_PUBLIC_SEGMENT_WRITE_KEY=""
8 |
9 | # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
10 | AUTH_SECRET=""
11 |
12 | # Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
13 | KV_URL=""
14 | KV_REST_API_URL=""
15 | KV_REST_API_TOKEN=""
16 | KV_REST_API_READ_ONLY_TOKEN=""
17 |
18 | # Instructions to create postgres database here: https://vercel.com/docs/storage/vercel-postgres/quickstart and
19 | POSTGRES_DATABASE=""
20 | POSTGRES_HOST=""
21 | POSTGRES_PASSWORD=""
22 | POSTGRES_PRISMA_URL=""
23 | POSTGRES_URL=""
24 | POSTGRES_URL_NON_POOLING=""
25 | POSTGRES_URL_NO_SSL=""
26 | POSTGRES_USER=""
--------------------------------------------------------------------------------
/lib/chat/actions.tsx:
--------------------------------------------------------------------------------
1 | import 'server-only'
2 |
3 | import {
4 | createAI,
5 | createStreamableUI,
6 | getMutableAIState,
7 | getAIState,
8 | render,
9 | createStreamableValue
10 | } from 'ai/rsc'
11 | import OpenAI from 'openai'
12 |
13 | import {
14 | spinner,
15 | BotCard,
16 | BotMessage,
17 | SystemMessage,
18 | Stock,
19 | Purchase
20 | } from '@/components/stocks'
21 |
22 | import { z } from 'zod'
23 | import { EventsSkeleton } from '@/components/stocks/events-skeleton'
24 | import { Events } from '@/components/stocks/event'
25 | import { StocksSkeleton } from '@/components/stocks/stocks-skeleton'
26 | import { Stocks } from '@/components/stocks/stocks'
27 | import { StockSkeleton } from '@/components/stocks/stock-skeleton'
28 | import {
29 | formatNumber,
30 | runAsyncFnWithoutBlocking,
31 | sleep,
32 | nanoid
33 | } from '@/lib/utils'
34 | import { saveChat } from '@/app/actions'
35 | import { SpinnerMessage, UserMessage } from '@/components/stocks/message'
36 | import { Chat } from '@/lib/types'
37 | import { auth } from '@/auth'
38 | import analytics from '@/app/analyticsInstance'
39 |
40 | const openai = new OpenAI({
41 | apiKey: process.env.OPENAI_API_KEY || ''
42 | })
43 |
44 | async function confirmPurchase(symbol: string, price: number, amount: number) {
45 | 'use server'
46 |
47 | const aiState = getMutableAIState()
48 |
49 | const purchasing = createStreamableUI(
50 |
51 | {spinner}
52 |
53 | Purchasing {amount} ${symbol}...
54 |
55 |
56 | )
57 |
58 | const systemMessage = createStreamableUI(null)
59 |
60 | runAsyncFnWithoutBlocking(async () => {
61 | await sleep(1000)
62 |
63 | purchasing.update(
64 |
65 | {spinner}
66 |
67 | Purchasing {amount} ${symbol}... working on it...
68 |
69 |
70 | )
71 |
72 | await sleep(1000)
73 |
74 | purchasing.done(
75 |
76 |
77 | You have successfully purchased {amount} ${symbol}. Total cost:{' '}
78 | {formatNumber(amount * price)}
79 |
80 |
81 | )
82 | // send purchase event to Segment
83 | analytics.track({
84 | userId: "123",
85 | event: "Stock Purchased",
86 | properties: {
87 | stock_symbol: symbol,
88 | amount: amount,
89 | total: amount*price
90 | }
91 | });
92 |
93 | systemMessage.done(
94 |
95 | You have purchased {amount} shares of {symbol} at ${price}. Total cost ={' '}
96 | {formatNumber(amount * price)}.
97 |
98 | )
99 |
100 | aiState.done({
101 | ...aiState.get(),
102 | messages: [
103 | ...aiState.get().messages.slice(0, -1),
104 | {
105 | id: nanoid(),
106 | role: 'function',
107 | name: 'showStockPurchase',
108 | content: JSON.stringify({
109 | name: symbol,
110 | price,
111 | defaultAmount: amount,
112 | status: 'completed'
113 | })
114 | },
115 | {
116 | id: nanoid(),
117 | role: 'system',
118 | content: `[User has purchased ${amount} shares of ${symbol} at ${price}. Total cost = ${
119 | amount * price
120 | }]`
121 | }
122 | ]
123 | })
124 | })
125 |
126 | return {
127 | purchasingUI: purchasing.value,
128 | newMessage: {
129 | id: nanoid(),
130 | display: systemMessage.value
131 | }
132 | }
133 | }
134 |
135 | async function submitUserMessage(content: string) {
136 | 'use server'
137 |
138 | const aiState = getMutableAIState()
139 |
140 | aiState.update({
141 | ...aiState.get(),
142 | messages: [
143 | ...aiState.get().messages,
144 | {
145 | id: nanoid(),
146 | role: 'user',
147 | content
148 | }
149 | ]
150 | })
151 |
152 | let textStream: undefined | ReturnType>
153 | let textNode: undefined | React.ReactNode
154 |
155 | const ui = render({
156 | model: 'gpt-3.5-turbo',
157 | provider: openai,
158 | initial: ,
159 | messages: [
160 | {
161 | role: 'system',
162 | content: `\
163 | You are a stock trading conversation bot and you can help users buy stocks, step by step.
164 | You and the user can discuss stock prices and the user can adjust the amount of stocks they want to buy, or place an order, in the UI.
165 |
166 | Messages inside [] means that it's a UI element or a user event. For example:
167 | - "[Price of AAPL = 100]" means that an interface of the stock price of AAPL is shown to the user.
168 | - "[User has changed the amount of AAPL to 10]" means that the user has changed the amount of AAPL to 10 in the UI.
169 |
170 | If the user requests purchasing a stock, call \`show_stock_purchase_ui\` to show the purchase UI.
171 | If the user just wants the price, call \`show_stock_price\` to show the price.
172 | If you want to show trending stocks, call \`list_stocks\`.
173 | If you want to show events, call \`get_events\`.
174 | If the user wants to sell stock, or complete another impossible task, respond that you are a demo and cannot do that.
175 |
176 | Besides that, you can also chat with users and do some calculations if needed.`
177 | },
178 | ...aiState.get().messages.map((message: any) => ({
179 | role: message.role,
180 | content: message.content,
181 | name: message.name
182 | }))
183 | ],
184 | text: ({ content, done, delta }) => {
185 | if (!textStream) {
186 | textStream = createStreamableValue('')
187 | textNode =
188 | }
189 |
190 | // picking up copilot response and sending into segment
191 | if (done) {
192 | analytics.track({
193 | userId: '123',
194 | event: 'Message Received',
195 | properties: {
196 | content,
197 | conversationId: aiState.get().chatId,
198 | }
199 | })
200 |
201 | textStream.done()
202 | aiState.done({
203 | ...aiState.get(),
204 | messages: [
205 | ...aiState.get().messages,
206 | {
207 | id: nanoid(),
208 | role: 'assistant',
209 | content
210 | }
211 | ]
212 | })
213 | } else {
214 | textStream.update(delta)
215 | }
216 |
217 | return textNode
218 | },
219 | functions: {
220 | listStocks: {
221 | description: 'List three imaginary stocks that are trending.',
222 | parameters: z.object({
223 | stocks: z.array(
224 | z.object({
225 | symbol: z.string().describe('The symbol of the stock'),
226 | price: z.number().describe('The price of the stock'),
227 | delta: z.number().describe('The change in price of the stock')
228 | })
229 | )
230 | }),
231 | render: async function* ({ stocks }) {
232 | yield (
233 |
234 |
235 |
236 | )
237 |
238 | // send custom stock list component load to Segment
239 | analytics.track({
240 | userId: "123",
241 | event: "Component Loaded",
242 | properties: {
243 | type: 'Stock List',
244 | stock_list: JSON.stringify(stocks.map(({ symbol, price, delta }) => ({ symbol, price, change: delta }))),
245 | conversationId: aiState.get().chatId
246 | }
247 | });
248 |
249 | await sleep(1000)
250 |
251 | aiState.done({
252 | ...aiState.get(),
253 | messages: [
254 | ...aiState.get().messages,
255 | {
256 | id: nanoid(),
257 | role: 'function',
258 | name: 'listStocks',
259 | content: JSON.stringify(stocks)
260 | }
261 | ]
262 | })
263 |
264 | return (
265 |
266 |
267 |
268 | )
269 | }
270 | },
271 | showStockPrice: {
272 | description:
273 | 'Get the current stock price of a given stock or currency. Use this to show the price to the user.',
274 | parameters: z.object({
275 | symbol: z
276 | .string()
277 | .describe(
278 | 'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.'
279 | ),
280 | price: z.number().describe('The price of the stock.'),
281 | delta: z.number().describe('The change in price of the stock')
282 | }),
283 | render: async function* ({ symbol, price, delta }) {
284 | yield (
285 |
286 |
287 |
288 | )
289 | // send component load to Segment
290 | analytics.track({
291 | userId: "123",
292 | event: "Component Loaded",
293 | properties: {
294 | type: 'Stock Price Chart',
295 | stock_symbol: symbol,
296 | conversationId: aiState.get().chatId
297 | }
298 | });
299 | await sleep(1000)
300 |
301 | aiState.done({
302 | ...aiState.get(),
303 | messages: [
304 | ...aiState.get().messages,
305 | {
306 | id: nanoid(),
307 | role: 'function',
308 | name: 'showStockPrice',
309 | content: JSON.stringify({ symbol, price, delta })
310 | }
311 | ]
312 | })
313 |
314 | return (
315 |
316 |
317 |
318 | )
319 | }
320 | },
321 | showStockPurchase: {
322 | description:
323 | 'Show price and the UI to purchase a stock or currency. Use this if the user wants to purchase a stock or currency.',
324 | parameters: z.object({
325 | symbol: z
326 | .string()
327 | .describe(
328 | 'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.'
329 | ),
330 | price: z.number().describe('The price of the stock.'),
331 | numberOfShares: z
332 | .number()
333 | .describe(
334 | 'The **number of shares** for a stock or currency to purchase. Can be optional if the user did not specify it.'
335 | )
336 | }),
337 | render: async function* ({ symbol, price, numberOfShares = 100 }) {
338 |
339 | // send component load to Segment
340 | analytics.track({
341 | userId: "123",
342 | event: "Component Loaded",
343 | properties: {
344 | type: 'Stock Purchase Interface',
345 | stock_symbol: symbol,
346 | conversationId: aiState.get().chatId
347 | }
348 | });
349 |
350 | if (numberOfShares <= 0 || numberOfShares > 1000) {
351 | aiState.done({
352 | ...aiState.get(),
353 | messages: [
354 | ...aiState.get().messages,
355 | {
356 | id: nanoid(),
357 | role: 'system',
358 | content: `[User has selected an invalid amount]`
359 | }
360 | ]
361 | })
362 |
363 | return
364 | }
365 |
366 | aiState.done({
367 | ...aiState.get(),
368 | messages: [
369 | ...aiState.get().messages,
370 | {
371 | id: nanoid(),
372 | role: 'function',
373 | name: 'showStockPurchase',
374 | content: JSON.stringify({
375 | symbol,
376 | price,
377 | numberOfShares
378 | })
379 | }
380 | ]
381 |
382 | })
383 |
384 | return (
385 | <>
386 |
394 |
395 |
403 | >
404 | )
405 | }
406 | },
407 | getEvents: {
408 | description:
409 | 'List funny imaginary events between user highlighted dates that describe stock activity.',
410 | parameters: z.object({
411 | events: z.array(
412 | z.object({
413 | date: z
414 | .string()
415 | .describe('The date of the event, in ISO-8601 format'),
416 | headline: z.string().describe('The headline of the event'),
417 | description: z.string().describe('The description of the event')
418 | })
419 | )
420 | }),
421 | render: async function* ({ events }) {
422 | yield (
423 |
424 |
425 |
426 | )
427 |
428 | await sleep(1000)
429 |
430 | aiState.done({
431 | ...aiState.get(),
432 | messages: [
433 | ...aiState.get().messages,
434 | {
435 | id: nanoid(),
436 | role: 'function',
437 | name: 'getEvents',
438 | content: JSON.stringify(events)
439 | }
440 | ]
441 | })
442 |
443 | return (
444 |
445 |
446 |
447 | )
448 | }
449 | }
450 | }
451 | })
452 |
453 | return {
454 | id: nanoid(),
455 | display: ui
456 | }
457 | }
458 |
459 | export type Message = {
460 | role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
461 | content: string
462 | id?: string
463 | name?: string
464 | }
465 |
466 | export type AIState = {
467 | chatId: string
468 | messages: {
469 | role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
470 | content: string
471 | id: string
472 | name?: string
473 | }[]
474 | }
475 |
476 | export type UIState = {
477 | id: string
478 | display: React.ReactNode
479 | }[]
480 |
481 | export const AI = createAI({
482 | actions: {
483 | submitUserMessage,
484 | confirmPurchase
485 | },
486 | initialUIState: [],
487 | initialAIState: { chatId: nanoid(), messages: [] },
488 | unstable_onGetUIState: async () => {
489 | 'use server'
490 |
491 | const session = await auth()
492 |
493 | if (session && session.user) {
494 | const aiState = getAIState()
495 |
496 | if (aiState) {
497 | const uiState = getUIStateFromAIState(aiState)
498 | return uiState
499 | }
500 | } else {
501 | return
502 | }
503 | },
504 | unstable_onSetAIState: async ({ state }) => {
505 | 'use server'
506 |
507 | const session = await auth()
508 |
509 | if (session && session.user) {
510 | const { chatId, messages } = state
511 |
512 | const createdAt = new Date()
513 | const userId = session.user.id as string
514 | const path = `/chat/${chatId}`
515 | const title = messages[0].content.substring(0, 100)
516 |
517 | const chat: Chat = {
518 | id: chatId,
519 | title,
520 | userId,
521 | createdAt,
522 | messages,
523 | path
524 | }
525 |
526 | await saveChat(chat)
527 | } else {
528 | return
529 | }
530 | }
531 | })
532 |
533 | export const getUIStateFromAIState = (aiState: Chat) => {
534 | return aiState.messages
535 | .filter(message => message.role !== 'system')
536 | .map((message, index) => ({
537 | id: `${aiState.chatId}-${index}`,
538 | display:
539 | message.role === 'function' ? (
540 | message.name === 'listStocks' ? (
541 |
542 |
543 |
544 | ) : message.name === 'showStockPrice' ? (
545 |
546 |
547 |
548 | ) : message.name === 'showStockPurchase' ? (
549 |
550 |
551 |
552 | ) : message.name === 'getEvents' ? (
553 |
554 |
555 |
556 | ) : null
557 | ) : message.role === 'user' ? (
558 | {message.content}
559 | ) : (
560 |
561 | )
562 | }))
563 | }
564 |
--------------------------------------------------------------------------------
/lib/hooks/use-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset
11 | )
12 | }
13 |
14 | window.addEventListener('scroll', handleScroll, { passive: true })
15 | handleScroll()
16 |
17 | return () => {
18 | window.removeEventListener('scroll', handleScroll)
19 | }
20 | }, [offset])
21 |
22 | return isAtBottom
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number
7 | }
8 |
9 | export function useCopyToClipboard({
10 | timeout = 2000
11 | }: useCopyToClipboardProps) {
12 | const [isCopied, setIsCopied] = React.useState(false)
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
16 | return
17 | }
18 |
19 | if (!value) {
20 | return
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true)
25 |
26 | setTimeout(() => {
27 | setIsCopied(false)
28 | }, timeout)
29 | })
30 | }
31 |
32 | return { isCopied, copyToClipboard }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from 'react'
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject
5 | onKeyDown: (event: React.KeyboardEvent) => void
6 | } {
7 | const formRef = useRef(null)
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent
11 | ): void => {
12 | if (
13 | event.key === 'Enter' &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit()
18 | event.preventDefault()
19 | }
20 | }
21 |
22 | return { formRef, onKeyDown: handleKeyDown }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useLocalStorage = (
4 | key: string,
5 | initialValue: T
6 | ): [T, (value: T) => void] => {
7 | const [storedValue, setStoredValue] = useState(initialValue)
8 |
9 | useEffect(() => {
10 | // Retrieve from localStorage
11 | const item = window.localStorage.getItem(key)
12 | if (item) {
13 | setStoredValue(JSON.parse(item))
14 | }
15 | }, [key])
16 |
17 | const setValue = (value: T) => {
18 | // Save state
19 | setStoredValue(value)
20 | // Save to localStorage
21 | window.localStorage.setItem(key, JSON.stringify(value))
22 | }
23 | return [storedValue, setValue]
24 | }
25 |
--------------------------------------------------------------------------------
/lib/hooks/use-sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | const LOCAL_STORAGE_KEY = 'sidebar'
6 |
7 | interface SidebarContext {
8 | isSidebarOpen: boolean
9 | toggleSidebar: () => void
10 | isLoading: boolean
11 | }
12 |
13 | const SidebarContext = React.createContext(
14 | undefined
15 | )
16 |
17 | export function useSidebar() {
18 | const context = React.useContext(SidebarContext)
19 | if (!context) {
20 | throw new Error('useSidebarContext must be used within a SidebarProvider')
21 | }
22 | return context
23 | }
24 |
25 | interface SidebarProviderProps {
26 | children: React.ReactNode
27 | }
28 |
29 | export function SidebarProvider({ children }: SidebarProviderProps) {
30 | const [isSidebarOpen, setSidebarOpen] = React.useState(true)
31 | const [isLoading, setLoading] = React.useState(true)
32 |
33 | React.useEffect(() => {
34 | const value = localStorage.getItem(LOCAL_STORAGE_KEY)
35 | if (value) {
36 | setSidebarOpen(JSON.parse(value))
37 | }
38 | setLoading(false)
39 | }, [])
40 |
41 | const toggleSidebar = () => {
42 | setSidebarOpen(value => {
43 | const newState = !value
44 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState))
45 | return newState
46 | })
47 | }
48 |
49 | if (isLoading) {
50 | return null
51 | }
52 |
53 | return (
54 |
57 | {children}
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/lib/hooks/use-streamable-text.ts:
--------------------------------------------------------------------------------
1 | import { StreamableValue, readStreamableValue } from 'ai/rsc'
2 | import { useEffect, useState } from 'react'
3 |
4 | export const useStreamableText = (
5 | content: string | StreamableValue
6 | ) => {
7 | const [rawContent, setRawContent] = useState(
8 | typeof content === 'string' ? content : ''
9 | );
10 | const [streamingComplete, setStreamingComplete] = useState(false);
11 |
12 | useEffect(() => {
13 | let value = '';
14 | if (typeof content === 'object') {
15 | (async () => {
16 | for await (const delta of readStreamableValue(content)) {
17 | if (typeof delta === 'string') {
18 | value += delta;
19 | setRawContent(value);
20 | }
21 | }
22 | setStreamingComplete(true);
23 | })();
24 | }
25 | }, [content]);
26 |
27 | return rawContent;
28 | }
29 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { Message } from 'ai'
2 |
3 | export interface Chat extends Record {
4 | id: string
5 | title: string
6 | createdAt: Date
7 | userId: string
8 | path: string
9 | messages: Message[]
10 | sharePath?: string
11 | }
12 |
13 | export type ServerActionResult = Promise<
14 | | Result
15 | | {
16 | error: string
17 | }
18 | >
19 |
20 | export interface Session {
21 | user: {
22 | id: string
23 | email: string
24 | }
25 | }
26 |
27 | export interface AuthResult {
28 | type: string
29 | message: string
30 | }
31 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { customAlphabet } from 'nanoid'
3 | import { twMerge } from 'tailwind-merge'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export const nanoid = customAlphabet(
10 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
11 | 7
12 | ) // 7-character random string
13 |
14 | export async function fetcher(
15 | input: RequestInfo,
16 | init?: RequestInit
17 | ): Promise {
18 | const res = await fetch(input, init)
19 |
20 | if (!res.ok) {
21 | const json = await res.json()
22 | if (json.error) {
23 | const error = new Error(json.error) as Error & {
24 | status: number
25 | }
26 | error.status = res.status
27 | throw error
28 | } else {
29 | throw new Error('An unexpected error occurred')
30 | }
31 | }
32 |
33 | return res.json()
34 | }
35 |
36 | export function formatDate(input: string | number | Date): string {
37 | const date = new Date(input)
38 | return date.toLocaleDateString('en-US', {
39 | month: 'long',
40 | day: 'numeric',
41 | year: 'numeric'
42 | })
43 | }
44 |
45 | export const formatNumber = (value: number) =>
46 | new Intl.NumberFormat('en-US', {
47 | style: 'currency',
48 | currency: 'USD'
49 | }).format(value)
50 |
51 | export const runAsyncFnWithoutBlocking = (
52 | fn: (...args: any) => Promise
53 | ) => {
54 | fn()
55 | }
56 |
57 | export const sleep = (ms: number) =>
58 | new Promise(resolve => setTimeout(resolve, ms))
59 |
60 | export const getStringFromBuffer = (buffer: ArrayBuffer) =>
61 | Array.from(new Uint8Array(buffer))
62 | .map(b => b.toString(16).padStart(2, '0'))
63 | .join('')
64 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth'
2 | import { authConfig } from './auth.config'
3 |
4 | export default NextAuth(authConfig).auth
5 |
6 | export const config = {
7 | matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
8 | }
9 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: 'avatars.githubusercontent.com',
8 | port: '',
9 | pathname: '**'
10 | }
11 | ]
12 | },
13 | reactStrictMode: false
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "next dev",
5 | "build": "next build",
6 | "start": "next start",
7 | "lint": "next lint",
8 | "lint:fix": "next lint --fix",
9 | "preview": "next build && next start",
10 | "seed": "node -r dotenv/config ./scripts/seed.mjs",
11 | "type-check": "tsc --noEmit",
12 | "format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache",
13 | "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
14 | },
15 | "dependencies": {
16 | "@radix-ui/react-alert-dialog": "^1.0.5",
17 | "@radix-ui/react-dialog": "^1.0.5",
18 | "@radix-ui/react-dropdown-menu": "^2.0.6",
19 | "@radix-ui/react-icons": "^1.3.0",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-select": "^2.0.0",
22 | "@radix-ui/react-separator": "^1.0.3",
23 | "@radix-ui/react-slot": "^1.0.2",
24 | "@radix-ui/react-switch": "^1.0.3",
25 | "@radix-ui/react-tooltip": "^1.0.7",
26 | "@segment/analytics-node": "^2.1.0",
27 | "@vercel/analytics": "^1.1.2",
28 | "@vercel/kv": "^1.0.1",
29 | "@vercel/og": "^0.6.2",
30 | "@vercel/postgres": "^0.7.2",
31 | "ai": "^3.0.12",
32 | "class-variance-authority": "^0.7.0",
33 | "clsx": "^2.1.0",
34 | "d3-scale": "^4.0.2",
35 | "date-fns": "^3.3.1",
36 | "focus-trap-react": "^10.2.3",
37 | "framer-motion": "^10.18.0",
38 | "geist": "^1.2.1",
39 | "nanoid": "^5.0.4",
40 | "next": "14.1.3",
41 | "next-auth": "5.0.0-beta.4",
42 | "next-themes": "^0.2.1",
43 | "nookies": "^2.5.2",
44 | "openai": "^4.24.7",
45 | "react": "^18.2.0",
46 | "react-dom": "^18.2.0",
47 | "react-intersection-observer": "^9.5.3",
48 | "react-markdown": "^8.0.7",
49 | "react-syntax-highlighter": "^15.5.0",
50 | "react-textarea-autosize": "^8.5.3",
51 | "remark-gfm": "^3.0.1",
52 | "remark-math": "^5.1.1",
53 | "sonner": "^1.4.3",
54 | "usehooks-ts": "^2.16.0",
55 | "zod": "^3.22.4"
56 | },
57 | "devDependencies": {
58 | "@tailwindcss/typography": "^0.5.10",
59 | "@types/d3-scale": "^4.0.8",
60 | "@types/node": "^20.11.5",
61 | "@types/react": "^18.2.48",
62 | "@types/react-dom": "^18.2.18",
63 | "@types/react-syntax-highlighter": "^15.5.11",
64 | "@typescript-eslint/parser": "^6.19.0",
65 | "autoprefixer": "^10.4.17",
66 | "dotenv": "^16.4.5",
67 | "eslint": "^8.56.0",
68 | "eslint-config-next": "14.1.0",
69 | "eslint-config-prettier": "^9.1.0",
70 | "eslint-plugin-tailwindcss": "^3.14.0",
71 | "postcss": "^8.4.33",
72 | "prettier": "^3.2.4",
73 | "tailwind-merge": "^2.2.0",
74 | "tailwindcss": "^3.4.1",
75 | "tailwindcss-animate": "^1.0.7",
76 | "typescript": "^5.3.3"
77 | },
78 | "packageManager": "pnpm@8.6.3"
79 | }
80 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: 'lf',
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: true,
7 | arrowParens: 'avoid',
8 | tabWidth: 2,
9 | trailingComma: 'none',
10 | importOrder: [
11 | '^(react/(.*)$)|^(react$)',
12 | '^(next/(.*)$)|^(next$)',
13 | '',
14 | '',
15 | '^types$',
16 | '^@/types/(.*)$',
17 | '^@/config/(.*)$',
18 | '^@/lib/(.*)$',
19 | '^@/hooks/(.*)$',
20 | '^@/components/ui/(.*)$',
21 | '^@/components/(.*)$',
22 | '^@/registry/(.*)$',
23 | '^@/styles/(.*)$',
24 | '^@/app/(.*)$',
25 | '',
26 | '^[./]'
27 | ],
28 | importOrderSeparation: false,
29 | importOrderSortSpecifiers: true,
30 | importOrderBuiltinModulesToTop: true,
31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
32 | importOrderMergeDuplicateImports: true,
33 | importOrderCombineTypeAndValueImports: true
34 | }
35 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaithschmitz/segment-ai-copilot/b285cecb633814a627e6af7ce336240a2dce662c/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaithschmitz/segment-ai-copilot/b285cecb633814a627e6af7ce336240a2dce662c/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaithschmitz/segment-ai-copilot/b285cecb633814a627e6af7ce336240a2dce662c/public/favicon.ico
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/repo_img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaithschmitz/segment-ai-copilot/b285cecb633814a627e6af7ce336240a2dce662c/public/repo_img.png
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/seed.mjs:
--------------------------------------------------------------------------------
1 | import { db } from '@vercel/postgres'
2 |
3 | async function seedUsers(client) {
4 | try {
5 | await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`
6 | const createTable = await client.sql`
7 | CREATE TABLE IF NOT EXISTS users (
8 | id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
9 | email TEXT NOT NULL UNIQUE,
10 | password TEXT NOT NULL,
11 | salt TEXT NOT NULL
12 | );
13 | `
14 |
15 | console.log(`Created "users" table`)
16 |
17 | return {
18 | createTable,
19 | }
20 | } catch (error) {
21 | console.error('Error seeding users:', error)
22 | throw error
23 | }
24 | }
25 |
26 | async function main() {
27 | const client = await db.connect()
28 | await seedUsers(client)
29 | await client.end()
30 | }
31 |
32 | main().catch(err => {
33 | console.error('An error occurred while attempting to seed the database:', err)
34 | })
35 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}'
9 | ],
10 | prefix: '',
11 | theme: {
12 | container: {
13 | center: true,
14 | padding: '2rem',
15 | screens: {
16 | '2xl': '1400px'
17 | }
18 | },
19 | extend: {
20 | fontFamily: {
21 | sans: ['var(--font-geist-sans)'],
22 | mono: ['var(--font-geist-mono)']
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 | borderRadius: {
60 | lg: 'var(--radius)',
61 | md: 'calc(var(--radius) - 2px)',
62 | sm: 'calc(var(--radius) - 4px)'
63 | },
64 | keyframes: {
65 | 'accordion-down': {
66 | from: { height: '0' },
67 | to: { height: 'var(--radix-accordion-content-height)' }
68 | },
69 | 'accordion-up': {
70 | from: { height: 'var(--radix-accordion-content-height)' },
71 | to: { height: '0' }
72 | }
73 | },
74 | animation: {
75 | 'accordion-down': 'accordion-down 0.2s ease-out',
76 | 'accordion-up': 'accordion-up 0.2s ease-out'
77 | }
78 | }
79 | },
80 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
81 | }
82 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "incremental": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": ["./*"]
19 | },
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "strictNullChecks": true
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "next-auth.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx",
32 | ".next/types/**/*.ts"
33 | ],
34 | "exclude": ["node_modules"]
35 | }
36 |
--------------------------------------------------------------------------------