mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 04:42:39 +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,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