Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…th-redis into fix-image-preview-on-chat
  • Loading branch information
thiagobarbosa committed Nov 11, 2024
2 parents b8d9008 + e2f3412 commit bbbf10a
Show file tree
Hide file tree
Showing 20 changed files with 671 additions and 542 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ BLOB_READ_WRITE_TOKEN=****

# Instructions to create a database here: https://vercel.com/docs/storage/vercel-postgres/quickstart
POSTGRES_URL=****

# Get your Upstash Redis REST API URL and Token here: https://console.upstash.com/
UPSTASH_REDIS_REST_URL=****
UPSTASH_REDIS_REST_TOKEN=****
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ build
# misc
.DS_Store
*.pem
.idea

# debug
npm-debug.log*
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<a href="https://chat.vercel.ai/">
<img alt="Next.js 14 and App Router-ready AI chatbot." src="app/(chat)/opengraph-image.png">
<h1 align="center">Next.js AI Chatbot</h1>
<h1 align="center">Next.js AI Chatbot With Redis</h1>
</a>

<p align="center">
An Open-Source AI Chatbot Template Built With Next.js and the AI SDK by Vercel.
This is a fork of the <a href="https://github.com/vercel/ai-chatbot">Next.js AI Chatbot</a> template but with Redis as the data store instead of Postgres.
</p>

<p align="center">
Expand All @@ -13,7 +12,15 @@
<a href="#deploy-your-own"><strong>Deploy Your Own</strong></a> ·
<a href="#running-locally"><strong>Running locally</strong></a>
</p>
<br/>

## About Redis
The Redis support is provided by the [Upstash](https://upstash.com/) service. You will need to create an account and add the following environment variables to your `.env` files:

- `UPSTASH_REDIS_REST_URL`: The REST URL for your Upstash Redis instance.
- `UPSTASH_REDIS_REST_TOKEN`: The REST token for your Upstash Redis instance.

Both those values can be found in the [Upstash console](https://console.upstash.com/redis).\
Upstash have a free tier that allows **10,000** commands per day, which should be more than enough for small projects - even on production environments.

## Features

Expand All @@ -28,7 +35,7 @@
- Styling with [Tailwind CSS](https://tailwindcss.com)
- Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility
- Data Persistence
- [Vercel Postgres powered by Neon](https://vercel.com/storage/postgres) for saving chat history and user data
- [Upstash Redis](https://upstash.com) for saving chat history and user data
- [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage
- [NextAuth.js](https://github.com/nextauthjs/next-auth)
- Simple and secure authentication
Expand Down
67 changes: 33 additions & 34 deletions app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
"use server";
'use server'

import { z } from "zod";
import { z } from 'zod'

import { createUser, getUser } from "@/db/queries";

import { signIn } from "./auth";
import { signIn } from '@/app/(auth)/auth'
import { createUser, getUser } from '@/db/users'

const authFormSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
})

export interface LoginActionState {
status: "idle" | "in_progress" | "success" | "failed" | "invalid_data";
status: 'idle' | 'in_progress' | 'success' | 'failed' | 'invalid_data';
}

export const login = async (
Expand All @@ -21,34 +20,34 @@ export const login = async (
): Promise<LoginActionState> => {
try {
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});
email: formData.get('email'),
password: formData.get('password'),
})

await signIn("credentials", {
await signIn('credentials', {
email: validatedData.email,
password: validatedData.password,
redirect: false,
});
})

return { status: "success" };
return { status: 'success' }
} catch (error) {
if (error instanceof z.ZodError) {
return { status: "invalid_data" };
return { status: 'invalid_data' }
}

return { status: "failed" };
return { status: 'failed' }
}
};
}

export interface RegisterActionState {
status:
| "idle"
| "in_progress"
| "success"
| "failed"
| "user_exists"
| "invalid_data";
| 'idle'
| 'in_progress'
| 'success'
| 'failed'
| 'user_exists'
| 'invalid_data';
}

export const register = async (
Expand All @@ -57,29 +56,29 @@ export const register = async (
): Promise<RegisterActionState> => {
try {
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});
email: formData.get('email'),
password: formData.get('password'),
})

let [user] = await getUser(validatedData.email);
let user = await getUser(validatedData.email)

if (user) {
return { status: "user_exists" } as RegisterActionState;
return { status: 'user_exists' } as RegisterActionState
} else {
await createUser(validatedData.email, validatedData.password);
await signIn("credentials", {
await createUser(validatedData.email, validatedData.password)
await signIn('credentials', {
email: validatedData.email,
password: validatedData.password,
redirect: false,
});
})

return { status: "success" };
return { status: 'success' }
}
} catch (error) {
if (error instanceof z.ZodError) {
return { status: "invalid_data" };
return { status: 'invalid_data' }
}

return { status: "failed" };
return { status: 'failed' }
}
};
}
29 changes: 14 additions & 15 deletions app/(auth)/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { compare } from "bcrypt-ts";
import NextAuth, { User, Session } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { compare } from 'bcrypt-ts'
import NextAuth, { Session, User } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'

