import { useNavigate } from "@tanstack/react-router"; import { DownloadIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { type ProviderDriverKind, type ProviderInstanceId } from "@t3tools/contracts"; import { ensureLocalApi } from "../localApi"; import { useDismissedProviderUpdateNotificationKeys } from "../providerUpdateDismissal"; import { useServerProviders } from "../rpc/serverState"; import { PROVIDER_ICON_BY_PROVIDER } from "./chat/providerIconUtils"; import { canOneClickUpdateProviderCandidate, collectProviderUpdateCandidates, collectUpdatedProviderSnapshots, firstRejectedProviderUpdateMessage, getProviderUpdateInitialToastView, getProviderUpdateProgressToastView, getProviderUpdateRejectedToastView, getProviderUpdateRunningToastView, providerUpdateNotificationKey, type ProviderUpdateToastView, } from "./ProviderUpdateLaunchNotification.logic"; import { stackedThreadToast, toastManager } from "./ui/toast"; const seenProviderUpdateNotificationKeys = new Set(); type ProviderUpdateToastId = ReturnType; type ActiveProviderUpdateToast = | { readonly kind: "prompt"; readonly key: string; readonly toastId: ProviderUpdateToastId } | { readonly kind: "update"; readonly key: string; readonly toastId: ProviderUpdateToastId; readonly providerInstanceIds: ReadonlySet; readonly providerCount: number; }; function ProviderUpdateToastIcon({ provider }: { provider: ProviderDriverKind }) { const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[provider]; if (!ProviderIcon) { return ( ); } return ( ); } function updateProviderUpdateToast(input: { readonly toastId: ProviderUpdateToastId; readonly view: ProviderUpdateToastView; readonly openSettings: () => void; }) { if (input.view.type === "loading" || input.view.type === "success") { toastManager.update(input.toastId, { type: input.view.type, title: input.view.title, description: input.view.description, timeout: 0, data: { hideCopyButton: true, ...(input.view.dismissAfterVisibleMs !== undefined ? { dismissAfterVisibleMs: input.view.dismissAfterVisibleMs } : {}), }, }); return; } toastManager.update( input.toastId, stackedThreadToast({ type: input.view.type, title: input.view.title, description: input.view.description, timeout: 0, actionProps: { children: "Settings", onClick: input.openSettings, }, actionVariant: "outline", data: { hideCopyButton: true, }, }), ); } function isTerminalProviderUpdateToastView(view: ProviderUpdateToastView) { return view.phase === "failed" || view.phase === "unchanged" || view.phase === "succeeded"; } export function ProviderUpdateLaunchNotification() { const navigate = useNavigate(); const providers = useServerProviders(); const activeToastRef = useRef(null); const { dismissedNotificationKeys, dismissNotificationKey } = useDismissedProviderUpdateNotificationKeys(); const updateProviders = useMemo(() => collectProviderUpdateCandidates(providers), [providers]); const notificationKey = useMemo( () => providerUpdateNotificationKey(updateProviders), [updateProviders], ); const oneClickProviders = useMemo( () => updateProviders.filter((provider) => canOneClickUpdateProviderCandidate(provider, providers)), [providers, updateProviders], ); const openProviderSettings = useCallback( (toastId?: ProviderUpdateToastId) => { const activeToast = activeToastRef.current; if (toastId !== undefined) { toastManager.close(toastId); } else if (activeToast) { toastManager.close(activeToast.toastId); } if (activeToast && (toastId === undefined || activeToast.toastId === toastId)) { activeToastRef.current = null; } void navigate({ to: "/settings/providers" }); }, [navigate], ); useEffect(() => { const activeToast = activeToastRef.current; if (activeToast?.kind !== "update") { return; } const activeProviders = providers.filter((provider) => activeToast.providerInstanceIds.has(provider.instanceId), ); const view = getProviderUpdateProgressToastView({ providers: activeProviders, providerCount: activeToast.providerCount, }); updateProviderUpdateToast({ toastId: activeToast.toastId, view, openSettings: () => openProviderSettings(activeToast.toastId), }); if (isTerminalProviderUpdateToastView(view)) { activeToastRef.current = null; } }, [providers, openProviderSettings]); useEffect(() => { const activeToast = activeToastRef.current; if (activeToast?.kind === "prompt" && activeToast.key !== notificationKey) { toastManager.close(activeToast.toastId); activeToastRef.current = null; } if ( !notificationKey || dismissedNotificationKeys.has(notificationKey) || seenProviderUpdateNotificationKeys.has(notificationKey) || activeToastRef.current ) { return; } seenProviderUpdateNotificationKeys.add(notificationKey); const initialView = getProviderUpdateInitialToastView({ updateProviders, oneClickProviders }); let toastId!: ProviderUpdateToastId; let updateStarted = false; const openSettings = () => openProviderSettings(toastId); const dismissPrompt = () => { dismissNotificationKey(notificationKey); }; const runUpdates = () => { if (updateStarted || oneClickProviders.length === 0) { return; } updateStarted = true; const providerCount = oneClickProviders.length; const providerInstanceIds = new Set(oneClickProviders.map((provider) => provider.instanceId)); activeToastRef.current = { kind: "update", key: notificationKey, toastId, providerInstanceIds, providerCount, }; updateProviderUpdateToast({ toastId, view: getProviderUpdateRunningToastView(providerCount), openSettings, }); void Promise.allSettled( oneClickProviders.map(async (provider) => ensureLocalApi().server.updateProvider({ provider: provider.driver, instanceId: provider.instanceId, }), ), ).then((results) => { const activeUpdateToast = activeToastRef.current; if (activeUpdateToast?.kind !== "update" || activeUpdateToast.toastId !== toastId) { return; } const rejectedMessage = firstRejectedProviderUpdateMessage(results); if (rejectedMessage) { updateProviderUpdateToast({ toastId, view: getProviderUpdateRejectedToastView(providerCount, rejectedMessage), openSettings, }); activeToastRef.current = null; return; } const updatedProviderSnapshots = collectUpdatedProviderSnapshots({ results, providerInstanceIds, }); const view = getProviderUpdateProgressToastView({ providers: updatedProviderSnapshots, providerCount, }); updateProviderUpdateToast({ toastId, view, openSettings, }); if (isTerminalProviderUpdateToastView(view)) { activeToastRef.current = null; } }); }; toastId = toastManager.add( stackedThreadToast({ type: initialView.type, title: initialView.title, description: initialView.description, timeout: 0, actionProps: oneClickProviders.length > 0 ? { children: "Update", onClick: runUpdates, } : { children: "Settings", onClick: openSettings, }, actionVariant: oneClickProviders.length > 0 ? "default" : "outline", data: { leadingIcon: updateProviders.length === 1 ? ( ) : undefined, hideCopyButton: true, onClose: dismissPrompt, ...(oneClickProviders.length > 0 ? { secondaryActionProps: { children: "Settings", onClick: openSettings, }, secondaryActionVariant: "outline" as const, } : {}), }, }), ); activeToastRef.current = { kind: "prompt", key: notificationKey, toastId }; }, [ dismissNotificationKey, dismissedNotificationKeys, notificationKey, oneClickProviders, openProviderSettings, updateProviders, ]); return null; }