mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-27 19:25:15 +02:00
feat: add public chat snapshots components
This commit is contained in:
parent
d890c562d4
commit
ea2dd20a78
5 changed files with 280 additions and 0 deletions
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { atomWithMutation } from "jotai-tanstack-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { PublicChatSnapshotDeleteRequest } from "@/contracts/types/chat-threads.types";
|
||||||
|
import { chatThreadsApiService } from "@/lib/apis/chat-threads-api.service";
|
||||||
|
|
||||||
|
export const deletePublicChatSnapshotMutationAtom = atomWithMutation(() => ({
|
||||||
|
mutationFn: async (request: PublicChatSnapshotDeleteRequest) => {
|
||||||
|
return chatThreadsApiService.deletePublicChatSnapshot(request);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Public link deleted");
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
console.error("Failed to delete public chat link:", error);
|
||||||
|
toast.error("Failed to delete public link");
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Copy, MessageSquare, Trash2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
|
||||||
|
|
||||||
|
interface PublicChatSnapshotRowProps {
|
||||||
|
snapshot: PublicChatSnapshotDetail;
|
||||||
|
canDelete: boolean;
|
||||||
|
onCopy: (snapshot: PublicChatSnapshotDetail) => void;
|
||||||
|
onDelete: (snapshot: PublicChatSnapshotDetail) => void;
|
||||||
|
isDeleting?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicChatSnapshotRow({
|
||||||
|
snapshot,
|
||||||
|
canDelete,
|
||||||
|
onCopy,
|
||||||
|
onDelete,
|
||||||
|
isDeleting = false,
|
||||||
|
}: PublicChatSnapshotRowProps) {
|
||||||
|
const formattedDate = new Date(snapshot.created_at).toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-3 px-4 border-b last:border-b-0 hover:bg-muted/50 transition-colors">
|
||||||
|
<div className="flex-1 min-w-0 mr-4">
|
||||||
|
<h4 className="text-sm font-medium truncate" title={snapshot.thread_title}>
|
||||||
|
{snapshot.thread_title}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
||||||
|
<span>{formattedDate}</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MessageSquare className="h-3 w-3" />
|
||||||
|
{snapshot.message_count}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onCopy(snapshot)}
|
||||||
|
className="h-8 px-2"
|
||||||
|
title="Copy link"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{canDelete && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDelete(snapshot)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="h-8 px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
title="Delete link"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Link2Off } from "lucide-react";
|
||||||
|
|
||||||
|
interface PublicChatSnapshotsEmptyStateProps {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicChatSnapshotsEmptyState({
|
||||||
|
title = "No public chat links",
|
||||||
|
description = "When you create public links to share chats, they will appear here.",
|
||||||
|
}: PublicChatSnapshotsEmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div className="rounded-full bg-muted p-3 mb-4">
|
||||||
|
<Link2Off className="h-6 w-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-medium text-foreground mb-1">{title}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-sm">{description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
|
||||||
|
import { PublicChatSnapshotRow } from "./public-chat-snapshot-row";
|
||||||
|
import { PublicChatSnapshotsEmptyState } from "./public-chat-snapshots-empty-state";
|
||||||
|
|
||||||
|
interface PublicChatSnapshotsListProps {
|
||||||
|
snapshots: PublicChatSnapshotDetail[];
|
||||||
|
canDelete: boolean;
|
||||||
|
onCopy: (snapshot: PublicChatSnapshotDetail) => void;
|
||||||
|
onDelete: (snapshot: PublicChatSnapshotDetail) => void;
|
||||||
|
deletingId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicChatSnapshotsList({
|
||||||
|
snapshots,
|
||||||
|
canDelete,
|
||||||
|
onCopy,
|
||||||
|
onDelete,
|
||||||
|
deletingId,
|
||||||
|
}: PublicChatSnapshotsListProps) {
|
||||||
|
if (snapshots.length === 0) {
|
||||||
|
return <PublicChatSnapshotsEmptyState />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border rounded-md divide-y">
|
||||||
|
{snapshots.map((snapshot) => (
|
||||||
|
<PublicChatSnapshotRow
|
||||||
|
key={snapshot.id}
|
||||||
|
snapshot={snapshot}
|
||||||
|
canDelete={canDelete}
|
||||||
|
onCopy={onCopy}
|
||||||
|
onDelete={onDelete}
|
||||||
|
isDeleting={deletingId === snapshot.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { Globe, Info } from "lucide-react";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { deletePublicChatSnapshotMutationAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-mutation.atoms";
|
||||||
|
import { publicChatSnapshotsAtom } from "@/atoms/public-chat-snapshots/public-chat-snapshots-query.atoms";
|
||||||
|
import { myAccessAtom } from "@/atoms/members/members-query.atoms";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
|
||||||
|
import { PublicChatSnapshotsList } from "./public-chat-snapshots-list";
|
||||||
|
|
||||||
|
interface PublicChatSnapshotsManagerProps {
|
||||||
|
searchSpaceId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PublicChatSnapshotsManager({
|
||||||
|
searchSpaceId: _searchSpaceId,
|
||||||
|
}: PublicChatSnapshotsManagerProps) {
|
||||||
|
const [deletingId, setDeletingId] = useState<number | undefined>();
|
||||||
|
|
||||||
|
// Data fetching
|
||||||
|
const { data: snapshotsData, isLoading, refetch } = useAtomValue(publicChatSnapshotsAtom);
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
const { data: access } = useAtomValue(myAccessAtom);
|
||||||
|
const canView = useMemo(() => {
|
||||||
|
if (!access) return false;
|
||||||
|
if (access.is_owner) return true;
|
||||||
|
return access.permissions?.includes("public_sharing:view") ?? false;
|
||||||
|
}, [access]);
|
||||||
|
|
||||||
|
const canDelete = useMemo(() => {
|
||||||
|
if (!access) return false;
|
||||||
|
if (access.is_owner) return true;
|
||||||
|
return access.permissions?.includes("public_sharing:delete") ?? false;
|
||||||
|
}, [access]);
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
const { mutateAsync: deleteSnapshot } = useAtomValue(deletePublicChatSnapshotMutationAtom);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleCopy = useCallback((snapshot: PublicChatSnapshotDetail) => {
|
||||||
|
const publicUrl = `${window.location.origin}/public/${snapshot.share_token}`;
|
||||||
|
navigator.clipboard.writeText(publicUrl);
|
||||||
|
toast.success("Link copied to clipboard");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
async (snapshot: PublicChatSnapshotDetail) => {
|
||||||
|
try {
|
||||||
|
setDeletingId(snapshot.id);
|
||||||
|
await deleteSnapshot({
|
||||||
|
thread_id: snapshot.thread_id,
|
||||||
|
snapshot_id: snapshot.id,
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete snapshot:", error);
|
||||||
|
} finally {
|
||||||
|
setDeletingId(undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deleteSnapshot, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 md:space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||||
|
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
|
||||||
|
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-3 md:px-6 pb-3 md:pb-6">
|
||||||
|
<Skeleton className="h-24 w-full" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission denied
|
||||||
|
if (!canView) {
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
You don't have permission to view public chat links in this search space.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshots = snapshotsData?.snapshots ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 md:space-y-6">
|
||||||
|
<Alert className="py-3 md:py-4">
|
||||||
|
<Globe className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||||
|
<AlertDescription className="text-xs md:text-sm">
|
||||||
|
Public chat links allow anyone with the URL to view a snapshot of a chat. These links do
|
||||||
|
not update when the original chat changes.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||||
|
<CardTitle className="text-base md:text-lg flex items-center gap-2">
|
||||||
|
<Globe className="h-4 w-4 md:h-5 md:w-5" />
|
||||||
|
Public Chat Links
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs md:text-sm">
|
||||||
|
Manage public links to chats in this search space.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-3 md:px-6 pb-3 md:pb-6">
|
||||||
|
<PublicChatSnapshotsList
|
||||||
|
snapshots={snapshots}
|
||||||
|
canDelete={canDelete}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
deletingId={deletingId}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue