React Component

/**
 * TabbedListView React Component
 *
 * Displays a tabbed interface for viewing llm_clients owned by the current user
 * and llm_clients shared with the user.
 *
 * Tabs:
 * - Your LLMClients
 * - Shared LLMClients
 *
 * View Modes:
 * - List
 * - Thumbnail card
 *
 * Features:
 * - Loads owned and shared llm_client lists from the backend using session context.
 * - Hydrates the UI from cached results before the initial fetch resolves.
 * - Shows loading and error states during fetches.
 * - Allows switching between list and card views.
 * - Persists the selected view mode in sessionStorage.
 * - Uses cookie-backed counts (owned/shared) to size loading skeleton rows.
 * - Supports requery with cache invalidation.
 *
 * Props:
 * - sessionContext (SessionContext): Authentication and API context used for requests.
 *
 * State:
 * - isLoadingOwned: Loading state for owned llm_clients.
 * - isLoadingShared: Loading state for shared llm_clients.
 * - errorMessage: Error text for failed requests.
 * - userListObjects: Owned llm_client list.
 * - sharedListObjects: Shared llm_client list.
 * - invalidateCacheFlag: Indicates whether backend cache should be invalidated on load.
 * - viewMode: Current display mode ("list" or "thumbnail").
 * - activeTab: Current tab ("user" or "shared").
 *
 * Internal Helpers:
 * - getCookie: Reads cookie values used for skeleton sizing.
 * - load (from ./load): Fetches llm_client data and updates state via setters.
 *
 * Page Rendering Performance and Caching behavior:
 * - Improves the perceived load time by rendering cached results immediately when
 *   available while a fresh backend fetch is still in flight. It is not uncommon
 *   for the backend response to take up to 1-2 seconds, so this is important from
 *   a UX perspective.
 * - Reads the most recent owned/shared llm_client results from sessionStorage on mount,
 *   keyed by API URL and tab.
 * - Writes successful fetch results back to the cache so the next initial page load
 *   can show recent data without waiting on the network.
 *
 * Usage:
 * <TabbedListView sessionContext={sessionContext} />
 */
import { useEffect, useRef, useState } from "react";
import type { SessionContext } from "@smarter/common";

import ListView from "@/components/ListView";
import CardView from "@/components/CardView";
import ToggleButton from "@/components/ToggleButton";
import type { ViewMode } from "@/components/ToggleButton";

import type { LLMClient, TabKey } from "@/lib/Types";
import { getCookie } from "./cookie";
import { TabNav } from "./TabNavigation";
import { load } from "./load";
import { makeCacheKey, readCache, writeCache } from "./cache";
import "./styles.css";


export default function TabbedListView({ sessionContext }: { sessionContext: SessionContext }) {
  const [errorMessage, setErrorMessage] = useState<string | null>(null);

  // list state management for owned/shared object lists.
  const [isLoadingOwned, setIsLoadingOwned] = useState<boolean>(true);
  const [isLoadingShared, setIsLoadingShared] = useState<boolean>(true);
  const [userListObjects, setUserListObjects] = useState<LLMClient[]>([]);
  const [sharedListObjects, setSharedListObjects] = useState<LLMClient[]>([]);

  // cache keys for session-based local caching of owned/shared lists
  // to improve perceived load times on repeat visits
  const sharedListCacheKey = makeCacheKey(sessionContext.ApiUrl, "shared");
  const ownedListCacheKey = makeCacheKey(sessionContext.ApiUrl, "owned");

  // controls whether to invalidate backend (Django-Redis) cache on next load
  // toggled by requery action.
  const [invalidateCacheFlag, setInvalidateCacheFlag] = useState<boolean>(false);

  // define 2-tab layout with cookie-based persistent active tab state
  const tabs: { key: TabKey; label: string }[] = [
    { key: "user", label: "Your LLMClients" },
    { key: "shared", label: "Shared LLMClients" },
  ];
  const [activeTab, setActiveTab] = useState<"user" | "shared">("user");
  const [viewMode, _setViewMode] = useState<ViewMode>(() => {
    const saved = sessionStorage.getItem("viewMode");
    return saved === "thumbnail" ? "thumbnail" : "list";
  });
  const setViewMode = (mode: ViewMode) => {
    _setViewMode(mode);
    sessionStorage.setItem("viewMode", mode);
  };

  // throttle duration for requerying to prevent excessive backend requests
  const REQUERY_THROTTLE_MS = 2000;
  const requeryRef = useRef<number | null>(null);

  // for sizing the skeleton loaders that are rendered while data is loading
  const maxGhostRows = 25;
  const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val));
  const userGhostCount = clamp(getCookie("owned", "llm_client_count") || 6, 0, maxGhostRows);
  const sharedGhostCount = clamp(getCookie("shared", "llm_client_count") || 6, 0, maxGhostRows);

  // initiate load of both owned and shared llm_client lists on component mount and whenever session context changes
  const handleLoad = async () => {
    const ownedObjects = await load(sessionContext, invalidateCacheFlag, setIsLoadingOwned, "owned", setErrorMessage);
    setUserListObjects(ownedObjects);
    writeCache(ownedListCacheKey, ownedObjects);

    const sharedObjects = await load(
      sessionContext,
      invalidateCacheFlag,
      setIsLoadingShared,
      "shared",
      setErrorMessage,
    );
    setSharedListObjects(sharedObjects);
    writeCache(sharedListCacheKey, sharedObjects);
  };

  const onRequery = () => {
    setInvalidateCacheFlag(true);
    if (isLoadingOwned || isLoadingShared) {
      return;
    }
    // throttle to prevent excessive requerying if user clicks multiple times in a short span
    const now = Date.now();
    if (requeryRef.current && now - requeryRef.current < REQUERY_THROTTLE_MS) {
      return;
    }
    requeryRef.current = now;
    setIsLoadingOwned(true);
    setIsLoadingShared(true);
    handleLoad();
  };

  useEffect(() => {
    const ownedCached = readCache(ownedListCacheKey);
    if (ownedCached) setUserListObjects(ownedCached);

    const sharedCached = readCache(sharedListCacheKey);
    if (sharedCached) setSharedListObjects(sharedCached);

    void handleLoad();
  }, [sessionContext]);

  if (errorMessage) {
    return <div className="alert alert-danger">{errorMessage}</div>;
  }

  return (
    <div className="pt-5 pb-5 card card-flush h-xl-100">
      <div className="card-header rounded align-items-start ps-3" data-bs-theme="light">
        <TabNav activeTab={activeTab} onTabChange={setActiveTab} tabs={tabs} />
      </div>
      <div className="m-0 p-0 card-body list-view">
        <ToggleButton viewMode={viewMode} setViewMode={setViewMode} />

        {activeTab === "user" ? (
          viewMode === "list" ? (
            <ListView
              isLoading={isLoadingOwned}
              ghostRows={userGhostCount}
              sessionContext={sessionContext}
              objects={userListObjects}
              onRequery={onRequery}
            />
          ) : (
            <CardView sessionContext={sessionContext} objects={userListObjects} onRequery={onRequery} />
          )
        ) : viewMode === "list" ? (
          <ListView
            isLoading={isLoadingShared}
            ghostRows={sharedGhostCount}
            sessionContext={sessionContext}
            objects={sharedListObjects}
            onRequery={onRequery}
          />
        ) : (
          <CardView sessionContext={sessionContext} objects={sharedListObjects} onRequery={onRequery} />
        )}
      </div>
    </div>
  );
}