Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to use hooks and supabase #45

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
# Then get your Google Gemini API Key here: https://cloud.google.com/vertex-ai
# Get your Google Gemini API Key here https://cloud.google.com/vertex-ai
GOOGLE_GENERATIVE_AI_API_KEY=XXXXXXXX

# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=XXXXXXXX

# Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and
KV_URL=XXXXXXXX
KV_REST_API_URL=XXXXXXXX
KV_REST_API_TOKEN=XXXXXXXX
KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX

# Get your kasada configurations here: https://kasada.io
KASADA_API_ENDPOINT=XXXXXXXX
KASADA_API_VERSION=XXXXXXXX
KASADA_HEADER_HOST=XXXXXXXX
# Create an access token here https://supabase.com/dashboard/account/tokens
SUPABASE_ACCESS_TOKEN=****
9 changes: 9 additions & 0 deletions ai/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// import { openai } from "@ai-sdk/openai";
import { experimental_wrapLanguageModel as wrapLanguageModel } from "ai";
import { ragMiddleware } from "./rag-middleware";
import { google } from "@ai-sdk/google";

export const customModel = wrapLanguageModel({
model: google("gemini-1.5-pro-002"),
middleware: ragMiddleware,
});
3 changes: 3 additions & 0 deletions ai/rag-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Experimental_LanguageModelV1Middleware } from "ai";

export const ragMiddleware: Experimental_LanguageModelV1Middleware = {};
80 changes: 80 additions & 0 deletions app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use server";

import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { createClient } from "@/utils/supabase/server";

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

