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 33ec64696..38501fcab 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 @@ -38,6 +38,7 @@ 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"; @@ -137,6 +138,7 @@ export default function NewChatPage() { const params = useParams(); const queryClient = useQueryClient(); const [isInitializing, setIsInitializing] = useState(true); + const [isCompletingClone, setIsCompletingClone] = useState(false); const [threadId, setThreadId] = useState(null); const [currentThread, setCurrentThread] = useState(null); const [messages, setMessages] = useState([]); @@ -323,6 +325,34 @@ export default function NewChatPage() { initializeThread(); }, [initializeThread]); + // Handle clone completion when thread has clone_pending flag + useEffect(() => { + if (!currentThread?.clone_pending || isCompletingClone) 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."); + } finally { + setIsCompletingClone(false); + } + }; + + completeClone(); + }, [currentThread?.clone_pending, currentThread?.id, isCompletingClone, initializeThread, queryClient]); + // Handle scroll to comment from URL query params (e.g., from inbox item click) const searchParams = useSearchParams(); const targetCommentId = searchParams.get("commentId"); @@ -1388,6 +1418,16 @@ 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/components/public-chat/public-chat-footer.tsx b/surfsense_web/components/public-chat/public-chat-footer.tsx index cc54d4150..cf4501c23 100644 --- a/surfsense_web/components/public-chat/public-chat-footer.tsx +++ b/surfsense_web/components/public-chat/public-chat-footer.tsx @@ -22,22 +22,15 @@ export function PublicChatFooter({ shareToken }: PublicChatFooterProps) { setIsCloning(true); try { - await publicChatApiService.clonePublicChat({ + const response = await publicChatApiService.clonePublicChat({ share_token: shareToken, }); - // Force PGlite to resync notifications on next dashboard load - localStorage.setItem("surfsense_force_notif_resync", "true"); - - toast.success("Copying chat to your account...", { - description: "You'll be notified when it's ready.", - }); - - router.push("/dashboard"); + // Redirect to the new chat page (content will be loaded there) + 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"; toast.error(message); - } finally { setIsCloning(false); } }, [shareToken, router]); diff --git a/surfsense_web/contracts/types/public-chat.types.ts b/surfsense_web/contracts/types/public-chat.types.ts index 709bedcb7..f7aea5969 100644 --- a/surfsense_web/contracts/types/public-chat.types.ts +++ b/surfsense_web/contracts/types/public-chat.types.ts @@ -39,16 +39,28 @@ export const getPublicChatResponse = z.object({ }); /** - * Clone public chat + * Clone public chat (init) */ export const clonePublicChatRequest = z.object({ share_token: z.string(), }); export const clonePublicChatResponse = z.object({ + thread_id: z.number(), + search_space_id: z.number(), + share_token: z.string(), +}); + +/** + * Complete clone + */ +export const completeCloneRequest = z.object({ + thread_id: z.number(), +}); + +export const completeCloneResponse = z.object({ status: z.string(), - task_id: z.string(), - message: z.string(), + message_count: z.number(), }); // Type exports @@ -59,3 +71,5 @@ export type GetPublicChatRequest = z.infer; export type GetPublicChatResponse = z.infer; export type ClonePublicChatRequest = z.infer; export type ClonePublicChatResponse = z.infer; +export type CompleteCloneRequest = z.infer; +export type CompleteCloneResponse = z.infer; diff --git a/surfsense_web/lib/apis/public-chat-api.service.ts b/surfsense_web/lib/apis/public-chat-api.service.ts index 52a7c1363..49b1bd686 100644 --- a/surfsense_web/lib/apis/public-chat-api.service.ts +++ b/surfsense_web/lib/apis/public-chat-api.service.ts @@ -1,8 +1,12 @@ import { type ClonePublicChatRequest, type ClonePublicChatResponse, + type CompleteCloneRequest, + type CompleteCloneResponse, clonePublicChatRequest, clonePublicChatResponse, + completeCloneRequest, + completeCloneResponse, type GetPublicChatRequest, type GetPublicChatResponse, getPublicChatRequest, @@ -29,6 +33,7 @@ class PublicChatApiService { /** * Clone a public chat to the user's account. + * Creates an empty thread and returns thread_id for redirect. * Requires authentication. */ clonePublicChat = async (request: ClonePublicChatRequest): Promise => { @@ -44,6 +49,25 @@ class PublicChatApiService { clonePublicChatResponse ); }; + + /** + * Complete the clone by copying messages and podcasts. + * Called from the chat page after redirect. + * Requires authentication. + */ + completeClone = async (request: CompleteCloneRequest): Promise => { + const parsed = completeCloneRequest.safeParse(request); + + if (!parsed.success) { + const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.post( + `/api/v1/threads/${parsed.data.thread_id}/complete-clone`, + completeCloneResponse + ); + }; } export const publicChatApiService = new PublicChatApiService(); diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index 2188d9cec..540fbdc70 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -26,6 +26,7 @@ export interface ThreadRecord { has_comments?: boolean; public_share_enabled?: boolean; public_share_token?: string | null; + clone_pending?: boolean; } export interface MessageRecord {