Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/src/components/MarkdownActions.tsx

tsx119 行查看原文
import { Check, Copy, X } from "lucide-react";
import type { ReactNode } from "react";
import { createContext, useContext, useEffect, useRef, useState } from "react";
import type { FileLocation, FileOpenHandler, LinkContext, WebOpenOptions } from "../link-target.ts";
import type { FileActionHandler } from "../types.ts";

export interface MarkdownServices {
  streaming: boolean;
  workspace?: LinkContext;
  onLink?: (url: string, options?: WebOpenOptions) => void;
  onFile?: FileOpenHandler;
  onFileAction?: FileActionHandler;
  onCopy?: (text: string) => Promise<void>;
  onImage?: (path: string, location?: FileLocation) => Promise<string>;
  onDownloadImage?: (src: string) => Promise<void>;
}
export const MarkdownContext = createContext<MarkdownServices>({ streaming: false });
export const useMarkdown = () => useContext(MarkdownContext);

export function MarkdownCopy({
  text,
  label,
  success = "已复制",
  disabled = false,
}: {
  text: string | (() => string);
  label: string;
  success?: string;
  disabled?: boolean;
}) {
  const { onCopy } = useMarkdown();
  const [status, setStatus] = useState<"idle" | "copying" | "success" | "error">("idle");
  useEffect(() => {
    if (status !== "success") return;
    const timer = setTimeout(() => setStatus("idle"), 2000);
    return () => clearTimeout(timer);
  }, [status]);
  const title = status === "error" ? "复制失败,点击重试" : status === "success" ? success : label;
  return (
    <button
      type="button"
      className="markdown-action"
      aria-label={title}
      title={title}
      disabled={disabled || !onCopy || status === "copying"}
      onClick={async () => {
        if (!onCopy) return;
        setStatus("copying");
        try {
          await onCopy(typeof text === "function" ? text() : text);
          setStatus("success");
        } catch {
          setStatus("error");
        }
      }}
    >
      {status === "success" ? <Check size={14} /> : <Copy size={14} />}
      <span className="sr-only" role={status === "error" ? "alert" : "status"} aria-live="polite">
        {status === "success" || status === "error" ? title : ""}
      </span>
    </button>
  );
}

export function MarkdownPreview({
  title,
  description,
  children,
  onClose,
  className = "",
}: {
  title: string;
  description?: string;
  children: ReactNode;
  onClose: () => void;
  className?: string;
}) {
  const dialog = useRef<HTMLDialogElement>(null);
  useEffect(() => {
    const opener = document.activeElement;
    dialog.current?.showModal();
    return () => {
      if (opener instanceof HTMLElement && opener.isConnected) opener.focus();
    };
  }, []);
  return (
    <dialog
      ref={dialog}
      className={`markdown-preview ${className}`}
      data-find-ignore
      aria-label={title}
      onCancel={(event) => {
        event.preventDefault();
        onClose();
      }}
    >
      <header>
        <div>
          <h2>{title}</h2>
          {description && <p>{description}</p>}
        </div>
        <button type="button" className="markdown-action" aria-label={`关闭${title}`} onClick={onClose}>
          <X size={16} />
        </button>
      </header>
      {children}
    </dialog>
  );
}

export function downloadBlob(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.click();
  URL.revokeObjectURL(url);
}