Skip to content

Commit 7df5d0d

Browse files
Refactor to use hooks and supabase
1 parent 93c6417 commit 7df5d0d

File tree

156 files changed

+10102
-9346
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

156 files changed

+10102
-9346
lines changed

.env.example

+4-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
1-
# Then get your Google Gemini API Key here: https://cloud.google.com/vertex-ai
2-
GOOGLE_GENERATIVE_AI_API_KEY=XXXXXXXX
1+
# Create an API key here https://platform.openai.com/account/api-keys
2+
OPENAI_API_KEY=****
33

4-
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
5-
AUTH_SECRET=XXXXXXXX
6-
7-
# Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
8-
KV_URL=XXXXXXXX
9-
KV_REST_API_URL=XXXXXXXX
10-
KV_REST_API_TOKEN=XXXXXXXX
11-
KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX
12-
13-
# Get your kasada configurations here: https://kasada.io
14-
KASADA_API_ENDPOINT=XXXXXXXX
15-
KASADA_API_VERSION=XXXXXXXX
16-
KASADA_HEADER_HOST=XXXXXXXX
4+
# Create an access token here https://supabase.com/dashboard/account/tokens
5+
SUPABASE_ACCESS_TOKEN=****

ai/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// import { openai } from "@ai-sdk/openai";
2+
import { experimental_wrapLanguageModel as wrapLanguageModel } from "ai";
3+
import { ragMiddleware } from "./rag-middleware";
4+
import { google } from "@ai-sdk/google";
5+
6+
export const customModel = wrapLanguageModel({
7+
model: google("gemini-1.5-pro-002"),
8+
middleware: ragMiddleware,
9+
});

ai/rag-middleware.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Experimental_LanguageModelV1Middleware } from "ai";
2+
3+
export const ragMiddleware: Experimental_LanguageModelV1Middleware = {};

app/(auth)/actions.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use server";
2+
3+
import { redirect } from "next/navigation";
4+
import { revalidatePath } from "next/cache";
5+
import { createClient } from "@/utils/supabase/server";
6+
7+
export interface LoginActionState {
8+
status: "idle" | "in_progress" | "success" | "failed";
9+
}
10+
11+
export const login = async (
12+
_: LoginActionState,
13+
formData: FormData,
14+
): Promise<LoginActionState> => {
15+
const supabase = createClient();
16+
17+
const { error } = await supabase.auth.signInWithPassword({
18+
email: formData.get("email") as string,
19+
password: formData.get("password") as string,
20+
});
21+
22+
if (error) {
23+
return { status: "failed" } as LoginActionState;
24+
}
25+
26+
revalidatePath("/", "layout");
27+
redirect("/");
28+
};
29+
30+
export interface RegisterActionState {
31+
status: "idle" | "in_progress" | "success" | "failed" | "user_exists";
32+
}
33+
34+
export const register = async (_: RegisterActionState, formData: FormData) => {
35+
const supabase = createClient();
36+
37+
let email = formData.get("email") as string;
38+
let password = formData.get("password") as string;
39+
40+
const { data, error } = await supabase.auth.signUp({ email, password });
41+
42+
if (error) {
43+
if (error.code === "user_already_exists") {
44+
return { status: "user_exists" } as RegisterActionState;
45+
}
46+
}
47+
48+
const { user, session } = data;
49+
50+
if (user && session) {
51+
const { error } = await supabase.auth.signInWithPassword({
52+
email: formData.get("email") as string,
53+
password: formData.get("password") as string,
54+
});
55+
56+
if (error) {
57+
return { status: "failed" } as LoginActionState;
58+
}
59+
60+
revalidatePath("/", "layout");
61+
redirect("/");
62+
} else {
63+
return { status: "failed" } as RegisterActionState;
64+
}
65+
};
66+
67+
export const getUserFromSession = async () => {
68+
const supabase = createClient();
69+
const { data } = await supabase.auth.getUser();
70+
return data.user;
71+
};
72+
73+
export const signOut = async () => {
74+
const supabase = createClient();
75+
const { error } = await supabase.auth.signOut();
76+
77+
if (!error) {
78+
redirect("/login");
79+
}
80+
};

