From 3d9556fd16e6bc6a7c8e0f2e700c508218643b7a Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 13:35:45 -0500 Subject: [PATCH 01/13] create shared_conversations table and public page --- .../conversations/[id]/conversation.tsx | 4 +- .../o/[slug]/conversations/[id]/page.tsx | 2 +- app/(shared)/share/[shareId]/layout.tsx | 76 +++++++++ app/(shared)/share/[shareId]/page.tsx | 17 ++ components/chatbot/index.tsx | 3 +- drizzle/0032_yummy_ronan.sql | 32 ++++ drizzle/meta/0032_snapshot.json | 160 +++++------------- drizzle/meta/_journal.json | 2 +- lib/server/service.tsx | 17 ++ lib/server/utils.ts | 7 + middleware.ts | 6 +- 11 files changed, 200 insertions(+), 126 deletions(-) create mode 100644 app/(shared)/share/[shareId]/layout.tsx create mode 100644 app/(shared)/share/[shareId]/page.tsx create mode 100644 drizzle/0032_yummy_ronan.sql diff --git a/app/(main)/o/[slug]/conversations/[id]/conversation.tsx b/app/(main)/o/[slug]/conversations/[id]/conversation.tsx index e2e3b457..1e0adf63 100644 --- a/app/(main)/o/[slug]/conversations/[id]/conversation.tsx +++ b/app/(main)/o/[slug]/conversations/[id]/conversation.tsx @@ -24,9 +24,10 @@ interface Props { overrideRerank: boolean | null; overridePrioritizeRecent: boolean | null; }; + readOnly: boolean; } -export default function Conversation({ id, tenant }: Props) { +export default function Conversation({ id, tenant, readOnly }: Props) { const [documentId, setDocumentId] = useState(null); const { initialMessage, setInitialMessage, initialModel, setInitialModel } = useGlobalState(); @@ -57,6 +58,7 @@ export default function Conversation({ id, tenant }: Props) { conversationId={id} initMessage={initialMessage} onSelectedDocumentId={handleSelectedDocumentId} + readOnly={readOnly} /> {documentId && (
diff --git a/app/(main)/o/[slug]/conversations/[id]/page.tsx b/app/(main)/o/[slug]/conversations/[id]/page.tsx index 8f265d9b..524ae30e 100644 --- a/app/(main)/o/[slug]/conversations/[id]/page.tsx +++ b/app/(main)/o/[slug]/conversations/[id]/page.tsx @@ -11,5 +11,5 @@ export default async function ConversationPage({ params }: Props) { const { tenant } = await authOrRedirect(p.slug); const { id } = p; - return ; + return ; } diff --git a/app/(shared)/share/[shareId]/layout.tsx b/app/(shared)/share/[shareId]/layout.tsx new file mode 100644 index 00000000..2137fa61 --- /dev/null +++ b/app/(shared)/share/[shareId]/layout.tsx @@ -0,0 +1,76 @@ +import { ReactNode } from "react"; + +import Header from "@/app/(main)/o/[slug]/header"; +import RagieLogo from "@/components/ragie-logo"; +import { SearchSettings } from "@/lib/api"; +import { LLMModel } from "@/lib/llm/types"; +import { getShareByShareId, getUserById } from "@/lib/server/service"; +import { getOptionalSession } from "@/lib/server/utils"; + +interface Props { + params: Promise<{ shareId: string }>; + children?: ReactNode; +} + +export async function getShareData(shareId: string) { + const shareResult = await getShareByShareId(shareId); + if (!shareResult) { + return null; + } + + const { share, tenant, conversation } = shareResult; + if (!share || !tenant || !conversation) { + return null; + } + + // Format tenant object + const formattedTenant = { + name: tenant?.name || "", + logoUrl: tenant?.logoUrl || null, + slug: tenant?.slug || "", + id: tenant?.id || "", + enabledModels: (tenant?.enabledModels as LLMModel[]) || [], + defaultModel: (tenant?.defaultModel as LLMModel | null) || null, + searchSettings: null as SearchSettings | null, + }; + + return { share, formattedTenant, conversation }; +} + +export default async function SharedLayout({ children, params }: Props) { + const { shareId } = await params; + const session = await getOptionalSession(); + + // 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) { + return
Share not found
; + } + + const { formattedTenant } = shareData; + + return ( +
+
+
+
+ {children} +
+
+ +
+
Powered by
+
+ + + +
+
+
+ ); +} diff --git a/app/(shared)/share/[shareId]/page.tsx b/app/(shared)/share/[shareId]/page.tsx new file mode 100644 index 00000000..fdf5b365 --- /dev/null +++ b/app/(shared)/share/[shareId]/page.tsx @@ -0,0 +1,17 @@ +import Conversation from "@/app/(main)/o/[slug]/conversations/[id]/conversation"; + +import { getShareData } from "./layout"; + +export default async function SharedConversationPage({ params }: { params: Promise<{ shareId: string }> }) { + const p = await params; + const { shareId } = p; + + const shareData = await getShareData(shareId); + if (!shareData) { + return
Share not found
; + } + + const { formattedTenant, conversation } = shareData; + + return ; +} diff --git a/components/chatbot/index.tsx b/components/chatbot/index.tsx index 838e8782..76b2ae4d 100644 --- a/components/chatbot/index.tsx +++ b/components/chatbot/index.tsx @@ -43,9 +43,10 @@ interface Props { }; initMessage?: string; onSelectedDocumentId: (id: string) => void; + readOnly: boolean; } -export default function Chatbot({ tenant, conversationId, initMessage, onSelectedDocumentId }: Props) { +export default function Chatbot({ tenant, conversationId, initMessage, onSelectedDocumentId, readOnly }: Props) { const [localInitMessage, setLocalInitMessage] = useState(initMessage); const [messages, setMessages] = useState([]); const [sourceCache, setSourceCache] = useState>({}); diff --git a/drizzle/0032_yummy_ronan.sql b/drizzle/0032_yummy_ronan.sql new file mode 100644 index 00000000..2d7698cc --- /dev/null +++ b/drizzle/0032_yummy_ronan.sql @@ -0,0 +1,32 @@ +CREATE TYPE "public"."share_access_type" AS ENUM('public', 'organization', 'email');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "shared_conversations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant_id" uuid NOT NULL, + "conversation_id" uuid NOT NULL, + "created_by" uuid NOT NULL, + "access_type" "share_access_type" DEFAULT 'public' NOT NULL, + "recipient_emails" json DEFAULT '[]'::json, + "expires_at" timestamp with time zone +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "shared_conversations_conversation_id_idx" ON "shared_conversations" USING btree ("conversation_id"); \ No newline at end of file diff --git a/drizzle/meta/0032_snapshot.json b/drizzle/meta/0032_snapshot.json index 16e253d0..28d47738 100644 --- a/drizzle/meta/0032_snapshot.json +++ b/drizzle/meta/0032_snapshot.json @@ -96,12 +96,8 @@ "name": "accounts_user_id_users_id_fk", "tableFrom": "accounts", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -192,12 +188,8 @@ "name": "authenticators_user_id_users_id_fk", "tableFrom": "authenticators", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -207,9 +199,7 @@ "authenticators_credential_id_unique": { "name": "authenticators_credential_id_unique", "nullsNotDistinct": false, - "columns": [ - "credential_id" - ] + "columns": ["credential_id"] } }, "policies": {}, @@ -278,12 +268,8 @@ "name": "connections_tenant_id_tenants_id_fk", "tableFrom": "connections", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -293,9 +279,7 @@ "connections_ragie_connection_id_unique": { "name": "connections_ragie_connection_id_unique", "nullsNotDistinct": false, - "columns": [ - "ragie_connection_id" - ] + "columns": ["ragie_connection_id"] } }, "policies": {}, @@ -389,12 +373,8 @@ "name": "conversations_tenant_id_tenants_id_fk", "tableFrom": "conversations", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -402,12 +382,8 @@ "name": "conversations_profile_id_profiles_id_fk", "tableFrom": "conversations", "tableTo": "profiles", - "columnsFrom": [ - "profile_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["profile_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -475,12 +451,8 @@ "name": "invites_tenant_id_tenants_id_fk", "tableFrom": "invites", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -488,12 +460,8 @@ "name": "invites_invited_by_id_profiles_id_fk", "tableFrom": "invites", "tableTo": "profiles", - "columnsFrom": [ - "invited_by_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invited_by_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -503,10 +471,7 @@ "invites_tenant_id_email_unique": { "name": "invites_tenant_id_email_unique", "nullsNotDistinct": false, - "columns": [ - "tenant_id", - "email" - ] + "columns": ["tenant_id", "email"] } }, "policies": {}, @@ -641,12 +606,8 @@ "name": "messages_tenant_id_tenants_id_fk", "tableFrom": "messages", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -654,12 +615,8 @@ "name": "messages_conversation_id_conversations_id_fk", "tableFrom": "messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -737,12 +694,8 @@ "name": "profiles_tenant_id_tenants_id_fk", "tableFrom": "profiles", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -750,12 +703,8 @@ "name": "profiles_user_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -765,10 +714,7 @@ "profiles_tenant_id_user_id_unique": { "name": "profiles_tenant_id_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "tenant_id", - "user_id" - ] + "columns": ["tenant_id", "user_id"] } }, "policies": {}, @@ -855,12 +801,8 @@ "name": "search_settings_tenant_id_tenants_id_fk", "tableFrom": "search_settings", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -870,9 +812,7 @@ "search_settings_tenant_id_unique": { "name": "search_settings_tenant_id_unique", "nullsNotDistinct": false, - "columns": [ - "tenant_id" - ] + "columns": ["tenant_id"] } }, "policies": {}, @@ -941,12 +881,8 @@ "name": "sessions_user_id_users_id_fk", "tableFrom": "sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -956,9 +892,7 @@ "sessions_token_unique": { "name": "sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1085,9 +1019,7 @@ "tenants_slug_unique": { "name": "tenants_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -1164,12 +1096,8 @@ "name": "users_current_profile_id_profiles_id_fk", "tableFrom": "users", "tableTo": "profiles", - "columnsFrom": [ - "current_profile_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["current_profile_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1179,9 +1107,7 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1245,20 +1171,12 @@ "public.message_roles": { "name": "message_roles", "schema": "public", - "values": [ - "assistant", - "system", - "user" - ] + "values": ["assistant", "system", "user"] }, "public.roles": { "name": "roles", "schema": "public", - "values": [ - "admin", - "user", - "guest" - ] + "values": ["admin", "user", "guest"] } }, "schemas": {}, @@ -1271,4 +1189,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f72a1c30..1b4aada0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -241,4 +241,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/lib/server/service.tsx b/lib/server/service.tsx index 201f1d44..b70709c1 100644 --- a/lib/server/service.tsx +++ b/lib/server/service.tsx @@ -286,6 +286,23 @@ export async function findTenantBySlug(slug: string) { return tenants.length ? tenants[0] : null; } +export async function getShareByShareId(shareId: string) { + const rs = await db + .select({ + share: schema.sharedConversations, + conversation: schema.conversations, + tenant: schema.tenants, + }) + .from(schema.sharedConversations) + .leftJoin(schema.conversations, eq(schema.conversations.id, schema.sharedConversations.conversationId)) + .leftJoin(schema.tenants, eq(schema.tenants.id, schema.conversations.tenantId)) + .where(eq(schema.sharedConversations.id, shareId)) + .limit(1); + + assert(rs.length === 0 || rs.length === 1, "expect single record"); + return rs.length ? rs[0] : null; +} + export async function setCurrentProfileId(userId: string, profileId: string) { await db.transaction(async (tx) => { // Validate profile exists and is scoped to the userId diff --git a/lib/server/utils.ts b/lib/server/utils.ts index 5d5edec4..410d0eca 100644 --- a/lib/server/utils.ts +++ b/lib/server/utils.ts @@ -44,6 +44,13 @@ export async function requireAdminContext(slug: string) { return context; } +export async function getOptionalSession() { + const session = await auth.api.getSession({ + headers: await headers(), // you need to pass the headers object. + }); + return session; +} + export async function authOrRedirect(slug: string) { try { return await requireAuthContext(slug); diff --git a/middleware.ts b/middleware.ts index 392f2ff7..e582d818 100644 --- a/middleware.ts +++ b/middleware.ts @@ -15,7 +15,8 @@ export async function middleware(request: NextRequest) { pathname !== "/change-password" && !pathname.startsWith("/check") && !pathname.startsWith("/api/auth/callback") && - !pathname.startsWith("/healthz") + !pathname.startsWith("/healthz") && + !pathname.startsWith("/share") ) { const redirectPath = getUnauthenticatedRedirectPath(pathname); const newUrl = new URL(redirectPath, BASE_URL); @@ -37,6 +38,9 @@ function getUnauthenticatedRedirectPath(pathname: string) { if (pathname.startsWith("/o")) { const slug = pathname.split("/")[2]; return `/check/${slug}`; + } else if (pathname.startsWith("/share")) { + const shareId = pathname.split("/")[2]; + return `/share/${shareId}`; } else { return "/sign-in"; } From f199dc1f66ebf67e09e8bd12493c4d1b856a5fa8 Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 13:53:31 -0500 Subject: [PATCH 02/13] post, get, delete api routes --- .../shares/[shareId]/route.ts | 52 ++++++++++++++ .../[conversationId]/shares/route.ts | 71 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 app/api/conversations/[conversationId]/shares/[shareId]/route.ts create mode 100644 app/api/conversations/[conversationId]/shares/route.ts diff --git a/app/api/conversations/[conversationId]/shares/[shareId]/route.ts b/app/api/conversations/[conversationId]/shares/[shareId]/route.ts new file mode 100644 index 00000000..21477247 --- /dev/null +++ b/app/api/conversations/[conversationId]/shares/[shareId]/route.ts @@ -0,0 +1,52 @@ +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"; + +// Schema for validating request body +const deleteShareSchema = z.object({ + slug: z.string(), +}); + +// 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 validationResult = deleteShareSchema.safeParse(body); + if (!validationResult.success) return new NextResponse("Invalid request body", { status: 400 }); + + const { slug } = validationResult.data; + 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 }); + } +} diff --git a/app/api/conversations/[conversationId]/shares/route.ts b/app/api/conversations/[conversationId]/shares/route.ts new file mode 100644 index 00000000..148521c2 --- /dev/null +++ b/app/api/conversations/[conversationId]/shares/route.ts @@ -0,0 +1,71 @@ +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +import { createShareRequestSchema } from "@/lib/api"; +import db from "@/lib/server/db"; +import { sharedConversations } from "@/lib/server/db/schema"; +import { getConversation } from "@/lib/server/service"; +import { requireAuthContext, requireAuthContextFromRequest } from "@/lib/server/utils"; + +export async function POST(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { + try { + const { conversationId } = await params; + + // Parse the request body + const body = await request.json().catch(() => ({})); + const validationResult = createShareRequestSchema.safeParse(body); + if (!validationResult.success) return new NextResponse("Invalid request body", { status: 400 }); + + const { slug } = validationResult.data; + const { profile, tenant } = await requireAuthContext(slug); + + // Verify conversation ownership + const conversation = await getConversation(tenant.id, profile.id, conversationId); + // Create share record + const [share] = await db + .insert(sharedConversations) + .values({ + conversationId: conversation.id, + tenantId: tenant.id, + createdBy: profile.userId, + accessType: body.accessType, + recipientEmails: body.recipientEmails || [], + expiresAt: body.expiresAt, + }) + .returning(); + + return Response.json({ shareId: share.id }); + } catch (error) { + console.error("Failed to create share:", error); + return new Response("Internal Server Error", { status: 500 }); + } +} + +// Get all shares for a conversation +export async function GET(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { + try { + const { profile, tenant } = await requireAuthContextFromRequest(request); + const { conversationId } = await params; + + // Verify conversation ownership + await getConversation(tenant.id, profile.id, conversationId); + + // Get all shares for this conversation + const shares = await db + .select({ + shareId: sharedConversations.id, + accessType: sharedConversations.accessType, + createdAt: sharedConversations.createdAt, + expiresAt: sharedConversations.expiresAt, + recipientEmails: sharedConversations.recipientEmails, + }) + .from(sharedConversations) + .where(eq(sharedConversations.conversationId, conversationId)) + .orderBy(sharedConversations.createdAt); + + return Response.json(shares); + } catch (error) { + console.error("Error fetching shares:", error); + return new Response("Internal Server Error", { status: 500 }); + } +} From 169f474009e90d5331c9e5ea3a5d779caf4615b3 Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 14:28:41 -0500 Subject: [PATCH 03/13] share button rough draft --- app/(main)/o/[slug]/header.tsx | 144 ++++++++++++++++++--------------- components/share-button.tsx | 69 ++++++++++++++++ components/share-dialog.tsx | 44 ++++++++++ components/shared-dialog.tsx | 84 +++++++++++++++++++ public/icons/share.svg | 5 ++ 5 files changed, 282 insertions(+), 64 deletions(-) create mode 100644 components/share-button.tsx create mode 100644 components/share-dialog.tsx create mode 100644 components/shared-dialog.tsx create mode 100644 public/icons/share.svg diff --git a/app/(main)/o/[slug]/header.tsx b/app/(main)/o/[slug]/header.tsx index ece60f36..f4d3d921 100644 --- a/app/(main)/o/[slug]/header.tsx +++ b/app/(main)/o/[slug]/header.tsx @@ -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"; @@ -54,6 +55,8 @@ const HeaderPopoverContent = ({ export default function Header({ isAnonymous, tenant, name, email, onNavClick = () => {} }: Props) { const router = useRouter(); const [tenants, setTenants] = useState>([]); + const [conversationId, setConversationId] = useState(""); + const pathname = usePathname(); useEffect(() => { (async () => { @@ -63,6 +66,16 @@ export default function Header({ isAnonymous, tenant, name, email, onNavClick = })(); }, []); + useEffect(() => { + const pathSegments = pathname.split("/"); + const conversationsIndex = pathSegments.indexOf("conversations"); + if (conversationsIndex !== -1 && pathSegments[conversationsIndex + 1]) { + setConversationId(pathSegments[conversationsIndex + 1]); + } else { + setConversationId(""); + } + }, [pathname]); + const handleLogOutClick = async () => await signOut({ fetchOptions: { @@ -109,72 +122,75 @@ export default function Header({ isAnonymous, tenant, name, email, onNavClick = {name
) : ( - - -
- -
-
- -
{email}
- - {/* Scrollable container for tenants list */} -
-
    - {tenants.map((tenantItem, i) => ( -
  • handleProfileClick(tenantItem)} - > -
    -
    - {tenant.id === tenantItem.id && selected} -
    - -
    - {tenantItem.name} -
    - {tenantItem.userCount ?? 1} User{(tenantItem.userCount ?? 1) === 1 ? "" : "s"} +
    + {conversationId && } + + +
    + +
    +
    + +
    {email}
    + + {/* Scrollable container for tenants list */} +
    +
      + {tenants.map((tenantItem, i) => ( +
    • handleProfileClick(tenantItem)} + > +
      +
      + {tenant.id === tenantItem.id && selected} +
      + +
      + {tenantItem.name} +
      + {tenantItem.userCount ?? 1} User{(tenantItem.userCount ?? 1) === 1 ? "" : "s"} +
      -
    -
  • - ))} -
-
- - {/* Fixed bottom options */} -
-
- - - New Chatbot - New Chatbot - - -
- -
- Log out - Log out + + ))} +
-
-
-
+ + {/* Fixed bottom options */} +
+
+ + + New Chatbot + New Chatbot + + +
+ +
+ Log out + Log out +
+
+ + + )} ); diff --git a/components/share-button.tsx b/components/share-button.tsx new file mode 100644 index 00000000..0cc7df6e --- /dev/null +++ b/components/share-button.tsx @@ -0,0 +1,69 @@ +import Image from "next/image"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Dialog, DialogTrigger } from "@/components/ui/dialog"; +import { ShareSettings } from "@/lib/api"; +import ShareIcon from "@/public/icons/share.svg"; + +import ShareDialog from "./share-dialog"; +import SharedDialog from "./shared-dialog"; + +interface ShareButtonProps { + conversationId: string; + slug: string; +} + +export function ShareButton({ conversationId, slug }: ShareButtonProps) { + const [isShared, setIsShared] = useState(false); + const [shareSettings, setShareSettings] = useState< + (ShareSettings & { shareId?: string; conversationId: string }) | null + >(null); + const [isLoading, setIsLoading] = useState(false); + + const handleShare = async (settings: ShareSettings) => { + try { + setIsLoading(true); + const response = await fetch(`/api/conversations/${conversationId}/shares`, { + method: "POST", + body: JSON.stringify({ + slug, + accessType: settings.accessType, + recipientEmails: settings.accessType === "email" ? [settings.email!] : undefined, + expiresAt: settings.expiresAt ? new Date(settings.expiresAt).toISOString() : undefined, + }), + }); + + if (!response.ok) { + throw new Error("Failed to create share link"); + } + + const { shareId } = await response.json(); + setShareSettings({ + ...settings, + shareId, + conversationId, + }); + setIsShared(true); + } catch (error) { + toast.error("Failed to share conversation"); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + {isShared ? ( + setIsShared(false)} /> + ) : ( + + )} + + ); +} diff --git a/components/share-dialog.tsx b/components/share-dialog.tsx new file mode 100644 index 00000000..9808ae90 --- /dev/null +++ b/components/share-dialog.tsx @@ -0,0 +1,44 @@ +import { Loader2 } from "lucide-react"; +import { useState, useEffect } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { ShareSettings } from "@/lib/api"; + +export default function ShareDialog({ + conversationId, + onShare, + isLoading, + slug, +}: { + conversationId: string; + onShare: (data: ShareSettings) => void; + isLoading: boolean; + slug: string; +}) { + const handleShare = async () => { + await onShare({ + accessType: "public", + expiresAt: undefined, + slug, + }); + }; + + return ( + + + Share public link to this chat + Once created, anyone with the link can view this chat. + + +
+ + + +
+
+ ); +} diff --git a/components/shared-dialog.tsx b/components/shared-dialog.tsx new file mode 100644 index 00000000..463e0dc1 --- /dev/null +++ b/components/shared-dialog.tsx @@ -0,0 +1,84 @@ +import { Check, Copy, Loader2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ShareSettings } from "@/lib/api"; + +export default function SharedDialog({ + settings, + onClose, +}: { + settings: ShareSettings & { + shareId?: string; + conversationId: string; + }; + onClose: () => void; +}) { + const [isCopied, setIsCopied] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const shareUrl = settings.shareId ? new URL(`/share/${settings.shareId}`, window.location.origin).toString() : ""; + + const handleCopyUrl = async () => { + try { + await navigator.clipboard.writeText(shareUrl); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (err) { + toast.error("Failed to copy URL"); + } + }; + + const handleStopSharing = async () => { + if (!settings.shareId) return; + + try { + setIsDeleting(true); + const response = await fetch(`/api/conversations/${settings.conversationId}/shares/${settings.shareId}`, { + method: "DELETE", + body: JSON.stringify({ + slug: settings.slug, + }), + }); + + if (!response.ok) throw new Error("Failed to delete share"); + + toast.success("Share link deleted"); + onClose(); + } catch (error) { + toast.error("Failed to delete share link"); + } finally { + setIsDeleting(false); + } + }; + + return ( + + + Share Link Created + Anyone with this link can view this chat. + + +
+
+ +
+ + +
+
+ + + + +
+
+ ); +} diff --git a/public/icons/share.svg b/public/icons/share.svg new file mode 100644 index 00000000..71cfdf4a --- /dev/null +++ b/public/icons/share.svg @@ -0,0 +1,5 @@ + + + + + From 244847baa33a652dd288819e01960c10c9e5e076 Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 19:24:34 -0500 Subject: [PATCH 04/13] works for public --- .../conversations/[id]/conversation.tsx | 5 +- app/(main)/o/[slug]/header.tsx | 20 ++- app/(main)/o/[slug]/layout.tsx | 8 +- app/(shared)/share/[shareId]/layout.tsx | 8 +- app/(shared)/share/[shareId]/page.tsx | 10 +- .../[shareId]/read-only-conversation.tsx | 64 +++++++++ .../[conversationId]/shares/route.ts | 2 +- .../[conversationId]/messages/route.ts | 42 ++++++ components/chatbot/index.tsx | 3 +- components/chatbot/read-only-chatbot.tsx | 124 ++++++++++++++++++ drizzle/0032_yummy_ronan.sql | 32 ----- middleware.ts | 1 + 12 files changed, 267 insertions(+), 52 deletions(-) create mode 100644 app/(shared)/share/[shareId]/read-only-conversation.tsx create mode 100644 app/public/conversations/[conversationId]/messages/route.ts create mode 100644 components/chatbot/read-only-chatbot.tsx delete mode 100644 drizzle/0032_yummy_ronan.sql diff --git a/app/(main)/o/[slug]/conversations/[id]/conversation.tsx b/app/(main)/o/[slug]/conversations/[id]/conversation.tsx index 1e0adf63..6f5ad2e3 100644 --- a/app/(main)/o/[slug]/conversations/[id]/conversation.tsx +++ b/app/(main)/o/[slug]/conversations/[id]/conversation.tsx @@ -24,10 +24,9 @@ interface Props { overrideRerank: boolean | null; overridePrioritizeRecent: boolean | null; }; - readOnly: boolean; } -export default function Conversation({ id, tenant, readOnly }: Props) { +export default function Conversation({ id, tenant }: Props) { const [documentId, setDocumentId] = useState(null); const { initialMessage, setInitialMessage, initialModel, setInitialModel } = useGlobalState(); @@ -58,8 +57,8 @@ export default function Conversation({ id, tenant, readOnly }: Props) { conversationId={id} initMessage={initialMessage} onSelectedDocumentId={handleSelectedDocumentId} - readOnly={readOnly} /> + {documentId && (
void; + hasSession: boolean; } const HeaderPopoverContent = ({ @@ -52,19 +53,24 @@ const HeaderPopoverContent = ({ ); -export default function Header({ isAnonymous, tenant, name, email, onNavClick = () => {} }: Props) { +export default function Header({ isAnonymous, tenant, name, email, onNavClick = () => {}, hasSession }: Props) { const router = useRouter(); const [tenants, setTenants] = useState>([]); const [conversationId, setConversationId] = useState(""); 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 (hasSession) { + (async () => { + const res = await fetch("/api/tenants"); + if (res.ok) { + const tenants = tenantListResponseSchema.parse(await res.json()); + setTenants(tenants); + } + })(); + } + }, [hasSession]); // Add hasSession to the dependency array useEffect(() => { const pathSegments = pathname.split("/"); diff --git a/app/(main)/o/[slug]/layout.tsx b/app/(main)/o/[slug]/layout.tsx index f1864dce..5609808f 100644 --- a/app/(main)/o/[slug]/layout.tsx +++ b/app/(main)/o/[slug]/layout.tsx @@ -19,7 +19,13 @@ export default async function MainLayout({ children, params }: Props) { return (
-
+
{children} diff --git a/app/(shared)/share/[shareId]/layout.tsx b/app/(shared)/share/[shareId]/layout.tsx index 2137fa61..701d0d31 100644 --- a/app/(shared)/share/[shareId]/layout.tsx +++ b/app/(shared)/share/[shareId]/layout.tsx @@ -56,7 +56,13 @@ export default async function SharedLayout({ children, params }: Props) { return (
-
+
{children} diff --git a/app/(shared)/share/[shareId]/page.tsx b/app/(shared)/share/[shareId]/page.tsx index fdf5b365..8b0f43d1 100644 --- a/app/(shared)/share/[shareId]/page.tsx +++ b/app/(shared)/share/[shareId]/page.tsx @@ -1,6 +1,7 @@ -import Conversation from "@/app/(main)/o/[slug]/conversations/[id]/conversation"; +import { redirect } from "next/navigation"; import { getShareData } from "./layout"; +import ReadOnlyConversation from "./read-only-conversation"; export default async function SharedConversationPage({ params }: { params: Promise<{ shareId: string }> }) { const p = await params; @@ -8,10 +9,9 @@ export default async function SharedConversationPage({ params }: { params: Promi const shareData = await getShareData(shareId); if (!shareData) { - return
Share not found
; + redirect("/sign-in"); } + const { formattedTenant, conversation, share } = shareData; - const { formattedTenant, conversation } = shareData; - - return ; + return ; } diff --git a/app/(shared)/share/[shareId]/read-only-conversation.tsx b/app/(shared)/share/[shareId]/read-only-conversation.tsx new file mode 100644 index 00000000..baad08a1 --- /dev/null +++ b/app/(shared)/share/[shareId]/read-only-conversation.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useState } from "react"; +import { z } from "zod"; + +import ReadOnlyChatbot from "@/components/chatbot/read-only-chatbot"; +import { SearchSettings, ShareSettings } from "@/lib/api"; +import { LLMModel } from "@/lib/llm/types"; +import { sharedConversations } from "@/lib/server/db/schema"; + +import Summary from "../../../(main)/o/[slug]/conversations/[id]/summary"; +interface Props { + id: string; + tenant: { + name: string; + logoUrl?: string | null; + slug: string; + id: string; + enabledModels: LLMModel[]; + defaultModel: LLMModel | null; + searchSettings: SearchSettings | null; + }; + share: { + id: string; + createdAt: Date; + updatedAt: Date; + tenantId: string; + conversationId: string; + createdBy: string; + accessType: "email" | "public" | "organization"; + recipientEmails: string[] | null; + expiresAt: string | null; + }; +} + +export default function ReadOnlyConversation({ id, tenant, share }: Props) { + const [documentId, setDocumentId] = useState(null); + const createdBy = share.createdBy; + + const handleSelectedDocumentId = async (id: string) => { + setDocumentId(id); + }; + + return ( +
+ + {documentId && ( +
+ setDocumentId(null)} + /> +
+ )} +
+ ); +} diff --git a/app/api/conversations/[conversationId]/shares/route.ts b/app/api/conversations/[conversationId]/shares/route.ts index 148521c2..a7cca36d 100644 --- a/app/api/conversations/[conversationId]/shares/route.ts +++ b/app/api/conversations/[conversationId]/shares/route.ts @@ -27,7 +27,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ .values({ conversationId: conversation.id, tenantId: tenant.id, - createdBy: profile.userId, + createdBy: profile.id, accessType: body.accessType, recipientEmails: body.recipientEmails || [], expiresAt: body.expiresAt, diff --git a/app/public/conversations/[conversationId]/messages/route.ts b/app/public/conversations/[conversationId]/messages/route.ts new file mode 100644 index 00000000..50368703 --- /dev/null +++ b/app/public/conversations/[conversationId]/messages/route.ts @@ -0,0 +1,42 @@ +import { and, asc, eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { ZodError } from "zod"; + +import { conversationMessagesResponseSchema } from "@/lib/api"; +// Import necessary drizzle functions and schema/db +import db from "@/lib/server/db"; +import * as schema from "@/lib/server/db/schema"; + +export async function POST(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { + try { + const body = await request.json(); + const { tenantId } = body; + + const { conversationId } = await params; + + // Perform the query directly in the route handler + const messages = await db + .select() + .from(schema.messages) + .where(and(eq(schema.messages.tenantId, tenantId), eq(schema.messages.conversationId, conversationId))) + .orderBy(asc(schema.messages.createdAt)); + + // Validate the data structure before sending + const parsedMessages = conversationMessagesResponseSchema.parse(messages); + console.log(parsedMessages); + return Response.json(parsedMessages); + } catch (error) { + console.error("Error fetching public conversation messages:", error); + + // Handle Zod validation errors specifically for better client feedback + if (error instanceof ZodError) { + return NextResponse.json( + { error: "Invalid data format received from service.", details: error.errors }, + { status: 500 }, + ); + } + + // Handle generic errors + return NextResponse.json({ error: "Failed to load conversation messages." }, { status: 500 }); + } +} diff --git a/components/chatbot/index.tsx b/components/chatbot/index.tsx index 76b2ae4d..838e8782 100644 --- a/components/chatbot/index.tsx +++ b/components/chatbot/index.tsx @@ -43,10 +43,9 @@ interface Props { }; initMessage?: string; onSelectedDocumentId: (id: string) => void; - readOnly: boolean; } -export default function Chatbot({ tenant, conversationId, initMessage, onSelectedDocumentId, readOnly }: Props) { +export default function Chatbot({ tenant, conversationId, initMessage, onSelectedDocumentId }: Props) { const [localInitMessage, setLocalInitMessage] = useState(initMessage); const [messages, setMessages] = useState([]); const [sourceCache, setSourceCache] = useState>({}); diff --git a/components/chatbot/read-only-chatbot.tsx b/components/chatbot/read-only-chatbot.tsx new file mode 100644 index 00000000..f09a0635 --- /dev/null +++ b/components/chatbot/read-only-chatbot.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { Fragment, useEffect, useRef, useState } from "react"; +import { z } from "zod"; + +import { conversationMessagesResponseSchema } from "@/lib/api"; +import { LLMModel } from "@/lib/llm/types"; +import { getConversationMessages } from "@/lib/server/service"; + +import AssistantMessage from "./assistant-message"; +import { SourceMetadata } from "./types"; + +// Infer the message type directly from the Zod schema +type Message = z.infer[number]; + +const UserMessageDisplay = ({ content }: { content: string }) => ( +
{content}
+); + +interface Props { + conversationId: string; + tenant: { + name: string; + logoUrl?: string | null; + slug: string; + id: string; + }; + onSelectedDocumentId: (id: string) => void; + createdBy: string; +} + +export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocumentId, createdBy }: Props) { + // Use the inferred Message type for state + const [messages, setMessages] = useState([]); + const container = useRef(null); + + // Fetch messages on mount + useEffect(() => { + let isMounted = true; // Flag to prevent state updates on unmounted component + (async () => { + try { + const res = await fetch(`/public/conversations/${conversationId}/messages`, { + method: "POST", + body: JSON.stringify({ createdBy, tenantId: tenant.id }), + }); + console.log(res); + if (!res.ok) { + console.error("Could not load conversation:", res.statusText); + if (isMounted) { + setMessages([{ role: "system", content: "Error loading conversation.", id: "error-message" }]); + } + return; + } + const json = await res.json(); + const parsedMessages = conversationMessagesResponseSchema.parse(json); + console.log(parsedMessages); + if (isMounted) { + setMessages(parsedMessages); + } + } catch (error) { + console.error("Failed to fetch or parse messages:", error); + if (isMounted) { + setMessages([{ role: "system", content: "Error loading conversation.", id: "error-message" }]); + } + } + })(); + + return () => { + isMounted = false; // Cleanup function to set flag on unmount + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- Run only once on mount + }, [conversationId, tenant.slug]); + + // Scroll to bottom when messages load/change + useEffect(() => { + if (container.current) { + // Use timeout to ensure scroll happens after render potentially completes + setTimeout(() => { + if (container.current) { + container.current.scrollTop = container.current.scrollHeight; + } + }, 0); + } + }, [messages]); + + return ( +
+
+
+ {messages.map((message, i) => { + // Use message ID as key if available, otherwise index + const key = message.id || `msg-${i}`; + + if (message.role === "user") { + return ; + } + + if (message.role === "assistant") { + // Ensure assistant message specific props are accessed safely + return ( + + + + ); + } + + // Fallback for unknown roles (shouldn't happen with schema validation) + return null; + })} +
+
+
+ ); +} diff --git a/drizzle/0032_yummy_ronan.sql b/drizzle/0032_yummy_ronan.sql deleted file mode 100644 index 2d7698cc..00000000 --- a/drizzle/0032_yummy_ronan.sql +++ /dev/null @@ -1,32 +0,0 @@ -CREATE TYPE "public"."share_access_type" AS ENUM('public', 'organization', 'email');--> statement-breakpoint -CREATE TABLE IF NOT EXISTS "shared_conversations" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - "tenant_id" uuid NOT NULL, - "conversation_id" uuid NOT NULL, - "created_by" uuid NOT NULL, - "access_type" "share_access_type" DEFAULT 'public' NOT NULL, - "recipient_emails" json DEFAULT '[]'::json, - "expires_at" timestamp with time zone -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE INDEX IF NOT EXISTS "shared_conversations_conversation_id_idx" ON "shared_conversations" USING btree ("conversation_id"); \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index e582d818..5861d506 100644 --- a/middleware.ts +++ b/middleware.ts @@ -16,6 +16,7 @@ export async function middleware(request: NextRequest) { !pathname.startsWith("/check") && !pathname.startsWith("/api/auth/callback") && !pathname.startsWith("/healthz") && + !pathname.startsWith("/public") && !pathname.startsWith("/share") ) { const redirectPath = getUnauthenticatedRedirectPath(pathname); From e1c8bd589eb71d891dd3dea8f9b3a8a9707527a3 Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 19:37:19 -0500 Subject: [PATCH 05/13] cleanup --- .../o/[slug]/conversations/[id]/conversation.tsx | 1 - app/(main)/o/[slug]/conversations/[id]/page.tsx | 2 +- app/(main)/o/[slug]/header.tsx | 2 +- .../[conversationId]/messages/route.ts | 15 +-------------- app/(shared)/share/[shareId]/layout.tsx | 9 ++++----- components/chatbot/read-only-chatbot.tsx | 11 ++++------- lib/server/service.tsx | 2 +- 7 files changed, 12 insertions(+), 30 deletions(-) rename app/{ => (shared)}/public/conversations/[conversationId]/messages/route.ts (68%) diff --git a/app/(main)/o/[slug]/conversations/[id]/conversation.tsx b/app/(main)/o/[slug]/conversations/[id]/conversation.tsx index 6f5ad2e3..e2e3b457 100644 --- a/app/(main)/o/[slug]/conversations/[id]/conversation.tsx +++ b/app/(main)/o/[slug]/conversations/[id]/conversation.tsx @@ -58,7 +58,6 @@ export default function Conversation({ id, tenant }: Props) { initMessage={initialMessage} onSelectedDocumentId={handleSelectedDocumentId} /> - {documentId && (
; + return ; } diff --git a/app/(main)/o/[slug]/header.tsx b/app/(main)/o/[slug]/header.tsx index bd3c33d0..e1ba2303 100644 --- a/app/(main)/o/[slug]/header.tsx +++ b/app/(main)/o/[slug]/header.tsx @@ -70,7 +70,7 @@ export default function Header({ isAnonymous, tenant, name, email, onNavClick = } })(); } - }, [hasSession]); // Add hasSession to the dependency array + }, [hasSession]); useEffect(() => { const pathSegments = pathname.split("/"); diff --git a/app/public/conversations/[conversationId]/messages/route.ts b/app/(shared)/public/conversations/[conversationId]/messages/route.ts similarity index 68% rename from app/public/conversations/[conversationId]/messages/route.ts rename to app/(shared)/public/conversations/[conversationId]/messages/route.ts index 50368703..83b8ccfc 100644 --- a/app/public/conversations/[conversationId]/messages/route.ts +++ b/app/(shared)/public/conversations/[conversationId]/messages/route.ts @@ -3,7 +3,6 @@ import { NextRequest, NextResponse } from "next/server"; import { ZodError } from "zod"; import { conversationMessagesResponseSchema } from "@/lib/api"; -// Import necessary drizzle functions and schema/db import db from "@/lib/server/db"; import * as schema from "@/lib/server/db/schema"; @@ -11,32 +10,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ try { const body = await request.json(); const { tenantId } = body; - const { conversationId } = await params; - // Perform the query directly in the route handler + // query directly in the route handler const messages = await db .select() .from(schema.messages) .where(and(eq(schema.messages.tenantId, tenantId), eq(schema.messages.conversationId, conversationId))) .orderBy(asc(schema.messages.createdAt)); - // Validate the data structure before sending const parsedMessages = conversationMessagesResponseSchema.parse(messages); - console.log(parsedMessages); return Response.json(parsedMessages); } catch (error) { console.error("Error fetching public conversation messages:", error); - // Handle Zod validation errors specifically for better client feedback - if (error instanceof ZodError) { - return NextResponse.json( - { error: "Invalid data format received from service.", details: error.errors }, - { status: 500 }, - ); - } - - // Handle generic errors return NextResponse.json({ error: "Failed to load conversation messages." }, { status: 500 }); } } diff --git a/app/(shared)/share/[shareId]/layout.tsx b/app/(shared)/share/[shareId]/layout.tsx index 701d0d31..5e5e3472 100644 --- a/app/(shared)/share/[shareId]/layout.tsx +++ b/app/(shared)/share/[shareId]/layout.tsx @@ -1,19 +1,19 @@ +import { redirect } from "next/navigation"; import { ReactNode } from "react"; import Header from "@/app/(main)/o/[slug]/header"; import RagieLogo from "@/components/ragie-logo"; import { SearchSettings } from "@/lib/api"; import { LLMModel } from "@/lib/llm/types"; -import { getShareByShareId, getUserById } from "@/lib/server/service"; +import { getShareById, getUserById } from "@/lib/server/service"; import { getOptionalSession } from "@/lib/server/utils"; - interface Props { params: Promise<{ shareId: string }>; children?: ReactNode; } export async function getShareData(shareId: string) { - const shareResult = await getShareByShareId(shareId); + const shareResult = await getShareById(shareId); if (!shareResult) { return null; } @@ -49,9 +49,8 @@ export default async function SharedLayout({ children, params }: Props) { const shareData = await getShareData(shareId); if (!shareData) { - return
Share not found
; + redirect("/sign-in"); } - const { formattedTenant } = shareData; return ( diff --git a/components/chatbot/read-only-chatbot.tsx b/components/chatbot/read-only-chatbot.tsx index f09a0635..d336a606 100644 --- a/components/chatbot/read-only-chatbot.tsx +++ b/components/chatbot/read-only-chatbot.tsx @@ -96,25 +96,22 @@ export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocu } if (message.role === "assistant") { - // Ensure assistant message specific props are accessed safely return ( ); } - - // Fallback for unknown roles (shouldn't happen with schema validation) return null; })}
diff --git a/lib/server/service.tsx b/lib/server/service.tsx index b70709c1..bbf43421 100644 --- a/lib/server/service.tsx +++ b/lib/server/service.tsx @@ -286,7 +286,7 @@ export async function findTenantBySlug(slug: string) { return tenants.length ? tenants[0] : null; } -export async function getShareByShareId(shareId: string) { +export async function getShareById(shareId: string) { const rs = await db .select({ share: schema.sharedConversations, From 7294160c8634e6b8a038b4da57672c2c6b7114e6 Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 19:42:04 -0500 Subject: [PATCH 06/13] migrations after rebase --- drizzle/0033_petite_pet_avengers.sql | 32 ++++++ drizzle/meta/0033_snapshot.json | 148 +++++++-------------------- 2 files changed, 68 insertions(+), 112 deletions(-) create mode 100644 drizzle/0033_petite_pet_avengers.sql diff --git a/drizzle/0033_petite_pet_avengers.sql b/drizzle/0033_petite_pet_avengers.sql new file mode 100644 index 00000000..c3b490a1 --- /dev/null +++ b/drizzle/0033_petite_pet_avengers.sql @@ -0,0 +1,32 @@ +CREATE TYPE "public"."share_access_type" AS ENUM('public', 'organization', 'email');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "shared_conversations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant_id" uuid NOT NULL, + "conversation_id" uuid NOT NULL, + "created_by" uuid NOT NULL, + "access_type" "share_access_type" DEFAULT 'public' NOT NULL, + "recipient_emails" json DEFAULT '[]'::json, + "expires_at" timestamp with time zone +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_created_by_profiles_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "shared_conversations_conversation_id_idx" ON "shared_conversations" USING btree ("conversation_id"); \ No newline at end of file diff --git a/drizzle/meta/0033_snapshot.json b/drizzle/meta/0033_snapshot.json index 5bdffdba..e3e0fbf6 100644 --- a/drizzle/meta/0033_snapshot.json +++ b/drizzle/meta/0033_snapshot.json @@ -96,12 +96,8 @@ "name": "accounts_user_id_users_id_fk", "tableFrom": "accounts", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -192,12 +188,8 @@ "name": "authenticators_user_id_users_id_fk", "tableFrom": "authenticators", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -207,9 +199,7 @@ "authenticators_credential_id_unique": { "name": "authenticators_credential_id_unique", "nullsNotDistinct": false, - "columns": [ - "credential_id" - ] + "columns": ["credential_id"] } }, "policies": {}, @@ -278,12 +268,8 @@ "name": "connections_tenant_id_tenants_id_fk", "tableFrom": "connections", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -293,9 +279,7 @@ "connections_ragie_connection_id_unique": { "name": "connections_ragie_connection_id_unique", "nullsNotDistinct": false, - "columns": [ - "ragie_connection_id" - ] + "columns": ["ragie_connection_id"] } }, "policies": {}, @@ -389,12 +373,8 @@ "name": "conversations_tenant_id_tenants_id_fk", "tableFrom": "conversations", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -402,12 +382,8 @@ "name": "conversations_profile_id_profiles_id_fk", "tableFrom": "conversations", "tableTo": "profiles", - "columnsFrom": [ - "profile_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["profile_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -475,12 +451,8 @@ "name": "invites_tenant_id_tenants_id_fk", "tableFrom": "invites", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -488,12 +460,8 @@ "name": "invites_invited_by_id_profiles_id_fk", "tableFrom": "invites", "tableTo": "profiles", - "columnsFrom": [ - "invited_by_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invited_by_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -503,10 +471,7 @@ "invites_tenant_id_email_unique": { "name": "invites_tenant_id_email_unique", "nullsNotDistinct": false, - "columns": [ - "tenant_id", - "email" - ] + "columns": ["tenant_id", "email"] } }, "policies": {}, @@ -641,12 +606,8 @@ "name": "messages_tenant_id_tenants_id_fk", "tableFrom": "messages", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -654,12 +615,8 @@ "name": "messages_conversation_id_conversations_id_fk", "tableFrom": "messages", "tableTo": "conversations", - "columnsFrom": [ - "conversation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -737,12 +694,8 @@ "name": "profiles_tenant_id_tenants_id_fk", "tableFrom": "profiles", "tableTo": "tenants", - "columnsFrom": [ - "tenant_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["tenant_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -750,12 +703,8 @@ "name": "profiles_user_id_users_id_fk", "tableFrom": "profiles", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -765,10 +714,7 @@ "profiles_tenant_id_user_id_unique": { "name": "profiles_tenant_id_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "tenant_id", - "user_id" - ] + "columns": ["tenant_id", "user_id"] } }, "policies": {}, @@ -837,12 +783,8 @@ "name": "sessions_user_id_users_id_fk", "tableFrom": "sessions", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -852,9 +794,7 @@ "sessions_token_unique": { "name": "sessions_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -1023,9 +963,7 @@ "tenants_slug_unique": { "name": "tenants_slug_unique", "nullsNotDistinct": false, - "columns": [ - "slug" - ] + "columns": ["slug"] } }, "policies": {}, @@ -1102,12 +1040,8 @@ "name": "users_current_profile_id_profiles_id_fk", "tableFrom": "users", "tableTo": "profiles", - "columnsFrom": [ - "current_profile_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["current_profile_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1117,9 +1051,7 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1183,20 +1115,12 @@ "public.message_roles": { "name": "message_roles", "schema": "public", - "values": [ - "assistant", - "system", - "user" - ] + "values": ["assistant", "system", "user"] }, "public.roles": { "name": "roles", "schema": "public", - "values": [ - "admin", - "user", - "guest" - ] + "values": ["admin", "user", "guest"] } }, "schemas": {}, @@ -1209,4 +1133,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} From 3ce68a516cc71bcffbaf7f1081cc10cdc5b8262e Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 20:35:50 -0500 Subject: [PATCH 07/13] dialog styling --- components/share-dialog.tsx | 19 +++++++++--------- components/shared-dialog.tsx | 37 +++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/components/share-dialog.tsx b/components/share-dialog.tsx index 9808ae90..9e83ddf1 100644 --- a/components/share-dialog.tsx +++ b/components/share-dialog.tsx @@ -1,11 +1,10 @@ import { Loader2 } from "lucide-react"; -import { useState, useEffect } from "react"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { ShareSettings } from "@/lib/api"; +import PrimaryButton from "./primary-button"; + export default function ShareDialog({ conversationId, onShare, @@ -26,17 +25,19 @@ export default function ShareDialog({ }; return ( - + - Share public link to this chat - Once created, anyone with the link can view this chat. + Share public link to this chat + + Once created, anyone with the link can view this chat. +
- - +
diff --git a/components/shared-dialog.tsx b/components/shared-dialog.tsx index 463e0dc1..9d74b089 100644 --- a/components/shared-dialog.tsx +++ b/components/shared-dialog.tsx @@ -2,10 +2,8 @@ import { Check, Copy, Loader2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { ShareSettings } from "@/lib/api"; export default function SharedDialog({ @@ -56,27 +54,36 @@ export default function SharedDialog({ }; return ( - + - Share Link Created - Anyone with this link can view this chat. + Share public link to this chat + Anyone with this link can view this chat.
- -
- - +
+ +
- - + +
From 2227b56f12b14c3e87c5f9d39844333d1f00391d Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Tue, 8 Apr 2025 20:38:39 -0500 Subject: [PATCH 08/13] middleware change --- middleware.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 5861d506..e582d818 100644 --- a/middleware.ts +++ b/middleware.ts @@ -16,7 +16,6 @@ export async function middleware(request: NextRequest) { !pathname.startsWith("/check") && !pathname.startsWith("/api/auth/callback") && !pathname.startsWith("/healthz") && - !pathname.startsWith("/public") && !pathname.startsWith("/share") ) { const redirectPath = getUnauthenticatedRedirectPath(pathname); From eab8f4c64d7f34ca3d3926bf3bc09799c077799a Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Wed, 9 Apr 2025 11:49:42 -0500 Subject: [PATCH 09/13] simplify schema --- drizzle/0033_petite_pet_avengers.sql | 32 ---------------------------- lib/api.ts | 19 +++++++++++++++++ lib/server/db/schema.ts | 17 +++++++++++++++ 3 files changed, 36 insertions(+), 32 deletions(-) delete mode 100644 drizzle/0033_petite_pet_avengers.sql diff --git a/drizzle/0033_petite_pet_avengers.sql b/drizzle/0033_petite_pet_avengers.sql deleted file mode 100644 index c3b490a1..00000000 --- a/drizzle/0033_petite_pet_avengers.sql +++ /dev/null @@ -1,32 +0,0 @@ -CREATE TYPE "public"."share_access_type" AS ENUM('public', 'organization', 'email');--> statement-breakpoint -CREATE TABLE IF NOT EXISTS "shared_conversations" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - "tenant_id" uuid NOT NULL, - "conversation_id" uuid NOT NULL, - "created_by" uuid NOT NULL, - "access_type" "share_access_type" DEFAULT 'public' NOT NULL, - "recipient_emails" json DEFAULT '[]'::json, - "expires_at" timestamp with time zone -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_created_by_profiles_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE INDEX IF NOT EXISTS "shared_conversations_conversation_id_idx" ON "shared_conversations" USING btree ("conversation_id"); \ No newline at end of file diff --git a/lib/api.ts b/lib/api.ts index 48433500..f56200b0 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -108,3 +108,22 @@ export const setupSchema = z.object({ id: z.string(), }), }); + +export const sharedConversationResponseSchema = z.object({ + share: z.object({ + shareId: z.string(), + createdBy: z.string(), + }), + conversation: z.object({ + id: z.string(), + title: z.string(), + }), + tenant: z.object({ + id: z.string(), + slug: z.string(), + }), + messages: conversationMessagesResponseSchema, + isOwner: z.boolean(), +}); + +export type SharedConversationResponse = z.infer; diff --git a/lib/server/db/schema.ts b/lib/server/db/schema.ts index 96234809..3e988ba4 100644 --- a/lib/server/db/schema.ts +++ b/lib/server/db/schema.ts @@ -138,6 +138,23 @@ export const messages = pgTable( }), ); +export const sharedConversations = pgTable( + "shared_conversations", + { + ...baseTenantFields, + // original conversation + conversationId: uuid("conversation_id") + .references(() => conversations.id, { onDelete: "cascade" }) + .notNull(), + createdBy: uuid("created_by") + .references(() => profiles.id, { onDelete: "cascade" }) + .notNull(), + }, + (t) => ({ + conversationIdIdx: index("shared_conversations_conversation_id_idx").on(t.conversationId), + }), +); + /** Based on Auth.js example schema: https://authjs.dev/getting-started/adapters/drizzle */ export const users = pgTable("users", { From 22a802eafb09e0515288018263ee4df1eb47043f Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Wed, 9 Apr 2025 12:31:24 -0500 Subject: [PATCH 10/13] simplify shares for only public links --- app/(main)/o/[slug]/header.tsx | 17 ++++------- app/(main)/o/[slug]/layout.tsx | 2 +- .../[conversationId]/messages/route.ts | 9 ++---- app/(shared)/share/[shareId]/layout.tsx | 10 ++----- app/(shared)/share/[shareId]/page.tsx | 4 +-- .../[shareId]/read-only-conversation.tsx | 29 ++----------------- .../shares/[shareId]/route.ts | 10 +------ .../[conversationId]/shares/route.ts | 12 +------- components/chatbot/read-only-chatbot.tsx | 10 ++----- components/share-button.tsx | 26 +++++++---------- components/shared-dialog.tsx | 20 ++++++------- lib/server/utils.ts | 2 +- 12 files changed, 44 insertions(+), 107 deletions(-) diff --git a/app/(main)/o/[slug]/header.tsx b/app/(main)/o/[slug]/header.tsx index e1ba2303..14a3c694 100644 --- a/app/(main)/o/[slug]/header.tsx +++ b/app/(main)/o/[slug]/header.tsx @@ -31,9 +31,9 @@ interface Props { name: string | undefined | null; email: string | undefined | null; isAnonymous: boolean; + isLoggedIn: boolean; className?: string; onNavClick?: () => void; - hasSession: boolean; } const HeaderPopoverContent = ({ @@ -53,7 +53,7 @@ const HeaderPopoverContent = ({ ); -export default function Header({ isAnonymous, tenant, name, email, onNavClick = () => {}, hasSession }: Props) { +export default function Header({ isAnonymous, tenant, name, email, isLoggedIn, onNavClick = () => {} }: Props) { const router = useRouter(); const [tenants, setTenants] = useState>([]); const [conversationId, setConversationId] = useState(""); @@ -61,7 +61,7 @@ export default function Header({ isAnonymous, tenant, name, email, onNavClick = useEffect(() => { // Only fetch tenants if there is an active session - if (hasSession) { + if (isLoggedIn) { (async () => { const res = await fetch("/api/tenants"); if (res.ok) { @@ -70,16 +70,11 @@ export default function Header({ isAnonymous, tenant, name, email, onNavClick = } })(); } - }, [hasSession]); + }, [isLoggedIn]); useEffect(() => { - const pathSegments = pathname.split("/"); - const conversationsIndex = pathSegments.indexOf("conversations"); - if (conversationsIndex !== -1 && pathSegments[conversationsIndex + 1]) { - setConversationId(pathSegments[conversationsIndex + 1]); - } else { - setConversationId(""); - } + const conversationIdMatch = pathname.match(/\/o\/[^/]+\/conversations\/([^/]+)/); + setConversationId(conversationIdMatch ? conversationIdMatch[1] : ""); }, [pathname]); const handleLogOutClick = async () => diff --git a/app/(main)/o/[slug]/layout.tsx b/app/(main)/o/[slug]/layout.tsx index 5609808f..f7484949 100644 --- a/app/(main)/o/[slug]/layout.tsx +++ b/app/(main)/o/[slug]/layout.tsx @@ -24,7 +24,7 @@ export default async function MainLayout({ children, params }: Props) { tenant={tenant} name={session.user.name} email={user.email} - hasSession={!!session} + isLoggedIn={!!session} />
diff --git a/app/(shared)/public/conversations/[conversationId]/messages/route.ts b/app/(shared)/public/conversations/[conversationId]/messages/route.ts index 83b8ccfc..7625ab9a 100644 --- a/app/(shared)/public/conversations/[conversationId]/messages/route.ts +++ b/app/(shared)/public/conversations/[conversationId]/messages/route.ts @@ -1,22 +1,19 @@ -import { and, asc, eq } from "drizzle-orm"; +import { asc, eq } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; -import { ZodError } from "zod"; import { conversationMessagesResponseSchema } from "@/lib/api"; import db from "@/lib/server/db"; import * as schema from "@/lib/server/db/schema"; -export async function POST(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { +export async function GET(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { try { - const body = await request.json(); - const { tenantId } = body; const { conversationId } = await params; // query directly in the route handler const messages = await db .select() .from(schema.messages) - .where(and(eq(schema.messages.tenantId, tenantId), eq(schema.messages.conversationId, conversationId))) + .where(eq(schema.messages.conversationId, conversationId)) .orderBy(asc(schema.messages.createdAt)); const parsedMessages = conversationMessagesResponseSchema.parse(messages); diff --git a/app/(shared)/share/[shareId]/layout.tsx b/app/(shared)/share/[shareId]/layout.tsx index 5e5e3472..c51e5380 100644 --- a/app/(shared)/share/[shareId]/layout.tsx +++ b/app/(shared)/share/[shareId]/layout.tsx @@ -3,10 +3,9 @@ import { ReactNode } from "react"; import Header from "@/app/(main)/o/[slug]/header"; import RagieLogo from "@/components/ragie-logo"; -import { SearchSettings } from "@/lib/api"; import { LLMModel } from "@/lib/llm/types"; import { getShareById, getUserById } from "@/lib/server/service"; -import { getOptionalSession } from "@/lib/server/utils"; +import { getSession } from "@/lib/server/utils"; interface Props { params: Promise<{ shareId: string }>; children?: ReactNode; @@ -29,9 +28,6 @@ export async function getShareData(shareId: string) { logoUrl: tenant?.logoUrl || null, slug: tenant?.slug || "", id: tenant?.id || "", - enabledModels: (tenant?.enabledModels as LLMModel[]) || [], - defaultModel: (tenant?.defaultModel as LLMModel | null) || null, - searchSettings: null as SearchSettings | null, }; return { share, formattedTenant, conversation }; @@ -39,7 +35,7 @@ export async function getShareData(shareId: string) { export default async function SharedLayout({ children, params }: Props) { const { shareId } = await params; - const session = await getOptionalSession(); + 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; @@ -60,7 +56,7 @@ export default async function SharedLayout({ children, params }: Props) { tenant={formattedTenant} name={session?.user.name} email={session?.user.email} - hasSession={!!session} + isLoggedIn={!!session} />
diff --git a/app/(shared)/share/[shareId]/page.tsx b/app/(shared)/share/[shareId]/page.tsx index 8b0f43d1..fe3a3230 100644 --- a/app/(shared)/share/[shareId]/page.tsx +++ b/app/(shared)/share/[shareId]/page.tsx @@ -11,7 +11,7 @@ export default async function SharedConversationPage({ params }: { params: Promi if (!shareData) { redirect("/sign-in"); } - const { formattedTenant, conversation, share } = shareData; + const { formattedTenant, conversation } = shareData; - return ; + return ; } diff --git a/app/(shared)/share/[shareId]/read-only-conversation.tsx b/app/(shared)/share/[shareId]/read-only-conversation.tsx index baad08a1..94ef035b 100644 --- a/app/(shared)/share/[shareId]/read-only-conversation.tsx +++ b/app/(shared)/share/[shareId]/read-only-conversation.tsx @@ -1,14 +1,11 @@ "use client"; import { useState } from "react"; -import { z } from "zod"; import ReadOnlyChatbot from "@/components/chatbot/read-only-chatbot"; -import { SearchSettings, ShareSettings } from "@/lib/api"; -import { LLMModel } from "@/lib/llm/types"; -import { sharedConversations } from "@/lib/server/db/schema"; import Summary from "../../../(main)/o/[slug]/conversations/[id]/summary"; + interface Props { id: string; tenant: { @@ -16,26 +13,11 @@ interface Props { logoUrl?: string | null; slug: string; id: string; - enabledModels: LLMModel[]; - defaultModel: LLMModel | null; - searchSettings: SearchSettings | null; - }; - share: { - id: string; - createdAt: Date; - updatedAt: Date; - tenantId: string; - conversationId: string; - createdBy: string; - accessType: "email" | "public" | "organization"; - recipientEmails: string[] | null; - expiresAt: string | null; }; } -export default function ReadOnlyConversation({ id, tenant, share }: Props) { +export default function ReadOnlyConversation({ id, tenant }: Props) { const [documentId, setDocumentId] = useState(null); - const createdBy = share.createdBy; const handleSelectedDocumentId = async (id: string) => { setDocumentId(id); @@ -43,12 +25,7 @@ export default function ReadOnlyConversation({ id, tenant, share }: Props) { return (
- + {documentId && (
({})); - const validationResult = deleteShareSchema.safeParse(body); - if (!validationResult.success) return new NextResponse("Invalid request body", { status: 400 }); - - const { slug } = validationResult.data; + const { slug } = body; const { profile, tenant } = await requireAuthContext(slug); // Verify conversation ownership diff --git a/app/api/conversations/[conversationId]/shares/route.ts b/app/api/conversations/[conversationId]/shares/route.ts index a7cca36d..57815778 100644 --- a/app/api/conversations/[conversationId]/shares/route.ts +++ b/app/api/conversations/[conversationId]/shares/route.ts @@ -1,7 +1,6 @@ import { eq } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; -import { createShareRequestSchema } from "@/lib/api"; import db from "@/lib/server/db"; import { sharedConversations } from "@/lib/server/db/schema"; import { getConversation } from "@/lib/server/service"; @@ -13,10 +12,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Parse the request body const body = await request.json().catch(() => ({})); - const validationResult = createShareRequestSchema.safeParse(body); - if (!validationResult.success) return new NextResponse("Invalid request body", { status: 400 }); - - const { slug } = validationResult.data; + const { slug } = body; const { profile, tenant } = await requireAuthContext(slug); // Verify conversation ownership @@ -28,9 +24,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ conversationId: conversation.id, tenantId: tenant.id, createdBy: profile.id, - accessType: body.accessType, - recipientEmails: body.recipientEmails || [], - expiresAt: body.expiresAt, }) .returning(); @@ -54,10 +47,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const shares = await db .select({ shareId: sharedConversations.id, - accessType: sharedConversations.accessType, createdAt: sharedConversations.createdAt, - expiresAt: sharedConversations.expiresAt, - recipientEmails: sharedConversations.recipientEmails, }) .from(sharedConversations) .where(eq(sharedConversations.conversationId, conversationId)) diff --git a/components/chatbot/read-only-chatbot.tsx b/components/chatbot/read-only-chatbot.tsx index d336a606..44853f06 100644 --- a/components/chatbot/read-only-chatbot.tsx +++ b/components/chatbot/read-only-chatbot.tsx @@ -26,10 +26,9 @@ interface Props { id: string; }; onSelectedDocumentId: (id: string) => void; - createdBy: string; } -export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocumentId, createdBy }: Props) { +export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocumentId }: Props) { // Use the inferred Message type for state const [messages, setMessages] = useState([]); const container = useRef(null); @@ -39,10 +38,7 @@ export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocu let isMounted = true; // Flag to prevent state updates on unmounted component (async () => { try { - const res = await fetch(`/public/conversations/${conversationId}/messages`, { - method: "POST", - body: JSON.stringify({ createdBy, tenantId: tenant.id }), - }); + const res = await fetch(`/public/conversations/${conversationId}/messages`); console.log(res); if (!res.ok) { console.error("Could not load conversation:", res.statusText); @@ -68,7 +64,7 @@ export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocu return () => { isMounted = false; // Cleanup function to set flag on unmount }; - // eslint-disable-next-line react-hooks/exhaustive-deps -- Run only once on mount + }, [conversationId, tenant.slug]); // Scroll to bottom when messages load/change diff --git a/components/share-button.tsx b/components/share-button.tsx index 0cc7df6e..4fd85aff 100644 --- a/components/share-button.tsx +++ b/components/share-button.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { toast } from "sonner"; import { Dialog, DialogTrigger } from "@/components/ui/dialog"; -import { ShareSettings } from "@/lib/api"; import ShareIcon from "@/public/icons/share.svg"; import ShareDialog from "./share-dialog"; @@ -16,21 +15,15 @@ interface ShareButtonProps { export function ShareButton({ conversationId, slug }: ShareButtonProps) { const [isShared, setIsShared] = useState(false); - const [shareSettings, setShareSettings] = useState< - (ShareSettings & { shareId?: string; conversationId: string }) | null - >(null); const [isLoading, setIsLoading] = useState(false); - - const handleShare = async (settings: ShareSettings) => { + const [shareId, setShareId] = useState(null); + const handleShare = async () => { try { setIsLoading(true); const response = await fetch(`/api/conversations/${conversationId}/shares`, { method: "POST", body: JSON.stringify({ slug, - accessType: settings.accessType, - recipientEmails: settings.accessType === "email" ? [settings.email!] : undefined, - expiresAt: settings.expiresAt ? new Date(settings.expiresAt).toISOString() : undefined, }), }); @@ -38,12 +31,8 @@ export function ShareButton({ conversationId, slug }: ShareButtonProps) { throw new Error("Failed to create share link"); } - const { shareId } = await response.json(); - setShareSettings({ - ...settings, - shareId, - conversationId, - }); + const { shareIdResponse } = await response.json(); + setShareId(shareIdResponse); setIsShared(true); } catch (error) { toast.error("Failed to share conversation"); @@ -60,7 +49,12 @@ export function ShareButton({ conversationId, slug }: ShareButtonProps) { {isShared ? ( - setIsShared(false)} /> + setIsShared(false)} + /> ) : ( )} diff --git a/components/shared-dialog.tsx b/components/shared-dialog.tsx index 9d74b089..67c01751 100644 --- a/components/shared-dialog.tsx +++ b/components/shared-dialog.tsx @@ -4,21 +4,21 @@ import { toast } from "sonner"; import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { ShareSettings } from "@/lib/api"; export default function SharedDialog({ - settings, + shareId, + conversationId, + slug, onClose, }: { - settings: ShareSettings & { - shareId?: string; - conversationId: string; - }; + shareId: string | null; + conversationId: string; + slug: string; onClose: () => void; }) { const [isCopied, setIsCopied] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - const shareUrl = settings.shareId ? new URL(`/share/${settings.shareId}`, window.location.origin).toString() : ""; + const shareUrl = shareId ? new URL(`/share/${shareId}`, window.location.origin).toString() : ""; const handleCopyUrl = async () => { try { @@ -31,14 +31,14 @@ export default function SharedDialog({ }; const handleStopSharing = async () => { - if (!settings.shareId) return; + if (!shareId) return; try { setIsDeleting(true); - const response = await fetch(`/api/conversations/${settings.conversationId}/shares/${settings.shareId}`, { + const response = await fetch(`/api/conversations/${conversationId}/shares/${shareId}`, { method: "DELETE", body: JSON.stringify({ - slug: settings.slug, + slug, }), }); diff --git a/lib/server/utils.ts b/lib/server/utils.ts index 410d0eca..42e5d125 100644 --- a/lib/server/utils.ts +++ b/lib/server/utils.ts @@ -44,7 +44,7 @@ export async function requireAdminContext(slug: string) { return context; } -export async function getOptionalSession() { +export async function getSession() { const session = await auth.api.getSession({ headers: await headers(), // you need to pass the headers object. }); From 7fb1a7d676f19bc2b18dcbcc3eca2cd9684622f7 Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Wed, 9 Apr 2025 13:03:30 -0500 Subject: [PATCH 11/13] simplify api routes --- .../[conversationId]/messages/route.ts | 9 +- components/chatbot/read-only-chatbot.tsx | 5 +- components/share-button.tsx | 12 +- components/share-dialog.tsx | 19 +- drizzle/0034_tense_bill_hollister.sql | 28 + drizzle/meta/0034_snapshot.json | 1320 +++++++++++++++++ drizzle/meta/_journal.json | 9 +- 7 files changed, 1374 insertions(+), 28 deletions(-) rename app/(shared)/{public => share}/conversations/[conversationId]/messages/route.ts (79%) create mode 100644 drizzle/0034_tense_bill_hollister.sql create mode 100644 drizzle/meta/0034_snapshot.json diff --git a/app/(shared)/public/conversations/[conversationId]/messages/route.ts b/app/(shared)/share/conversations/[conversationId]/messages/route.ts similarity index 79% rename from app/(shared)/public/conversations/[conversationId]/messages/route.ts rename to app/(shared)/share/conversations/[conversationId]/messages/route.ts index 7625ab9a..be1415ab 100644 --- a/app/(shared)/public/conversations/[conversationId]/messages/route.ts +++ b/app/(shared)/share/conversations/[conversationId]/messages/route.ts @@ -1,4 +1,4 @@ -import { asc, eq } from "drizzle-orm"; +import { asc, eq, and } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; import { conversationMessagesResponseSchema } from "@/lib/api"; @@ -9,18 +9,21 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ try { const { conversationId } = await params; - // query directly in the route handler + // Query messages for shared conversations const messages = await db .select() .from(schema.messages) .where(eq(schema.messages.conversationId, conversationId)) .orderBy(asc(schema.messages.createdAt)); + if (messages.length === 0) { + return NextResponse.json({ error: "Conversation not found or not shared." }, { status: 404 }); + } + const parsedMessages = conversationMessagesResponseSchema.parse(messages); return Response.json(parsedMessages); } catch (error) { console.error("Error fetching public conversation messages:", error); - return NextResponse.json({ error: "Failed to load conversation messages." }, { status: 500 }); } } diff --git a/components/chatbot/read-only-chatbot.tsx b/components/chatbot/read-only-chatbot.tsx index 44853f06..31222d26 100644 --- a/components/chatbot/read-only-chatbot.tsx +++ b/components/chatbot/read-only-chatbot.tsx @@ -38,7 +38,7 @@ export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocu let isMounted = true; // Flag to prevent state updates on unmounted component (async () => { try { - const res = await fetch(`/public/conversations/${conversationId}/messages`); + const res = await fetch(`/share/conversations/${conversationId}/messages`); console.log(res); if (!res.ok) { console.error("Could not load conversation:", res.statusText); @@ -64,8 +64,7 @@ export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocu return () => { isMounted = false; // Cleanup function to set flag on unmount }; - - }, [conversationId, tenant.slug]); + }, [conversationId]); // Scroll to bottom when messages load/change useEffect(() => { diff --git a/components/share-button.tsx b/components/share-button.tsx index 4fd85aff..517de50f 100644 --- a/components/share-button.tsx +++ b/components/share-button.tsx @@ -17,6 +17,7 @@ export function ShareButton({ conversationId, slug }: ShareButtonProps) { const [isShared, setIsShared] = useState(false); const [isLoading, setIsLoading] = useState(false); const [shareId, setShareId] = useState(null); + const handleShare = async () => { try { setIsLoading(true); @@ -31,7 +32,7 @@ export function ShareButton({ conversationId, slug }: ShareButtonProps) { throw new Error("Failed to create share link"); } - const { shareIdResponse } = await response.json(); + const { shareId: shareIdResponse } = await response.json(); setShareId(shareIdResponse); setIsShared(true); } catch (error) { @@ -48,15 +49,18 @@ export function ShareButton({ conversationId, slug }: ShareButtonProps) { Share conversation - {isShared ? ( + {isShared && shareId ? ( setIsShared(false)} + onClose={() => { + setIsShared(false); + setShareId(null); + }} /> ) : ( - + )} ); diff --git a/components/share-dialog.tsx b/components/share-dialog.tsx index 9e83ddf1..c521fc6b 100644 --- a/components/share-dialog.tsx +++ b/components/share-dialog.tsx @@ -1,27 +1,12 @@ import { Loader2 } from "lucide-react"; import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { ShareSettings } from "@/lib/api"; import PrimaryButton from "./primary-button"; -export default function ShareDialog({ - conversationId, - onShare, - isLoading, - slug, -}: { - conversationId: string; - onShare: (data: ShareSettings) => void; - isLoading: boolean; - slug: string; -}) { +export default function ShareDialog({ onShare, isLoading }: { onShare: () => void; isLoading: boolean }) { const handleShare = async () => { - await onShare({ - accessType: "public", - expiresAt: undefined, - slug, - }); + await onShare(); }; return ( diff --git a/drizzle/0034_tense_bill_hollister.sql b/drizzle/0034_tense_bill_hollister.sql new file mode 100644 index 00000000..31926010 --- /dev/null +++ b/drizzle/0034_tense_bill_hollister.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS "shared_conversations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant_id" uuid NOT NULL, + "conversation_id" uuid NOT NULL, + "created_by" uuid NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_conversation_id_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversations"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "shared_conversations" ADD CONSTRAINT "shared_conversations_created_by_profiles_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "shared_conversations_conversation_id_idx" ON "shared_conversations" USING btree ("conversation_id"); \ No newline at end of file diff --git a/drizzle/meta/0034_snapshot.json b/drizzle/meta/0034_snapshot.json new file mode 100644 index 00000000..e37a32d4 --- /dev/null +++ b/drizzle/meta/0034_snapshot.json @@ -0,0 +1,1320 @@ +{ + "id": "6ad26bab-170a-4755-9454-259e1f3d9069", + "prevId": "c59ab382-5564-4cd8-8070-4d2a011e17ab", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.authenticators": { + "name": "authenticators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_public_key": { + "name": "credential_public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_device_type": { + "name": "credential_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_backed_up": { + "name": "credential_backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "authenticators_user_id_users_id_fk": { + "name": "authenticators_user_id_users_id_fk", + "tableFrom": "authenticators", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "authenticators_credential_id_unique": { + "name": "authenticators_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ragie_connection_id": { + "name": "ragie_connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sourceType": { + "name": "sourceType", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "connections_tenant_id_tenants_id_fk": { + "name": "connections_tenant_id_tenants_id_fk", + "tableFrom": "connections", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "connections_ragie_connection_id_unique": { + "name": "connections_ragie_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ragie_connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "conversations_profile_idx": { + "name": "conversations_profile_idx", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_tenant_profile_idx": { + "name": "conversations_tenant_profile_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_tenant_id_tenants_id_fk": { + "name": "conversations_tenant_id_tenants_id_fk", + "tableFrom": "conversations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_profile_id_profiles_id_fk": { + "name": "conversations_profile_id_profiles_id_fk", + "tableFrom": "conversations", + "tableTo": "profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invited_by_id": { + "name": "invited_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invites_tenant_id_tenants_id_fk": { + "name": "invites_tenant_id_tenants_id_fk", + "tableFrom": "invites", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invites_invited_by_id_profiles_id_fk": { + "name": "invites_invited_by_id_profiles_id_fk", + "tableFrom": "invites", + "tableTo": "profiles", + "columnsFrom": [ + "invited_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invites_tenant_id_email_unique": { + "name": "invites_tenant_id_email_unique", + "nullsNotDistinct": false, + "columns": [ + "tenant_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "message_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "is_breadth": { + "name": "is_breadth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rerank_enabled": { + "name": "rerank_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "prioritize_recent": { + "name": "prioritize_recent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "messages_conversation_idx": { + "name": "messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_tenant_conversation_idx": { + "name": "messages_tenant_conversation_idx", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_tenant_id_tenants_id_fk": { + "name": "messages_tenant_id_tenants_id_fk", + "tableFrom": "messages", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "profiles_role_idx": { + "name": "profiles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "profiles_tenant_id_tenants_id_fk": { + "name": "profiles_tenant_id_tenants_id_fk", + "tableFrom": "profiles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "profiles_user_id_users_id_fk": { + "name": "profiles_user_id_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profiles_tenant_id_user_id_unique": { + "name": "profiles_tenant_id_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tenant_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_conversations": { + "name": "shared_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "shared_conversations_conversation_id_idx": { + "name": "shared_conversations_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_conversations_tenant_id_tenants_id_fk": { + "name": "shared_conversations_tenant_id_tenants_id_fk", + "tableFrom": "shared_conversations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_conversations_conversation_id_conversations_id_fk": { + "name": "shared_conversations_conversation_id_conversations_id_fk", + "tableFrom": "shared_conversations", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_conversations_created_by_profiles_id_fk": { + "name": "shared_conversations_created_by_profiles_id_fk", + "tableFrom": "shared_conversations", + "tableTo": "profiles", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "question1": { + "name": "question1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "question2": { + "name": "question2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "question3": { + "name": "question3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grounding_prompt": { + "name": "grounding_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "welcome_message": { + "name": "welcome_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_file_name": { + "name": "logo_file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_object_name": { + "name": "logo_object_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled_models": { + "name": "enabled_models", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{\"gpt-4o\",\"gpt-3.5-turbo\",\"gemini-2.0-flash\",\"gemini-1.5-pro\",\"claude-3-7-sonnet-latest\",\"claude-3-5-haiku-latest\",\"meta-llama/llama-4-scout-17b-16e-instruct\"}'" + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'claude-3-7-sonnet-latest'" + }, + "is_breadth": { + "name": "is_breadth", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rerank_enabled": { + "name": "rerank_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "prioritize_recent": { + "name": "prioritize_recent", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "override_breadth": { + "name": "override_breadth", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "override_rerank": { + "name": "override_rerank", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "override_prioritize_recent": { + "name": "override_prioritize_recent", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenants_slug_unique": { + "name": "tenants_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_profile_id": { + "name": "current_profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "users_current_profile_id_profiles_id_fk": { + "name": "users_current_profile_id_profiles_id_fk", + "tableFrom": "users", + "tableTo": "profiles", + "columnsFrom": [ + "current_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.message_roles": { + "name": "message_roles", + "schema": "public", + "values": [ + "assistant", + "system", + "user" + ] + }, + "public.roles": { + "name": "roles", + "schema": "public", + "values": [ + "admin", + "user", + "guest" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1b4aada0..96d6b19f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -239,6 +239,13 @@ "when": 1744210399724, "tag": "0033_fresh_baron_zemo", "breakpoints": true + }, + { + "idx": 34, + "version": "7", + "when": 1744219921977, + "tag": "0034_tense_bill_hollister", + "breakpoints": true } ] -} +} \ No newline at end of file From 4242dfe3fdab83abe7c92e308828656b074a3adb Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Wed, 9 Apr 2025 13:43:00 -0500 Subject: [PATCH 12/13] move share specific api inside api directory --- .../share/conversations/[conversationId]/messages/route.ts | 0 components/chatbot/read-only-chatbot.tsx | 2 +- middleware.ts | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename app/{(shared) => api}/share/conversations/[conversationId]/messages/route.ts (100%) diff --git a/app/(shared)/share/conversations/[conversationId]/messages/route.ts b/app/api/share/conversations/[conversationId]/messages/route.ts similarity index 100% rename from app/(shared)/share/conversations/[conversationId]/messages/route.ts rename to app/api/share/conversations/[conversationId]/messages/route.ts diff --git a/components/chatbot/read-only-chatbot.tsx b/components/chatbot/read-only-chatbot.tsx index 31222d26..aba090c0 100644 --- a/components/chatbot/read-only-chatbot.tsx +++ b/components/chatbot/read-only-chatbot.tsx @@ -38,7 +38,7 @@ export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocu let isMounted = true; // Flag to prevent state updates on unmounted component (async () => { try { - const res = await fetch(`/share/conversations/${conversationId}/messages`); + const res = await fetch(`/api/share/conversations/${conversationId}/messages`); console.log(res); if (!res.ok) { console.error("Could not load conversation:", res.statusText); diff --git a/middleware.ts b/middleware.ts index e582d818..14b971f8 100644 --- a/middleware.ts +++ b/middleware.ts @@ -16,6 +16,7 @@ export async function middleware(request: NextRequest) { !pathname.startsWith("/check") && !pathname.startsWith("/api/auth/callback") && !pathname.startsWith("/healthz") && + !pathname.startsWith("/api/share") && !pathname.startsWith("/share") ) { const redirectPath = getUnauthenticatedRedirectPath(pathname); From 81ebc4be6acdfe80b9ac5de8e6738dc82d90e09c Mon Sep 17 00:00:00 2001 From: Stone Werner Date: Wed, 9 Apr 2025 13:55:04 -0500 Subject: [PATCH 13/13] fix build errors --- app/(main)/o/[slug]/welcome.tsx | 11 ++++++++++- app/(shared)/share/[shareId]/layout.tsx | 26 ++----------------------- app/(shared)/share/[shareId]/page.tsx | 3 ++- components/chatbot/index.tsx | 11 ++++++++++- lib/server/service.tsx | 22 +++++++++++++++++++++ 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/app/(main)/o/[slug]/welcome.tsx b/app/(main)/o/[slug]/welcome.tsx index 8c2dd172..12311080 100644 --- a/app/(main)/o/[slug]/welcome.tsx +++ b/app/(main)/o/[slug]/welcome.tsx @@ -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(() => { diff --git a/app/(shared)/share/[shareId]/layout.tsx b/app/(shared)/share/[shareId]/layout.tsx index c51e5380..d22b6644 100644 --- a/app/(shared)/share/[shareId]/layout.tsx +++ b/app/(shared)/share/[shareId]/layout.tsx @@ -3,36 +3,14 @@ import { ReactNode } from "react"; import Header from "@/app/(main)/o/[slug]/header"; import RagieLogo from "@/components/ragie-logo"; -import { LLMModel } from "@/lib/llm/types"; -import { getShareById, getUserById } from "@/lib/server/service"; +import { getShareData, getUserById } from "@/lib/server/service"; import { getSession } from "@/lib/server/utils"; + interface Props { params: Promise<{ shareId: string }>; children?: ReactNode; } -export async function getShareData(shareId: string) { - const shareResult = await getShareById(shareId); - if (!shareResult) { - return null; - } - - const { share, tenant, conversation } = shareResult; - if (!share || !tenant || !conversation) { - return null; - } - - // Format tenant object - const formattedTenant = { - name: tenant?.name || "", - logoUrl: tenant?.logoUrl || null, - slug: tenant?.slug || "", - id: tenant?.id || "", - }; - - return { share, formattedTenant, conversation }; -} - export default async function SharedLayout({ children, params }: Props) { const { shareId } = await params; const session = await getSession(); diff --git a/app/(shared)/share/[shareId]/page.tsx b/app/(shared)/share/[shareId]/page.tsx index fe3a3230..b5eb584f 100644 --- a/app/(shared)/share/[shareId]/page.tsx +++ b/app/(shared)/share/[shareId]/page.tsx @@ -1,6 +1,7 @@ import { redirect } from "next/navigation"; -import { getShareData } from "./layout"; +import { getShareData } from "@/lib/server/service"; + import ReadOnlyConversation from "./read-only-conversation"; export default async function SharedConversationPage({ params }: { params: Promise<{ shareId: string }> }) { diff --git a/components/chatbot/index.tsx b/components/chatbot/index.tsx index 838e8782..fba4dc79 100644 --- a/components/chatbot/index.tsx +++ b/components/chatbot/index.tsx @@ -136,7 +136,16 @@ export default function Chatbot({ tenant, conversationId, initMessage, onSelecte } } } - }, [enabledModels, tenant.overrideBreadth, tenant.overrideRerank, tenant.overridePrioritizeRecent]); + }, [ + tenant.isBreadth, + tenant.overrideBreadth, + tenant.rerankEnabled, + tenant.overrideRerank, + tenant.prioritizeRecent, + tenant.overridePrioritizeRecent, + tenant.defaultModel, + enabledModels, + ]); // Save user settings to localStorage whenever they change useEffect(() => { diff --git a/lib/server/service.tsx b/lib/server/service.tsx index bbf43421..9d2ba50c 100644 --- a/lib/server/service.tsx +++ b/lib/server/service.tsx @@ -303,6 +303,28 @@ export async function getShareById(shareId: string) { return rs.length ? rs[0] : null; } +export async function getShareData(shareId: string) { + const shareResult = await getShareById(shareId); + if (!shareResult) { + return null; + } + + const { share, tenant, conversation } = shareResult; + if (!share || !tenant || !conversation) { + return null; + } + + // Format tenant object + const formattedTenant = { + name: tenant?.name || "", + logoUrl: tenant?.logoUrl || null, + slug: tenant?.slug || "", + id: tenant?.id || "", + }; + + return { share, formattedTenant, conversation }; +} + export async function setCurrentProfileId(userId: string, profileId: string) { await db.transaction(async (tx) => { // Validate profile exists and is scoped to the userId