mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-18 23:11:12 +02:00
feat(frontend): two-phase cloning with loading state
This commit is contained in:
parent
0c8d1f3fef
commit
9a4da10b12
5 changed files with 85 additions and 13 deletions
|
|
@ -38,6 +38,7 @@ import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
|
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
|
||||||
import { useMessagesElectric } from "@/hooks/use-messages-electric";
|
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 { WriteTodosToolUI } from "@/components/tool-ui/write-todos";
|
||||||
import { getBearerToken } from "@/lib/auth-utils";
|
import { getBearerToken } from "@/lib/auth-utils";
|
||||||
import { createAttachmentAdapter, extractAttachmentContent } from "@/lib/chat/attachment-adapter";
|
import { createAttachmentAdapter, extractAttachmentContent } from "@/lib/chat/attachment-adapter";
|
||||||
|
|
@ -137,6 +138,7 @@ export default function NewChatPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
const [isCompletingClone, setIsCompletingClone] = useState(false);
|
||||||
const [threadId, setThreadId] = useState<number | null>(null);
|
const [threadId, setThreadId] = useState<number | null>(null);
|
||||||
const [currentThread, setCurrentThread] = useState<ThreadRecord | null>(null);
|
const [currentThread, setCurrentThread] = useState<ThreadRecord | null>(null);
|
||||||
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
|
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
|
||||||
|
|
@ -323,6 +325,34 @@ export default function NewChatPage() {
|
||||||
initializeThread();
|
initializeThread();
|
||||||
}, [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)
|
// Handle scroll to comment from URL query params (e.g., from inbox item click)
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const targetCommentId = searchParams.get("commentId");
|
const targetCommentId = searchParams.get("commentId");
|
||||||
|
|
@ -1388,6 +1418,16 @@ export default function NewChatPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show loading state while completing clone
|
||||||
|
if (isCompletingClone) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
<div className="text-sm text-muted-foreground">Copying chat content...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Show error state only if we tried to load an existing thread but failed
|
// 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)
|
// For new chats (urlChatId === 0), threadId being null is expected (lazy creation)
|
||||||
if (!threadId && urlChatId > 0) {
|
if (!threadId && urlChatId > 0) {
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,15 @@ export function PublicChatFooter({ shareToken }: PublicChatFooterProps) {
|
||||||
setIsCloning(true);
|
setIsCloning(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await publicChatApiService.clonePublicChat({
|
const response = await publicChatApiService.clonePublicChat({
|
||||||
share_token: shareToken,
|
share_token: shareToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Force PGlite to resync notifications on next dashboard load
|
// Redirect to the new chat page (content will be loaded there)
|
||||||
localStorage.setItem("surfsense_force_notif_resync", "true");
|
router.push(`/dashboard/${response.search_space_id}/new-chat/${response.thread_id}`);
|
||||||
|
|
||||||
toast.success("Copying chat to your account...", {
|
|
||||||
description: "You'll be notified when it's ready.",
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push("/dashboard");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to copy chat";
|
const message = error instanceof Error ? error.message : "Failed to copy chat";
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
|
||||||
setIsCloning(false);
|
setIsCloning(false);
|
||||||
}
|
}
|
||||||
}, [shareToken, router]);
|
}, [shareToken, router]);
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,28 @@ export const getPublicChatResponse = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone public chat
|
* Clone public chat (init)
|
||||||
*/
|
*/
|
||||||
export const clonePublicChatRequest = z.object({
|
export const clonePublicChatRequest = z.object({
|
||||||
share_token: z.string(),
|
share_token: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clonePublicChatResponse = z.object({
|
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(),
|
status: z.string(),
|
||||||
task_id: z.string(),
|
message_count: z.number(),
|
||||||
message: z.string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type exports
|
// Type exports
|
||||||
|
|
@ -59,3 +71,5 @@ export type GetPublicChatRequest = z.infer<typeof getPublicChatRequest>;
|
||||||
export type GetPublicChatResponse = z.infer<typeof getPublicChatResponse>;
|
export type GetPublicChatResponse = z.infer<typeof getPublicChatResponse>;
|
||||||
export type ClonePublicChatRequest = z.infer<typeof clonePublicChatRequest>;
|
export type ClonePublicChatRequest = z.infer<typeof clonePublicChatRequest>;
|
||||||
export type ClonePublicChatResponse = z.infer<typeof clonePublicChatResponse>;
|
export type ClonePublicChatResponse = z.infer<typeof clonePublicChatResponse>;
|
||||||
|
export type CompleteCloneRequest = z.infer<typeof completeCloneRequest>;
|
||||||
|
export type CompleteCloneResponse = z.infer<typeof completeCloneResponse>;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import {
|
import {
|
||||||
type ClonePublicChatRequest,
|
type ClonePublicChatRequest,
|
||||||
type ClonePublicChatResponse,
|
type ClonePublicChatResponse,
|
||||||
|
type CompleteCloneRequest,
|
||||||
|
type CompleteCloneResponse,
|
||||||
clonePublicChatRequest,
|
clonePublicChatRequest,
|
||||||
clonePublicChatResponse,
|
clonePublicChatResponse,
|
||||||
|
completeCloneRequest,
|
||||||
|
completeCloneResponse,
|
||||||
type GetPublicChatRequest,
|
type GetPublicChatRequest,
|
||||||
type GetPublicChatResponse,
|
type GetPublicChatResponse,
|
||||||
getPublicChatRequest,
|
getPublicChatRequest,
|
||||||
|
|
@ -29,6 +33,7 @@ class PublicChatApiService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone a public chat to the user's account.
|
* Clone a public chat to the user's account.
|
||||||
|
* Creates an empty thread and returns thread_id for redirect.
|
||||||
* Requires authentication.
|
* Requires authentication.
|
||||||
*/
|
*/
|
||||||
clonePublicChat = async (request: ClonePublicChatRequest): Promise<ClonePublicChatResponse> => {
|
clonePublicChat = async (request: ClonePublicChatRequest): Promise<ClonePublicChatResponse> => {
|
||||||
|
|
@ -44,6 +49,25 @@ class PublicChatApiService {
|
||||||
clonePublicChatResponse
|
clonePublicChatResponse
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete the clone by copying messages and podcasts.
|
||||||
|
* Called from the chat page after redirect.
|
||||||
|
* Requires authentication.
|
||||||
|
*/
|
||||||
|
completeClone = async (request: CompleteCloneRequest): Promise<CompleteCloneResponse> => {
|
||||||
|
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();
|
export const publicChatApiService = new PublicChatApiService();
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface ThreadRecord {
|
||||||
has_comments?: boolean;
|
has_comments?: boolean;
|
||||||
public_share_enabled?: boolean;
|
public_share_enabled?: boolean;
|
||||||
public_share_token?: string | null;
|
public_share_token?: string | null;
|
||||||
|
clone_pending?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageRecord {
|
export interface MessageRecord {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue