-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3e0f1f9
commit cac8c70
Showing
45 changed files
with
3,955 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,3 +33,6 @@ yarn-error.log* | |
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts | ||
|
||
.env | ||
*.http |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"singleQuote": true, | ||
"semi": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
} | ||
} |
Oops, something went wrong.