"use client"; import { CheckIcon, CopyIcon, DownloadIcon, ExternalLinkIcon, Loader2, Trash2Icon, } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { useI18n } from "@/core/i18n/hooks"; import type { SharePreview, ThreadShare, ThreadShareMode, } from "@/core/thread-shares/api"; import { buildPublicShareUrl, extractShareCode } from "@/core/thread-shares/api"; import { useCreateShare, useImportShare, usePreviewShare, useRevokeShare, } from "@/core/thread-shares/hooks"; import { cn } from "@/lib/utils"; function errorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } /** * Dialog that produces a share link for a single conversation. Two modes: * * - `import` — mints a short code; a recipient enters it in the import dialog * to copy the conversation into their own account. * - `view` — mints a public, read-only link that anyone can open without * logging in (a static page; viewers cannot send new messages). */ export function ShareConversationDialog({ threadId, open, onOpenChange, }: { threadId: string | null; open: boolean; onOpenChange: (open: boolean) => void; }) { const { t } = useI18n(); const tx = t.threadShare; const createShare = useCreateShare(); const revokeShare = useRevokeShare(); const [mode, setMode] = useState("view"); const [share, setShare] = useState(null); const [copied, setCopied] = useState(false); // Reset to the default mode (read-only link) every time the dialog opens. useEffect(() => { if (open) setMode("view"); }, [open, threadId]); // Mint / reuse the link whenever the dialog opens or the mode changes. useEffect(() => { if (!open || !threadId) return; setShare(null); setCopied(false); createShare .mutateAsync({ threadId, mode }) .then(setShare) .catch((err) => toast.error(errorMessage(err))); // createShare is stable enough; re-running only on open/threadId/mode is intended. // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, threadId, mode]); const isView = mode === "view"; const publicUrl = isView && share ? buildPublicShareUrl(share.share_code) : ""; async function handleCopy() { if (!share) return; const text = isView ? publicUrl : share.share_code; try { await navigator.clipboard.writeText(text); setCopied(true); toast.success(isView ? "链接已复制" : tx.codeCopied); setTimeout(() => setCopied(false), 1500); } catch { toast.error(t.clipboard.failedToCopyToClipboard); } } async function handleRevoke() { if (!share || !window.confirm(tx.revokeConfirm)) return; try { await revokeShare.mutateAsync(share.share_code); toast.success(tx.revokeSuccess); onOpenChange(false); } catch (err) { toast.error(errorMessage(err)); } } return ( {tx.shareTitle} {isView ? "生成一个只读链接,任何人无需登录都能查看这个对话,但不能继续提问。" : tx.shareDescription} {/* Mode selector */}
{( [ { value: "view" as const, label: "只读链接" }, { value: "import" as const, label: "导入副本" }, ] ).map((option) => ( ))}
{!share ? (
{tx.generating}
) : (

{isView ? "只读链接" : tx.shareCodeLabel}

{isView ? ( e.currentTarget.select()} className="flex-1 font-mono text-xs" /> ) : ( {share.share_code} )}
{isView ? (

拿到链接的人都能查看这个对话的全部内容(只读),无法继续发送问题。撤销后链接立即失效。

) : (

{tx.shareHint}

)}
)}
); } /** Dialog where a user pastes a share code to import someone's conversation. */ export function ImportConversationsDialog({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) { const { t } = useI18n(); const tx = t.threadShare; const [input, setInput] = useState(""); const [preview, setPreview] = useState(null); const previewShare = usePreviewShare(); const importShare = useImportShare(); // Reset state whenever the dialog closes. useEffect(() => { if (!open) { setInput(""); setPreview(null); } }, [open]); async function handlePreview() { const code = extractShareCode(input); if (!code) { toast.error(tx.emptyCode); return; } try { setPreview(await previewShare.mutateAsync(code)); } catch (err) { setPreview(null); toast.error(errorMessage(err)); } } async function handleImport() { const code = preview?.share_code ?? extractShareCode(input); if (!code) { toast.error(tx.emptyCode); return; } try { const result = await importShare.mutateAsync(code); if (result.imported > 0) { toast.success(tx.importSuccess); } else { toast.info(tx.alreadyImported); } onOpenChange(false); } catch (err) { toast.error(errorMessage(err)); } } return ( {tx.importTitle} {tx.importDescription}
{ setInput(e.target.value); setPreview(null); }} placeholder={tx.codeInputPlaceholder} className="font-mono" />
{preview && (

{tx.previewConversation.replace("{title}", preview.title)}

{tx.previewFrom.replace("{user}", preview.shared_by)}

)}
); }