Ctrl K
ComunidadGuías…

components/usage-budget-provider.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use client'

import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState
} from 'react'

import { UsageLimitDialog } from '@/components/usage-limit-dialog'

export interface UsageBudget {
  remaining: number
  limit: number
  resetAt: string
  refreshAt?: string
  costs?: {
    quick: number
    adaptive: number
  }
}

interface UsageBudgetContextValue {
  enabled: boolean
  usage: UsageBudget | null
  isLow: boolean
  isExhausted: boolean
  costs: { quick: number; adaptive: number }
  refreshUsage: () => Promise<UsageBudget | null>
  consume: (amount: number) => void
  setUsage: (usage: UsageBudget | null) => void
  showUsageLimit: (resetAt?: string) => void
}

const noop = () => undefined
const noopAsync = async () => null

const UsageBudgetContext = createContext<UsageBudgetContextValue>({
  enabled: false,
  usage: null,
  isLow: false,
  isExhausted: false,
  costs: { quick: 1, adaptive: 2 },
  refreshUsage: noopAsync,
  consume: noop,
  setUsage: noop,
  showUsageLimit: noop
})

function isUsageBudget(value: unknown): value is UsageBudget {
  if (!value || typeof value !== 'object') return false

  const candidate = value as Partial<UsageBudget>
  return (
    typeof candidate.remaining === 'number' &&
    Number.isFinite(candidate.remaining) &&
    typeof candidate.limit === 'number' &&
    Number.isFinite(candidate.limit) &&
    typeof candidate.resetAt === 'string' &&
    (candidate.refreshAt === undefined ||
      typeof candidate.refreshAt === 'string') &&
    (candidate.costs === undefined ||
      (typeof candidate.costs.quick === 'number' &&
        Number.isFinite(candidate.costs.quick) &&
        typeof candidate.costs.adaptive === 'number' &&
        Number.isFinite(candidate.costs.adaptive)))
  )
}

export function UsageBudgetProvider({
  initialUsage,
  enabled = initialUsage !== null,
  children
}: {
  initialUsage: UsageBudget | null
  enabled?: boolean
  children: ReactNode
}) {
  const [usage, setUsage] = useState<UsageBudget | null>(
    enabled ? initialUsage : null
  )
  const [limitDialogOpen, setLimitDialogOpen] = useState(false)
  const [limitResetAt, setLimitResetAt] = useState<string | undefined>()
  const refreshedBoundaryRef = useRef<string | null>(null)
  const refreshingBoundaryRef = useRef(false)

  const refreshUsage = useCallback(async () => {
    if (!enabled) return null

    try {
      const response = await fetch('/api/usage', {
        method: 'GET',
        headers: { Accept: 'application/json' }
      })

      if (!response.ok) return null

      const body: unknown = await response.json()
      const nextUsage = isUsageBudget(body)
        ? body
        : body && typeof body === 'object' && 'usage' in body
          ? (body as { usage: unknown }).usage
          : null

      if (!isUsageBudget(nextUsage)) return null

      setUsage(nextUsage)
      return nextUsage
    } catch {
      return null
    }
  }, [enabled])

  useEffect(() => {
    setUsage(enabled ? initialUsage : null)
    if (!enabled) {
      setLimitDialogOpen(false)
      setLimitResetAt(undefined)
      refreshedBoundaryRef.current = null
    }
  }, [enabled, initialUsage])

  useEffect(() => {
    if (!enabled || initialUsage !== null) return
    void refreshUsage()
  }, [enabled, initialUsage, refreshUsage])

  useEffect(() => {
    if (!enabled || !usage?.refreshAt) return

    const refreshAt = usage.refreshAt
    const refreshAtMs = new Date(refreshAt).getTime()
    if (!Number.isFinite(refreshAtMs)) return

    let timeout: number | undefined
    let cancelled = false
    const refreshAtBoundary = async () => {
      if (
        refreshedBoundaryRef.current === refreshAt ||
        refreshingBoundaryRef.current
      ) {
        return
      }

      refreshingBoundaryRef.current = true
      const refreshed = await refreshUsage()
      refreshingBoundaryRef.current = false
      if (cancelled) return

      if (refreshed) {
        refreshedBoundaryRef.current = refreshAt
      } else {
        timeout = window.setTimeout(() => {
          void refreshAtBoundary()
        }, 60_000)
      }
    }

    const scheduleRefresh = () => {
      const delay = refreshAtMs - Date.now()
      if (delay <= 0) {
        void refreshAtBoundary()
        return
      }

      timeout = window.setTimeout(
        scheduleRefresh,
        Math.min(delay + 1000, 2_147_483_647)
      )
    }

    scheduleRefresh()
    return () => {
      cancelled = true
      if (timeout !== undefined) window.clearTimeout(timeout)
    }
  }, [enabled, refreshUsage, usage?.refreshAt])

  const consume = useCallback(
    (amount: number) => {
      if (!enabled) return
      if (!Number.isFinite(amount) || amount <= 0) return

      setUsage(current =>
        current
          ? { ...current, remaining: Math.max(0, current.remaining - amount) }
          : null
      )
    },
    [enabled]
  )

  const showUsageLimit = useCallback(
    (resetAt?: string) => {
      if (!enabled) return
      setLimitResetAt(resetAt)
      setLimitDialogOpen(true)
    },
    [enabled]
  )

  const value = useMemo<UsageBudgetContextValue>(
    () => ({
      enabled,
      usage,
      isLow: usage !== null && usage.remaining <= 20,
      isExhausted: usage !== null && usage.remaining <= 0,
      costs: usage?.costs ?? { quick: 1, adaptive: 2 },
      refreshUsage,
      consume,
      setUsage,
      showUsageLimit
    }),
    [consume, enabled, refreshUsage, showUsageLimit, usage]
  )

  return (
    <UsageBudgetContext.Provider value={value}>
      {children}
      {enabled && (
        <UsageLimitDialog
          open={limitDialogOpen}
          onOpenChange={setLimitDialogOpen}
          resetAt={limitResetAt ?? usage?.resetAt}
        />
      )}
    </UsageBudgetContext.Provider>
  )
}

export function useUsageBudget() {
  return useContext(UsageBudgetContext)
}

export function UsageBudgetWarning() {
  const { enabled, usage, isLow } = useUsageBudget()

  if (!enabled || !usage || !isLow) return null

  return (
    <p
      className="mx-auto w-full max-w-3xl px-4 pb-2 text-sm text-destructive"
      role="status"
      aria-live="polite"
    >
      You&apos;re running low on monthly usage.{' '}
      <span className="tabular-nums">{Math.max(0, usage.remaining)}</span>{' '}
      remaining.
    </p>
  )
}