Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkuang0 committed Sep 17, 2023
1 parent 3e0f1f9 commit cac8c70
Show file tree
Hide file tree
Showing 45 changed files with 3,955 additions and 174 deletions.
30 changes: 30 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL= # /auth/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL= # /auth/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL= # /
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL= # /

# database
DATABASE_URL=

# aws s3
NEXT_PUBLIC_S3_ACCESS_KEY_ID=
NEXT_PUBLIC_S3_SECRET_ACCESS_KEY=
NEXT_PUBLIC_S3_BUCKET_NAME=
NEXT_PUBLIC_S3_REGION=

NEXT_PUBLIC_URL= # http://localhost:3000

# pinecone
PINECONE_ENVIRONMENT=
PINECONE_API_KEY=
PINECONE_INDEX_NAME=
PINECONE_MATCH_SCORE_THRESHOLD= # 0.7

# openai
OPENAI_API_KEY=

# stripe
STRIPE_API_KEY=
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "next/core-web-vitals"
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

.env
*.http
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"semi": false
}
73 changes: 73 additions & 0 deletions app/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client'

import { uploadToS3 } from '@/lib/s3'
import { Inbox, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { useDropzone } from 'react-dropzone'
import { toast } from 'react-hot-toast'
import { createChat } from './utils'
import { useRouter } from 'next/navigation'

const FileUpload = () => {
const [isUploading, setIsUploading] = useState(false)
const [isCreatingChat, setIsCreatingChat] = useState(false)
const router = useRouter()

const { getRootProps, getInputProps } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
maxFiles: 1,
onDrop: async (acceptedFiles) => {
const file = acceptedFiles[0]
if (file.size > 10 * 1024 * 1024) {
toast.error('File too large (limit: 10MB)')
return
}

try {
setIsUploading(true)
const fileKey = await uploadToS3(acceptedFiles[0])
toast.success('File uploaded successfully!')

setIsCreatingChat(true)
const chatId = await createChat(fileKey)
toast.success('Chat created successfully!')

router.push(`/chat/${chatId}`)
} catch (err) {
toast.error('Something went wrong ...')
console.log(err)
} finally {
setIsUploading(false)
setIsCreatingChat(false)
}
},
})

return (
<div className="p-2 bg-white rounded-2xl">
<div
{...getRootProps({
className:
'border-dashed border-2 rounded-xl cursor-pointer bg-gray-50 py-8 flex justify-center items-center flex-col hover:border-blue-300 focus:border-blue-300 focus:border-solid outline-none',
})}
>
<input {...getInputProps()} />
{isUploading || isCreatingChat ? (
<>
<Loader2 className="w-10 h-10 text-blue-500 animate-spin" />
<p className="mt-2 text-sm text-slate-400">Uploading...</p>
</>
) : (
<>
<Inbox className="w-10 h-10 text-blue-500" />
<p className="mt-2 text-sm text-slate-400">Drop PDF Here</p>
</>
)}
</div>
</div>
)
}

export default FileUpload
34 changes: 34 additions & 0 deletions app/SubscriptionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client'

import { Button } from '@/components/ui/button'
import { useState } from 'react'

const SubscriptionButton = ({ isPro }: { isPro: boolean }) => {
const { isLoading, handleSubscription } = useSubscriptionHandler()

return (
<Button disabled={isLoading} onClick={handleSubscription}>
{isPro ? 'Manage Subscriptions' : 'Get Pro'}
</Button>
)
}

function useSubscriptionHandler() {
const [isLoading, setIsLoading] = useState(false)
const handleSubscription = async () => {
try {
setIsLoading(true)
const res = await fetch(process.env.NEXT_PUBLIC_URL + '/api/stripe')
const { url } = await res.json()
window.location.href = url
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}

return { isLoading, handleSubscription }
}

export default SubscriptionButton
87 changes: 87 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from 'next/server'
import { Configuration, OpenAIApi } from 'openai-edge'
import { Message, OpenAIStream, StreamingTextResponse } from 'ai'
import { db } from '@/lib/db'
import { chats } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { getContext } from '@/lib/context'
import { messages as _messages } from '@/lib/db/schema'

const config = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
})

const openai = new OpenAIApi(config)

