Skip to content
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
159 changes: 88 additions & 71 deletions app/(main)/o/[slug]/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
import { z } from "zod";

import { ShareButton } from "@/components/share-button";
import Logo from "@/components/tenant/logo/logo";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { tenantListResponseSchema, updateCurrentProfileSchema } from "@/lib/api";
Expand All @@ -30,6 +31,7 @@ interface Props {
name: string | undefined | null;
email: string | undefined | null;
isAnonymous: boolean;
isLoggedIn: boolean;
className?: string;
onNavClick?: () => void;
}
Expand All @@ -51,17 +53,29 @@ const HeaderPopoverContent = ({
</PopoverContent>
);

export default function Header({ isAnonymous, tenant, name, email, onNavClick = () => {} }: Props) {
export default function Header({ isAnonymous, tenant, name, email, isLoggedIn, onNavClick = () => {} }: Props) {
const router = useRouter();
const [tenants, setTenants] = useState<z.infer<typeof tenantListResponseSchema>>([]);
const [conversationId, setConversationId] = useState<string>("");
const pathname = usePathname();

useEffect(() => {
(async () => {
const res = await fetch("/api/tenants");
const tenants = tenantListResponseSchema.parse(await res.json());
setTenants(tenants);
})();
}, []);
// Only fetch tenants if there is an active session
if (isLoggedIn) {
(async () => {
const res = await fetch("/api/tenants");
if (res.ok) {
const tenants = tenantListResponseSchema.parse(await res.json());
setTenants(tenants);
}
})();
}
}, [isLoggedIn]);

useEffect(() => {
const conversationIdMatch = pathname.match(/\/o\/[^/]+\/conversations\/([^/]+)/);
setConversationId(conversationIdMatch ? conversationIdMatch[1] : "");
}, [pathname]);

const handleLogOutClick = async () =>
await signOut({
Expand Down Expand Up @@ -109,72 +123,75 @@ export default function Header({ isAnonymous, tenant, name, email, onNavClick =
<Image src={AnonProfileIcon} alt={name || "Guest"} />
</div>
) : (
<Popover>
<PopoverTrigger asChild>
<div>
<Logo
name={name}
width={32}
height={32}
className="bg-[#66666E] font-semibold text-[16px] cursor-pointer"
initialCount={1}
/>
</div>
</PopoverTrigger>
<HeaderPopoverContent align="end" className="p-4 w-[332px] flex flex-col">
<div className="text-sm text-gray-500 font-semibold ml-6 mb-3 mt-3">{email}</div>

{/* Scrollable container for tenants list */}
<div className="max-h-[calc(100vh-330px)] overflow-y-auto pr-1 scrollbar-thin mb-4">
<ul>
{tenants.map((tenantItem, i) => (
<li
key={i}
className="hover:bg-black hover:bg-opacity-5 px-4 py-3 rounded-lg cursor-pointer"
onClick={() => handleProfileClick(tenantItem)}
>
<div className="flex items-center mb-1">
<div className="w-4">
{tenant.id === tenantItem.id && <Image src={CheckIcon} alt="selected" />}
</div>
<Logo
name={tenantItem.name}
url={tenantItem.logoUrl}
width={40}
height={40}
className="ml-3 text-[16px] w-[40px] h-[40px]"
tenantId={tenantItem.id}
/>
<div className="ml-4">
{tenantItem.name}
<div className="text-xs text-gray-500">
{tenantItem.userCount ?? 1} User{(tenantItem.userCount ?? 1) === 1 ? "" : "s"}
<div className="flex items-center gap-4">
{conversationId && <ShareButton conversationId={conversationId} slug={tenant.slug} />}
<Popover>
<PopoverTrigger asChild>
<div>
<Logo
name={name}
width={32}
height={32}
className="bg-[#66666E] font-semibold text-[16px] cursor-pointer"
initialCount={1}
/>
</div>
</PopoverTrigger>
<HeaderPopoverContent align="end" className="p-4 w-[332px] flex flex-col">
<div className="text-sm text-gray-500 font-semibold ml-6 mb-3 mt-3">{email}</div>

{/* Scrollable container for tenants list */}
<div className="max-h-[calc(100vh-330px)] overflow-y-auto pr-1 scrollbar-thin mb-4">
<ul>
{tenants.map((tenantItem, i) => (
<li
key={i}
className="hover:bg-black hover:bg-opacity-5 px-4 py-3 rounded-lg cursor-pointer"
onClick={() => handleProfileClick(tenantItem)}
>
<div className="flex items-center mb-1">
<div className="w-4">
{tenant.id === tenantItem.id && <Image src={CheckIcon} alt="selected" />}
</div>
<Logo
name={tenantItem.name}
url={tenantItem.logoUrl}
width={40}
height={40}
className="ml-3 text-[16px] w-[40px] h-[40px]"
tenantId={tenantItem.id}
/>
<div className="ml-4">
{tenantItem.name}
<div className="text-xs text-gray-500">
{tenantItem.userCount ?? 1} User{(tenantItem.userCount ?? 1) === 1 ? "" : "s"}
</div>
</div>
</div>
</div>
</li>
))}
</ul>
</div>

{/* Fixed bottom options */}
<div className="mt-auto">
<hr className="mb-4 bg-black border-none h-[1px] opacity-10" />

<Link className="flex cursor-pointer mb-4" href="/setup">
<Image src={PlusIcon} alt="New Chatbot" className="mr-3" />
New Chatbot
</Link>

<hr className="mb-4 bg-black border-none h-[1px] opacity-10" />

<div className="flex cursor-pointer" onClick={handleLogOutClick}>
<Image src={LogOutIcon} alt="Log out" className="mr-3" />
Log out
</li>
))}
</ul>
</div>
</div>
</HeaderPopoverContent>
</Popover>

{/* Fixed bottom options */}
<div className="mt-auto">
<hr className="mb-4 bg-black border-none h-[1px] opacity-10" />

<Link className="flex cursor-pointer mb-4" href="/setup">
<Image src={PlusIcon} alt="New Chatbot" className="mr-3" />
New Chatbot
</Link>

<hr className="mb-4 bg-black border-none h-[1px] opacity-10" />

<div className="flex cursor-pointer" onClick={handleLogOutClick}>
<Image src={LogOutIcon} alt="Log out" className="mr-3" />
Log out
</div>
</div>
</HeaderPopoverContent>
</Popover>
</div>
)}
</header>
);
Expand Down
8 changes: 7 additions & 1 deletion app/(main)/o/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ export default async function MainLayout({ children, params }: Props) {

return (
<div className="h-screen w-full flex flex-col items-center bg-white overflow-hidden">
<Header isAnonymous={user.isAnonymous} tenant={tenant} name={session.user.name} email={user.email} />
<Header
isAnonymous={user.isAnonymous}
tenant={tenant}
name={session.user.name}
email={user.email}
isLoggedIn={!!session}
/>
<main className="flex-1 w-full overflow-y-auto">
<div className="w-full max-w-[717px] lg:max-w-full px-4 mx-auto h-full flex flex-col items-center justify-center">
{children}
Expand Down
11 changes: 10 additions & 1 deletion app/(main)/o/[slug]/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,16 @@ export default function Welcome({ tenant, className }: Props) {
}
setSettingsLoaded(true);
}
}, [enabledModels, tenant.overrideBreadth, tenant.overrideRerank, tenant.overridePrioritizeRecent]);
}, [
tenant.isBreadth,
tenant.overrideBreadth,
tenant.rerankEnabled,
tenant.overrideRerank,
tenant.prioritizeRecent,
tenant.overridePrioritizeRecent,
tenant.defaultModel,
enabledModels,
]);

