mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-10 20:35:17 +02:00
feat: update UI for snapshot-based public sharing
This commit is contained in:
parent
6aff69f4ec
commit
98991d2ed4
6 changed files with 39 additions and 155 deletions
|
|
@ -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<number | null>(null);
|
||||
const [currentThread, setCurrentThread] = useState<ThreadRecord | null>(null);
|
||||
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
|
||||
|
|
@ -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 (
|
||||
<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
|
||||
// For new chats (urlChatId === 0), threadId being null is expected (lazy creation)
|
||||
if (!threadId && urlChatId > 0) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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<CurrentThreadState>(initialState);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
|
|
@ -211,67 +186,31 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
|||
{/* Divider */}
|
||||
<div className="border-t border-border my-1" />
|
||||
|
||||
{/* Public Share Option */}
|
||||
{/* Public Link Option */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePublicShareToggle}
|
||||
disabled={isTogglingPublic}
|
||||
onClick={handleCreatePublicLink}
|
||||
disabled={isCreatingSnapshot}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
|
||||
"hover:bg-accent/50 cursor-pointer",
|
||||
"focus:outline-none",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
isPublicEnabled && "bg-accent/80"
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"size-7 rounded-md shrink-0 grid place-items-center",
|
||||
isPublicEnabled ? "bg-primary/10" : "bg-muted"
|
||||
)}
|
||||
>
|
||||
<Globe
|
||||
className={cn(
|
||||
"size-4 block",
|
||||
isPublicEnabled ? "text-primary" : "text-muted-foreground"
|
||||
)}
|
||||
/>
|
||||
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted">
|
||||
<Globe className="size-4 block text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1 text-left min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={cn("text-sm font-medium", isPublicEnabled && "text-primary")}>
|
||||
Public
|
||||
<span className="text-sm font-medium">
|
||||
{isCreatingSnapshot ? "Creating link..." : "Create public link"}
|
||||
</span>
|
||||
{isPublicEnabled && (
|
||||
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">
|
||||
ON
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 leading-snug">
|
||||
Anyone with the link can read
|
||||
Creates a shareable snapshot of this chat
|
||||
</p>
|
||||
</div>
|
||||
{isPublicEnabled && publicShareToken && (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyPublicLink();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
handleCopyPublicLink();
|
||||
}
|
||||
}}
|
||||
className="shrink-0 p-1.5 rounded-md hover:bg-muted transition-colors cursor-pointer"
|
||||
title="Copy public link"
|
||||
>
|
||||
<Link2 className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue