V0 + Claude - RAG AI Agent Frontend

This commit is contained in:
Cole Medin
2024-09-08 17:37:30 -05:00
parent fdea47562d
commit 8bcf7b1b62
26 changed files with 7825 additions and 1 deletions

4
.gitignore vendored
View File

@@ -1,10 +1,12 @@
__pycache__
prep
.env
.env.local
chroma_db
test.py
data
creds
credentials
credentials.json
token.json
token.json
node_modules

View File

@@ -0,0 +1,21 @@
Prompt 1:
Create a full chatbot page where the chat will automatically scroll up as the conversation continues like a chat you'd have in FB messenger, Discord, Slack, etc. Every time the user enters a message, just respond with "placeholder message" after thinking for a couple of seconds.
Prompt 2:
Now change the chatbot so I can talk to my AI Agent through a webhook and still have the chat automatically scroll up as the conversation continues like a chat you'd have in FB messenger, Discord, Slack, etc.
The webhook to invoke for the AI Agent is:
[your webhook endpoint]
Include an authorization header with the authorization [your bearer token, this is optional if you turn it off in n8n]. The payload needs to include "sessionId" and "chatInput". Randomly generate a string for the sessionId and the chatInput is the user input in the frontend. The webhook will respond with a single object "output" in the JSON to display back to the user as the agent's response.
Prompt 3:
Parse the AI responses as Markdown so they look nice. Also make the UI a dark theme and have a navigation header with a bottom shadow. Have a couple of icons for navigation on the left and the title on the right. Also include a side bar on the left with some example conversations like you would see on chatgpt.com.
Prompt 4:
Add Supabase authentication into this component so someone has to sign in to get access to the chatbot and then the session ID can be the Supabase user ID.

View File

@@ -0,0 +1,740 @@
{
"name": "Supabase RAG AI Agent",
"nodes": [
{
"parameters": {
"model": "gpt-4o-mini",
"options": {}
},
"id": "b98c17c5-3bef-4378-8d81-61b127711661",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1,
"position": [
860,
520
],
"credentials": {
"openAiApi": {
"id": "JJjD91oisPv9cs01",
"name": "OpenAi account"
}
}
},
{
"parameters": {
"model": "gpt-4o-mini",
"options": {}
},
"id": "e850aa69-1910-4986-99f2-214a8759b777",
"name": "OpenAI Chat Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1,
"position": [
1880,
460
],
"credentials": {
"openAiApi": {
"id": "JJjD91oisPv9cs01",
"name": "OpenAi account"
}
}
},
{
"parameters": {
"jsonMode": "expressionData",
"jsonData": "={{ $json.data }}",
"options": {
"metadata": {
"metadataValues": [
{
"name": "=file_id",
"value": "={{ $('Set File ID').item.json.file_id }}"
}
]
}
}
},
"id": "9f5b6c56-58f6-4ae5-96fd-f3efd9b35fd6",
"name": "Default Data Loader",
"type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
"typeVersion": 1,
"position": [
2020,
1000
]
},
{
"parameters": {
"model": "text-embedding-3-small",
"options": {}
},
"id": "5eaf6315-fcd0-4a4d-a1b3-37f5793f6e5c",
"name": "Embeddings OpenAI1",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"typeVersion": 1,
"position": [
1860,
1000
],
"credentials": {
"openAiApi": {
"id": "JJjD91oisPv9cs01",
"name": "OpenAi account"
}
}
},
{
"parameters": {
"name": "user_documents",
"description": "Contains all the user's documents that you can check for context to answer user questions."
},
"id": "b3868e8d-fbce-4c16-8174-0e5475cfffba",
"name": "Retrieve Documents",
"type": "@n8n/n8n-nodes-langchain.toolVectorStore",
"typeVersion": 1,
"position": [
1660,
280
]
},
{
"parameters": {
"content": "## Agent Tools for RAG",
"height": 528.85546469693,
"width": 583.4552380860637,
"color": 4
},
"id": "d4a657c6-2f17-4071-8cee-0b1b12f74b1c",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1480,
160
]
},
{
"parameters": {
"content": "## Tool to Add a Google Drive File to Vector DB",
"height": 671.8877842322804,
"width": 2070.8894079025763,
"color": 5
},
"id": "38517d9e-61cf-4b36-bbb2-607a04e870c8",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
260,
700
]
},
{
"parameters": {
"operation": "download",
"fileId": {
"__rl": true,
"value": "={{ $('Set File ID').item.json.file_id }}",
"mode": "id"
},
"options": {
"googleFileConversion": {
"conversion": {
"docsToFormat": "text/plain"
}
}
}
},
"id": "d9e3d812-9158-44e2-b9e9-8c28c5cd0921",
"name": "Download File",
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
1360,
880
],
"executeOnce": true,
"credentials": {
"googleDriveOAuth2Api": {
"id": "cfNochbuJikPwwl2",
"name": "Google Drive account"
}
}
},
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"triggerOn": "specificFolder",
"folderToWatch": {
"__rl": true,
"value": "1914m3M7kRzkd5RJqAfzRY9EBcJrKemZC",
"mode": "list",
"cachedResultName": "Meeting Notes",
"cachedResultUrl": "https://drive.google.com/drive/folders/1914m3M7kRzkd5RJqAfzRY9EBcJrKemZC"
},
"event": "fileCreated",
"options": {}
},
"id": "6c24f48d-2a8e-471d-b477-dd9576e57eec",
"name": "File Created",
"type": "n8n-nodes-base.googleDriveTrigger",
"typeVersion": 1,
"position": [
320,
780
],
"credentials": {
"googleDriveOAuth2Api": {
"id": "cfNochbuJikPwwl2",
"name": "Google Drive account"
}
}
},
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"triggerOn": "specificFolder",
"folderToWatch": {
"__rl": true,
"value": "1914m3M7kRzkd5RJqAfzRY9EBcJrKemZC",
"mode": "list",
"cachedResultName": "Meeting Notes",
"cachedResultUrl": "https://drive.google.com/drive/folders/1914m3M7kRzkd5RJqAfzRY9EBcJrKemZC"
},
"event": "fileUpdated",
"options": {}
},
"id": "7bf930ea-3810-40e3-a252-c13da33660db",
"name": "File Updated",
"type": "n8n-nodes-base.googleDriveTrigger",
"typeVersion": 1,
"position": [
320,
1000
],
"credentials": {
"googleDriveOAuth2Api": {
"id": "cfNochbuJikPwwl2",
"name": "Google Drive account"
}
}
},
{
"parameters": {
"operation": "text",
"options": {}
},
"id": "81e0f199-024b-489d-88dd-e7bf6c3a453b",
"name": "Extract Document Text",
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1,
"position": [
1620,
880
],
"alwaysOutputData": true
},
{
"parameters": {
"model": "text-embedding-3-small",
"options": {}
},
"id": "c0f6a1f6-5732-48e3-8830-bb78263bd352",
"name": "Embeddings OpenAI",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"typeVersion": 1,
"position": [
1700,
560
],
"credentials": {
"openAiApi": {
"id": "JJjD91oisPv9cs01",
"name": "OpenAi account"
}
}
},
{
"parameters": {},
"id": "8920aea0-c877-47b3-9026-d8ab57e8579d",
"name": "Postgres Chat Memory",
"type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
"typeVersion": 1,
"position": [
1000,
520
],
"notesInFlow": false,
"credentials": {
"postgres": {
"id": "tzFFADvpFiUtaZtb",
"name": "Supabase Postgres"
}
}
},
{
"parameters": {
"options": {}
},
"id": "ed70601e-e7e9-4fc5-af9f-bdf4a931fc48",
"name": "Recursive Character Text Splitter",
"type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
"typeVersion": 1,
"position": [
2020,
1200
]
},
{
"parameters": {
"operation": "delete",
"tableId": "documents",
"filterType": "string",
"filterString": "=metadata->>file_id=like.*{{ $json.file_id }}*"
},
"id": "bd9eb3dc-75b9-4577-a210-1951988a389e",
"name": "Delete Old Doc Rows",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1060,
880
],
"alwaysOutputData": true,
"credentials": {
"supabaseApi": {
"id": "azIAMHxdSaq5XhoW",
"name": "Supabase account"
}
}
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "10646eae-ae46-4327-a4dc-9987c2d76173",
"name": "file_id",
"value": "={{ $json.id }}",
"type": "string"
}
]
},
"options": {}
},
"id": "0b439eba-c467-475b-a1a2-481f636f37fe",
"name": "Set File ID",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
740,
880
]
},
{
"parameters": {
"content": "## RAG AI Agent with Chat Interface",
"height": 464.8027193303974,
"width": 1035.6381264595484
},
"id": "7c003acd-f420-474f-9547-33e13c3c357f",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
432.49413148117685,
220
]
},
{
"parameters": {
"tableName": {
"__rl": true,
"value": "documents",
"mode": "list",
"cachedResultName": "documents"
},
"options": {
"queryName": "match_documents"
}
},
"id": "1bda5922-bcfb-489c-a0fb-516d9496d54a",
"name": "Supabase Vector Store",
"type": "@n8n/n8n-nodes-langchain.vectorStoreSupabase",
"typeVersion": 1,
"position": [
1540,
440
],
"credentials": {
"supabaseApi": {
"id": "azIAMHxdSaq5XhoW",
"name": "Supabase account"
}
}
},
{
"parameters": {
"mode": "insert",
"tableName": {
"__rl": true,
"value": "documents",
"mode": "list",
"cachedResultName": "documents"
},
"options": {
"queryName": "match_documents"
}
},
"id": "6c7181ba-9e33-4972-83b4-0efaac7e16a2",
"name": "Insert into Supabase Vectorstore",
"type": "@n8n/n8n-nodes-langchain.vectorStoreSupabase",
"typeVersion": 1,
"position": [
1900,
780
],
"credentials": {
"supabaseApi": {
"id": "azIAMHxdSaq5XhoW",
"name": "Supabase account"
}
}
},
{
"parameters": {
"options": {}
},
"id": "dd1086c5-e813-49ea-971f-ba01ccc6f803",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1300,
300
]
},
{
"parameters": {
"public": true,
"options": {}
},
"id": "e448d46a-7a01-436c-bebd-0cd21c2bfcb3",
"name": "When chat message received",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"typeVersion": 1.1,
"position": [
480,
300
],
"webhookId": "e985d15f-b2f6-456d-be15-97e0b1544a40"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "9a9a245e-f1a1-4282-bb02-a81ffe629f0f",
"name": "chatInput",
"value": "={{ $json?.chatInput || $json.body.chatInput }}",
"type": "string"
},
{
"id": "b80831d8-c653-4203-8706-adedfdb98f77",
"name": "sessionId",
"value": "={{ $json?.sessionId || $json.body.sessionId}}",
"type": "string"
}
]
},
"options": {}
},
"id": "4076ba7c-931d-40f5-872d-c9b2dc669ab1",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
740,
300
]
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.chatInput }}",
"options": {
"systemMessage": "You are a personal assistant who helps answer questions from a corpus of documents when you don't know the answer yourself."
}
},
"id": "50003c9d-29bf-4d0e-9e04-a49804742dc8",
"name": "RAG AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 1.6,
"position": [
960,
300
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "invoke_agent",
"authentication": "headerAuth",
"responseMode": "responseNode",
"options": {}
},
"id": "b575cc9f-71c7-4406-936c-ca39e6eff09b",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
480,
500
],
"webhookId": "67fc16e5-4a6d-4c00-b7a1-db48c4511e9f",
"credentials": {
"httpHeaderAuth": {
"id": "FWbnHWdNKFyEHa1W",
"name": "Header Auth account"
}
}
}
],
"pinData": {},
"connections": {
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "RAG AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"OpenAI Chat Model1": {
"ai_languageModel": [
[
{
"node": "Retrieve Documents",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Retrieve Documents": {
"ai_tool": [
[
{
"node": "RAG AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Download File": {
"main": [
[
{
"node": "Extract Document Text",
"type": "main",
"index": 0
}
]
]
},
"File Created": {
"main": [
[
{
"node": "Set File ID",
"type": "main",
"index": 0
}
]
]
},
"Embeddings OpenAI": {
"ai_embedding": [
[
{
"node": "Supabase Vector Store",
"type": "ai_embedding",
"index": 0
}
]
]
},
"Extract Document Text": {
"main": [
[
{
"node": "Insert into Supabase Vectorstore",
"type": "main",
"index": 0
}
]
]
},
"Embeddings OpenAI1": {
"ai_embedding": [
[
{
"node": "Insert into Supabase Vectorstore",
"type": "ai_embedding",
"index": 0
}
]
]
},
"Default Data Loader": {
"ai_document": [
[
{
"node": "Insert into Supabase Vectorstore",
"type": "ai_document",
"index": 0
}
]
]
},
"Postgres Chat Memory": {
"ai_memory": [
[
{
"node": "RAG AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"Recursive Character Text Splitter": {
"ai_textSplitter": [
[
{
"node": "Default Data Loader",
"type": "ai_textSplitter",
"index": 0
}
]
]
},
"Delete Old Doc Rows": {
"main": [
[
{
"node": "Download File",
"type": "main",
"index": 0
}
]
]
},
"Set File ID": {
"main": [
[
{
"node": "Delete Old Doc Rows",
"type": "main",
"index": 0
}
]
]
},
"File Updated": {
"main": [
[
{
"node": "Set File ID",
"type": "main",
"index": 0
}
]
]
},
"Supabase Vector Store": {
"ai_vectorStore": [
[
{
"node": "Retrieve Documents",
"type": "ai_vectorStore",
"index": 0
}
]
]
},
"When chat message received": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "RAG AI Agent",
"type": "main",
"index": 0
}
]
]
},
"RAG AI Agent": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "1f7ec7d0-ff5e-4448-b0b3-76f4e782baa2",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "620f0d7e3114cb344761d7d45a21ef2a32096f91d8696e7057756042e1999e2c"
},
"id": "cri0nBz0U55sWr4I",
"tags": []
}

View File

@@ -0,0 +1,3 @@
# Rename this to .env.local after adding the two values below!
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

View File

@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,16 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createRouteHandlerClient({ cookies })
await supabase.auth.exchangeCodeForSession(code)
}
// URL to redirect to after sign in process completes
return NextResponse.redirect(requestUrl.origin)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,33 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--radius: 0.5rem;
}
}

View File

@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,5 @@
import AiAgentChatbot from "@/components/ai-agent-chatbot"
export default function Page() {
return <AiAgentChatbot />
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@@ -0,0 +1,232 @@
'use client'
import { useState, useRef, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { SendIcon, MenuIcon, HomeIcon, MessageCircleIcon, SettingsIcon, LogOutIcon } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { Session, AuthChangeEvent } from '@supabase/supabase-js'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { useRouter } from 'next/navigation'
type Message = {
text: string
sender: 'user' | 'bot'
}
const exampleConversations = [
{ title: "Getting Started", id: "getting-started" },
{ title: "Task Planning", id: "task-planning" },
{ title: "Problem Solving", id: "problem-solving" },
{ title: "Creative Writing", id: "creative-writing" },
]
export default function AIAgentChatbot() {
const [messages, setMessages] = useState<Message[]>([])
const [inputMessage, setInputMessage] = useState('')
const [isTyping, setIsTyping] = useState(false)
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const [user, setUser] = useState<any>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const supabase = createClientComponentClient()
const router = useRouter()
useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
}
getUser()
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event: AuthChangeEvent, session: Session | null) => {
setUser(session?.user ?? null)
})
return () => subscription.unsubscribe()
}, [supabase.auth])
useEffect(() => {
scrollToBottom()
}, [messages])
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
const handleSignOut = async () => {
await supabase.auth.signOut()
router.refresh()
}
const handleSendMessage = async () => {
if (inputMessage.trim() === '') return
const newUserMessage: Message = { text: inputMessage, sender: 'user' }
setMessages(prevMessages => [...prevMessages, newUserMessage])
setInputMessage('')
setIsTyping(true)
try {
const response = await fetch('[YOUR n8n WORKFLOW WEBHOOK URL]', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer [YOUR AUTH BEARER TOKEN]'
},
body: JSON.stringify({
sessionId: user?.id,
chatInput: inputMessage
})
})
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
const botResponse: Message = { text: data.output, sender: 'bot' }
setMessages(prevMessages => [...prevMessages, botResponse])
} catch (error) {
console.error('Error:', error)
const errorMessage: Message = { text: 'Sorry, I encountered an error. Please try again.', sender: 'bot' }
setMessages(prevMessages => [...prevMessages, errorMessage])
} finally {
setIsTyping(false)
}
}
if (!user) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-gray-100">
<div className="w-full max-w-md p-8 space-y-8 bg-gray-800 rounded-lg shadow-md">
<h2 className="text-2xl font-bold text-center">Sign In to AI Agent Chatbot</h2>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme="dark"
providers={['google', 'github']}
redirectTo={`${window.location.origin}/auth/callback`}
/>
</div>
</div>
)
}
return (
<div className="flex h-screen bg-gray-900 text-gray-100">
{/* Sidebar */}
<div className={`fixed inset-y-0 left-0 z-50 w-64 bg-gray-800 transform ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out lg:relative lg:translate-x-0`}>
<div className="flex flex-col h-full">
<div className="flex items-center justify-center h-16 bg-gray-900">
<h2 className="text-xl font-bold">Conversations</h2>
</div>
<ScrollArea className="flex-1">
{exampleConversations.map((conv) => (
<button
key={conv.id}
className="w-full text-left px-4 py-2 hover:bg-gray-700 focus:outline-none focus:bg-gray-700"
>
{conv.title}
</button>
))}
</ScrollArea>
</div>
</div>
{/* Main content */}
<div className="flex flex-col flex-1">
{/* Navigation header */}
<header className="flex items-center justify-between px-4 h-16 bg-gray-800 shadow-md">
<div className="flex items-center space-x-4">
<Button
variant="ghost"
size="icon"
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="lg:hidden"
>
<MenuIcon className="h-6 w-6" />
<span className="sr-only">Toggle sidebar</span>
</Button>
<Button variant="ghost" size="icon">
<HomeIcon className="h-6 w-6" />
<span className="sr-only">Home</span>
</Button>
<Button variant="ghost" size="icon">
<MessageCircleIcon className="h-6 w-6" />
<span className="sr-only">Messages</span>
</Button>
<Button variant="ghost" size="icon">
<SettingsIcon className="h-6 w-6" />
<span className="sr-only">Settings</span>
</Button>
</div>
<h1 className="text-xl font-bold">AI Agent Chatbot</h1>
<Button variant="ghost" size="icon" onClick={handleSignOut}>
<LogOutIcon className="h-6 w-6" />
<span className="sr-only">Sign out</span>
</Button>
</header>
{/* Chat area */}
<ScrollArea className="flex-grow p-4">
{messages.map((message, index) => (
<div
key={index}
className={`mb-4 ${
message.sender === 'user' ? 'text-right' : 'text-left'
}`}
>
<span
className={`inline-block p-2 rounded-lg ${
message.sender === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-100'
}`}
>
{message.sender === 'bot' ? (
<ReactMarkdown>{message.text}</ReactMarkdown>
) : (
message.text
)}
</span>
</div>
))}
{isTyping && (
<div className="text-left mb-4">
<span className="inline-block p-2 rounded-lg bg-gray-700 text-gray-100">
Typing...
</span>
</div>
)}
<div ref={messagesEndRef} />
</ScrollArea>
{/* Input area */}
<div className="p-4 border-t border-gray-700">
<form
onSubmit={(e) => {
e.preventDefault()
handleSendMessage()
}}
className="flex space-x-2"
>
<Input
type="text"
placeholder="Type your message..."
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
className="flex-1 bg-gray-700 text-gray-100 border-gray-600 focus:border-blue-500"
/>
<Button type="submit" className="bg-blue-600 hover:bg-blue-700">
<SendIcon className="h-4 w-4" />
<span className="sr-only">Send</span>
</Button>
</form>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
{
variants: {
variant: {
default: "bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
destructive:
"bg-red-500 text-neutral-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
outline:
"border border-neutral-200 bg-white hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
secondary:
"bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
ghost: "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
type ButtonVariantProps = VariantProps<typeof buttonVariants>
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariantProps['variant']
size?: ButtonVariantProps['size']
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-neutral-950 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-neutral-200 dark:bg-neutral-800" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
{
"name": "v0-agent-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@supabase/auth-helpers-nextjs": "^0.10.0",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.45.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.439.0",
"next": "14.2.8",
"react": "^18",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.8",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@@ -0,0 +1,25 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)'
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
};
export default config;

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}