import { getUser } from "@/db/queries";

import { authConfig } from "./auth.config";
import { authConfig } from '@/app/(auth)/auth.config'
import { getUser } from '@/db/users'

interface ExtendedSession extends Session {
user: User;
Expand All @@ -21,20 +20,20 @@ export const {
Credentials({
credentials: {},
async authorize({ email, password }: any) {
let users = await getUser(email);
if (users.length === 0) return null;
let passwordsMatch = await compare(password, users[0].password!);
if (passwordsMatch) return users[0] as any;
let users = await getUser(email)
if (!users) return null
let passwordsMatch = await compare(password, users.password!)
if (passwordsMatch) return users as any
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.id = user.id
}

return token;
return token
},
async session({
session,
Expand All @@ -44,10 +43,10 @@ export const {
token: any;
}) {
if (session.user) {
session.user.id = token.id as string;
session.user.id = token.id as string
}

return session;
return session
},
},
});
})
2 changes: 1 addition & 1 deletion app/(chat)/api/document/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
deleteDocumentsByIdAfterTimestamp,
getDocumentsById,
saveDocument,
} from '@/db/queries';
} from '@/db/documents';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
Expand Down
56 changes: 28 additions & 28 deletions app/(chat)/api/files/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,69 @@
import { put } from "@vercel/blob";
import { NextResponse } from "next/server";
import { z } from "zod";
import { put } from '@vercel/blob'
import { NextResponse } from 'next/server'
import { z } from 'zod'

import { auth } from "@/app/(auth)/auth";
import { auth } from '@/app/(auth)/auth'

const FileSchema = z.object({
file: z
.instanceof(File)
.refine((file) => file.size <= 5 * 1024 * 1024, {
message: "File size should be less than 5MB",
message: 'File size should be less than 5MB',
})
.refine(
(file) =>
["image/jpeg", "image/png", "application/pdf"].includes(file.type),
['image/jpeg', 'image/png', 'application/pdf'].includes(file.type),
{
message: "File type should be JPEG, PNG, or PDF",
message: 'File type should be JPEG, PNG, or PDF',
},
),
});
})

export async function POST(request: Request) {
const session = await auth();
const session = await auth()

if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

if (request.body === null) {
return new Response("Request body is empty", { status: 400 });
return new Response('Request body is empty', { status: 400 })
}

try {
const formData = await request.formData();
const file = formData.get("file") as File;
const formData = await request.formData()
const file = formData.get('file') as File

if (!file) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
return NextResponse.json({ error: 'No file uploaded' }, { status: 400 })
}

const validatedFile = FileSchema.safeParse({ file });
const validatedFile = FileSchema.safeParse({ file })

if (!validatedFile.success) {
const errorMessage = validatedFile.error.errors
.map((error) => error.message)
.join(", ");
.join(', ')

return NextResponse.json({ error: errorMessage }, { status: 400 });
return NextResponse.json({ error: errorMessage }, { status: 400 })
}

const filename = file.name;
const fileBuffer = await file.arrayBuffer();
const filename = file.name
const fileBuffer = await file.arrayBuffer()

try {
const data = await put(`${filename}`, fileBuffer, {
access: "public",
});
const data = await put(`chats/${filename}`, fileBuffer, {
access: 'public',
})

return NextResponse.json(data);
} catch (error) {
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
return NextResponse.json(data)
} catch (error: any) {
return NextResponse.json({ error: `Failed to upload file: ${error.message}` }, { status: 500 })
}
} catch (error) {
} catch (error: any) {
return NextResponse.json(
{ error: "Failed to process request" },
{ error: `An error occurred while processing your request: ${error.message}` },
{ status: 500 },
);
)
}
}
2 changes: 1 addition & 1 deletion app/(chat)/api/history/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { auth } from "@/app/(auth)/auth";
import { getChatsByUserId } from "@/db/queries";
import { getChatsByUserId } from "@/db/chats";

export async function GET() {
const session = await auth();
Expand Down
2 changes: 1 addition & 1 deletion app/(chat)/api/suggestions/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { auth } from '@/app/(auth)/auth';
import { getSuggestionsByDocumentId } from '@/db/queries';
import { getSuggestionsByDocumentId } from '@/db/documents';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
Expand Down
5 changes: 5 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@
}
}

img[alt=md-image] {
width: 150px;
margin: 1rem auto;
}

.ProseMirror {
outline: none;
}
Expand Down
Loading

0 comments on commit bbbf10a

Please sign in to comment.