Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/src/components/ModelWaitNotice.tsx

tsx99 行查看原文
import type { ModelRetryStatus, ModelWaitStatus } from "@zpi-dev/ai";
import { useEffect, useRef, useState } from "react";
import { userErrorMessage } from "../user-error.ts";

/** A single local clock; waiting does not send periodic IPC or persist timer ticks. */
export function ModelWaitNotice({
  wait,
  retryStatus,
  onRetry,
}: {
  wait?: ModelWaitStatus | null;
  retryStatus?: ModelRetryStatus | null;
  onRetry?: () => Promise<unknown>;
}) {
  const [now, setNow] = useState(Date.now);
  const [pending, setPending] = useState(false);
  const [error, setError] = useState("");
  const retryPending = useRef(false);
  const revision = useRef(0);
  const phase = wait?.phase;
  const startedAt = retryStatus ? undefined : wait?.startedAt;
  const origin = retryStatus?.origin;
  const attempt = retryStatus?.attempt;
  useEffect(() => {
    revision.current++;
    retryPending.current = false;
    setPending(false);
    setError("");
    setNow(Date.now());
    const timer = startedAt === undefined ? undefined : setInterval(() => setNow(Date.now()), 1000);
    return () => {
      revision.current++;
      retryPending.current = false;
      clearInterval(timer);
    };
  }, [phase, startedAt, origin, attempt]);
  const retry = () => {
    if (!onRetry || retryPending.current) return;
    retryPending.current = true;
    const current = revision.current;
    setPending(true);
    setError("");
    void Promise.resolve()
      .then(() => (current === revision.current ? onRetry() : undefined))
      .catch((reason) => {
        if (current === revision.current) setError(userErrorMessage(reason));
      })
      .finally(() => {
        if (current !== revision.current) return;
        retryPending.current = false;
        setPending(false);
      });
  };
  const seconds = Math.max(0, Math.floor((now - (startedAt ?? now)) / 1000));
  const waitingLabel = phase === "first_response" ? "等待模型响应" : "等待新输出";
  const status =
    pending || retryStatus?.origin === "manual"
      ? "正在重新请求"
      : retryStatus
        ? "重新连接中"
        : wait
          ? waitingLabel
          : "正在等待重新连接";
  const message =
    pending || retryStatus?.origin === "manual"
      ? `${status}…`
      : retryStatus
        ? `${status}… ${retryStatus.attempt}/${retryStatus.maxRetries}`
        : `${status} · ${seconds} 秒`;
  const guidance = onRetry ? "可以继续等待,或立即重试。" : "可以继续等待。";
  return (
    <div className="model-wait">
      {(wait || retryStatus || pending || error) && (
        <span
          className={`model-wait-message${error ? " model-wait-error" : ""}`}
          data-testid={retryStatus ? "api-retry-status" : undefined}
          title={retryStatus?.errorStatus == null ? undefined : `HTTP ${retryStatus.errorStatus}`}
        >
          {error || message}
        </span>
      )}
      {onRetry && (
        <button
          type="button"
          className="model-wait-retry"
          aria-label={pending ? "正在重试" : "立即重试"}
          disabled={pending}
          onClick={retry}
        >
          {pending ? "重试中…" : "重试"}
        </button>
      )}
      <span className="sr-only" role={error ? "alert" : "status"}>
        {error || `${status}。${guidance}`}
      </span>
    </div>
  );
}