SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
3.9 KB · 97 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Braces, Download, FileCode2, FileText, Printer, Text } from "lucide-react";4import { Button, type ButtonProps } from "@/components/ui/button";5import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";6import { ActionSheet, type ActionSheetItem } from "@/components/ui/sheet";7import { Tooltip } from "@/components/ui/tooltip";8import { useIsMobile } from "@/lib/client/hooks";9import { EXPORT_OPTIONS, exportConversation, type ClientExportFormat } from "./export";1011export const EXPORT_ICONS: Record<ClientExportFormat, React.ReactNode> = {12  markdown: <FileText />,13  txt: <Text />,14  json: <Braces />,15  html: <FileCode2 />,16  pdf: <Printer />,17};1819export interface ExportMenuProps {20  conversationId: string;21  /** Shown as the sheet title on phones. */22  title?: string;23  /** Custom trigger; defaults to a ghost icon button with a tooltip. */24  children?: React.ReactNode;25  align?: "start" | "center" | "end";26  side?: "top" | "bottom" | "left" | "right";27  size?: ButtonProps["size"];28  variant?: ButtonProps["variant"];29  className?: string;30  onExported?: (format: ClientExportFormat) => void;31}3233/**34 * Export menu for a conversation — dropdown on desktop, ActionSheet on phones.35 * Used by the command palette; exported for the chat header (workstream A) and the sidebar.36 *37 *   <ExportMenu conversationId={id} title={title} />38 *   <ExportMenu conversationId={id}><Button variant="outline">Export</Button></ExportMenu>39 */40export function ExportMenu({ conversationId, title, children, align = "end", side, size = "icon-sm", variant = "ghost", className, onExported }: ExportMenuProps) {41  const isMobile = useIsMobile();42  const [open, setOpen] = React.useState(false);43  const [busy, setBusy] = React.useState<ClientExportFormat | null>(null);4445  const run = async (format: ClientExportFormat) => {46    setBusy(format);47    try {48      await exportConversation(conversationId, format);49      onExported?.(format);50    } catch {51      /* toasted */52    } finally {53      setBusy(null);54    }55  };5657  const trigger = children ?? (58    <Button variant={variant} size={size} className={className} aria-label="Export conversation" loading={busy !== null}>59      {busy === null ? <Download /> : null}60    </Button>61  );6263  if (isMobile) {64    const items: ActionSheetItem[] = EXPORT_OPTIONS.map((o) => ({ key: o.format, label: o.label, hint: o.hint, icon: EXPORT_ICONS[o.format], onSelect: () => void run(o.format) }));65    return (66      <>67        {React.isValidElement<{ onClick?: React.MouseEventHandler }>(trigger) ? React.cloneElement(trigger, { onClick: () => setOpen(true) }) : <span onClick={() => setOpen(true)}>{trigger}</span>}68        <ActionSheet open={open} onOpenChange={setOpen} title="Export conversation" description={title} items={items} />69      </>70    );71  }7273  return (74    <DropdownMenu>75      <Tooltip content={children ? undefined : "Export"}>76        <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>77      </Tooltip>78      <DropdownMenuContent align={align} side={side} className="w-64">79        <DropdownMenuLabel className="px-2 py-1.5 text-[11px] font-medium uppercase tracking-wide text-fg-subtle">Export</DropdownMenuLabel>80        {EXPORT_OPTIONS.slice(0, 4).map((o) => (81          <DropdownMenuItem key={o.format} onSelect={() => void run(o.format)}>82            {EXPORT_ICONS[o.format]}83            <span className="flex-1">{o.label}</span>84            <span className="font-mono text-[11px] text-fg-subtle">{o.hint}</span>85          </DropdownMenuItem>86        ))}87        <DropdownMenuSeparator />88        <DropdownMenuItem onSelect={() => void run("pdf")}>89          {EXPORT_ICONS.pdf}90          <span className="flex-1">PDF (print)</span>91          <span className="font-mono text-[11px] text-fg-subtle">Print dialog</span>92        </DropdownMenuItem>93      </DropdownMenuContent>94    </DropdownMenu>95  );96}97