app/(auth)/login/page.tsx

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { Form } from "@/components/form";
5+
import { SubmitButton } from "@/components/submit-button";
6+
import { useActionState, useEffect } from "react";
7+
import { login, LoginActionState } from "../actions";
8+
import { toast } from "sonner";
9+
import { useRouter } from "next/navigation";
10+
11+
export default function Page() {
12+
const router = useRouter();
13+
14+
const [state, formAction] = useActionState<LoginActionState, FormData>(
15+
login,
16+
{
17+
status: "idle",
18+
},
19+
);
20+
21+
useEffect(() => {
22+
if (state.status === "failed") {
23+
toast.error("Invalid credentials!");
24+
} else if (state.status === "success") {
25+
router.refresh();
26+
}
27+
}, [state.status, router]);
28+
29+
return (
30+
<div className="flex h-screen w-screen items-center justify-center bg-background">
31+
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
32+
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
33+
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign In</h3>
34+
<p className="text-sm text-gray-500 dark:text-zinc-400">
35+
Use your email and password to sign in
36+
</p>
37+
</div>
38+
<Form action={formAction}>
39+
<SubmitButton>Sign in</SubmitButton>
40+
<p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
41+
{"Don't have an account? "}
42+
<Link
43+
href="/register"
44+
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
45+
>
46+
Sign up
47+
</Link>
48+
{" for free."}
49+
</p>
50+
</Form>
51+
</div>
52+
</div>
53+
);
54+
}

app/(auth)/register/page.tsx

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { Form } from "@/components/form";
5+
import { SubmitButton } from "@/components/submit-button";
6+
import { register, RegisterActionState } from "../actions";
7+
import { useActionState, useEffect } from "react";
8+
import { toast } from "sonner";
9+
import { useRouter } from "next/navigation";
10+
11+
export default function Page() {
12+
const router = useRouter();
13+
const [state, formAction] = useActionState<RegisterActionState, FormData>(
14+
register,
15+
{
16+
status: "idle",
17+
},
18+
);
19+
20+
useEffect(() => {
21+
if (state.status === "user_exists") {
22+
toast.error("Account already exists");
23+
} else if (state.status === "failed") {
24+
toast.error("Failed to create account");
25+
} else if (state.status === "success") {
26+
toast.success("Account created successfully");
27+
router.refresh();
28+
}
29+
}, [state, router]);
30+
31+
return (
32+
<div className="flex h-screen w-screen items-center justify-center bg-background">
33+
<div className="w-full max-w-md overflow-hidden rounded-2xl gap-12 flex flex-col">
34+
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
35+
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign Up</h3>
36+
<p className="text-sm text-gray-500 dark:text-zinc-400">
37+
Create an account with your email and password
38+
</p>
39+
</div>
40+
<Form action={formAction}>
41+
<SubmitButton>Sign Up</SubmitButton>
42+
<p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
43+
{"Already have an account? "}
44+
<Link
45+
href="/login"
46+
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
47+
>
48+
Sign in
49+
</Link>
50+
{" instead."}
51+
</p>
52+
</Form>
53+
</div>
54+
</div>
55+
);
56+
}

app/(chat)/[id]/page.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Message } from "ai";
2+
import { Chat } from "@/utils/supabase/schema";
3+
import { getChatById } from "../actions";
4+
import { notFound } from "next/navigation";
5+
import { Chat as PreviewChat } from "@/components/chat";
6+
7+
export default async function Page({ params }: { params: any }) {
8+
const { id } = params;
9+
const chatFromDb = await getChatById({ id });
10+
11+
if (!chatFromDb) {
12+
notFound();
13+
}
14+
15+
// type casting
16+
const chat: Chat = {
17+
...chatFromDb,
18+
messages: chatFromDb.messages as Message[],
19+
};
20+
21+
return <PreviewChat id={chat.id} initialMessages={chat.messages} />;
22+
}

