diff --git a/app/(main)/o/[slug]/header.tsx b/app/(main)/o/[slug]/header.tsx index ece60f36..14a3c694 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"; @@ -30,6 +31,7 @@ interface Props { name: string | undefined | null; email: string | undefined | null; isAnonymous: boolean; + isLoggedIn: boolean; className?: string; onNavClick?: () => void; } @@ -51,17 +53,29 @@ const HeaderPopoverContent = ({ ); -export default function Header({ isAnonymous, tenant, name, email, onNavClick = () => {} }: Props) { +export default function Header({ isAnonymous, tenant, name, email, isLoggedIn, onNavClick = () => {} }: Props) { const router = useRouter(); const [tenants, setTenants] = useState>([]); + 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 (isLoggedIn) { + (async () => { + const res = await fetch("/api/tenants"); + if (res.ok) { + const tenants = tenantListResponseSchema.parse(await res.json()); + setTenants(tenants); + } + })(); + } + }, [isLoggedIn]); + + useEffect(() => { + const conversationIdMatch = pathname.match(/\/o\/[^/]+\/conversations\/([^/]+)/); + setConversationId(conversationIdMatch ? conversationIdMatch[1] : ""); + }, [pathname]); const handleLogOutClick = async () => await signOut({ @@ -109,72 +123,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/app/(main)/o/[slug]/layout.tsx b/app/(main)/o/[slug]/layout.tsx index f1864dce..f7484949 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/(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 new file mode 100644 index 00000000..d22b6644 --- /dev/null +++ b/app/(shared)/share/[shareId]/layout.tsx @@ -0,0 +1,55 @@ +import { redirect } from "next/navigation"; +import { ReactNode } from "react"; + +import Header from "@/app/(main)/o/[slug]/header"; +import RagieLogo from "@/components/ragie-logo"; +import { getShareData, getUserById } from "@/lib/server/service"; +import { getSession } from "@/lib/server/utils"; + +interface Props { + params: Promise<{ shareId: string }>; + children?: ReactNode; +} + +export default async function SharedLayout({ children, params }: Props) { + const { shareId } = await params; + const session = await getSession(); + + // can show logged in users a proper header if they are logged in, but it is not required + let user = null; + if (session) { + user = await getUserById(session.user.id); + } + + const shareData = await getShareData(shareId); + if (!shareData) { + redirect("/sign-in"); + } + const { formattedTenant } = shareData; + + return ( +
+
+
+
+ {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..b5eb584f --- /dev/null +++ b/app/(shared)/share/[shareId]/page.tsx @@ -0,0 +1,18 @@ +import { redirect } from "next/navigation"; + +import { getShareData } from "@/lib/server/service"; + +import ReadOnlyConversation from "./read-only-conversation"; + +export default async function SharedConversationPage({ params }: { params: Promise<{ shareId: string }> }) { + const p = await params; + const { shareId } = p; + + const shareData = await getShareData(shareId); + if (!shareData) { + redirect("/sign-in"); + } + const { formattedTenant, conversation } = shareData; + + return ; +} 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..94ef035b --- /dev/null +++ b/app/(shared)/share/[shareId]/read-only-conversation.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useState } from "react"; + +import ReadOnlyChatbot from "@/components/chatbot/read-only-chatbot"; + +import Summary from "../../../(main)/o/[slug]/conversations/[id]/summary"; + +interface Props { + id: string; + tenant: { + name: string; + logoUrl?: string | null; + slug: string; + id: string; + }; +} + +export default function ReadOnlyConversation({ id, tenant }: Props) { + const [documentId, setDocumentId] = useState(null); + + const handleSelectedDocumentId = async (id: string) => { + setDocumentId(id); + }; + + return ( +
+ + {documentId && ( +
+ setDocumentId(null)} + /> +
+ )} +
+ ); +} 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..05418420 --- /dev/null +++ b/app/api/conversations/[conversationId]/shares/[shareId]/route.ts @@ -0,0 +1,44 @@ +import { eq, and } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +import db from "@/lib/server/db"; +import { sharedConversations } from "@/lib/server/db/schema"; +import { getConversation } from "@/lib/server/service"; +import { requireAuthContext } from "@/lib/server/utils"; + +// Delete a share +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ conversationId: string; shareId: string }> }, +) { + const { conversationId, shareId } = await params; + try { + // Parse the request body + const body = await request.json().catch(() => ({})); + const { slug } = body; + const { profile, tenant } = await requireAuthContext(slug); + + // Verify conversation ownership + await getConversation(tenant.id, profile.id, conversationId); + + // Delete share + const [deletedShare] = await db + .delete(sharedConversations) + .where( + and( + eq(sharedConversations.conversationId, conversationId), + eq(sharedConversations.id, shareId), + eq(sharedConversations.tenantId, tenant.id), + ), + ) + .returning(); + if (!deletedShare) { + return new Response("Share not found", { status: 404 }); + } + return new Response(null, { status: 204 }); + } catch (error) { + console.error("Failed to delete conversation:", error); + return new NextResponse("Internal Server Error", { status: 500 }); + } +} diff --git a/app/api/conversations/[conversationId]/shares/route.ts b/app/api/conversations/[conversationId]/shares/route.ts new file mode 100644 index 00000000..57815778 --- /dev/null +++ b/app/api/conversations/[conversationId]/shares/route.ts @@ -0,0 +1,61 @@ +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +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 { slug } = body; + 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.id, + }) + .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, + createdAt: sharedConversations.createdAt, + }) + .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 }); + } +} diff --git a/app/api/share/conversations/[conversationId]/messages/route.ts b/app/api/share/conversations/[conversationId]/messages/route.ts new file mode 100644 index 00000000..be1415ab --- /dev/null +++ b/app/api/share/conversations/[conversationId]/messages/route.ts @@ -0,0 +1,29 @@ +import { asc, eq, and } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +import { conversationMessagesResponseSchema } from "@/lib/api"; +import db from "@/lib/server/db"; +import * as schema from "@/lib/server/db/schema"; + +export async function GET(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { + try { + const { conversationId } = await params; + + // 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/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/components/chatbot/read-only-chatbot.tsx b/components/chatbot/read-only-chatbot.tsx new file mode 100644 index 00000000..aba090c0 --- /dev/null +++ b/components/chatbot/read-only-chatbot.tsx @@ -0,0 +1,116 @@ +"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; +} + +export default function ReadOnlyChatbot({ tenant, conversationId, onSelectedDocumentId }: 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(`/api/share/conversations/${conversationId}/messages`); + 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 + }; + }, [conversationId]); + + // 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") { + return ( + + + + ); + } + return null; + })} +
+
+
+ ); +} diff --git a/components/share-button.tsx b/components/share-button.tsx new file mode 100644 index 00000000..517de50f --- /dev/null +++ b/components/share-button.tsx @@ -0,0 +1,67 @@ +import Image from "next/image"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Dialog, DialogTrigger } from "@/components/ui/dialog"; +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 [isLoading, setIsLoading] = useState(false); + 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, + }), + }); + + if (!response.ok) { + throw new Error("Failed to create share link"); + } + + const { shareId: shareIdResponse } = await response.json(); + setShareId(shareIdResponse); + setIsShared(true); + } catch (error) { + toast.error("Failed to share conversation"); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + {isShared && shareId ? ( + { + setIsShared(false); + setShareId(null); + }} + /> + ) : ( + + )} + + ); +} diff --git a/components/share-dialog.tsx b/components/share-dialog.tsx new file mode 100644 index 00000000..c521fc6b --- /dev/null +++ b/components/share-dialog.tsx @@ -0,0 +1,30 @@ +import { Loader2 } from "lucide-react"; + +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; + +import PrimaryButton from "./primary-button"; + +export default function ShareDialog({ onShare, isLoading }: { onShare: () => void; isLoading: boolean }) { + const handleShare = async () => { + await onShare(); + }; + + return ( + + + Share public link to this chat + + Once created, anyone with the link can view this chat. + + + +
+ + + {isLoading ? : "Create public link"} + + +
+
+ ); +} diff --git a/components/shared-dialog.tsx b/components/shared-dialog.tsx new file mode 100644 index 00000000..67c01751 --- /dev/null +++ b/components/shared-dialog.tsx @@ -0,0 +1,91 @@ +import { Check, Copy, Loader2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; + +export default function SharedDialog({ + shareId, + conversationId, + slug, + onClose, +}: { + shareId: string | null; + conversationId: string; + slug: string; + onClose: () => void; +}) { + const [isCopied, setIsCopied] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const shareUrl = shareId ? new URL(`/share/${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 (!shareId) return; + + try { + setIsDeleting(true); + const response = await fetch(`/api/conversations/${conversationId}/shares/${shareId}`, { + method: "DELETE", + body: JSON.stringify({ + 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 public link to this chat + Anyone with this link can view this chat. + + +
+
+
+ + +
+
+ + + + +
+
+ ); +} 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/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/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 +} 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 f72a1c30..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 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", { diff --git a/lib/server/service.tsx b/lib/server/service.tsx index 201f1d44..9d2ba50c 100644 --- a/lib/server/service.tsx +++ b/lib/server/service.tsx @@ -286,6 +286,45 @@ export async function findTenantBySlug(slug: string) { return tenants.length ? tenants[0] : null; } +export async function getShareById(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 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 diff --git a/lib/server/utils.ts b/lib/server/utils.ts index 5d5edec4..42e5d125 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 getSession() { + 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..14b971f8 100644 --- a/middleware.ts +++ b/middleware.ts @@ -15,7 +15,9 @@ 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("/api/share") && + !pathname.startsWith("/share") ) { const redirectPath = getUnauthenticatedRedirectPath(pathname); const newUrl = new URL(redirectPath, BASE_URL); @@ -37,6 +39,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"; } 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 @@ + + + + +