diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 803bd6661..5d00232f6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -42,7 +42,6 @@ import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user- import { Spinner } from "@/components/ui/spinner"; import { useChatSessionStateSync } from "@/hooks/use-chat-session-state"; import { useMessagesElectric } from "@/hooks/use-messages-electric"; -import { publicChatApiService } from "@/lib/apis/public-chat-api.service"; // import { WriteTodosToolUI } from "@/components/tool-ui/write-todos"; import { getBearerToken } from "@/lib/auth-utils"; import { createAttachmentAdapter, extractAttachmentContent } from "@/lib/chat/attachment-adapter"; @@ -142,8 +141,6 @@ export default function NewChatPage() { const params = useParams(); const queryClient = useQueryClient(); const [isInitializing, setIsInitializing] = useState(true); - const [isCompletingClone, setIsCompletingClone] = useState(false); - const [cloneError, setCloneError] = useState(false); const [threadId, setThreadId] = useState(null); const [currentThread, setCurrentThread] = useState(null); const [messages, setMessages] = useState([]); @@ -332,42 +329,6 @@ export default function NewChatPage() { initializeThread(); }, [initializeThread]); - // Handle clone completion when thread has clone_pending flag - useEffect(() => { - if (!currentThread?.clone_pending || isCompletingClone || cloneError) return; - - const completeClone = async () => { - setIsCompletingClone(true); - - try { - await publicChatApiService.completeClone({ thread_id: currentThread.id }); - - // Re-initialize thread to fetch cloned content using existing logic - await initializeThread(); - - // Invalidate threads query to update sidebar - queryClient.invalidateQueries({ - predicate: (query) => Array.isArray(query.queryKey) && query.queryKey[0] === "threads", - }); - } catch (error) { - console.error("[NewChatPage] Failed to complete clone:", error); - toast.error("Failed to copy chat content. Please try again."); - setCloneError(true); - } finally { - setIsCompletingClone(false); - } - }; - - completeClone(); - }, [ - currentThread?.clone_pending, - currentThread?.id, - isCompletingClone, - cloneError, - initializeThread, - queryClient, - ]); - // Handle scroll to comment from URL query params (e.g., from inbox item click) const searchParams = useSearchParams(); const targetCommentIdParam = searchParams.get("commentId"); @@ -394,8 +355,6 @@ export default function NewChatPage() { visibility: currentThread?.visibility ?? null, hasComments: currentThread?.has_comments ?? false, addingCommentToMessageId: null, - publicShareEnabled: currentThread?.public_share_enabled ?? false, - publicShareToken: currentThread?.public_share_token ?? null, })); }, [currentThread, setCurrentThreadState]); @@ -1420,16 +1379,6 @@ export default function NewChatPage() { ); } - // Show loading state while completing clone - if (isCompletingClone) { - return ( -
- -
Copying chat content...
-
- ); - } - // Show error state only if we tried to load an existing thread but failed // For new chats (urlChatId === 0), threadId being null is expected (lazy creation) if (!threadId && urlChatId > 0) { diff --git a/surfsense_web/atoms/chat/chat-thread-mutation.atoms.ts b/surfsense_web/atoms/chat/chat-thread-mutation.atoms.ts index a844a45fb..d8c158fd1 100644 --- a/surfsense_web/atoms/chat/chat-thread-mutation.atoms.ts +++ b/surfsense_web/atoms/chat/chat-thread-mutation.atoms.ts @@ -1,28 +1,31 @@ import { atomWithMutation } from "jotai-tanstack-query"; import { toast } from "sonner"; import type { - TogglePublicShareRequest, - TogglePublicShareResponse, + CreateSnapshotRequest, + CreateSnapshotResponse, } from "@/contracts/types/chat-threads.types"; import { chatThreadsApiService } from "@/lib/apis/chat-threads-api.service"; -export const togglePublicShareMutationAtom = atomWithMutation(() => ({ - mutationFn: async (request: TogglePublicShareRequest) => { - return chatThreadsApiService.togglePublicShare(request); +export const createSnapshotMutationAtom = atomWithMutation(() => ({ + mutationFn: async (request: CreateSnapshotRequest) => { + return chatThreadsApiService.createSnapshot(request); }, - onSuccess: (response: TogglePublicShareResponse) => { - if (response.enabled && response.share_token) { - const publicUrl = `${window.location.origin}/public/${response.share_token}`; - navigator.clipboard.writeText(publicUrl); - toast.success("Public link copied to clipboard", { - description: "Anyone with this link can view the chat", + onSuccess: (response: CreateSnapshotResponse) => { + // Construct URL using frontend origin (backend returns its own URL which differs) + const publicUrl = `${window.location.origin}/public/${response.share_token}`; + navigator.clipboard.writeText(publicUrl); + if (response.is_new) { + toast.success("Public link created and copied to clipboard", { + description: "Anyone with this link can view a snapshot of this chat", }); } else { - toast.success("Public sharing disabled"); + toast.success("Public link copied to clipboard", { + description: "This snapshot already exists", + }); } }, onError: (error: Error) => { - console.error("Failed to toggle public share:", error); - toast.error("Failed to update public sharing"); + console.error("Failed to create snapshot:", error); + toast.error("Failed to create public link"); }, })); diff --git a/surfsense_web/atoms/chat/current-thread.atom.ts b/surfsense_web/atoms/chat/current-thread.atom.ts index 54f2190fe..5de11eb92 100644 --- a/surfsense_web/atoms/chat/current-thread.atom.ts +++ b/surfsense_web/atoms/chat/current-thread.atom.ts @@ -19,8 +19,6 @@ interface CurrentThreadState { addingCommentToMessageId: number | null; /** Whether the right-side comments panel is collapsed (desktop only) */ commentsCollapsed: boolean; - publicShareEnabled: boolean; - publicShareToken: string | null; } const initialState: CurrentThreadState = { @@ -29,8 +27,6 @@ const initialState: CurrentThreadState = { hasComments: false, addingCommentToMessageId: null, commentsCollapsed: false, - publicShareEnabled: false, - publicShareToken: null, }; export const currentThreadAtom = atom(initialState); diff --git a/surfsense_web/components/new-chat/chat-share-button.tsx b/surfsense_web/components/new-chat/chat-share-button.tsx index 9a1b3c426..f523752f1 100644 --- a/surfsense_web/components/new-chat/chat-share-button.tsx +++ b/surfsense_web/components/new-chat/chat-share-button.tsx @@ -2,10 +2,10 @@ import { useQueryClient } from "@tanstack/react-query"; import { useAtomValue, useSetAtom } from "jotai"; -import { Globe, Link2, User, Users } from "lucide-react"; +import { Globe, User, Users } from "lucide-react"; import { useCallback, useState } from "react"; import { toast } from "sonner"; -import { togglePublicShareMutationAtom } from "@/atoms/chat/chat-thread-mutation.atoms"; +import { createSnapshotMutationAtom } from "@/atoms/chat/chat-thread-mutation.atoms"; import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -49,19 +49,15 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS // Use Jotai atom for visibility (single source of truth) const currentThreadState = useAtomValue(currentThreadAtom); - const setCurrentThreadState = useSetAtom(currentThreadAtom); const setThreadVisibility = useSetAtom(setThreadVisibilityAtom); - // Public share mutation - const { mutateAsync: togglePublicShare, isPending: isTogglingPublic } = useAtomValue( - togglePublicShareMutationAtom + // Snapshot creation mutation + const { mutateAsync: createSnapshot, isPending: isCreatingSnapshot } = useAtomValue( + createSnapshotMutationAtom ); // Use Jotai visibility if available (synced from chat page), otherwise fall back to thread prop const currentVisibility = currentThreadState.visibility ?? thread?.visibility ?? "PRIVATE"; - const isPublicEnabled = - currentThreadState.publicShareEnabled ?? thread?.public_share_enabled ?? false; - const publicShareToken = currentThreadState.publicShareToken ?? null; const handleVisibilityChange = useCallback( async (newVisibility: ChatVisibility) => { @@ -96,45 +92,24 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS [thread, currentVisibility, onVisibilityChange, queryClient, setThreadVisibility] ); - const handlePublicShareToggle = useCallback(async () => { + const handleCreatePublicLink = useCallback(async () => { if (!thread) return; try { - const response = await togglePublicShare({ - thread_id: thread.id, - enabled: !isPublicEnabled, - }); - - // Update atom state with response - setCurrentThreadState((prev) => ({ - ...prev, - publicShareEnabled: response.enabled, - publicShareToken: response.share_token, - })); + await createSnapshot({ thread_id: thread.id }); + setOpen(false); } catch (error) { - console.error("Failed to toggle public share:", error); + console.error("Failed to create public link:", error); } - }, [thread, isPublicEnabled, togglePublicShare, setCurrentThreadState]); - - const handleCopyPublicLink = useCallback(async () => { - if (!publicShareToken) return; - - const publicUrl = `${window.location.origin}/public/${publicShareToken}`; - await navigator.clipboard.writeText(publicUrl); - toast.success("Public link copied to clipboard"); - }, [publicShareToken]); + }, [thread, createSnapshot]); // Don't show if no thread (new chat that hasn't been created yet) if (!thread) { return null; } - const CurrentIcon = isPublicEnabled ? Globe : currentVisibility === "PRIVATE" ? User : Users; - const buttonLabel = isPublicEnabled - ? "Public" - : currentVisibility === "PRIVATE" - ? "Private" - : "Shared"; + const CurrentIcon = currentVisibility === "PRIVATE" ? User : Users; + const buttonLabel = currentVisibility === "PRIVATE" ? "Private" : "Shared"; return ( @@ -211,67 +186,31 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS {/* Divider */}
- {/* Public Share Option */} + {/* Public Link Option */}
diff --git a/surfsense_web/components/public-chat/public-chat-footer.tsx b/surfsense_web/components/public-chat/public-chat-footer.tsx index cf4501c23..2211f3142 100644 --- a/surfsense_web/components/public-chat/public-chat-footer.tsx +++ b/surfsense_web/components/public-chat/public-chat-footer.tsx @@ -26,7 +26,7 @@ export function PublicChatFooter({ shareToken }: PublicChatFooterProps) { share_token: shareToken, }); - // Redirect to the new chat page (content will be loaded there) + // Redirect to the new chat page with cloned content router.push(`/dashboard/${response.search_space_id}/new-chat/${response.thread_id}`); } catch (error) { const message = error instanceof Error ? error.message : "Failed to copy chat"; diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index 540fbdc70..08c08ba78 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -24,9 +24,6 @@ export interface ThreadRecord { created_at: string; updated_at: string; has_comments?: boolean; - public_share_enabled?: boolean; - public_share_token?: string | null; - clone_pending?: boolean; } export interface MessageRecord {