// Save settings to localStorage whenever they change
useEffect(() => {
Expand Down
55 changes: 55 additions & 0 deletions app/(shared)/share/[shareId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { redirect } from "next/navigation";
import { ReactNode } from "react";

import Header from "@/app/(main)/o/[slug]/header";
import RagieLogo from "@/components/ragie-logo";
import { getShareData, getUserById } from "@/lib/server/service";
import { getSession } from "@/lib/server/utils";

interface Props {
params: Promise<{ shareId: string }>;
children?: ReactNode;
}

export default async function SharedLayout({ children, params }: Props) {
const { shareId } = await params;
const session = await getSession();

// can show logged in users a proper header if they are logged in, but it is not required
let user = null;
if (session) {
user = await getUserById(session.user.id);
}

const shareData = await getShareData(shareId);
if (!shareData) {
redirect("/sign-in");
}
const { formattedTenant } = shareData;

return (
<div className="h-screen w-full flex flex-col items-center bg-white overflow-hidden">
<Header
isAnonymous={!user}
tenant={formattedTenant}
name={session?.user.name}
email={session?.user.email}
isLoggedIn={!!session}
/>
<main className="flex-1 w-full overflow-y-auto">
<div className="w-full max-w-[717px] lg:max-w-full px-4 mx-auto h-full flex flex-col items-center justify-center">
{children}
</div>
</main>

<div className="h-20 shrink-0 w-full bg-[#27272A] flex items-center justify-center">
<div className={`mr-2.5 text-md text-[#FEFEFE]`}>Powered by</div>
<div>
<a href="https://ragie.ai/?utm_source=oss-chatbot">
<RagieLogo />
</a>
</div>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions app/(shared)/share/[shareId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { redirect } from "next/navigation";

import { getShareData } from "@/lib/server/service";

import ReadOnlyConversation from "./read-only-conversation";

export default async function SharedConversationPage({ params }: { params: Promise<{ shareId: string }> }) {
const p = await params;
const { shareId } = p;

const shareData = await getShareData(shareId);
if (!shareData) {
redirect("/sign-in");
}
const { formattedTenant, conversation } = shareData;

return <ReadOnlyConversation tenant={formattedTenant} id={conversation.id} />;
}
41 changes: 41 additions & 0 deletions app/(shared)/share/[shareId]/read-only-conversation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client";

import { useState } from "react";

import ReadOnlyChatbot from "@/components/chatbot/read-only-chatbot";

import Summary from "../../../(main)/o/[slug]/conversations/[id]/summary";

interface Props {
id: string;
tenant: {
name: string;
logoUrl?: string | null;
slug: string;
id: string;
};
}

export default function ReadOnlyConversation({ id, tenant }: Props) {
const [documentId, setDocumentId] = useState<string | null>(null);

const handleSelectedDocumentId = async (id: string) => {
setDocumentId(id);
};

return (
<div className="relative lg:flex h-full w-full">
<ReadOnlyChatbot tenant={tenant} conversationId={id} onSelectedDocumentId={handleSelectedDocumentId} />
{documentId && (
<div className="absolute top-0 left-0 right-0 lg:static">
<Summary
className="flex-1 w-full lg:min-w-[400px] lg:w-[400px] rounded-[24px] p-8 mr-6 mb-4 bg-[#F5F5F7] overflow-y-auto"
documentId={documentId}
slug={tenant.slug}
onCloseClick={() => setDocumentId(null)}
/>
</div>
)}
</div>
);
}
44 changes: 44 additions & 0 deletions app/api/conversations/[conversationId]/shares/[shareId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { eq, and } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

import db from "@/lib/server/db";
import { sharedConversations } from "@/lib/server/db/schema";
import { getConversation } from "@/lib/server/service";
import { requireAuthContext } from "@/lib/server/utils";

// Delete a share
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ conversationId: string; shareId: string }> },
) {
const { conversationId, shareId } = await params;
try {
// Parse the request body
const body = await request.json().catch(() => ({}));
const { slug } = body;
const { profile, tenant } = await requireAuthContext(slug);

// Verify conversation ownership
await getConversation(tenant.id, profile.id, conversationId);

// Delete share
const [deletedShare] = await db
.delete(sharedConversations)
.where(
and(
eq(sharedConversations.conversationId, conversationId),
eq(sharedConversations.id, shareId),
eq(sharedConversations.tenantId, tenant.id),
),
)
.returning();
if (!deletedShare) {
return new Response("Share not found", { status: 404 });
}
return new Response(null, { status: 204 });
} catch (error) {
console.error("Failed to delete conversation:", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
Loading