export const login = async (
_: LoginActionState,
formData: FormData,
): Promise<LoginActionState> => {
const supabase = createClient();

const { error } = await supabase.auth.signInWithPassword({
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (error) {
return { status: "failed" } as LoginActionState;
}

revalidatePath("/", "layout");
redirect("/");
};

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

export const register = async (_: RegisterActionState, formData: FormData) => {
const supabase = createClient();

let email = formData.get("email") as string;
let password = formData.get("password") as string;

const { data, error } = await supabase.auth.signUp({ email, password });

if (error) {
if (error.code === "user_already_exists") {
return { status: "user_exists" } as RegisterActionState;
}
}

const { user, session } = data;

if (user && session) {
const { error } = await supabase.auth.signInWithPassword({
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (error) {
return { status: "failed" } as LoginActionState;
}

revalidatePath("/", "layout");
redirect("/");
} else {
return { status: "failed" } as RegisterActionState;
}
};

export const getUserFromSession = async () => {
const supabase = createClient();
const { data } = await supabase.auth.getUser();
return data.user;
};

export const signOut = async () => {
const supabase = createClient();
const { error } = await supabase.auth.signOut();

if (!error) {
redirect("/login");
}
};
54 changes: 54 additions & 0 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import Link from "next/link";
import { Form } from "@/components/form";
import { SubmitButton } from "@/components/submit-button";
import { useActionState, useEffect } from "react";
import { login, LoginActionState } from "../actions";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

export default function Page() {
const router = useRouter();

const [state, formAction] = useActionState<LoginActionState, FormData>(
login,
{
status: "idle",
},
);

useEffect(() => {
if (state.status === "failed") {
toast.error("Invalid credentials!");
} else if (state.status === "success") {
router.refresh();
}
}, [state.status, router]);

return (
<div className="flex h-screen w-screen items-center justify-center bg-background">
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign In</h3>
<p className="text-sm text-gray-500 dark:text-zinc-400">
Use your email and password to sign in
</p>
</div>
<Form action={formAction}>
<SubmitButton>Sign in</SubmitButton>
<p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
{"Don't have an account? "}
<Link
href="/register"
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
>
Sign up
</Link>
{" for free."}
</p>
</Form>
</div>
</div>
);
}
56 changes: 56 additions & 0 deletions app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import Link from "next/link";
import { Form } from "@/components/form";
import { SubmitButton } from "@/components/submit-button";
import { register, RegisterActionState } from "../actions";
import { useActionState, useEffect } from "react";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

export default function Page() {
const router = useRouter();
const [state, formAction] = useActionState<RegisterActionState, FormData>(
register,
{
status: "idle",
},
);

useEffect(() => {
if (state.status === "user_exists") {
toast.error("Account already exists");
} else if (state.status === "failed") {
toast.error("Failed to create account");
} else if (state.status === "success") {
toast.success("Account created successfully");
router.refresh();
}
}, [state, router]);

return (
<div className="flex h-screen w-screen items-center justify-center bg-background">
<div className="w-full max-w-md overflow-hidden rounded-2xl gap-12 flex flex-col">
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign Up</h3>
<p className="text-sm text-gray-500 dark:text-zinc-400">
Create an account with your email and password
</p>
</div>
<Form action={formAction}>
<SubmitButton>Sign Up</SubmitButton>
<p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
{"Already have an account? "}
<Link
href="/login"
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
>
Sign in
</Link>
{" instead."}
</p>
</Form>
</div>
</div>
);
}
22 changes: 22 additions & 0 deletions app/(chat)/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Message } from "ai";
import { Chat } from "@/utils/supabase/schema";
import { getChatById } from "../actions";
import { notFound } from "next/navigation";
import { Chat as PreviewChat } from "@/components/chat";

export default async function Page({ params }: { params: any }) {
const { id } = params;
const chatFromDb = await getChatById({ id });

if (!chatFromDb) {
notFound();
}

// type casting
const chat: Chat = {
...chatFromDb,
messages: chatFromDb.messages as Message[],
};

return <PreviewChat id={chat.id} initialMessages={chat.messages} />;
}
31 changes: 31 additions & 0 deletions app/(chat)/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createClient } from "@/utils/supabase/server";

export async function saveChat({
id,
messages,
userId,
}: {
id: string;
messages: any;
userId: string;
}) {
const supabase = createClient();

await supabase.from("chat").upsert({
id,
messages,
userId,
});
}

export async function getChatById({ id }: { id: string }) {
const supabase = createClient();

const { data: chat } = await supabase
.from("chat")
.select("*")
.eq("id", id)
.single();

return chat;
}
82 changes: 82 additions & 0 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { customModel } from "@/ai";
import { saveChat } from "@/app/(chat)/actions";
import { convertToCoreMessages, streamText } from "ai";
import { getUserFromSession } from "@/app/(auth)/actions";
import { createClient } from "@/utils/supabase/server";
import { z } from "zod";

export async function POST(request: Request) {
const { id, messages, selectedFilePathnames } = await request.json();

const user = await getUserFromSession();

if (!user) {
return new Response("Unauthorized", { status: 401 });
}

const result = await streamText({
model: customModel,
system:
"you are a friendly assistant! keep your responses concise and helpful.",
messages: convertToCoreMessages(messages),
experimental_providerMetadata: {
files: {
selection: selectedFilePathnames,
},
},
maxSteps: 5,
tools: {
getWeather: {
description: "Get the current weather at a location",
parameters: z.object({
latitude: z.number(),
longitude: z.number(),
}),
execute: async ({ latitude, longitude }) => {
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`,
);

const weatherData = await response.json();
return weatherData;
},
},
},
onFinish: async ({ text }) => {
await saveChat({
id,
messages: [...messages, { role: "assistant", content: text }],
userId: user.id,
});
},
experimental_telemetry: {
isEnabled: true,
functionId: "stream-text",
},
});

return result.toDataStreamResponse({});
}

export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");

if (!id) {
return new Response("Not Found", { status: 404 });
}

const supabase = createClient();

try {
const { data, error } = await supabase.from("chat").delete().eq("id", id);

if (error) throw error;

return Response.json(data);
} catch (error) {
return new Response("An error occurred while processing your request", {
status: 500,
});
}
}
Loading