app/(chat)/actions.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createClient } from "@/utils/supabase/server";
2+
3+
export async function saveChat({
4+
id,
5+
messages,
6+
userId,
7+
}: {
8+
id: string;
9+
messages: any;
10+
userId: string;
11+
}) {
12+
const supabase = createClient();
13+
14+
await supabase.from("chat").upsert({
15+
id,
16+
messages,
17+
userId,
18+
});
19+
}
20+
21+
export async function getChatById({ id }: { id: string }) {
22+
const supabase = createClient();
23+
24+
const { data: chat } = await supabase
25+
.from("chat")
26+
.select("*")
27+
.eq("id", id)
28+
.single();
29+
30+
return chat;
31+
}

app/(chat)/api/chat/route.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { customModel } from "@/ai";
2+
import { saveChat } from "@/app/(chat)/actions";
3+
import { convertToCoreMessages, streamText } from "ai";
4+
import { getUserFromSession } from "@/app/(auth)/actions";
5+
import { createClient } from "@/utils/supabase/server";
6+
7+
export async function POST(request: Request) {
8+
const { id, messages, selectedFilePathnames } = await request.json();
9+
10+
const user = await getUserFromSession();
11+
12+
if (!user) {
13+
return new Response("Unauthorized", { status: 401 });
14+
}
15+
16+
const result = await streamText({
17+
model: customModel,
18+
system:
19+
"you are a friendly assistant! keep your responses concise and helpful.",
20+
messages: convertToCoreMessages(messages),
21+
experimental_providerMetadata: {
22+
files: {
23+
selection: selectedFilePathnames,
24+
},
25+
},
26+
onFinish: async ({ text }) => {
27+
await saveChat({
28+
id,
29+
messages: [...messages, { role: "assistant", content: text }],
30+
userId: user.id,
31+
});
32+
},
33+
experimental_telemetry: {
34+
isEnabled: true,
35+
functionId: "stream-text",
36+
},
37+
});
38+
39+
return result.toDataStreamResponse({});
40+
}
41+
42+
export async function DELETE(request: Request) {
43+
const { searchParams } = new URL(request.url);
44+
const id = searchParams.get("id");
45+
46+
if (!id) {
47+
return new Response("Not Found", { status: 404 });
48+
}
49+
50+
const supabase = createClient();
51+
52+
try {
53+
const { data, error } = await supabase.from("chat").delete().eq("id", id);
54+
55+
if (error) throw error;
56+
57+
return Response.json(data);
58+
} catch (error) {
59+
return new Response("An error occurred while processing your request", {
60+
status: 500,
61+
});
62+
}
63+
}

app/(chat)/api/files/upload/route.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { getUserFromSession } from "@/app/(auth)/actions";
2+
import { createClient } from "@/utils/supabase/server";
3+
import { NextResponse } from "next/server";
4+
5+
export async function POST(request: Request) {
6+
const user = await getUserFromSession();
7+
8+
if (!user) {
9+
return Response.redirect("/login");
10+
}
11+
12+
if (request.body === null) {
13+
return new Response("Request body is empty", { status: 400 });
14+
}
15+
16+
const supabase = createClient();
17+
18+
try {
19+
const formData = await request.formData();
20+
const file = formData.get("file") as File;
21+
22+
if (!file) {
23+
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
24+
}
25+
26+
const filename = file.name;
27+
const fileBuffer = await file.arrayBuffer();
28+
29+
const { data, error } = await supabase.storage
30+
.from("attachments")
31+
.upload(filename, fileBuffer, {
32+
contentType: file.type,
33+
upsert: true,
34+
});
35+
36+
if (error) {
37+
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
38+
}
39+
40+
return NextResponse.json({ data });
41+
} catch (error) {
42+
return NextResponse.json(
43+
{ error: "Failed to process request" },
44+
{ status: 500 },
45+
);
46+
}
47+
}

0 commit comments

Comments
 (0)