export async function POST(request: NextRequest) {
try {
const {
messages,
chatId,
}: {
messages: Message[]
chatId: number
} = await request.json()

const fileKey = await getFileKeyByChatId(chatId, () => {
return NextResponse.json({ error: 'Chat not found' })
})

const lastMessage = messages[messages.length - 1]
const context = await getContext(lastMessage.content, fileKey)

const prompt = {
role: 'system',
content: `AI assistant is a brand new, powerful, human-like artificial intelligence.
The traits of AI include expert knowledge, helpfulness, cleverness, and articulateness.
AI is a well-behaved and well-mannered individual.
AI is always friendly, kind, and inspiring, and he is eager to provide vivid and thoughtful responses to the user.
AI has the sum of all knowledge in their brain, and is able to accurately answer nearly any question about any topic in conversation.
START CONTEXT BLOCK
${context}
END OF CONTEXT BLOCK
AI assistant will take into account any CONTEXT BLOCK that is provided in a conversation.
If the context does not provide the answer to question, the AI assistant will say, "I'm sorry, but I don't know the answer to that question".
AI assistant will not apologize for previous responses, but instead will indicated new information was gained.
AI assistant will not invent anything that is not drawn directly from the context.`,
}

const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
prompt as Message,
...messages.filter((msg) => msg.role === 'user'),
],
stream: true,
})

const stream = OpenAIStream(response, {
onStart: async () => {
await db.insert(_messages).values({
chatId,
content: lastMessage.content,
role: 'user',
})
},
onCompletion: async (completion) => {
await db.insert(_messages).values({
chatId,
content: completion,
role: 'system',
})
},
})
return new StreamingTextResponse(stream)
} catch (error) {
return NextResponse.json(
{ error: 'Something is going wrong ...' },
{ status: 500 }
)
}
}

async function getFileKeyByChatId(chatId: number, onError: Function) {
const _chats = await db.select().from(chats).where(eq(chats.id, chatId))
if (_chats.length !== 1) onError()
return _chats[0].fileKey
}
56 changes: 56 additions & 0 deletions app/api/create-chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadS3IntoPinecone } from '@/lib/pinecone'
import { db } from '@/lib/db'
import { chats } from '@/lib/db/schema'
import { auth } from '@clerk/nextjs'
import { z } from 'zod'

const postBodySchema = z.object({
fileKey: z.string(),
})

function getS3Url(file_key: string) {
return `https://${process.env.NEXT_PUBLIC_S3_BUCKET_NAME}.s3.${process.env.NEXT_PUBLIC_S3_REGION}.amazonaws.com/${file_key}`
}

export async function POST(request: NextRequest) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

try {
const postBody = await request.json()
const validation = postBodySchema.safeParse(postBody)

if (!validation.success) {
return NextResponse.json(
{ error: validation.error.errors },
{ status: 400 }
)
}

const { fileKey } = postBody
await loadS3IntoPinecone(fileKey)

const returns = await db
.insert(chats)
.values({
fileKey,
pdfName: fileKey.replace('uploads/', ''),
pdfUrl: getS3Url(fileKey),
userId,
})
.returning({
insertedId: chats.id,
})

return NextResponse.json({ error: false, chatId: returns[0].insertedId })
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
14 changes: 14 additions & 0 deletions app/api/get-messages/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { db } from '@/lib/db'
import { messages } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
const { chatId }: { chatId: number } = await request.json()
const _messages = await db
.select()
.from(messages)
.where(eq(messages.chatId, chatId))

return NextResponse.json(_messages)
}
67 changes: 67 additions & 0 deletions app/api/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { db } from '@/lib/db'
import { userSubscriptions } from '@/lib/db/schema'
import { stripe } from '@/lib/stripe'
import { auth, currentUser } from '@clerk/nextjs'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'

const return_url = process.env.NEXT_PUBLIC_URL + '/'

export async function GET() {
try {
const { userId } = await auth()
const user = await currentUser()

if (!userId) {
return new NextResponse('Unauthorized', { status: 401 })
}

const _userSubscriptions = await db
.select()
.from(userSubscriptions)
.where(eq(userSubscriptions.userId, userId))

// trying to cancel at the billing portal
if (_userSubscriptions[0] && _userSubscriptions[0].stripeCustomerId) {
const stripeSession = await stripe.billingPortal.sessions.create({
customer: _userSubscriptions[0].stripeCustomerId,
return_url,
})
return NextResponse.json({ url: stripeSession.url })
}

// user's first time strying to subscribe
const stripeSession = await stripe.checkout.sessions.create({
success_url: return_url,
cancel_url: return_url,
payment_method_types: ['card'],
mode: 'subscription',
billing_address_collection: 'auto',
customer_email: user?.emailAddresses[0].emailAddress,
line_items: [
{
price_data: {
currency: 'USD',
product_data: {
name: 'ChatPDF Clone Pro',
description: 'Unlimited PDF uploads!',
},
unit_amount: 2000,
recurring: {
interval: 'month',
},
},
quantity: 1,
},
],
metadata: {
userId,
},
})

return NextResponse.json({ url: stripeSession.url })
} catch (error) {
console.log('stripe error', error)
return new NextResponse('Internal server error', { status: 500 })
}
}
Loading

0 comments on commit cac8c70

Please sign in to comment.