TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Check, Copy, ExternalLink, Eye, Link2, ShieldAlert } from "lucide-react";4import { ResponsiveDialog } from "@/components/ui/sheet";5import { Button } from "@/components/ui/button";6import { Spinner } from "@/components/ui/misc";7import { toast } from "@/components/ui/toast";8import { api, useApi } from "@/lib/client/api";9import { useCopy } from "@/lib/client/hooks";10import { formatRelative } from "@/lib/utils";1112interface ShareStatus {13 share: { id: string; createdAt: string; viewCount: number } | null;14}1516/**17 * Public link for an Arena session. Shows a privacy warning before the first link is created; afterwards the18 * URL, view count, refresh and revoke. Bottom sheet on phones, dialog on desktop.19 */20export function ArenaShareSheet({ sessionId, open, onOpenChange }: { sessionId: string | null; open: boolean; onOpenChange: (o: boolean) => void }) {21 const status = useApi<ShareStatus>(open && sessionId ? `/api/arena/${sessionId}/share` : null);22 const [busy, setBusy] = React.useState(false);23 const [copied, copy] = useCopy();24 const share = status.data?.share ?? null;25 const url = share ? `${typeof window !== "undefined" ? window.location.origin : ""}/share/arena/${share.id}` : null;2627 const create = async () => {28 if (!sessionId) return;29 setBusy(true);30 try {31 const { share: created } = await api<{ share: ShareStatus["share"] }>(`/api/arena/${sessionId}/share`, { method: "POST" });32 await status.mutate({ share: created }, { revalidate: false });33 toast.success(share ? "Snapshot refreshed" : "Public link created", "Anyone with the link can read this comparison.");34 } catch (e) {35 toast.error("Could not create the link", (e as Error).message);36 } finally {37 setBusy(false);38 }39 };40 const revoke = async () => {41 if (!sessionId) return;42 setBusy(true);43 try {44 await api(`/api/arena/${sessionId}/share`, { method: "DELETE" });45 await status.mutate({ share: null }, { revalidate: false });46 toast.success("Link revoked", "The public page now returns 404.");47 } catch (e) {48 toast.error("Could not revoke", (e as Error).message);49 } finally {50 setBusy(false);51 }52 };5354 return (55 <ResponsiveDialog56 open={open}57 onOpenChange={onOpenChange}58 title="Share this comparison"59 description="A frozen public snapshot — prompt, system prompt, responses, metrics and votes."60 size="sm"61 footer={62 share ? (63 <div className="flex items-center gap-2">64 <Button variant="danger-soft" size="sm" onClick={revoke} loading={busy}>65 Revoke link66 </Button>67 <Button variant="ghost" size="sm" onClick={create} disabled={busy} className="ml-auto">68 Refresh snapshot69 </Button>70 </div>71 ) : (72 <div className="flex items-center justify-end gap-2">73 <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>74 Cancel75 </Button>76 <Button variant="accent" size="sm" onClick={create} loading={busy} disabled={status.isLoading}>77 <Link2 /> Create public link78 </Button>79 </div>80 )81 }82 >83 {status.isLoading && !status.data ? (84 <div className="flex items-center gap-2 py-4 text-[13px] text-fg-muted">85 <Spinner /> Checking existing links…86 </div>87 ) : share && url ? (88 <div className="space-y-3 py-1">89 <div className="flex items-center gap-2 rounded-lg border border-border bg-bg-subtle px-3 py-2">90 <Link2 className="size-4 shrink-0 text-fg-subtle" />91 <span className="min-w-0 flex-1 truncate font-mono text-[12.5px]">{url}</span>92 <Button variant="ghost" size="icon-sm" aria-label="Copy link" onClick={() => copy(url)}>93 {copied ? <Check className="text-success" /> : <Copy />}94 </Button>95 <Button asChild variant="ghost" size="icon-sm" aria-label="Open link">96 <a href={url} target="_blank" rel="noopener noreferrer">97 <ExternalLink />98 </a>99 </Button>100 </div>101 <p className="flex items-center gap-2 text-[12.5px] text-fg-muted">102 <Eye className="size-3.5" /> {share.viewCount} view{share.viewCount === 1 ? "" : "s"} · created {formatRelative(share.createdAt)}103 </p>104 <p className="text-[12px] text-fg-subtle">Votes cast after sharing are not reflected until you refresh the snapshot.</p>105 </div>106 ) : (107 <div className="space-y-3 py-1">108 <div className="flex gap-3 rounded-lg border border-warning/40 bg-warning-soft px-3 py-2.5 text-[13px] text-fg">109 <ShieldAlert className="mt-0.5 size-4 shrink-0 text-warning" />110 <div className="space-y-1">111 <p className="font-medium">Before you share</p>112 <ul className="list-disc space-y-0.5 pl-4 text-[12.5px] text-fg-muted">113 <li>The prompt, system prompt and every response are published as-is — check for personal data or secrets.</li>114 <li>Attachments are never published; only their count is shown.</li>115 <li>Model names are revealed (Blind Arena sessions are marked as blind).</li>116 <li>The page is public but not indexed; anyone with the link can read it. You can revoke it anytime.</li>117 </ul>118 </div>119 </div>120 </div>121 )}122 </ResponsiveDialog>123 );124}125