├── .env.example
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── app
├── actions.ts
├── api
│ ├── auth
│ │ └── callback
│ │ │ └── route.ts
│ ├── chat
│ │ └── route.ts
│ └── web3auth
│ │ ├── login
│ │ └── route.ts
│ │ └── nonce
│ │ └── route.ts
├── chat
│ └── [id]
│ │ └── page.tsx
├── globals.css
├── layout.tsx
├── opengraph-image.png
├── page.tsx
├── share
│ └── [id]
│ │ └── page.tsx
├── sign-in
│ └── page.tsx
└── twitter-image.png
├── assets
└── fonts
│ ├── Inter-Bold.woff
│ └── Inter-Regular.woff
├── auth.ts
├── codegen.yml
├── components
├── button-scroll-to-bottom.tsx
├── chat-list.tsx
├── chat-message-actions.tsx
├── chat-message.tsx
├── chat-panel.tsx
├── chat-scroll-anchor.tsx
├── chat.tsx
├── clear-history.tsx
├── empty-screen.tsx
├── external-link.tsx
├── footer.tsx
├── header.tsx
├── login-button-metamask.tsx
├── markdown.tsx
├── profile.tsx
├── prompt-form.tsx
├── providers.tsx
├── sidebar-actions.tsx
├── sidebar-footer.tsx
├── sidebar-item.tsx
├── sidebar-list.tsx
├── sidebar.tsx
├── tailwind-indicator.tsx
├── theme-toggle.tsx
├── toaster.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
│ ├── switch.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
└── user-menu.tsx
├── docs
├── README_improvements_ideas.md
├── README_web3_auth_supabase.md
├── README_web3_tokens_and_networks.md
└── assets
│ ├── metamask_add_network.png
│ ├── ocean_publish_datatoken.png
│ ├── supabase_jwt_policy.png
│ ├── supabase_user_table.png
│ ├── table_users_linked_id.png
│ ├── tokengated_chatbot.png
│ └── your_datatoken.png
├── graphql
├── graphqlClient.ts
├── operations
│ └── queries
│ │ ├── getOrder.graphql
│ │ ├── getOrders.graphql
│ │ └── getUser.graphql
├── schema.graphql
└── sdk.ts
├── lib
├── analytics.ts
├── db_types.ts
├── fonts.ts
├── hooks
│ ├── use-at-bottom.tsx
│ ├── use-copy-to-clipboard.tsx
│ ├── use-enter-submit.tsx
│ └── use-local-storage.ts
├── types.ts
└── utils.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.cjs
├── public
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon.ico
├── next.svg
├── thirteen.svg
└── vercel.svg
├── supabase
├── .gitignore
├── config.toml
├── functions
│ └── client.ts
├── migrations
│ └── 20230707053030_init.sql
└── seed.sql
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
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=your-open-ai-key
4 |
5 | # Update these with your Supabase details from your project settings > API
6 | # https://app.supabase.com/project/_/settings/api
7 | # In local dev you can get these by running `supabase status`.
8 | NEXT_PUBLIC_SUPABASE_URL=your-supabase-project-url
9 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
10 | NEXT_PUBLIC_SUPABASE_SERVICE_KEY=your-supabase-service-key
11 | NEXT_PUBLIC_SUPABASE_JWT_SECRET=your-supabase-jwt-key
12 |
13 | ## Wallet Connect
14 | NEXT_PUBLIC_WC2_PROJECT_ID=your-wallet-connect-project-id
15 |
16 | ## Infura API Key
17 | NEXT_PUBLIC_INFURA_API_KEY=your-infura-api-key
18 |
19 | ## Auth message to validate the web3 signature
20 | NEXT_PUBLIC_WEB3AUTH_MESSAGE=Please sign this message to confirm your identity. Nonce:
21 |
22 | ## Ocean Protocol Token Configuration
23 | NEXT_PUBLIC_WEB3AUTH_TTL=3600
24 | NEXT_PUBLIC_DATATOKEN_ADDRESS=0x2eaa179769d1Db4678Ce5FCD93E29F81aD0C5146
25 | NEXT_PUBLIC_SUBGRAPH_URL=https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph
26 |
27 | ## App Configuration
28 | NEXT_PUBLIC_APP_DOMAN=@yourdomain.com
29 |
--------------------------------------------------------------------------------
/.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 | },
13 | "settings": {
14 | "tailwindcss": {
15 | "callees": ["cn", "cva"],
16 | "config": "tailwind.config.js"
17 | }
18 | },
19 | "overrides": [
20 | {
21 | "files": ["*.ts", "*.tsx"],
22 | "parser": "@typescript-eslint/parser"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # graphql sdk
4 | graphql/sdk.ts
5 |
6 | # dependencies
7 | node_modules
8 | .pnp
9 | .pnp.js
10 |
11 | # testing
12 | coverage
13 |
14 | # next.js
15 | .next/
16 | out/
17 | build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env.local
31 | .env.development.local
32 | .env.test.local
33 | .env.production.local
34 |
35 | # turbo
36 | .turbo
37 |
38 | .contentlayer
39 | .env
40 | .vercel
41 | .vscode
42 |
--------------------------------------------------------------------------------
/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 | Ocean Protocol Tokengated Chatbot
4 |
5 |
6 |
7 | A Web3 powered, Ocean Protocol tokengated, open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Supabase.
8 |
9 |
10 |
11 | Features ·
12 | Web3 Powered ·
13 | Model Providers ·
14 | Security Warning ·
15 | Deploy Vercel App ·
16 | Publish Datatoken ·
17 | Running locally ·
18 | Authors
19 |
20 |
21 |
22 | ## Features
23 |
24 | - Create a Web3 Tokengated app where users gain access by [purchasing a DataNFT](https://market.oceanprotocol.com/asset/did:op:6897592e137286774c718ddcb5c3e5177aba5575d868a2f997ebfa3ffaa5213a) on the [Mumbai Network](#publish-datatoken).
25 | - Sign a Web3 transaction to identify yourself and login via a custom [Supabase Web3Auth](#supabase-web3auth).
26 | - Prompt OpenAI (default), Anthropic, Hugging Face, or custom AI chat models and/or LangChain
27 | - Chat History with [Supabase Postgres DB](https://supabase.com)
28 | - [Next.js](https://nextjs.org) App Router
29 | - React Server Components (RSCs), Suspense, and Server Actions
30 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
31 | - [shadcn/ui](https://ui.shadcn.com)
32 | - Styling with [Tailwind CSS](https://tailwindcss.com)
33 | - [Radix UI](https://radix-ui.com) for headless component primitives
34 | - Icons from [Phosphor Icons](https://phosphoricons.com)
35 | - [Rainbowkit](https://www.rainbowkit.com/) and [Wagmi](https://wagmi.sh/) as wallet providers and React hooks
36 | - [Ethers.js](https://docs.ethers.org/v5/) and [Infura](https://app.infura.io/) for the low-level work
37 |
38 | ## Web3 Powered
39 |
40 | With a few clicks, you can deploy a Web3 enabled, tokengated, AI dApp that uses Ocean Protocol's DataNFT to prove ownership and access of the application.
41 |
42 | What does this do?
43 | 1. Provides users with access by purchasing a DataNFT from the [Ocean Marketplace](https://market.oceanprotocol.com/).
44 | 2. Allows users to login and authorize with an off-chain Web3 signature.
45 |
46 | ## Model Providers
47 |
48 | 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), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code.
49 |
50 | ## Security Warning
51 |
52 | For the Web3 implementation to work, we need to implement **Supabase's service_key** in the app. You can learn more about this by reading the [Supabase Web3Auth](#supabase-web3auth) for more intuition.
53 |
54 | If you are not careful with this as a developer you can easily expose your Supabase's admin role to the user. Please be careful to not expose getServiceSupabase() or NEXT_PUBLIC_SUPABASE_SERVICE_KEY.
55 |
56 | What does this mean?
57 |
58 | **[Never use a service key on the client](https://supabase.com/docs/guides/auth/row-level-security#never-use-a-service-key-on-the-client)**
59 |
60 | ## Deploy Vercel App
61 |
62 | Before hopping into code, let's launch the app and play with it.
63 | 1. Fork this repository: [tokengated-next-chatgpt](https://github.com/oceanprotocol/tokengated-next-chatgpt/) via Github.
64 | 1. Get a new [OpenAI API key](https://platform.openai.com/apps)
65 | 1. Deploy a new [DB in Supabase](https://supabase.com/dashboard/sign-in)
66 | 1. Setup your `public.users` table inside Supabase. We have provided you a screenshot of what ours looks like so you can configure it in the exact same way.
.
67 | 1. Please note that your `public.users.id` should link to your `auth.users.id` record
. **Important:** Make sure you make the `id` column nullable.
68 | 1. Setup your Supabase Row-Level Security (RLS) by executing the [scripts located below](#configure-supabase) inside the Supabase SQL Editor.
69 | 1. Get an [infura API key](https://www.infura.io/)
70 | 1. Get a [Wallet Connect Project ID](https://cloud.walletconnect.com/sign-in)
71 | 1. Hop onto Vercel and [Deploy your forked repository](https://vercel.com/new/) as a new Vercel project and configure your environment variables.
72 | 1. Configure your Vercel->project->settings to rebuild `sdk.ts` by overriding the build command with: `yarn generate && yarn build`
73 | 1. You should now have all the initial ENV_VARS required to deploy the initial version of the app.
74 | 1. Finally, after Vercel is deployed, update your Supabase's Project: [Authentication / URL Configuration / Site URL](https://supabase.com/dashboard/project/) to be your Vercel's app URL.
75 |
76 | ```
77 | OPENAI_API_KEY=your-open-ai-key
78 | NEXT_PUBLIC_SUPABASE_URL=your-supabase-project-url
79 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
80 | NEXT_PUBLIC_SUPABASE_SERVICE_KEY=your-supabase-service-key
81 | NEXT_PUBLIC_SUPABASE_JWT_SECRET=your-supabase-jwt-key
82 | NEXT_PUBLIC_WC2_PROJECT_ID=your-wallet-connect-project-id
83 | NEXT_PUBLIC_INFURA_API_KEY=your-infura-api-key
84 | NEXT_PUBLIC_WEB3AUTH_MESSAGE=Please sign this message to confirm your identity. Nonce:
85 | NEXT_PUBLIC_APP_DOMAN=@yourdomain.com
86 | ```
87 | _Initial Environment Variables required for Vercel app to work_
88 |
89 | ## Configure Supabase
90 | In the SQL Editor, we're going to create the remainder of the role-level security policies we'll need to keep the DB secure.
91 |
92 | You should already have a `public.users` table from the work you did in [Deploy Vercel App](#deploy-vercel-app)
93 |
94 | ```
95 | -- Create view of auth.users and set strict access.
96 | create view public.auth_users as select * from auth.users;
97 | revoke all on public.auth_users from anon, authenticated;
98 |
99 | -- service-role policy
100 | CREATE POLICY service_role_access ON public.users
101 | AS PERMISSIVE FOR ALL
102 | TO service_role
103 | USING (auth.role() = 'service_role')
104 | WITH CHECK (auth.role() = 'service_role');
105 |
106 | -- authenticated user policy
107 | CREATE POLICY authenticated_users_can_write ON public.users
108 | AS PERMISSIVE FOR UPDATE
109 | TO authenticated
110 | USING (auth.role() = 'authenticated')
111 | WITH CHECK (auth.role() = 'authenticated');
112 |
113 | -- web3 auth policy
114 | CREATE POLICY web3_auth ON public.users
115 | AS PERMISSIVE FOR UPDATE
116 | TO authenticated
117 | USING ((current_setting('request.jwt.claims', true))::json ->> 'address' = address)
118 | WITH CHECK ((current_setting('request.jwt.claims', true))::json ->> 'address' = address);
119 | ```
120 |
121 | ## Publish Datatoken
122 |
123 | We recommend using the [Mumbai Testnet](https://www.alchemy.com/overviews/mumbai-testnet) to deploy your datatoken. It will be fast and free.
124 | ```
125 | Network Name: Mumbai Testnet
126 | New RPC URL: https://polygon-mumbai.g.alchemy.com/v2/your-api-key
127 | Chain ID: 80001
128 | Currency Symbol: MATIC
129 | Block Explorer URL: https://mumbai.polygonscan.com/
130 | ```
131 | _The Mumbai network_
132 |
133 | 1. Let's begin by adding the Mumbai network to your wallet.
134 | 1. Now connect your wallet to the Mumbai network.
135 | 1. Now get your wallet `0x address` for later.
136 | 1. We need some tokens to make transactions, [collect MATIC from this faucet](https://mumbaifaucet.com/) so we can create the Data token.
137 | 1. Make sure to also [collect OCEAN from this faucet](https://faucet.mumbai.oceanprotocol.com/) so you can also buy some tokens.
138 | 1. Deploy a Datatoken (DT) inside the [OCEAN marketplace](https://market.oceanprotocol.com/). On Step-2, select File-type "URL" and use the Vercel url as the address so you can complete the wizard (this architecture doesn't use it). You can now see your datatoken, copy the `0x address`.
139 | 1. You have now published a Datatoken. When a user purchases this, they will gain access to our application. So, let's make sure to buy one so we can obtain access to the app after we deploy it.
140 |
141 |
142 | ### Complete Vercel Configuration
143 |
144 | You can now complete configuring the Vercel app.
145 |
146 | Go back to your Vercel->project->settings->Environment Variables and add the rest of them.
147 | ```
148 | NEXT_PUBLIC_WEB3AUTH_TTL=3600
149 | NEXT_PUBLIC_DATATOKEN_ADDRESS=0x2eaa179769d1Db4678Ce5FCD93E29F81aD0C5146
150 | NEXT_PUBLIC_SUBGRAPH_URL=https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph
151 | ```
152 | _Ocean Protocol and Datatoken Environment Variables_
153 |
154 | User subscriptions are verified at login based on when the Datatoken was purchased + TTL. Users are only authorized to prompt until the subscription expires.
155 |
156 | ## Running locally
157 |
158 | **Before you start,** make sure you have followed every step from [Deploy Vercel App](#deploy-vercel-app) so your application can be configured correctly.
159 |
160 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run OP's Tokengated AI Chatbot.
161 |
162 | Copy the `.env.example` file and populate the required env vars:
163 |
164 | ```bash
165 | cp .env.example .env
166 | ```
167 |
168 | [Install the Supabase CLI](https://supabase.com/docs/guides/cli) and start the local Supabase stack:
169 |
170 | ```bash
171 | npm install supabase --save-dev
172 | npx supabase start
173 | ```
174 |
175 | Install the local dependencies and start dev mode:
176 |
177 | ```bash
178 | pnpm install
179 | pnpm dev
180 | ```
181 |
182 | Your app template should now be running on [localhost:3000](http://localhost:3000/).
183 |
184 | ## Building GQL SDK
185 |
186 | Vercel currently does not support `graphql-generate` as part of the build, so you'll have to do this ahead of time.
187 |
188 | As you write more GQL, please run the `yarn generate` command to update your local GQL library and SDK. This will help you maintain good code and avoid type safety issues.
189 |
190 | You can then add the newly built SDK before deploying a new Vercel Build.
191 | ```
192 | yarn generate
193 | git add .
194 | git commit -m "updating gql"
195 | git push
196 | ```
197 |
198 | ## Additional Readmes
199 |
200 | The following READMEs have been created to provide guidance to the reader.
201 | 1. [Improvements and Ideas](/docs/README_improvements_ideas.md)
202 | 1. [Supabase Web3Auth](/docs/README_web3_auth_supabase.md)
203 | 1. [Web3 Tokens and Networks](/docs/README_web3_tokens_and_networks.md)
204 |
205 | ## Authors
206 |
207 | This scaffolding is an extension of the fantastic [Vercel AI-Chatbot](https://github.com/supabase-community/vercel-ai-chatbot) project.
208 |
209 | Ocean Protocol ([@oceanprotocol](https://twitter.com/oceanprotocol)) has provided the work to build a custom Web3 Auth on top of Supabase, token-gated access with a DataNFT, and to provide a Web3 scaffolding to create AI dApps.
210 |
211 | Special thanks to [@kdetry](https://github.com/kdetry) and [@idiom-bytes](https://github.com/idiom-bytes) for assembling this.
212 |
213 | No sealife was harmed in the making of this repository.
214 |
--------------------------------------------------------------------------------
/app/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
4 | import { cookies } from 'next/headers'
5 | import { Database } from '@/lib/db_types'
6 | import { revalidatePath } from 'next/cache'
7 | import { redirect } from 'next/navigation'
8 |
9 | import { type Chat } from '@/lib/types'
10 | import { auth } from '@/auth'
11 |
12 | const supabase = createServerActionClient({ cookies })
13 |
14 | export async function getChats(userId?: string | null) {
15 | if (!userId) {
16 | return []
17 | }
18 |
19 | try {
20 | const { data } = await supabase
21 | .from('chats')
22 | .select('payload')
23 | .order('payload->createdAt', { ascending: false })
24 | .throwOnError()
25 |
26 | return (data?.map(entry => entry.payload) as Chat[]) ?? []
27 | } catch (error) {
28 | return []
29 | }
30 | }
31 |
32 | export async function getChat(id: string) {
33 | const { data } = await supabase
34 | .from('chats')
35 | .select('payload')
36 | .eq('id', id)
37 | .maybeSingle()
38 |
39 | return (data?.payload as Chat) ?? null
40 | }
41 |
42 | export async function removeChat({ id, path }: { id: string; path: string }) {
43 | try {
44 | await supabase.from('chats').delete().eq('id', id).throwOnError()
45 |
46 | revalidatePath('/')
47 | return revalidatePath(path)
48 | } catch (error) {
49 | return {
50 | error: 'Unauthorized'
51 | }
52 | }
53 | }
54 |
55 | export async function clearChats() {
56 | try {
57 | const user = await auth()
58 | await supabase
59 | .from('chats')
60 | .delete()
61 | .eq('user_id', user?.id)
62 | .throwOnError()
63 | revalidatePath('/')
64 | return redirect('/')
65 | } catch (error) {
66 | console.log('clear chats error', error)
67 | return {
68 | error: 'Unauthorized'
69 | }
70 | }
71 | }
72 |
73 | export async function getSharedChat(id: string) {
74 | const { data } = await supabase
75 | .from('chats')
76 | .select('payload')
77 | .eq('id', id)
78 | .not('payload->sharePath', 'is', null)
79 | .maybeSingle()
80 |
81 | return (data?.payload as Chat) ?? null
82 | }
83 |
84 | export async function shareChat(chat: Chat) {
85 | const payload = {
86 | ...chat,
87 | sharePath: `/share/${chat.id}`
88 | }
89 |
90 | await supabase
91 | .from('chats')
92 | .update({ payload: payload as any })
93 | .eq('id', chat.id)
94 | .throwOnError()
95 |
96 | return payload
97 | }
98 |
--------------------------------------------------------------------------------
/app/api/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
2 | import { cookies } from 'next/headers'
3 | import { NextResponse } from 'next/server'
4 |
5 | export async function GET(request: Request) {
6 | // The `/auth/callback` route is required for the server-side auth flow implemented
7 | // by the Auth Helpers package. It exchanges an auth code for the user's session.
8 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
9 | const requestUrl = new URL(request.url)
10 | const code = requestUrl.searchParams.get('code')
11 |
12 | if (code) {
13 | const supabase = createRouteHandlerClient({ cookies })
14 | await supabase.auth.exchangeCodeForSession(code)
15 | }
16 |
17 | // URL to redirect to after sign in process completes
18 | return NextResponse.redirect(requestUrl.origin)
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { OpenAIStream, StreamingTextResponse } from 'ai'
2 | import { Configuration, OpenAIApi } from 'openai-edge'
3 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
4 | import { cookies } from 'next/headers'
5 | import { Database } from '@/lib/db_types'
6 |
7 | import { auth } from '@/auth'
8 | import { nanoid } from '@/lib/utils'
9 |
10 | export const runtime = 'nodejs'
11 |
12 | const configuration = new Configuration({
13 | apiKey: process.env.OPENAI_API_KEY
14 | })
15 |
16 | const openai = new OpenAIApi(configuration)
17 |
18 | export async function POST(req: Request) {
19 | const supabase = createRouteHandlerClient({ cookies })
20 | const json = await req.json()
21 | const { messages, previewToken } = json
22 | const user = await auth()
23 | const userId = user?.id
24 |
25 | if (!userId) {
26 | return new Response('Unauthorized', {
27 | status: 401
28 | })
29 | }
30 |
31 | if (previewToken) {
32 | configuration.apiKey = previewToken
33 | }
34 |
35 | const res = await openai.createChatCompletion({
36 | model: 'gpt-3.5-turbo',
37 | messages,
38 | temperature: 0.7,
39 | stream: true
40 | })
41 |
42 | const stream = OpenAIStream(res, {
43 | async onCompletion(completion) {
44 | const title = json.messages[0].content.substring(0, 100)
45 | const id = json.id ?? nanoid()
46 | const createdAt = Date.now()
47 | const path = `/chat/${id}`
48 | const payload = {
49 | id,
50 | title,
51 | userId,
52 | createdAt,
53 | path,
54 | messages: [
55 | ...messages,
56 | {
57 | content: completion,
58 | role: 'assistant'
59 | }
60 | ]
61 | }
62 | // Insert chat into database.
63 | await supabase.from('chats').upsert({ id, payload }).throwOnError()
64 | }
65 | })
66 |
67 | return new StreamingTextResponse(stream)
68 | }
69 |
--------------------------------------------------------------------------------
/app/api/web3auth/login/route.ts:
--------------------------------------------------------------------------------
1 | import { OrderDirection, Order_OrderBy } from '@/graphql/sdk'
2 |
3 | import { NextResponse } from 'next/server'
4 | import { ethers } from 'ethers'
5 | import { getServiceRoleServerSupabaseClient } from '@/supabase/functions/client'
6 | import { signToken } from '@/lib/utils'
7 | import { subgraphSDK } from '@/graphql/graphqlClient'
8 |
9 | export async function POST(req: Request) {
10 | const srSupabase = getServiceRoleServerSupabaseClient()
11 | const json = await req.json()
12 | const { address, signedMessage, nonce } = json
13 |
14 | // 1. Verify the signed message matches the requested address
15 | const message = process.env.NEXT_PUBLIC_WEB3AUTH_MESSAGE + nonce
16 | const recoveredAddress = ethers.utils.verifyMessage(message, signedMessage)
17 |
18 | if (recoveredAddress !== address) {
19 | return NextResponse.json(
20 | { error: 'Signature verification failed' },
21 | { status: 401 }
22 | )
23 | }
24 |
25 | try {
26 | // 2. Select * from public.user table to get nonce
27 | const { data: user, error: userError } = await srSupabase
28 | .from('users')
29 | .select('*')
30 | .eq('address', address)
31 | .single()
32 |
33 | if (user && !userError) {
34 | // 3. Verify the nonce included in the request matches what's already in public.users table for that address
35 | if (user?.auth.genNonce !== nonce) {
36 | return NextResponse.json(
37 | { error: 'Nonce verification failed' },
38 | { status: 401 }
39 | )
40 | }
41 |
42 | let finalAuthUser = null
43 | // 2. Select * from public.auth_users view where address matches
44 | const { data: authUser, error: authUserError } = await srSupabase
45 | .from('auth_users')
46 | .select('*')
47 | .eq('raw_user_meta_data->>address', address)
48 | .single()
49 |
50 | if (!authUser || authUserError) {
51 | // 4. If there's no auth.users.id for that address
52 | const { data: newUser, error: newUserError } =
53 | await srSupabase.auth.admin.createUser({
54 | email: address + process.env.NEXT_PUBLIC_APP_DOMAN,
55 | user_metadata: { address: address },
56 | email_confirm: true
57 | })
58 |
59 | if (newUserError || !newUser) {
60 | return NextResponse.json(
61 | { error: 'Failed to create auth user' },
62 | { status: 500 }
63 | )
64 | }
65 |
66 | // response object is different from auth.users view
67 | finalAuthUser = newUser.user
68 | } else {
69 | // selection from auth.users view is the user, assign
70 | finalAuthUser = authUser
71 | }
72 |
73 | // 5. Update public.users.id with auth.users.id
74 | await srSupabase
75 | .from('users')
76 | .update([
77 | {
78 | id: finalAuthUser?.id,
79 | auth: {
80 | genNonce: nonce,
81 | lastAuth: new Date().toISOString(),
82 | lastAuthStatus: 'success'
83 | }
84 | }
85 | ])
86 | .eq('address', address)
87 | .select()
88 |
89 | const ttl = await getTTLofUser(address)
90 |
91 | if (!ttl) {
92 | return NextResponse.json({ error: 'Token expired' }, { status: 401 })
93 | }
94 |
95 | // 6. We sign the token and return it to client
96 | const token = await signToken(
97 | {
98 | address: address,
99 | sub: user.id,
100 | aud: 'authenticated'
101 | },
102 | { expiresIn: `${ttl}s` }
103 | )
104 |
105 | const response = NextResponse.json('success', { status: 200 })
106 | response.cookies.set('address', address)
107 | response.cookies.set('web3jwt', token)
108 | return response
109 | }
110 |
111 | return NextResponse.json(
112 | { error: userError?.message || 'Internal Server Error' },
113 | { status: 500 }
114 | )
115 | } catch (error: any) {
116 | return NextResponse.json(
117 | { error: error?.message || 'Internal Server Error' },
118 | { status: 500 }
119 | )
120 | }
121 | }
122 |
123 | const getTTLofUser = async (address: string) => {
124 | const ttl = process.env.NEXT_PUBLIC_WEB3AUTH_TTL
125 | if (!ttl) {
126 | throw new Error('NEXT_PUBLIC_WEB3AUTH_TTL is not set')
127 | }
128 |
129 | const userOnChain = await subgraphSDK.GetUser({
130 | id: address.toLowerCase()
131 | })
132 |
133 | if (userOnChain?.user?.id) {
134 | const datatoken =
135 | process.env.NEXT_PUBLIC_DATATOKEN_ADDRESS?.toLowerCase() || ''
136 | const orders = await subgraphSDK.GetOrders({
137 | where: {
138 | consumer: userOnChain.user.id,
139 | datatoken: datatoken
140 | },
141 | orderBy: Order_OrderBy.CreatedTimestamp,
142 | orderDirection: OrderDirection.Desc
143 | })
144 |
145 | if (orders?.orders?.length > 0) {
146 | const createdTimestamp = orders?.orders[0].createdTimestamp
147 | //add 1 day to the timestamp
148 | const expiryTimestamp = createdTimestamp + 86400
149 | const now = Math.floor(Date.now() / 1000)
150 | if (now > expiryTimestamp) {
151 | return 0
152 | }
153 |
154 | return expiryTimestamp - now
155 | }
156 | return 0
157 | }
158 | return 0
159 | }
160 |
--------------------------------------------------------------------------------
/app/api/web3auth/nonce/route.ts:
--------------------------------------------------------------------------------
1 | import { getServiceRoleServerSupabaseClient } from '@/supabase/functions/client'
2 | import { NextResponse } from 'next/server'
3 | import { auth } from '@/auth'
4 |
5 | export async function POST(req: Request) {
6 | const srSupabase = getServiceRoleServerSupabaseClient()
7 | const json = await req.json()
8 | const { address } = json
9 | const userId = (await auth())?.id
10 |
11 | if (userId) {
12 | return new Response('Unauthorized', {status: 401})
13 | }
14 |
15 | try {
16 | const nonce = Math.floor(Math.random() * 1000000);
17 | let { data, error } = await srSupabase
18 | .from('users')
19 | .select('*')
20 | .eq('address', address)
21 | .single()
22 |
23 | if (!data || error) {
24 | const { data: user, error: upsertError } = await srSupabase
25 | .from('users')
26 | .upsert([
27 | {
28 | address: address,
29 | auth: {
30 | genNonce: nonce,
31 | lastAuth: new Date().toISOString(),
32 | lastAuthStatus: "pending"
33 | },
34 | }
35 | ])
36 | .select()
37 |
38 | if (data || !upsertError) {
39 | return NextResponse.json({ user }, { status: 200 })
40 | }
41 | throw new Error("Failed to create user")
42 | } else {
43 | const { data: user, error: updateError } = await srSupabase
44 | .from('users')
45 | .update([
46 | {
47 | auth: {
48 | genNonce: nonce,
49 | lastAuth: new Date().toISOString(),
50 | lastAuthStatus: "pending"
51 | },
52 | }
53 | ])
54 | .eq('address', address)
55 | .select()
56 |
57 | if (data || !updateError) {
58 | return NextResponse.json({ user }, { status: 200 })
59 | }
60 | throw new Error("Failed to update user")
61 | }
62 | } catch (error) {
63 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
64 | }
65 | }
--------------------------------------------------------------------------------
/app/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 } from '@/app/actions'
6 | import { Chat } from '@/components/chat'
7 |
8 | export const runtime = 'nodejs'
9 | export const preferredRegion = 'home'
10 |
11 | export interface ChatPageProps {
12 | params: {
13 | id: string
14 | }
15 | }
16 |
17 | export async function generateMetadata({
18 | params
19 | }: ChatPageProps): Promise {
20 | const user = await auth()
21 |
22 | if (!user) {
23 | return {}
24 | }
25 |
26 | const chat = await getChat(params.id)
27 | return {
28 | title: chat?.title.toString().slice(0, 50) ?? 'Chat'
29 | }
30 | }
31 |
32 | export default async function ChatPage({ params }: ChatPageProps) {
33 | const user = await auth()
34 |
35 | if (!user) {
36 | redirect(`/sign-in?next=/chat/${params.id}`)
37 | }
38 |
39 | const chat = await getChat(params.id)
40 |
41 | if (!chat) {
42 | notFound()
43 | }
44 |
45 | if (chat?.userId !== user?.id) {
46 | notFound()
47 | }
48 |
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/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 | --muted: 240 4.8% 95.9%;
11 | --muted-foreground: 240 3.8% 46.1%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 240 10% 3.9%;
18 |
19 | --border: 240 5.9% 90%;
20 | --input: 240 5.9% 90%;
21 |
22 | --primary: 240 5.9% 10%;
23 | --primary-foreground: 0 0% 98%;
24 |
25 | --secondary: 240 4.8% 95.9%;
26 | --secondary-foreground: 240 5.9% 10%;
27 |
28 | --accent: 240 4.8% 95.9%;
29 | --accent-foreground: ;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 0 0% 98%;
33 |
34 | --ring: 240 5% 64.9%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 240 10% 3.9%;
41 | --foreground: 0 0% 98%;
42 |
43 | --muted: 240 3.7% 15.9%;
44 | --muted-foreground: 240 5% 64.9%;
45 |
46 | --popover: 240 10% 3.9%;
47 | --popover-foreground: 0 0% 98%;
48 |
49 | --card: 240 10% 3.9%;
50 | --card-foreground: 0 0% 98%;
51 |
52 | --border: 240 3.7% 15.9%;
53 | --input: 240 3.7% 15.9%;
54 |
55 | --primary: 0 0% 98%;
56 | --primary-foreground: 240 5.9% 10%;
57 |
58 | --secondary: 240 3.7% 15.9%;
59 | --secondary-foreground: 0 0% 98%;
60 |
61 | --accent: 240 3.7% 15.9%;
62 | --accent-foreground: ;
63 |
64 | --destructive: 0 62.8% 30.6%;
65 | --destructive-foreground: 0 85.7% 97.3%;
66 |
67 | --ring: 240 3.7% 15.9%;
68 | }
69 | }
70 |
71 | @layer base {
72 | * {
73 | @apply border-border;
74 | }
75 | body {
76 | @apply bg-background text-foreground;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next'
2 |
3 | import { Toaster } from 'react-hot-toast'
4 |
5 | import '@/app/globals.css'
6 | import '@rainbow-me/rainbowkit/styles.css';
7 | import { fontMono, fontSans } from '@/lib/fonts'
8 | import { cn } from '@/lib/utils'
9 | import { TailwindIndicator } from '@/components/tailwind-indicator'
10 | import { Providers } from '@/components/providers'
11 | import { Header } from '@/components/header'
12 |
13 | export const metadata: Metadata = {
14 | title: {
15 | default: 'Next.js AI Chatbot',
16 | template: `%s - Next.js AI Chatbot`
17 | },
18 | description: 'An AI-powered chatbot template built with Next.js and Vercel.',
19 | themeColor: [
20 | { media: '(prefers-color-scheme: light)', color: 'white' },
21 | { media: '(prefers-color-scheme: dark)', color: 'black' }
22 | ],
23 | icons: {
24 | icon: '/favicon.ico',
25 | shortcut: '/favicon-16x16.png',
26 | apple: '/apple-touch-icon.png'
27 | }
28 | }
29 |
30 | interface RootLayoutProps {
31 | children: React.ReactNode
32 | }
33 |
34 | export default function RootLayout({ children }: RootLayoutProps) {
35 | return (
36 |
37 |
38 |
45 |
46 |
47 |
48 | {/* @ts-ignore */}
49 |
50 | {children}
51 |
52 |
53 |
54 |
55 |
56 | )
57 | }
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/app/opengraph-image.png
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from '@/lib/utils'
2 | import { Chat } from '@/components/chat'
3 |
4 | export const runtime = 'nodejs'
5 |
6 | export default function IndexPage() {
7 | const id = nanoid()
8 |
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/app/share/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next'
2 | import { notFound } 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 |
9 | export const runtime = 'nodejs'
10 | export const preferredRegion = 'home'
11 |
12 | interface SharePageProps {
13 | params: {
14 | id: string
15 | }
16 | }
17 |
18 | export async function generateMetadata({
19 | params
20 | }: SharePageProps): Promise {
21 | const chat = await getSharedChat(params.id)
22 |
23 | return {
24 | title: chat?.title.slice(0, 50) ?? 'Chat'
25 | }
26 | }
27 |
28 | export default async function SharePage({ params }: SharePageProps) {
29 | const chat = await getSharedChat(params.id)
30 |
31 | if (!chat || !chat?.sharePath) {
32 | notFound()
33 | }
34 |
35 | return (
36 | <>
37 |
38 |
39 |
40 |
41 |
{chat.title}
42 |
43 | {formatDate(chat.createdAt)} · {chat.messages.length} messages
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | >
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/app/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from '@/auth'
2 | import { LoginButtonMetamask } from '@/components/login-button-metamask'
3 | import { redirect } from 'next/navigation'
4 |
5 | export default async function SignInPage() {
6 | const user = await auth()
7 |
8 | // redirect to home if user is already logged in
9 | if (user) {
10 | redirect('/')
11 | }
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/app/twitter-image.png
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/assets/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/assets/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import { jsonResponse, verifyToken } from '@/lib/utils'
2 |
3 | import { cookies } from 'next/headers'
4 | import { createClient } from '@supabase/supabase-js'
5 | import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
6 |
7 | export const auth = async () => {
8 | const address = cookies().get('address')?.value || ''
9 | const web3jwt = cookies().get('web3jwt')?.value || ''
10 |
11 | const validToken = await verifyToken(web3jwt, address)
12 | if (web3jwt && validToken) {
13 | const url = process.env.NEXT_PUBLIC_SUPABASE_URL || ''
14 | const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
15 | const headers = {
16 | global: {
17 | headers: { Authorization: `Bearer ${web3jwt}` }
18 | },
19 | auth: { persistSession: false }
20 | }
21 |
22 | const supabase = createClient(url, anonKey, headers)
23 |
24 | const {
25 | data: { user }
26 | } = await supabase.auth.getUser()
27 | console.log('user: ', user)
28 |
29 | if (user) {
30 | return user
31 | }
32 | } else {
33 | if (address) cookies().delete('address')
34 | if (web3jwt) cookies().delete('web3jwt')
35 | }
36 |
37 | return null
38 | }
39 |
--------------------------------------------------------------------------------
/codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: 'https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph'
3 | documents: 'graphql/operations/**/*.graphql'
4 | generates:
5 | graphql/schema.graphql:
6 | plugins:
7 | - 'schema-ast'
8 | graphql/sdk.ts:
9 | plugins:
10 | - typescript
11 | - typescript-operations
12 | - typescript-graphql-request
13 | config:
14 | add:
15 | content: '/* eslint-disable */'
--------------------------------------------------------------------------------
/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 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/chat-list.tsx:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | import { Separator } from '@/components/ui/separator'
4 | import { ChatMessage } from '@/components/chat-message'
5 |
6 | export interface ChatList {
7 | messages: Message[]
8 | }
9 |
10 | export function ChatList({ messages }: ChatList) {
11 | if (!messages.length) {
12 | return null
13 | }
14 |
15 | return (
16 |
17 | {messages.map((message, index) => (
18 |
19 |
20 | {index < messages.length - 1 && (
21 |
22 | )}
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/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 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | import { Message } from 'ai'
2 | import remarkGfm from 'remark-gfm'
3 | import remarkMath from 'remark-math'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { CodeBlock } from '@/components/ui/codeblock'
7 | import { MemoizedReactMarkdown } from '@/components/markdown'
8 | import { IconOpenAI, IconUser } from '@/components/ui/icons'
9 | import { ChatMessageActions } from '@/components/chat-message-actions'
10 |
11 | export interface ChatMessageProps {
12 | message: Message
13 | }
14 |
15 | export function ChatMessage({ message, ...props }: ChatMessageProps) {
16 | return (
17 |
21 |
29 | {message.role === 'user' ? : }
30 |
31 |
32 | {children}
38 | },
39 | code({ node, inline, className, children, ...props }) {
40 | if (children.length) {
41 | if (children[0] == '▍') {
42 | return (
43 | ▍
44 | )
45 | }
46 |
47 | children[0] = (children[0] as string).replace('`▍`', '▍')
48 | }
49 |
50 | const match = /language-(\w+)/.exec(className || '')
51 |
52 | if (inline) {
53 | return (
54 |
55 | {children}
56 |
57 | )
58 | }
59 |
60 | return (
61 |
67 | )
68 | }
69 | }}
70 | >
71 | {message.content}
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/components/chat-panel.tsx:
--------------------------------------------------------------------------------
1 | import { type UseChatHelpers } from 'ai/react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { PromptForm } from '@/components/prompt-form'
5 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
6 | import { IconRefresh, IconStop } from '@/components/ui/icons'
7 | import { FooterText } from '@/components/footer'
8 |
9 | export interface ChatPanelProps
10 | extends Pick<
11 | UseChatHelpers,
12 | | 'append'
13 | | 'isLoading'
14 | | 'reload'
15 | | 'messages'
16 | | 'stop'
17 | | 'input'
18 | | 'setInput'
19 | > {
20 | id?: string
21 | }
22 |
23 | export function ChatPanel({
24 | id,
25 | isLoading,
26 | stop,
27 | append,
28 | reload,
29 | input,
30 | setInput,
31 | messages
32 | }: ChatPanelProps) {
33 | return (
34 |
35 |
36 |
37 |
38 | {isLoading ? (
39 |
47 | ) : (
48 | messages?.length > 0 && (
49 |
57 | )
58 | )}
59 |
60 |
61 |
{
63 | await append({
64 | id,
65 | content: value,
66 | role: 'user'
67 | })
68 | }}
69 | input={input}
70 | setInput={setInput}
71 | isLoading={isLoading}
72 | />
73 |
74 |
75 |
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/components/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useInView } from 'react-intersection-observer'
5 |
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 |
8 | interface ChatScrollAnchorProps {
9 | trackVisibility?: boolean
10 | }
11 |
12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
13 | const isAtBottom = useAtBottom()
14 | const { ref, entry, inView } = useInView({
15 | trackVisibility,
16 | delay: 100,
17 | rootMargin: '0px 0px -150px 0px'
18 | })
19 |
20 | React.useEffect(() => {
21 | if (isAtBottom && trackVisibility && !inView) {
22 | entry?.target.scrollIntoView({
23 | block: 'start'
24 | })
25 | }
26 | }, [inView, entry, isAtBottom, trackVisibility])
27 |
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/components/chat.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useChat, type Message } from 'ai/react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { ChatList } from '@/components/chat-list'
7 | import { ChatPanel } from '@/components/chat-panel'
8 | import { EmptyScreen } from '@/components/empty-screen'
9 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
10 | import { useLocalStorage } from '@/lib/hooks/use-local-storage'
11 | import {
12 | Dialog,
13 | DialogContent,
14 | DialogDescription,
15 | DialogFooter,
16 | DialogHeader,
17 | DialogTitle
18 | } from '@/components/ui/dialog'
19 | import { useState } from 'react'
20 | import { Button } from './ui/button'
21 | import { Input } from './ui/input'
22 | import { toast } from 'react-hot-toast'
23 |
24 | const IS_PREVIEW = process.env.VERCEL_ENV === 'preview'
25 | export interface ChatProps extends React.ComponentProps<'div'> {
26 | initialMessages?: Message[]
27 | id?: string
28 | }
29 |
30 | export function Chat({ id, initialMessages, className }: ChatProps) {
31 | const [previewToken, setPreviewToken] = useLocalStorage(
32 | 'ai-token',
33 | null
34 | )
35 | const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW)
36 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '')
37 | const { messages, append, reload, stop, isLoading, input, setInput } =
38 | useChat({
39 | initialMessages,
40 | id,
41 | body: {
42 | id,
43 | previewToken
44 | },
45 | onResponse(response) {
46 | if (response.status === 401) {
47 | toast.error(response.statusText)
48 | }
49 | }
50 | })
51 | return (
52 | <>
53 |
54 | {messages.length ? (
55 | <>
56 |
57 |
58 | >
59 | ) : (
60 |
61 | )}
62 |
63 |
73 |
74 |
109 | >
110 | )
111 | }
112 |
--------------------------------------------------------------------------------
/components/clear-history.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
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 | clearChats: () => ServerActionResult
24 | }
25 |
26 | export function ClearHistory({ clearChats }: ClearHistoryProps) {
27 | const [open, setOpen] = React.useState(false)
28 | const [isPending, startTransition] = React.useTransition()
29 | const router = useRouter()
30 |
31 | return (
32 |
33 |
34 |
38 |
39 |
40 |
41 | Are you absolutely sure?
42 |
43 | This will permanently delete your chat history and remove your data
44 | from our servers.
45 |
46 |
47 |
48 | Cancel
49 | {
52 | event.preventDefault()
53 | startTransition(async () => {
54 | const result = await clearChats()
55 |
56 | if (result && 'error' in result) {
57 | toast.error(result.error)
58 | return
59 | }
60 |
61 | setOpen(false)
62 | router.push('/')
63 | })
64 | }}
65 | >
66 | {isPending && }
67 | Delete
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/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 | const exampleMessages = [
8 | {
9 | heading: 'Explain technical concepts',
10 | message: `What is a "serverless function"?`
11 | },
12 | {
13 | heading: 'Summarize an article',
14 | message: 'Summarize the following article for a 2nd grader: \n'
15 | },
16 | {
17 | heading: 'Draft an email',
18 | message: `Draft an email to my boss about the following: \n`
19 | }
20 | ]
21 |
22 | export function EmptyScreen({ setInput }: Pick) {
23 | return (
24 |
25 |
26 |
27 | Welcome to the Supabaseified Next.js AI Chatbot!
28 |
29 |
30 | This is an open source AI chatbot app template built with{' '}
31 | Next.js and{' '}
32 | Supabase.
33 |
34 |
35 | You can start a conversation here or try the following examples:
36 |
37 |
38 | {exampleMessages.map((message, index) => (
39 |
48 | ))}
49 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/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 |
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 | Next.js and{' '}
17 | Supabase.
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { Button, buttonVariants } from '@/components/ui/button'
4 | import {
5 | IconNextChat,
6 | IconSeparator,
7 | } from '@/components/ui/icons'
8 |
9 | import { ClearHistory } from '@/components/clear-history'
10 | import Link from 'next/link'
11 | import { Profile } from '@/components/profile'
12 | import { Sidebar } from '@/components/sidebar'
13 | import { SidebarFooter } from '@/components/sidebar-footer'
14 | import { SidebarList } from '@/components/sidebar-list'
15 | import { ThemeToggle } from '@/components/theme-toggle'
16 | import { UserMenu } from '@/components/user-menu'
17 | import { auth } from '@/auth'
18 | import { clearChats } from '@/app/actions'
19 |
20 | export async function Header() {
21 | const user = await auth()
22 |
23 | return (
24 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/components/login-button-metamask.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useEffect } from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { Button, type ButtonProps } from '@/components/ui/button'
7 | import { IconMetamask, IconSpinner } from '@/components/ui/icons'
8 |
9 | import { useRouter } from 'next/navigation'
10 |
11 | import { useAccount } from 'wagmi'
12 |
13 | import { signMessage } from '@wagmi/core'
14 |
15 | interface LoginButtonProps extends ButtonProps {
16 | showIcon?: boolean
17 | text?: string
18 | }
19 |
20 | export function LoginButtonMetamask({
21 | text = 'Login with Metamask',
22 | showIcon = true,
23 | className,
24 | ...props
25 | }: LoginButtonProps) {
26 | const [isLoading, setIsLoading] = useState(false)
27 | const [isAuthenticated, setIsAuthenticated] = useState(false)
28 | const router = useRouter()
29 |
30 | const { address, isConnected } = useAccount()
31 | // const { chains } = useNetwork();
32 |
33 | useEffect(() => {
34 | if (isAuthenticated) {
35 | console.log('User is authenticated, redirecting to home page')
36 | router.refresh()
37 | router.push('/')
38 | }
39 | }, [isAuthenticated, router])
40 |
41 | return (
42 |
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/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/profile.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ConnectButton } from '@rainbow-me/rainbowkit';
4 |
5 | export function Profile() {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/components/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Link from 'next/link'
3 | import Textarea from 'react-textarea-autosize'
4 | import { UseChatHelpers } from 'ai/react'
5 |
6 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
7 | import { cn } from '@/lib/utils'
8 | import { Button, buttonVariants } from '@/components/ui/button'
9 | import {
10 | Tooltip,
11 | TooltipContent,
12 | TooltipTrigger
13 | } from '@/components/ui/tooltip'
14 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
15 |
16 | export interface PromptProps
17 | extends Pick {
18 | onSubmit: (value: string) => Promise
19 | isLoading: boolean
20 | }
21 |
22 | export function PromptForm({
23 | onSubmit,
24 | input,
25 | setInput,
26 | isLoading
27 | }: PromptProps) {
28 | const { formRef, onKeyDown } = useEnterSubmit()
29 | const inputRef = React.useRef(null)
30 |
31 | React.useEffect(() => {
32 | if (inputRef.current) {
33 | inputRef.current.focus()
34 | }
35 | }, [])
36 |
37 | return (
38 |
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/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 { TooltipProvider } from '@/components/ui/tooltip'
7 |
8 | import {
9 | RainbowKitProvider,
10 | getDefaultWallets,
11 | connectorsForWallets,
12 | } from '@rainbow-me/rainbowkit';
13 | import {
14 | argentWallet,
15 | trustWallet,
16 | ledgerWallet,
17 | } from '@rainbow-me/rainbowkit/wallets';
18 | import {
19 | configureChains,
20 | createConfig,
21 | WagmiConfig
22 | } from 'wagmi';
23 | import {
24 | mainnet,
25 | polygon,
26 | goerli,
27 | } from 'wagmi/chains';
28 | import { publicProvider } from 'wagmi/providers/public';
29 |
30 | const { chains, publicClient, webSocketPublicClient } = configureChains(
31 | [
32 | mainnet,
33 | polygon,
34 | ...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === 'true' ? [goerli] : []),
35 | ],
36 | [publicProvider()]
37 | );
38 |
39 | const projectId = process.env.NEXT_PUBLIC_WC2_PROJECT_ID || '';
40 |
41 | const { wallets } = getDefaultWallets({
42 | appName: 'RainbowKit demo',
43 | projectId,
44 | chains,
45 | });
46 |
47 | const demoAppInfo = {
48 | appName: 'Rainbowkit Demo',
49 | };
50 |
51 | const connectors = connectorsForWallets([
52 | ...wallets,
53 | {
54 | groupName: 'Other',
55 | wallets: [
56 | argentWallet({ projectId, chains }),
57 | trustWallet({ projectId, chains }),
58 | ledgerWallet({ projectId, chains }),
59 | ],
60 | },
61 | ]);
62 |
63 | const wagmiConfig = createConfig({
64 | autoConnect: true,
65 | connectors,
66 | publicClient,
67 | webSocketPublicClient,
68 | });
69 |
70 | export function Providers({ children, ...props }: ThemeProviderProps) {
71 | const [mounted, setMounted] = React.useState(false);
72 | React.useEffect(() => setMounted(true), []);
73 |
74 | return (
75 |
76 |
77 |
78 |
79 | {mounted && children}
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/components/sidebar-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
6 |
7 | import { type Chat, ServerActionResult } from '@/lib/types'
8 | import { cn, formatDate } from '@/lib/utils'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle
18 | } from '@/components/ui/alert-dialog'
19 | import { Button } from '@/components/ui/button'
20 | import {
21 | Dialog,
22 | DialogContent,
23 | DialogDescription,
24 | DialogFooter,
25 | DialogHeader,
26 | DialogTitle
27 | } from '@/components/ui/dialog'
28 | import {
29 | IconShare,
30 | IconSpinner,
31 | IconTrash,
32 | IconUsers
33 | } from '@/components/ui/icons'
34 | import Link from 'next/link'
35 | import { badgeVariants } from '@/components/ui/badge'
36 | import {
37 | Tooltip,
38 | TooltipContent,
39 | TooltipTrigger
40 | } from '@/components/ui/tooltip'
41 |
42 | interface SidebarActionsProps {
43 | chat: Chat
44 | removeChat: (args: { id: string; path: string }) => ServerActionResult
45 | shareChat: (chat: Chat) => ServerActionResult
46 | }
47 |
48 | export function SidebarActions({
49 | chat,
50 | removeChat,
51 | shareChat
52 | }: SidebarActionsProps) {
53 | const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
54 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
55 | const [isRemovePending, startRemoveTransition] = React.useTransition()
56 | const [isSharePending, startShareTransition] = React.useTransition()
57 | const router = useRouter()
58 |
59 | const copyShareLink = React.useCallback(async (chat: Chat) => {
60 | if (!chat.sharePath) {
61 | return toast.error('Could not copy share link to clipboard')
62 | }
63 |
64 | const url = new URL(window.location.href)
65 | url.pathname = chat.sharePath
66 | navigator.clipboard.writeText(url.toString())
67 | setShareDialogOpen(false)
68 | toast.success('Share link copied to clipboard', {
69 | style: {
70 | borderRadius: '10px',
71 | background: '#333',
72 | color: '#fff',
73 | fontSize: '14px'
74 | },
75 | iconTheme: {
76 | primary: 'white',
77 | secondary: 'black'
78 | }
79 | })
80 | }, [])
81 |
82 | return (
83 | <>
84 |
85 |
86 |
87 |
95 |
96 | Share chat
97 |
98 |
99 |
100 |
109 |
110 | Delete chat
111 |
112 |
113 |
174 |
175 |
176 |
177 | Are you absolutely sure?
178 |
179 | This will permanently delete your chat message and remove your
180 | data from our servers.
181 |
182 |
183 |
184 |
185 | Cancel
186 |
187 | {
190 | event.preventDefault()
191 | startRemoveTransition(async () => {
192 | const result = await removeChat({
193 | id: chat.id,
194 | path: chat.path
195 | })
196 |
197 | if (result && 'error' in result) {
198 | toast.error(result.error)
199 | return
200 | }
201 |
202 | setDeleteDialogOpen(false)
203 | router.refresh()
204 | router.push('/')
205 | toast.success('Chat deleted')
206 | })
207 | }}
208 | >
209 | {isRemovePending && }
210 | Delete
211 |
212 |
213 |
214 |
215 | >
216 | )
217 | }
218 |
--------------------------------------------------------------------------------
/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 Link from 'next/link'
4 | import { usePathname } from 'next/navigation'
5 |
6 | import { type Chat } from '@/lib/types'
7 | import { cn } from '@/lib/utils'
8 | import { buttonVariants } from '@/components/ui/button'
9 | import { IconMessage, IconUsers } from '@/components/ui/icons'
10 | import {
11 | Tooltip,
12 | TooltipContent,
13 | TooltipTrigger
14 | } from '@/components/ui/tooltip'
15 |
16 | interface SidebarItemProps {
17 | chat: Chat
18 | children: React.ReactNode
19 | }
20 |
21 | export function SidebarItem({ chat, children }: SidebarItemProps) {
22 | const pathname = usePathname()
23 | const isActive = pathname === chat.path
24 |
25 | if (!chat?.id) return null
26 |
27 | return (
28 |
29 |
30 | {chat.sharePath ? (
31 |
32 |
36 |
37 |
38 | This is a shared chat.
39 |
40 | ) : (
41 |
42 | )}
43 |
44 |
52 |
56 | {chat.title}
57 |
58 |
59 | {isActive &&
{children}
}
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/components/sidebar-list.tsx:
--------------------------------------------------------------------------------
1 | import { getChats, removeChat, shareChat } from '@/app/actions'
2 | import { SidebarActions } from '@/components/sidebar-actions'
3 | import { SidebarItem } from '@/components/sidebar-item'
4 |
5 | export interface SidebarListProps {
6 | userId?: string
7 | }
8 |
9 | export async function SidebarList({ userId }: SidebarListProps) {
10 | const chats = await getChats(userId)
11 |
12 | return (
13 |
14 | {chats?.length ? (
15 |
16 | {chats.map(
17 | chat =>
18 | chat && (
19 |
20 |
25 |
26 | )
27 | )}
28 |
29 | ) : (
30 |
31 |
No chat history
32 |
33 | )}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import {
7 | Sheet,
8 | SheetContent,
9 | SheetHeader,
10 | SheetTitle,
11 | SheetTrigger
12 | } from '@/components/ui/sheet'
13 | import { IconSidebar } from '@/components/ui/icons'
14 |
15 | export interface SidebarProps {
16 | children?: React.ReactNode
17 | }
18 |
19 | export function Sidebar({ children }: SidebarProps) {
20 | return (
21 |
22 |
23 |
27 |
28 |
29 |
30 | Chat History
31 |
32 | {children}
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/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 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/components/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export { Toaster } from 'react-hot-toast'
4 |
--------------------------------------------------------------------------------
/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 = ({
14 | className,
15 | children,
16 | ...props
17 | }: AlertDialogPrimitive.AlertDialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
25 |
26 | const AlertDialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, children, ...props }, ref) => (
30 |
38 | ))
39 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
40 |
41 | const AlertDialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
46 |
47 |
55 |
56 | ))
57 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
58 |
59 | const AlertDialogHeader = ({
60 | className,
61 | ...props
62 | }: React.HTMLAttributes) => (
63 |
70 | )
71 | AlertDialogHeader.displayName = 'AlertDialogHeader'
72 |
73 | const AlertDialogFooter = ({
74 | className,
75 | ...props
76 | }: React.HTMLAttributes) => (
77 |
84 | )
85 | AlertDialogFooter.displayName = 'AlertDialogFooter'
86 |
87 | const AlertDialogTitle = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => (
91 |
96 | ))
97 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
98 |
99 | const AlertDialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogDescription.displayName =
110 | AlertDialogPrimitive.Description.displayName
111 |
112 | const AlertDialogAction = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, ...props }, ref) => (
116 |
121 | ))
122 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
123 |
124 | const AlertDialogCancel = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, ...props }, ref) => (
128 |
137 | ))
138 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
139 |
140 | export {
141 | AlertDialog,
142 | AlertDialogTrigger,
143 | AlertDialogContent,
144 | AlertDialogHeader,
145 | AlertDialogFooter,
146 | AlertDialogTitle,
147 | AlertDialogDescription,
148 | AlertDialogAction,
149 | AlertDialogCancel
150 | }
151 |
--------------------------------------------------------------------------------
/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-full 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 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 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 rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 shadow-none hover:underline'
22 | },
23 | size: {
24 | default: 'h-8 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-8 w-8 p-0'
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 | 'use client'
2 |
3 | import { FC, memo } from 'react'
4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
5 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
6 |
7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
9 | import { Button } from '@/components/ui/button'
10 |
11 | interface Props {
12 | language: string
13 | value: string
14 | }
15 |
16 | interface languageMap {
17 | [key: string]: string | undefined
18 | }
19 |
20 | export const programmingLanguages: languageMap = {
21 | javascript: '.js',
22 | python: '.py',
23 | java: '.java',
24 | c: '.c',
25 | cpp: '.cpp',
26 | 'c++': '.cpp',
27 | 'c#': '.cs',
28 | ruby: '.rb',
29 | php: '.php',
30 | swift: '.swift',
31 | 'objective-c': '.m',
32 | kotlin: '.kt',
33 | typescript: '.ts',
34 | go: '.go',
35 | perl: '.pl',
36 | rust: '.rs',
37 | scala: '.scala',
38 | haskell: '.hs',
39 | lua: '.lua',
40 | shell: '.sh',
41 | sql: '.sql',
42 | html: '.html',
43 | css: '.css'
44 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
45 | }
46 |
47 | export const generateRandomString = (length: number, lowercase = false) => {
48 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
49 | let result = ''
50 | for (let i = 0; i < length; i++) {
51 | result += chars.charAt(Math.floor(Math.random() * chars.length))
52 | }
53 | return lowercase ? result.toLowerCase() : result
54 | }
55 |
56 | const CodeBlock: FC = memo(({ language, value }) => {
57 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
58 |
59 | const downloadAsFile = () => {
60 | if (typeof window === 'undefined') {
61 | return
62 | }
63 | const fileExtension = programmingLanguages[language] || '.file'
64 | const suggestedFileName = `file-${generateRandomString(
65 | 3,
66 | true
67 | )}${fileExtension}`
68 | const fileName = window.prompt('Enter file name' || '', suggestedFileName)
69 |
70 | if (!fileName) {
71 | // User pressed cancel on prompt.
72 | return
73 | }
74 |
75 | const blob = new Blob([value], { type: 'text/plain' })
76 | const url = URL.createObjectURL(blob)
77 | const link = document.createElement('a')
78 | link.download = fileName
79 | link.href = url
80 | link.style.display = 'none'
81 | document.body.appendChild(link)
82 | link.click()
83 | document.body.removeChild(link)
84 | URL.revokeObjectURL(url)
85 | }
86 |
87 | const onCopy = () => {
88 | if (isCopied) return
89 | copyToClipboard(value)
90 | }
91 |
92 | return (
93 |
94 |
95 |
{language}
96 |
97 |
106 |
115 |
116 |
117 |
135 | {value}
136 |
137 |
138 | )
139 | })
140 | CodeBlock.displayName = 'CodeBlock'
141 |
142 | export { CodeBlock }
143 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
55 | {children}
56 |
57 |
58 | Close
59 |
60 |
61 |
62 | ))
63 | DialogContent.displayName = DialogPrimitive.Content.displayName
64 |
65 | const DialogHeader = ({
66 | className,
67 | ...props
68 | }: React.HTMLAttributes) => (
69 |
76 | )
77 | DialogHeader.displayName = 'DialogHeader'
78 |
79 | const DialogFooter = ({
80 | className,
81 | ...props
82 | }: React.HTMLAttributes) => (
83 |
90 | )
91 | DialogFooter.displayName = 'DialogFooter'
92 |
93 | const DialogTitle = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
105 | ))
106 | DialogTitle.displayName = DialogPrimitive.Title.displayName
107 |
108 | const DialogDescription = React.forwardRef<
109 | React.ElementRef,
110 | React.ComponentPropsWithoutRef
111 | >(({ className, ...props }, ref) => (
112 |
117 | ))
118 | DialogDescription.displayName = DialogPrimitive.Description.displayName
119 |
120 | export {
121 | Dialog,
122 | DialogTrigger,
123 | DialogContent,
124 | DialogHeader,
125 | DialogFooter,
126 | DialogTitle,
127 | DialogDescription
128 | }
129 |
--------------------------------------------------------------------------------
/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 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
19 |
20 | const DropdownMenuSubContent = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
32 | ))
33 | DropdownMenuSubContent.displayName =
34 | DropdownMenuPrimitive.SubContent.displayName
35 |
36 | const DropdownMenuContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, sideOffset = 4, ...props }, ref) => (
40 |
41 |
50 |
51 | ))
52 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
53 |
54 | const DropdownMenuItem = React.forwardRef<
55 | React.ElementRef,
56 | React.ComponentPropsWithoutRef & {
57 | inset?: boolean
58 | }
59 | >(({ className, inset, ...props }, ref) => (
60 |
69 | ))
70 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
71 |
72 | const DropdownMenuLabel = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef & {
75 | inset?: boolean
76 | }
77 | >(({ className, inset, ...props }, ref) => (
78 |
87 | ))
88 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
89 |
90 | const DropdownMenuSeparator = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ))
100 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
101 |
102 | const DropdownMenuShortcut = ({
103 | className,
104 | ...props
105 | }: React.HTMLAttributes) => {
106 | return (
107 |
111 | )
112 | }
113 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
114 |
115 | export {
116 | DropdownMenu,
117 | DropdownMenuTrigger,
118 | DropdownMenuContent,
119 | DropdownMenuItem,
120 | DropdownMenuLabel,
121 | DropdownMenuSeparator,
122 | DropdownMenuShortcut,
123 | DropdownMenuGroup,
124 | DropdownMenuPortal,
125 | DropdownMenuSub,
126 | DropdownMenuSubContent,
127 | DropdownMenuRadioGroup
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | function IconNextChat({
8 | className,
9 | inverted,
10 | ...props
11 | }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12 | const id = React.useId()
13 |
14 | return (
15 |
88 | )
89 | }
90 |
91 | function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92 | return (
93 |
104 | )
105 | }
106 |
107 | function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108 | return (
109 |
121 | )
122 | }
123 |
124 | function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125 | return (
126 |
137 | )
138 | }
139 |
140 | function IconMetamask({ className, ...props }: React.ComponentProps<'svg'>) {
141 | return (
142 |
146 | )
147 | }
148 |
149 | function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
150 | return (
151 |
165 | )
166 | }
167 |
168 | function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
169 | return (
170 |
179 | )
180 | }
181 |
182 | function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
183 | return (
184 |
193 | )
194 | }
195 |
196 | function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
197 | return (
198 |
207 | )
208 | }
209 |
210 | function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
211 | return (
212 |
221 | )
222 | }
223 |
224 | function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
225 | return (
226 |
235 | )
236 | }
237 |
238 | function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
239 | return (
240 |
249 | )
250 | }
251 |
252 | function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
253 | return (
254 |
263 | )
264 | }
265 |
266 | function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
267 | return (
268 |
277 | )
278 | }
279 |
280 | function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
281 | return (
282 |
291 | )
292 | }
293 |
294 | function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
295 | return (
296 |
305 | )
306 | }
307 |
308 | function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
309 | return (
310 |
319 | )
320 | }
321 |
322 | function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
323 | return (
324 |
333 | )
334 | }
335 |
336 | function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
337 | return (
338 |
347 | )
348 | }
349 |
350 | function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
351 | return (
352 |
361 | )
362 | }
363 |
364 | function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
365 | return (
366 |
375 | )
376 | }
377 |
378 | function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
379 | return (
380 |
389 | )
390 | }
391 |
392 | function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
393 | return (
394 |
403 | )
404 | }
405 |
406 | function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
407 | return (
408 |
423 | )
424 | }
425 |
426 | function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
427 | return (
428 |
437 | )
438 | }
439 |
440 | function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
441 | return (
442 |
451 | )
452 | }
453 |
454 | function IconExternalLink({
455 | className,
456 | ...props
457 | }: React.ComponentProps<'svg'>) {
458 | return (
459 |
468 | )
469 | }
470 |
471 | function IconChevronUpDown({
472 | className,
473 | ...props
474 | }: React.ComponentProps<'svg'>) {
475 | return (
476 |
485 | )
486 | }
487 |
488 | export {
489 | IconEdit,
490 | IconNextChat,
491 | IconOpenAI,
492 | IconVercel,
493 | IconGitHub,
494 | IconMetamask,
495 | IconSeparator,
496 | IconArrowDown,
497 | IconArrowRight,
498 | IconUser,
499 | IconPlus,
500 | IconArrowElbow,
501 | IconSpinner,
502 | IconMessage,
503 | IconTrash,
504 | IconRefresh,
505 | IconStop,
506 | IconSidebar,
507 | IconMoon,
508 | IconSun,
509 | IconCopy,
510 | IconCheck,
511 | IconDownload,
512 | IconClose,
513 | IconShare,
514 | IconUsers,
515 | IconExternalLink,
516 | IconChevronUpDown
517 | }
518 |
--------------------------------------------------------------------------------
/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 * as SelectPrimitive from '@radix-ui/react-select'
5 |
6 | import { cn } from '@/lib/utils'
7 | import {
8 | IconArrowDown,
9 | IconCheck,
10 | IconChevronUpDown
11 | } from '@/components/ui/icons'
12 |
13 | const Select = SelectPrimitive.Root
14 |
15 | const SelectGroup = SelectPrimitive.Group
16 |
17 | const SelectValue = SelectPrimitive.Value
18 |
19 | const SelectTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
31 | {children}
32 |
33 |
34 |
35 |
36 | ))
37 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
38 |
39 | const SelectContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, children, position = 'popper', ...props }, ref) => (
43 |
44 |
54 |
61 | {children}
62 |
63 |
64 |
65 | ))
66 | SelectContent.displayName = SelectPrimitive.Content.displayName
67 |
68 | const SelectLabel = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
77 | ))
78 | SelectLabel.displayName = SelectPrimitive.Label.displayName
79 |
80 | const SelectItem = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, children, ...props }, ref) => (
84 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | ))
100 | SelectItem.displayName = SelectPrimitive.Item.displayName
101 |
102 | const SelectSeparator = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
113 |
114 | export {
115 | Select,
116 | SelectGroup,
117 | SelectValue,
118 | SelectTrigger,
119 | SelectContent,
120 | SelectLabel,
121 | SelectItem,
122 | SelectSeparator
123 | }
124 |
--------------------------------------------------------------------------------
/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 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Sheet = SheetPrimitive.Root
10 |
11 | const SheetTrigger = SheetPrimitive.Trigger
12 |
13 | const SheetClose = SheetPrimitive.Close
14 |
15 | const SheetPortal = ({
16 | className,
17 | children,
18 | ...props
19 | }: SheetPrimitive.DialogPortalProps) => (
20 |
24 | {children}
25 |
26 | )
27 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
28 |
29 | const SheetOverlay = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef
32 | >(({ className, children, ...props }, ref) => (
33 |
41 | ))
42 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
43 |
44 | const SheetContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, children, ...props }, ref) => (
48 |
49 |
57 | {children}
58 |
59 |
60 | Close
61 |
62 |
63 |
64 | ))
65 | SheetContent.displayName = SheetPrimitive.Content.displayName
66 |
67 | const SheetHeader = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
72 | )
73 | SheetHeader.displayName = 'SheetHeader'
74 |
75 | const SheetFooter = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetFooter.displayName = 'SheetFooter'
88 |
89 | const SheetTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | SheetTitle.displayName = SheetPrimitive.Title.displayName
100 |
101 | const SheetDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SheetDescription.displayName = SheetPrimitive.Description.displayName
112 |
113 | export {
114 | Sheet,
115 | SheetTrigger,
116 | SheetClose,
117 | SheetContent,
118 | SheetHeader,
119 | SheetFooter,
120 | SheetTitle,
121 | SheetDescription
122 | }
123 |
--------------------------------------------------------------------------------
/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 | 'use client'
2 |
3 | import Image from 'next/image'
4 | import { type Session } from '@supabase/auth-helpers-nextjs'
5 | import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
6 | import { useRouter } from 'next/navigation'
7 |
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuSeparator,
14 | DropdownMenuTrigger
15 | } from '@/components/ui/dropdown-menu'
16 | import { IconExternalLink } from '@/components/ui/icons'
17 |
18 | export interface UserMenuProps {
19 | user: Session['user']
20 | }
21 |
22 | function getUserInitials(name: string) {
23 | const [firstName, lastName] = name.split(' ')
24 | return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
25 | }
26 |
27 | export function UserMenu({ user }: UserMenuProps) {
28 | const router = useRouter()
29 | const supabase = createClientComponentClient()
30 |
31 | const signOut = async () => {
32 | await supabase.auth.signOut()
33 | router.refresh()
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
62 |
63 |
64 |
65 |
66 | {user?.user_metadata.name}
67 |
68 | {user?.email}
69 |
70 |
71 |
72 |
78 | Vercel Homepage
79 |
80 |
81 |
82 |
83 | Log Out
84 |
85 |
86 |
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/docs/README_improvements_ideas.md:
--------------------------------------------------------------------------------
1 | # Improvements and Ideas
2 |
3 | ## Improve this project
4 | 1. Make it easier to configure web3: ocean datatoken, multiple networks, subgraph
5 | 1. Tighten web3auth: clean it, make it more secure, improve handling cookie/supabase/ocean datatoken TTL
6 | 1. Improve bootstrap by reducing the number of APIs/SDKs: Wallet Connect, Infura, others?
7 | 1. Improve bootstrap by automating steps found in the doc and tightening the README.md.
8 | 1. Chats and Chat History aren't working on the original template, fix it
9 | 1. Next edge-runtime was disabled. What's next?
10 |
11 | ## Build new products
12 | 1. Expand template to focus on Langchain, Vector DB, and Embeddings
13 | 1. Extract embeddings from [Ocean Protocol](https://github.com/oceanprotocol) and [New Order](https://github.com/new-order-network) GitHub repos so users can prompt it.
14 | 1. Extract embeddings from quarterly reports of the Top 10 S&P so users can prompt iot.
15 | 1. Build a simple ETL pipeline that generates a FAISS vector store and loads it via Langchain, package this project as a pre-trained model and deploy it to Ocean Marketplace.
--------------------------------------------------------------------------------
/docs/README_web3_auth_supabase.md:
--------------------------------------------------------------------------------
1 | # Supabase Web3Auth
2 |
3 | In order to have the user login to our app and only see his own data, we need to manage Authentication, and make sure the user can only access their own rows.
4 |
5 | To establish this in Supabase, we needed something custom because of how we sign in the user via a Web3 wallet transaction. Supabase custom auth does not provide you with a session, instead it is up to us to validate the JWT and the user.
6 |
7 | ## Nonce and Login
8 |
9 | The logic for `/api/nonce` and `api/login` looks a bit like this:
10 | 1. Every time the user needs to login, he'll have to sign a tx.
11 | 1. To sign this tx, the client requests the server /api/web3auth/nonce for a new nonce
12 | 1. A user is created if needed, has the nonce generated, and returns to the client
13 | 1. The client/wallet now must sign the nonce => then send to server to login
14 | /api/web3auth/login then verifies the msg is signed by the wallet and starts the auth process
15 | 1. Supabase auth.users row is created by the backend, and configured to have the address in the metadata
16 | 1. For the server to read and find the auth.users via the address inside metadata (previous step), it creates an auth_users view inside supabase public schema. public.users.id is then linked to auth.users.id, enabling us to complete the auth process.
17 |
18 | Now, only the user's rows from the database will be available to them.
19 |
20 | # Supabase RLS and JWT
21 |
22 | To make sure Row Level-Security (RLS) work with the JWT, we need to create a special security policy in Supabase's DB. The following blog post from [Grace Wang](https://medium.com/@gracew/using-supabase-rls-with-a-custom-auth-provider-b31564172d5d) and further discussions inside Supabase's github repo, provided insights for this configuration.
23 |
24 |
25 |
26 | If your app is up and running, you have already completed this setup by running the following command in your SQL editor.
27 | ```
28 | -- web3 auth policy
29 | CREATE POLICY web3_auth ON public.users
30 | AS PERMISSIVE FOR UPDATE
31 | TO authenticated
32 | USING ((current_setting('request.jwt.claims', true))::json ->> 'address' = address)
33 | WITH CHECK ((current_setting('request.jwt.claims', true))::json ->> 'address' = address);
34 | ```
35 |
36 | By creating this policy, we make sure that the `public.user` table and related security we want to implement, are configured to verify against the `jwt.address` parameter.
37 |
38 | **Important TLDR:**
39 | 1. If you want to secure a table for a user, such that only their rows are visible, then... You should have a `varchar address` column so the user rows can be identifed by the policy.
40 | 2. You will also need to link the `table.user.id` column, to the `auth.user.id` column so when using supabase to `supabase = createClient()` to retrieve an authorized user, the `supabase.auth.getUser().id` will work with the table you're trying to query. Such that when using `supabase.from('table').select('column').eq('id', authUser.id).single()`, you will only find rows where the user matches the `auth.user.id`.
--------------------------------------------------------------------------------
/docs/README_web3_tokens_and_networks.md:
--------------------------------------------------------------------------------
1 | # Expanding Web3 Support
2 |
3 | Below is a breakdown on some concepts as they relate to web3 integration.
4 |
5 | Think this is a lot of work? Please see [Readme - Project Improvements](README_improvements.md#improve-this-project) on how you can help, and make it easier to support more chains.
6 |
7 | ## Sign to Login
8 |
9 | You do not need to be on a specific chain to sign your offchain transaction and login. The server simplify verifies that your address is who you say you are.
10 |
11 | To verify the Ocean Protocol DataNFT access, the server needs the right configuration so it can verify token + purchase on the right chain.
12 |
13 | ## Adding new Networks
14 |
15 | Adding new Networks enables you to implement direct-to-contract functionality such as manipulating DataNFTs.
16 |
17 | If your goal is to do a lot of DataNFT oriented work with ocean, then we recommend looking into [ocean.js](https://github.com/oceanprotocol/ocean.js) which could be integrated directly into this project.
18 |
19 | To add a new network to the app via Rainbokit & Wagmi:
20 | 1. Go to `/components/providers.tsx` and add more chain providers through wagmi. Wagmi provides a lot of interfaces to different RPC/chains, and you can easily add more EVM based chains. Simply search for network RPCs and how to add a new network, and how to configure it in wagmi.
21 |
22 | Below is an example of the current implementation
23 | ```
24 | import {
25 | mainnet,
26 | polygon,
27 | goerli,
28 | } from 'wagmi/chains';
29 |
30 | const { chains, publicClient, webSocketPublicClient } = configureChains(
31 | [
32 | mainnet,
33 | polygon,
34 | ...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === 'true' ? [goerli] : []),
35 | ],
36 | [publicProvider()]
37 | );
38 | ```
39 |
40 | ## Tokengating app on new networks
41 |
42 | Now we're going to make sure the backend will validate the token purchase & access to the app.
43 |
44 | Follow these steps
45 | 1. Decide on the network you're going to want to use. If you choose Ethereum Mainnet, aka `mainnet`, you should use it for the remainder of this tutorial.
46 | 1. Change the `NEXT_PUBLIC_SUBGRAPH_URL` inside of your .env file to one of our other subgraphs (below)
47 | 1. Update `codegen.yml` with the same subgraph url that you used in the previous step.
48 | 1. Finally, after configuring the app to be on the network of your choice, make sure the project is [rebuilt correctly via Local or Vercel](#deploy-build).
49 |
50 |
51 | Subgraph Endpoints
52 | ```
53 | Ethereum Mainnet - https://v4.subgraph.mainnet.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph
54 | Polygon - https://v4.subgraph.polygon.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph
55 | Goerli - https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph
56 | Mumbai - https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph
57 | ```
58 |
59 | ## Tokengating with different currencies
60 |
61 | If you want to tokengate your app with a DataNFT that is offered in a different ERC20 currency other than OCEAN, you totally can!
62 |
63 | However, to do this we're going to have to run some python code through ocean.py to configure out token beyond what market.oceanprotocol.com can do. To illustrate how to publish and purchase with H2O, the data-backed stablecoin, we're going to do the following.
64 |
65 | This script [follows this ocean.py tutorial](https://github.com/oceanprotocol/ocean.py/blob/main/READMEs/main-flow.md) which is our recommended approach for publishing your Datatoken in a custom fashion but with relative ease.
66 |
67 | In this DX scenario, you would be continuing from the final part of the tutorials linked above with alice in this case, having access to some Ethereum. You would not need any H2O tokens to publish the token, just to purchase it.
68 |
69 | This is a pseudo implementation that has not been tested.
70 | ```
71 | from ocean_lib.ocean.util import to_wei
72 |
73 | from brownie import Contract
74 | from brownie.network.contract import InterfaceContainer
75 |
76 | # H2O is correct, ABI is exemplified for brevity
77 | H2O_address = "0x0642026e7f0b6ccac5925b4e7fa61384250e1701"
78 | H2O_ABI = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"type":"function"}...yadda... yadda]
79 |
80 | # Load the ERC20 token contract
81 | H2O_contract = Contract.from_abi(
82 | "H2O", # This is just a name and can be anything you want
83 | H2O_address, # This is the address where the contract is deployed
84 | InterfaceContainer(H2O_ABI) # This is the contract's ABI
85 | )
86 |
87 | # Alice posts for sale, bob buys
88 | # 1. Alice creates exchange
89 | price = to_wei(100)
90 | exchange = datatoken.create_exchange({"from": alice}, price, H2O_address)
91 |
92 | # 2. Alice makes 100 datatokens available on the exchange
93 | datatoken.mint(alice, to_wei(100), {"from": alice})
94 | datatoken.approve(exchange.address, to_wei(100), {"from": alice})
95 |
96 | # 3. Bob lets exchange pull the H2O needed
97 | H2O_needed = exchange.BT_needed(to_wei(1), consume_market_fee=0)
98 | H2O_contract.approve(exchange.address, H2O_needed, {"from":bob})
99 |
100 | # 4 Bob buys datatoken
101 | exchange.buy_DT(to_wei(1), consume_market_fee=0, tx_dict={"from": bob})
102 |
103 | print(datatoken.address) # you need the datatoken address (0x address)
104 | print(exchange.details) # you need the exchange details (url)
105 | ```
106 |
107 | ## Deploy Build
108 |
109 | ### Update Local
110 |
111 | After updating everything, just enter your terminal, and run `yarn generate && yarn build` from your terminal/root project folder.
112 |
113 | ### Update Vercel
114 |
115 | If you are using Vercel, then simply update your `NEXT_PUBLIC_SUBGRAPH_URL` Vercel->Project->Settings->Environment Variable
116 |
117 | Vercel should recompoile your GraphQL SDK.js if you configure your Vercel->Project->Settings->General, Build Command to be: `yarn generate && yarn build`.
118 |
119 |
--------------------------------------------------------------------------------
/docs/assets/metamask_add_network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/metamask_add_network.png
--------------------------------------------------------------------------------
/docs/assets/ocean_publish_datatoken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/ocean_publish_datatoken.png
--------------------------------------------------------------------------------
/docs/assets/supabase_jwt_policy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/supabase_jwt_policy.png
--------------------------------------------------------------------------------
/docs/assets/supabase_user_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/supabase_user_table.png
--------------------------------------------------------------------------------
/docs/assets/table_users_linked_id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/table_users_linked_id.png
--------------------------------------------------------------------------------
/docs/assets/tokengated_chatbot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/tokengated_chatbot.png
--------------------------------------------------------------------------------
/docs/assets/your_datatoken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/docs/assets/your_datatoken.png
--------------------------------------------------------------------------------
/graphql/graphqlClient.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLClient } from 'graphql-request'
2 | import { getSdk } from './sdk'
3 |
4 | export const graphqlClient = new GraphQLClient(
5 | process.env.NEXT_PUBLIC_SUBGRAPH_URL || ''
6 | )
7 |
8 | export const subgraphSDK = getSdk(graphqlClient)
9 |
--------------------------------------------------------------------------------
/graphql/operations/queries/getOrder.graphql:
--------------------------------------------------------------------------------
1 | query GetOrder(
2 | $block: Block_height
3 | $id: ID!
4 | $subgraphError: _SubgraphErrorPolicy_! = deny
5 | ) {
6 | order(block: $block, id: $id, subgraphError: $subgraphError) {
7 | id
8 | eventIndex
9 | datatoken {
10 | id
11 | }
12 | consumer {
13 | id
14 | }
15 | payer {
16 | id
17 | }
18 | amount
19 | serviceIndex
20 | nftOwner {
21 | id
22 | }
23 | publishingMarket {
24 | id
25 | }
26 | publishingMarketToken {
27 | id
28 | }
29 | providerFee
30 | providerFeeValidUntil
31 | consumerMarket {
32 | id
33 | }
34 | consumerMarketToken {
35 | id
36 | }
37 | createdTimestamp
38 | tx
39 | eventIndex
40 | block
41 | lastPriceToken {
42 | id
43 | }
44 | lastPriceValue
45 | estimatedUSDValue
46 | gasUsed
47 | gasPrice
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/graphql/operations/queries/getOrders.graphql:
--------------------------------------------------------------------------------
1 | query GetOrders(
2 | $block: Block_height
3 | $first: Int = 100
4 | $orderBy: Order_orderBy
5 | $orderDirection: OrderDirection
6 | $skip: Int = 0
7 | $subgraphError: _SubgraphErrorPolicy_! = deny
8 | $where: Order_filter
9 | ) {
10 | orders(
11 | block: $block
12 | first: $first
13 | orderBy: $orderBy
14 | orderDirection: $orderDirection
15 | skip: $skip
16 | subgraphError: $subgraphError
17 | where: $where
18 | ) {
19 | id
20 | block
21 | consumer {
22 | id
23 | }
24 | amount
25 | createdTimestamp
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/graphql/operations/queries/getUser.graphql:
--------------------------------------------------------------------------------
1 | query GetUser(
2 | $block: Block_height
3 | $id: ID!
4 | $subgraphError: _SubgraphErrorPolicy_! = deny
5 | ) {
6 | user(block: $block, id: $id, subgraphError: $subgraphError) {
7 | id
8 | totalOrders
9 | totalSales
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/lib/analytics.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest } from 'next'
2 | import type { NextFetchEvent, NextRequest } from 'next/server'
3 |
4 | export const initAnalytics = ({
5 | request,
6 | event
7 | }: {
8 | request: NextRequest | NextApiRequest | Request
9 | event?: NextFetchEvent
10 | }) => {
11 | const endpoint = process.env.VERCEL_URL
12 |
13 | return {
14 | track: async (eventName: string, data?: any) => {
15 | try {
16 | if (!endpoint && process.env.NODE_ENV === 'development') {
17 | console.log(
18 | `[Vercel Web Analytics] Track "${eventName}"` +
19 | (data ? ` with data ${JSON.stringify(data || {})}` : '')
20 | )
21 | return
22 | }
23 |
24 | const headers: { [key: string]: string } = {}
25 | Object.entries(request.headers).map(([key, value]) => {
26 | headers[key] = value
27 | })
28 |
29 | const body = {
30 | o: headers.referer,
31 | ts: new Date().getTime(),
32 | r: '',
33 | en: eventName,
34 | ed: data
35 | }
36 |
37 | const promise = fetch(
38 | `https://${process.env.VERCEL_URL}/_vercel/insights/event`,
39 | {
40 | headers: {
41 | 'content-type': 'application/json',
42 | 'user-agent': headers['user-agent'] as string,
43 | 'x-forwarded-for': headers['x-forwarded-for'] as string,
44 | 'x-va-server': '1'
45 | },
46 | body: JSON.stringify(body),
47 | method: 'POST'
48 | }
49 | )
50 |
51 | if (event) {
52 | event.waitUntil(promise)
53 | }
54 | {
55 | await promise
56 | }
57 | } catch (err) {
58 | console.error(err)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/db_types.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | { [key: string]: Json | undefined }
7 | | Json[]
8 |
9 | export interface Database {
10 | graphql_public: {
11 | Tables: {
12 | [_ in never]: never
13 | }
14 | Views: {
15 | [_ in never]: never
16 | }
17 | Functions: {
18 | graphql: {
19 | Args: {
20 | operationName?: string
21 | query?: string
22 | variables?: Json
23 | extensions?: Json
24 | }
25 | Returns: Json
26 | }
27 | }
28 | Enums: {
29 | [_ in never]: never
30 | }
31 | CompositeTypes: {
32 | [_ in never]: never
33 | }
34 | }
35 | public: {
36 | Tables: {
37 | chats: {
38 | Row: {
39 | id: string
40 | payload: Json | null
41 | user_id: string | null
42 | }
43 | Insert: {
44 | id: string
45 | payload?: Json | null
46 | user_id?: string | null
47 | }
48 | Update: {
49 | id?: string
50 | payload?: Json | null
51 | user_id?: string | null
52 | }
53 | Relationships: [
54 | {
55 | foreignKeyName: "chats_user_id_fkey"
56 | columns: ["user_id"]
57 | referencedRelation: "users"
58 | referencedColumns: ["id"]
59 | }
60 | ]
61 | }
62 | }
63 | Views: {
64 | [_ in never]: never
65 | }
66 | Functions: {
67 | [_ in never]: never
68 | }
69 | Enums: {
70 | [_ in never]: never
71 | }
72 | CompositeTypes: {
73 | [_ in never]: never
74 | }
75 | }
76 | storage: {
77 | Tables: {
78 | buckets: {
79 | Row: {
80 | allowed_mime_types: string[] | null
81 | avif_autodetection: boolean | null
82 | created_at: string | null
83 | file_size_limit: number | null
84 | id: string
85 | name: string
86 | owner: string | null
87 | public: boolean | null
88 | updated_at: string | null
89 | }
90 | Insert: {
91 | allowed_mime_types?: string[] | null
92 | avif_autodetection?: boolean | null
93 | created_at?: string | null
94 | file_size_limit?: number | null
95 | id: string
96 | name: string
97 | owner?: string | null
98 | public?: boolean | null
99 | updated_at?: string | null
100 | }
101 | Update: {
102 | allowed_mime_types?: string[] | null
103 | avif_autodetection?: boolean | null
104 | created_at?: string | null
105 | file_size_limit?: number | null
106 | id?: string
107 | name?: string
108 | owner?: string | null
109 | public?: boolean | null
110 | updated_at?: string | null
111 | }
112 | Relationships: [
113 | {
114 | foreignKeyName: "buckets_owner_fkey"
115 | columns: ["owner"]
116 | referencedRelation: "users"
117 | referencedColumns: ["id"]
118 | }
119 | ]
120 | }
121 | migrations: {
122 | Row: {
123 | executed_at: string | null
124 | hash: string
125 | id: number
126 | name: string
127 | }
128 | Insert: {
129 | executed_at?: string | null
130 | hash: string
131 | id: number
132 | name: string
133 | }
134 | Update: {
135 | executed_at?: string | null
136 | hash?: string
137 | id?: number
138 | name?: string
139 | }
140 | Relationships: []
141 | }
142 | objects: {
143 | Row: {
144 | bucket_id: string | null
145 | created_at: string | null
146 | id: string
147 | last_accessed_at: string | null
148 | metadata: Json | null
149 | name: string | null
150 | owner: string | null
151 | path_tokens: string[] | null
152 | updated_at: string | null
153 | version: string | null
154 | }
155 | Insert: {
156 | bucket_id?: string | null
157 | created_at?: string | null
158 | id?: string
159 | last_accessed_at?: string | null
160 | metadata?: Json | null
161 | name?: string | null
162 | owner?: string | null
163 | path_tokens?: string[] | null
164 | updated_at?: string | null
165 | version?: string | null
166 | }
167 | Update: {
168 | bucket_id?: string | null
169 | created_at?: string | null
170 | id?: string
171 | last_accessed_at?: string | null
172 | metadata?: Json | null
173 | name?: string | null
174 | owner?: string | null
175 | path_tokens?: string[] | null
176 | updated_at?: string | null
177 | version?: string | null
178 | }
179 | Relationships: [
180 | {
181 | foreignKeyName: "objects_bucketId_fkey"
182 | columns: ["bucket_id"]
183 | referencedRelation: "buckets"
184 | referencedColumns: ["id"]
185 | }
186 | ]
187 | }
188 | }
189 | Views: {
190 | [_ in never]: never
191 | }
192 | Functions: {
193 | can_insert_object: {
194 | Args: {
195 | bucketid: string
196 | name: string
197 | owner: string
198 | metadata: Json
199 | }
200 | Returns: undefined
201 | }
202 | extension: {
203 | Args: {
204 | name: string
205 | }
206 | Returns: string
207 | }
208 | filename: {
209 | Args: {
210 | name: string
211 | }
212 | Returns: string
213 | }
214 | foldername: {
215 | Args: {
216 | name: string
217 | }
218 | Returns: unknown
219 | }
220 | get_size_by_bucket: {
221 | Args: Record
222 | Returns: {
223 | size: number
224 | bucket_id: string
225 | }[]
226 | }
227 | search: {
228 | Args: {
229 | prefix: string
230 | bucketname: string
231 | limits?: number
232 | levels?: number
233 | offsets?: number
234 | search?: string
235 | sortcolumn?: string
236 | sortorder?: string
237 | }
238 | Returns: {
239 | name: string
240 | id: string
241 | updated_at: string
242 | created_at: string
243 | last_accessed_at: string
244 | metadata: Json
245 | }[]
246 | }
247 | }
248 | Enums: {
249 | [_ in never]: never
250 | }
251 | CompositeTypes: {
252 | [_ in never]: never
253 | }
254 | }
255 | }
256 |
257 |
--------------------------------------------------------------------------------
/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google'
2 |
3 | export const fontSans = FontSans({
4 | subsets: ['latin'],
5 | variable: '--font-sans'
6 | })
7 |
8 | export const fontMono = FontMono({
9 | subsets: ['latin'],
10 | variable: '--font-mono'
11 | })
12 |
--------------------------------------------------------------------------------
/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/types.ts:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | // TODO refactor and remove unneccessary duplicate data.
4 | export interface Chat extends Record {
5 | id: string
6 | title: string
7 | createdAt: Date
8 | userId: string
9 | path: string
10 | messages: Message[]
11 | sharePath?: string // Refactor to use RLS
12 | }
13 |
14 | export type ServerActionResult = Promise<
15 | | Result
16 | | {
17 | error: string
18 | }
19 | >
20 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { customAlphabet } from 'nanoid'
3 | import { twMerge } from 'tailwind-merge'
4 |
5 | import { SignJWT, jwtVerify } from 'jose';
6 |
7 | export function cn(...inputs: ClassValue[]) {
8 | return twMerge(clsx(inputs))
9 | }
10 |
11 | export const nanoid = customAlphabet(
12 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
13 | 7
14 | ) // 7-character random string
15 |
16 | export async function fetcher(
17 | input: RequestInfo,
18 | init?: RequestInit
19 | ): Promise {
20 | const res = await fetch(input, init)
21 |
22 | if (!res.ok) {
23 | const json = await res.json()
24 | if (json.error) {
25 | const error = new Error(json.error) as Error & {
26 | status: number
27 | }
28 | error.status = res.status
29 | throw error
30 | } else {
31 | throw new Error('An unexpected error occurred')
32 | }
33 | }
34 |
35 | return res.json()
36 | }
37 |
38 | export function formatDate(input: string | number | Date): string {
39 | const date = new Date(input)
40 | return date.toLocaleDateString('en-US', {
41 | month: 'long',
42 | day: 'numeric',
43 | year: 'numeric'
44 | })
45 | }
46 |
47 | interface UserJwtPayload {
48 | jti: string
49 | iat: number
50 | }
51 |
52 | export async function signToken(payload: any, options: any) {
53 | const token = await new SignJWT(payload)
54 | .setProtectedHeader({ alg: 'HS256' })
55 | .setJti(nanoid())
56 | .setIssuedAt()
57 | .setExpirationTime('2h')
58 | .sign(new TextEncoder().encode(process.env.NEXT_PUBLIC_SUPABASE_JWT_SECRET))
59 |
60 | return token
61 | }
62 |
63 | export async function verifyToken(token: string, address: string) {
64 | try {
65 | const verified = await jwtVerify(
66 | token,
67 | new TextEncoder().encode(process.env.NEXT_PUBLIC_SUPABASE_JWT_SECRET)
68 | )
69 | return verified.payload as UserJwtPayload
70 | } catch (err) {
71 | return jsonResponse(401, { error: { message: 'Your token has expired.' } })
72 | }
73 | }
74 |
75 | export function jsonResponse(status: number, data: any, init?: ResponseInit) {
76 | return new Response(JSON.stringify(data), {
77 | ...init,
78 | status,
79 | headers: {
80 | ...init?.headers,
81 | 'Content-Type': 'application/json',
82 | },
83 | })
84 | }
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import type { NextRequest } from 'next/server'
3 | import { verifyToken } from '@/lib/utils'
4 |
5 | export async function middleware(req: NextRequest) {
6 | const res = NextResponse.next()
7 |
8 | const address = req.cookies.get('address')?.value || ''
9 | const web3jwt = req.cookies.get('web3jwt')?.value || ''
10 | const validToken = verifyToken(web3jwt, address)
11 |
12 | // would be good to maybe verify web3jwt here, but error w/ edge functions
13 | if( !web3jwt || !validToken ) {
14 | if( !req.url.includes('/sign-in') ) {
15 | const redirectUrl = req.nextUrl.clone()
16 | redirectUrl.pathname = '/sign-in'
17 | redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
18 | return NextResponse.redirect(redirectUrl)
19 | }
20 | }
21 |
22 | return res
23 | }
24 |
25 | export const config = {
26 | matcher: [
27 | /*
28 | * Match all request paths except for the ones starting with:
29 | * - share (publicly shared chats)
30 | * - api (API routes)
31 | * - _next/static (static files)
32 | * - _next/image (image optimization files)
33 | * - favicon.ico (favicon file)
34 | */
35 | '/((?!share|api|_next/static|_next/image|favicon.ico).*)'
36 | ]
37 | }
--------------------------------------------------------------------------------
/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 | reactStrictMode: true,
4 | experimental: {
5 | serverActions: true,
6 | appDir: true
7 | },
8 | webpack: config => {
9 | config.resolve.fallback = { fs: false, net: false, tls: false };
10 | config.externals.push('pino-pretty', 'lokijs', 'encoding');
11 | return config;
12 | },
13 | images: {
14 | remotePatterns: [
15 | {
16 | protocol: 'https',
17 | hostname: '**.githubusercontent.com'
18 | }
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-template",
3 | "version": "0.0.2",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "preview": "next build && next start",
12 | "type-check": "tsc --noEmit",
13 | "format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache",
14 | "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache",
15 | "generate": "graphql-codegen"
16 | },
17 | "dependencies": {
18 | "@apollo/client": "^3.7.17",
19 | "@radix-ui/react-alert-dialog": "^1.0.4",
20 | "@radix-ui/react-dialog": "^1.0.4",
21 | "@radix-ui/react-dropdown-menu": "^2.0.5",
22 | "@radix-ui/react-label": "^2.0.2",
23 | "@radix-ui/react-select": "^1.2.2",
24 | "@radix-ui/react-separator": "^1.0.3",
25 | "@radix-ui/react-slot": "^1.0.2",
26 | "@radix-ui/react-switch": "^1.0.3",
27 | "@radix-ui/react-tooltip": "^1.0.6",
28 | "@rainbow-me/rainbowkit": "^1.0.6",
29 | "@supabase/auth-helpers-nextjs": "^0.7.2",
30 | "@supabase/supabase-js": "^2.26.0",
31 | "@vercel/analytics": "^1.0.1",
32 | "@vercel/og": "^0.5.7",
33 | "ai": "^2.1.6",
34 | "class-variance-authority": "^0.6.1",
35 | "clsx": "^1.2.1",
36 | "ethers": "^5.7.2",
37 | "focus-trap-react": "^10.1.1",
38 | "graphql": "^16.7.1",
39 | "jose": "4.14.4",
40 | "nanoid": "^4.0.2",
41 | "next": "13.4.7-canary.1",
42 | "next-auth": "^4.20.1",
43 | "next-themes": "^0.2.1",
44 | "openai-edge": "^0.5.1",
45 | "react": "^18.2.0",
46 | "react-dom": "^18.2.0",
47 | "react-hot-toast": "^2.4.1",
48 | "react-intersection-observer": "^9.4.4",
49 | "react-markdown": "^8.0.7",
50 | "react-syntax-highlighter": "^15.5.0",
51 | "react-textarea-autosize": "^8.4.1",
52 | "remark-gfm": "^3.0.1",
53 | "remark-math": "^5.1.1",
54 | "viem": "~1.2.9",
55 | "wagmi": "1.3.8"
56 | },
57 | "devDependencies": {
58 | "@graphql-codegen/cli": "^4.0.1",
59 | "@graphql-codegen/typescript": "^4.0.1",
60 | "@graphql-codegen/typescript-graphql-request": "^5.0.0",
61 | "@graphql-codegen/typescript-operations": "^4.0.1",
62 | "@tailwindcss/typography": "^0.5.9",
63 | "@types/node": "^17.0.45",
64 | "@types/react": "^18.2.6",
65 | "@types/react-dom": "^18.2.4",
66 | "@types/react-syntax-highlighter": "^15.5.6",
67 | "@typescript-eslint/parser": "^5.59.7",
68 | "autoprefixer": "^10.4.14",
69 | "eslint": "^8.40.0",
70 | "eslint-config-next": "13.4.7-canary.1",
71 | "eslint-config-prettier": "^8.8.0",
72 | "eslint-plugin-tailwindcss": "^3.12.0",
73 | "postcss": "^8.4.23",
74 | "prettier": "^2.8.8",
75 | "tailwind-merge": "^1.12.0",
76 | "tailwindcss": "^3.3.2",
77 | "tailwindcss-animate": "^1.0.5",
78 | "typescript": "^5.1.3"
79 | },
80 | "packageManager": "pnpm@8.6.3"
81 | }
82 |
--------------------------------------------------------------------------------
/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/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/public/favicon.ico
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working
2 | # directory name when running `supabase init`.
3 | project_id = "vercel-ai-chatbot"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = ["public", "storage", "graphql_public"]
11 | # Extra schemas to add to the search_path of every request. public is always included.
12 | extra_search_path = ["public", "extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 1000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
21 | # server_version;` on the remote database to check.
22 | major_version = 15
23 |
24 | [studio]
25 | # Port to use for Supabase Studio.
26 | port = 54323
27 |
28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
29 | # are monitored, and you can view the emails that would have been sent from the web interface.
30 | [inbucket]
31 | # Port to use for the email testing server web interface.
32 | port = 54324
33 | smtp_port = 54325
34 | pop3_port = 54326
35 |
36 | [storage]
37 | # The maximum file size allowed (e.g. "5MB", "500KB").
38 | file_size_limit = "50MiB"
39 |
40 | [auth]
41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
42 | # in emails.
43 | site_url = "http://localhost:3000"
44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
45 | additional_redirect_urls = ["http://localhost:3000/api/auth/callback"]
46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
47 | # week).
48 | jwt_expiry = 3600*24
49 | # Allow/disallow new user signups to your project.
50 | enable_signup = true
51 |
52 | [auth.email]
53 | # Allow/disallow new user signups via email to your project.
54 | enable_signup = false
55 | # If enabled, a user will be required to confirm any email change on both the old, and new email
56 | # addresses. If disabled, only the new email is required to confirm.
57 | double_confirm_changes = false
58 | # If enabled, users need to confirm their email address before signing in.
59 | enable_confirmations = false
60 |
61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
64 | [auth.external.github]
65 | enabled = true
66 | client_id = "env(AUTH_GITHUB_ID)"
67 | secret = "env(AUTH_GITHUB_SECRET)"
68 | # Overrides the default auth redirectUrl.
69 | redirect_uri = ""
70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
71 | # or any other third-party OIDC providers.
72 | url = ""
73 |
74 | [analytics]
75 | enabled = false
76 | port = 54327
77 | vector_port = 54328
78 | # Setup BigQuery project to enable log viewer on local development stack.
79 | # See: https://supabase.com/docs/guides/getting-started/local-development#enabling-local-logging
80 | gcp_project_id = ""
81 | gcp_project_number = ""
82 | gcp_jwt_path = "supabase/gcloud.json"
83 |
--------------------------------------------------------------------------------
/supabase/functions/client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createClientComponentClient,
3 | createRouteHandlerClient
4 | } from '@supabase/auth-helpers-nextjs'
5 |
6 | import { Database } from '@/lib/db_types'
7 | import { cookies } from 'next/headers'
8 | import { createClient } from '@supabase/supabase-js'
9 |
10 | // Supabase admin/service_role client for use in server-side code
11 | export const getServiceRoleServerSupabaseClient = () =>
12 | createClient(
13 | process.env.NEXT_PUBLIC_SUPABASE_URL || '',
14 | process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY || '',
15 | {
16 | auth: {
17 | persistSession: false
18 | }
19 | }
20 | )
21 |
22 | // Supabase authorized client for use in server-side code
23 | export const getServerSupabaseClient = () => {
24 | const token = cookies().get('web3jwt')
25 | if (!token) {
26 | return createRouteHandlerClient({ cookies })
27 | } else {
28 | const url = process.env.NEXT_PUBLIC_SUPABASE_URL || ''
29 | const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
30 | const options = {
31 | global: {
32 | headers: { Authorization: `Bearer ${token}` }
33 | },
34 | auth: {
35 | persistSession: false
36 | }
37 | }
38 | return createClient(url, anonKey, options)
39 | }
40 | }
41 |
42 | // Supabase authorized client for use in client-side code
43 | export const getClientSupabaseClient = () => {
44 | const token = cookies().get('web3jwt')
45 |
46 | if (!token) {
47 | return createClientComponentClient()
48 | } else {
49 | const url = process.env.NEXT_PUBLIC_SUPABASE_URL || ''
50 | const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''
51 | const headers = {
52 | global: { headers: { Authorization: `Bearer ${token}` } },
53 | auth: { persistSession: false }
54 | }
55 | return createClient(url, anonKey, headers)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/supabase/migrations/20230707053030_init.sql:
--------------------------------------------------------------------------------
1 | create table "public"."chats" (
2 | "id" text not null,
3 | "user_id" uuid null default auth.uid (),
4 | "payload" jsonb
5 | );
6 |
7 | CREATE UNIQUE INDEX chats_pkey ON public.chats USING btree (id);
8 |
9 | alter table "public"."chats" add constraint "chats_pkey" PRIMARY KEY using index "chats_pkey";
10 |
11 | alter table "public"."chats" add constraint "chats_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
12 |
13 | alter table "public"."chats" validate constraint "chats_user_id_fkey";
14 |
15 | -- RLS
16 | alter table "public"."chats" enable row level security;
17 |
18 | create policy "Allow public read for shared chats"
19 | on "public"."chats"
20 | as permissive
21 | for select
22 | to public
23 | using (((payload ->> 'sharePath'::text) IS NOT NULL));
24 |
25 | create policy "Allow full access to own chats"
26 | on "public"."chats"
27 | as permissive
28 | for all
29 | to authenticated
30 | using ((auth.uid() = user_id))
31 | with check ((auth.uid() = user_id));
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oceanprotocol/tokengated-next-chatgpt/215b3776ef8055a461792f05b38a89bd345a7759/supabase/seed.sql
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme')
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ['class'],
6 | content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: '2rem',
11 | screens: {
12 | '2xl': '1400px'
13 | }
14 | },
15 | extend: {
16 | fontFamily: {
17 | sans: ['var(--font-sans)', ...fontFamily.sans]
18 | },
19 | colors: {
20 | border: 'hsl(var(--border))',
21 | input: 'hsl(var(--input))',
22 | ring: 'hsl(var(--ring))',
23 | background: 'hsl(var(--background))',
24 | foreground: 'hsl(var(--foreground))',
25 | primary: {
26 | DEFAULT: 'hsl(var(--primary))',
27 | foreground: 'hsl(var(--primary-foreground))'
28 | },
29 | secondary: {
30 | DEFAULT: 'hsl(var(--secondary))',
31 | foreground: 'hsl(var(--secondary-foreground))'
32 | },
33 | destructive: {
34 | DEFAULT: 'hsl(var(--destructive))',
35 | foreground: 'hsl(var(--destructive-foreground))'
36 | },
37 | muted: {
38 | DEFAULT: 'hsl(var(--muted))',
39 | foreground: 'hsl(var(--muted-foreground))'
40 | },
41 | accent: {
42 | DEFAULT: 'hsl(var(--accent))',
43 | foreground: 'hsl(var(--accent-foreground))'
44 | },
45 | popover: {
46 | DEFAULT: 'hsl(var(--popover))',
47 | foreground: 'hsl(var(--popover-foreground))'
48 | },
49 | card: {
50 | DEFAULT: 'hsl(var(--card))',
51 | foreground: 'hsl(var(--card-foreground))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: `var(--radius)`,
56 | md: `calc(var(--radius) - 2px)`,
57 | sm: 'calc(var(--radius) - 4px)'
58 | },
59 | keyframes: {
60 | 'accordion-down': {
61 | from: { height: 0 },
62 | to: { height: 'var(--radix-accordion-content-height)' }
63 | },
64 | 'accordion-up': {
65 | from: { height: 'var(--radix-accordion-content-height)' },
66 | to: { height: 0 }
67 | },
68 | 'slide-from-left': {
69 | '0%': {
70 | transform: 'translateX(-100%)'
71 | },
72 | '100%': {
73 | transform: 'translateX(0)'
74 | }
75 | },
76 | 'slide-to-left': {
77 | '0%': {
78 | transform: 'translateX(0)'
79 | },
80 | '100%': {
81 | transform: 'translateX(-100%)'
82 | }
83 | }
84 | },
85 | animation: {
86 | 'slide-from-left':
87 | 'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
88 | 'slide-to-left':
89 | 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
90 | 'accordion-down': 'accordion-down 0.2s ease-out',
91 | 'accordion-up': 'accordion-up 0.2s ease-out'
92 | }
93 | }
94 | },
95 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
96 | }
97 |
--------------------------------------------------------------------------------
/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": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------