Skip to content
Open
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
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ BLOB_READ_WRITE_TOKEN=****
POSTGRES_URL=****


# DB
POSTGRES_USER=demo
POSTGRES_PASSWORD=demo
POSTGRES_DB=chatbot

DATABASE_URL=postgresql://demo:demo@localhost:5434/chatbot
POSTGRES_URL=postgresql://demo:demo@localhost:5434/chatbot

# Instructions to create a Redis store here:
# https://vercel.com/docs/redis
REDIS_URL=****
REDIS_URL=redis://localhost:6379
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ yarn-error.log*
/playwright-report/
/blob-report/
/playwright/*
db-data
redis-data
35 changes: 0 additions & 35 deletions app/(chat)/layout.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions app/[locale]/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Suspense } from "react";
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import { vazirmatn } from "@/lib/fonts";

export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}

export default async function Layout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;

// Validate locale
if (!hasLocale(routing.locales, locale)) {
notFound();
}

// Enable static rendering
setRequestLocale(locale);

const messages = await getMessages();
const isPersian = locale === 'fa';

return (
<Suspense fallback={<div className="flex h-dvh" />}>
<div className={isPersian ? vazirmatn.variable : ''} dir={isPersian ? 'rtl' : 'ltr'} lang={locale}>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</div>
</Suspense>
);
}
29 changes: 18 additions & 11 deletions app/(auth)/login/page.tsx → app/[locale]/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useActionState, useEffect, useState } from "react";
import { useTranslations } from "next-intl";

import { AuthForm } from "@/components/auth-form";
import { SubmitButton } from "@/components/submit-button";
import { toast } from "@/components/toast";
import { type LoginActionState, login } from "../actions";
import { type LoginActionState, login } from "@/app/(auth)/actions";

export default function Page() {
const router = useRouter();
const t = useTranslations('auth.signIn');
const formLabels = useTranslations('auth.form');

const [email, setEmail] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
Expand All @@ -30,12 +33,12 @@ export default function Page() {
if (state.status === "failed") {
toast({
type: "error",
description: "Invalid credentials!",
description: t('errors.invalidCredentials'),
});
} else if (state.status === "invalid_data") {
toast({
type: "error",
description: "Failed validating your submission!",
description: t('errors.validationFailed'),
});
} else if (state.status === "success") {
setIsSuccessful(true);
Expand All @@ -53,22 +56,26 @@ export default function Page() {
<div className="flex h-dvh w-screen items-start justify-center bg-background pt-12 md:items-center md:pt-0">
<div className="flex w-full max-w-md flex-col gap-12 overflow-hidden rounded-2xl">
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
<h3 className="font-semibold text-xl dark:text-zinc-50">Sign In</h3>
<h3 className="font-semibold text-xl dark:text-zinc-50">{t('title')}</h3>
<p className="text-gray-500 text-sm dark:text-zinc-400">
Use your email and password to sign in
{t('description')}
</p>
</div>
<AuthForm action={handleSubmit} defaultEmail={email}>
<SubmitButton isSuccessful={isSuccessful}>Sign in</SubmitButton>
<AuthForm action={handleSubmit} defaultEmail={email} labels={{
email: formLabels('email'),
emailPlaceholder: formLabels('emailPlaceholder'),
password: formLabels('password'),
}}>
<SubmitButton isSuccessful={isSuccessful}>{t('button')}</SubmitButton>
<p className="mt-4 text-center text-gray-600 text-sm dark:text-zinc-400">
{"Don't have an account? "}
{t('noAccount')}{' '}
<Link
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
href="/register"
>
Sign up
</Link>
{" for free."}
{t('signUpLink')}
</Link>{' '}
{t('forFree')}
</p>
</AuthForm>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useActionState, useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { AuthForm } from "@/components/auth-form";
import { SubmitButton } from "@/components/submit-button";
import { toast } from "@/components/toast";
import { type RegisterActionState, register } from "../actions";
import { type RegisterActionState, register } from "@/app/(auth)/actions";

export default function Page() {
const router = useRouter();
const t = useTranslations('auth.signUp');
const formLabels = useTranslations('auth.form');

const [email, setEmail] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
Expand All @@ -27,16 +30,16 @@ export default function Page() {
// biome-ignore lint/correctness/useExhaustiveDependencies: router and updateSession are stable refs
useEffect(() => {
if (state.status === "user_exists") {
toast({ type: "error", description: "Account already exists!" });
toast({ type: "error", description: t('errors.accountExists') });
} else if (state.status === "failed") {
toast({ type: "error", description: "Failed to create account!" });
toast({ type: "error", description: t('errors.createFailed') });
} else if (state.status === "invalid_data") {
toast({
type: "error",
description: "Failed validating your submission!",
description: t('errors.validationFailed'),
});
} else if (state.status === "success") {
toast({ type: "success", description: "Account created successfully!" });
toast({ type: "success", description: t('success') });

setIsSuccessful(true);
updateSession();
Expand All @@ -53,22 +56,26 @@ export default function Page() {
<div className="flex h-dvh w-screen items-start justify-center bg-background pt-12 md:items-center md:pt-0">
<div className="flex w-full max-w-md flex-col gap-12 overflow-hidden rounded-2xl">
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
<h3 className="font-semibold text-xl dark:text-zinc-50">Sign Up</h3>
<h3 className="font-semibold text-xl dark:text-zinc-50">{t('title')}</h3>
<p className="text-gray-500 text-sm dark:text-zinc-400">
Create an account with your email and password
{t('description')}
</p>
</div>
<AuthForm action={handleSubmit} defaultEmail={email}>
<SubmitButton isSuccessful={isSuccessful}>Sign Up</SubmitButton>
<AuthForm action={handleSubmit} defaultEmail={email} labels={{
email: formLabels('email'),
emailPlaceholder: formLabels('emailPlaceholder'),
password: formLabels('password'),
}}>
<SubmitButton isSuccessful={isSuccessful}>{t('button')}</SubmitButton>
<p className="mt-4 text-center text-gray-600 text-sm dark:text-zinc-400">
{"Already have an account? "}
{t('hasAccount')}{' '}
<Link
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
href="/login"
>
Sign in
</Link>
{" instead."}
{t('signInLink')}
</Link>{' '}
{t('instead')}
</p>
</AuthForm>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";
import { Suspense } from "react";
import { setRequestLocale } from "next-intl/server";

import { auth } from "@/app/(auth)/auth";
import { Chat } from "@/components/chat";
Expand All @@ -9,7 +10,10 @@ import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { getChatById, getMessagesByChatId } from "@/lib/db/queries";
import { convertToUIMessages } from "@/lib/utils";

export default function Page(props: { params: Promise<{ id: string }> }) {
export default async function Page(props: { params: Promise<{ id: string; locale: string }> }) {
const { locale } = await props.params;
setRequestLocale(locale);

return (
<Suspense fallback={<div className="flex h-dvh" />}>
<ChatPage params={props.params} />
Expand Down
71 changes: 71 additions & 0 deletions app/[locale]/(chat)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { cookies } from "next/headers";
import Script from "next/script";
import { Suspense } from "react";
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { AppSidebar } from "@/components/app-sidebar";
import { DataStreamProvider } from "@/components/data-stream-provider";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { auth } from "@/app/(auth)/auth";
import { routing } from "@/i18n/routing";
import { vazirmatn } from "@/lib/fonts";

export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}

export default async function Layout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;

// Validate locale
if (!hasLocale(routing.locales, locale)) {
notFound();
}

// Enable static rendering
setRequestLocale(locale);

const messages = await getMessages();
const isPersian = locale === 'fa';

return (
<>
<Script
src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"
strategy="beforeInteractive"
/>
<DataStreamProvider>
<Suspense fallback={<div className="flex h-dvh" />}>
<NextIntlClientProvider messages={messages}>
<div className={isPersian ? vazirmatn.variable : ''} dir={isPersian ? 'rtl' : 'ltr'} lang={locale}>
<SidebarWrapper>{children}</SidebarWrapper>
</div>
</NextIntlClientProvider>
</Suspense>
</DataStreamProvider>
</>
);
}

async function SidebarWrapper({
children,
}: {
children: React.ReactNode;
}) {
const [session, cookieStore] = await Promise.all([auth(), cookies()]);
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";

return (
<SidebarProvider defaultOpen={!isCollapsed}>
<AppSidebar user={session?.user} />
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
);
}
6 changes: 5 additions & 1 deletion app/(chat)/page.tsx → app/[locale]/(chat)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { cookies } from "next/headers";
import { Suspense } from "react";
import { setRequestLocale } from "next-intl/server";
import { Chat } from "@/components/chat";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { generateUUID } from "@/lib/utils";

export default function Page() {
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
setRequestLocale(locale);

return (
<Suspense fallback={<div className="flex h-dvh" />}>
<NewChatPage />
Expand Down
22 changes: 22 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@
/* include utility classes in streamdown */
@source "../node_modules/streamdown/dist/index.js";

/* Vazirmatn font for Persian */
@font-face {
font-family: "Vazirmatn";
src: url("/fonts/Vazirmatn-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}

@font-face {
font-family: "Vazirmatn";
src: url("/fonts/Vazirmatn-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}

/* Apply Vazirmatn for RTL elements */
* {
font-family: "Vazirmatn", var(--font-geist), sans-serif;
}

/* custom variant for setting dark mode programmatically */
@custom-variant dark (&:is(.dark, .dark *));

Expand Down
Loading