import { useCallback, useEffect, useRef, useState } from "react"; import QRCode from "qrcode"; import { Check, Loader2, Network, RotateCcw } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { cancelChannelConnect, pollChannelConnect, startChannelConnect, } from "@/lib/api"; import type { ChannelConnectPayload, NanobotFeaturesPayload, } from "@/lib/types"; export type ChannelQrConnectLabels = { qrAlt: string; scanTitle: string; scanDescription: string; waiting: string; connected: string; stopped: string; connecting: string; scanAgain: string; connect: string; }; export function ChannelQrConnectFlow({ token, channelName, startOptions = {}, idleLabel, connectRequestId, labels, onFeaturesUpdate, }: { token: string; channelName: "feishu" | "weixin"; startOptions?: { domain?: "feishu" | "lark"; instanceId?: string; mode?: "replace" | "create"; force?: boolean; }; idleLabel?: string; connectRequestId?: number; labels: ChannelQrConnectLabels; onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const [connect, setConnect] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [handledRequestId, setHandledRequestId] = useState(0); const pollInFlight = useRef(false); const startDomain = startOptions.domain; const startInstanceId = startOptions.instanceId; const startMode = startOptions.mode; const startForce = startOptions.force; const pending = connect?.status === "pending"; const succeeded = connect?.status === "succeeded"; const canStart = !pending && !busy; useEffect(() => { if (!connect?.qr_url) { setQrDataUrl(""); return; } let cancelled = false; void QRCode.toDataURL(connect.qr_url, { width: 184, margin: 1, color: { dark: "#111827", light: "#ffffff" }, }) .then((url) => { if (!cancelled) setQrDataUrl(url); }) .catch(() => { if (!cancelled) setQrDataUrl(""); }); return () => { cancelled = true; }; }, [connect?.qr_url]); useEffect(() => { if (!connect?.session_id || connect.status !== "pending") return; let cancelled = false; const poll = async () => { if (pollInFlight.current) return; pollInFlight.current = true; try { const payload = await pollChannelConnect(token, channelName, connect.session_id); if (cancelled) return; setConnect((current) => ({ ...(current ?? payload), ...payload, qr_url: payload.qr_url ?? current?.qr_url, })); if (payload.nanobot_features) { onFeaturesUpdate(payload.nanobot_features); } if (payload.status !== "pending") { setError(null); } } catch (err) { if (!cancelled) setError((err as Error).message); } finally { pollInFlight.current = false; } }; const initial = window.setTimeout(() => void poll(), 900); const interval = window.setInterval( () => void poll(), Math.max(2500, connect.interval_ms ?? 5000), ); return () => { cancelled = true; window.clearTimeout(initial); window.clearInterval(interval); }; }, [channelName, connect?.interval_ms, connect?.session_id, connect?.status, onFeaturesUpdate, token]); const start = useCallback(async (force = false) => { setBusy(true); setError(null); try { const payload = await startChannelConnect(token, channelName, { domain: startDomain, instanceId: startInstanceId, mode: startMode, force: force || startForce, }); setConnect(payload); } catch (err) { setError((err as Error).message); } finally { setBusy(false); } }, [channelName, startDomain, startForce, startInstanceId, startMode, token]); useEffect(() => { if (!connectRequestId || connectRequestId === handledRequestId) return; setHandledRequestId(connectRequestId); void start(); }, [connectRequestId, handledRequestId, start]); const cancel = async () => { if (!connect?.session_id) { setConnect(null); return; } setBusy(true); try { const payload = await cancelChannelConnect(token, channelName, connect.session_id); setConnect(payload); } catch (err) { setError((err as Error).message); } finally { setBusy(false); } }; return (
{pending ? (
{qrDataUrl ? ( {labels.qrAlt} ) : ( )}
{labels.scanTitle}

{labels.scanDescription}

{labels.waiting}
) : null} {succeeded ? (
{connect.message ?? labels.connected}
) : null} {connect && ["expired", "failed", "cancelled"].includes(connect.status) ? (
{connect.message || labels.stopped}
) : null} {error ? (
{error}
) : null}
); } export function FeishuConnectFlow({ token, instanceId = "default", mode = "replace", idleLabel, connectRequestId, onFeaturesUpdate, }: { token: string; instanceId?: string; mode?: "replace" | "create"; idleLabel?: string; connectRequestId?: number; onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return ( ); } export function WeixinConnectFlow({ token, idleLabel, connectRequestId, onFeaturesUpdate, }: { token: string; idleLabel?: string; connectRequestId?: number; onFeaturesUpdate: (payload: NanobotFeaturesPayload) => void; }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); return ( ); }