"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { Button, CardSkeleton, Input } from "@/shared/components"; import { CLI_TOOLS } from "@/shared/constants/cliTools"; import { EXPECTED_CODE_COUNT } from "@/shared/schemas/cliCatalog"; import { CliToolCard, CliConceptCard, CliComparisonCard } from "@/shared/components/cli"; import { useToolBatchStatuses } from "@/shared/hooks/cli/useToolBatchStatuses"; import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog"; // ── Static catalogue slice ──────────────────────────────────────────────────── const CODE_TOOLS: [string, CliCatalogEntry][] = Object.entries(CLI_TOOLS).filter( ([, tool]) => tool.category === "code" && tool.baseUrlSupport !== "none" ) as [string, CliCatalogEntry][]; // Cardinality guard (D15) — non-blocking, log only if (CODE_TOOLS.length !== EXPECTED_CODE_COUNT) { console.warn( `[CliCodePage] Expected ${EXPECTED_CODE_COUNT} code tools, found ${CODE_TOOLS.length}. ` + "Check F1 catalog edits." ); } // ── Types ───────────────────────────────────────────────────────────────────── type DetectionFilter = "all" | "installed" | "not_installed"; type BaseUrlFilter = "all" | "full" | "partial"; interface ProviderConnection { isActive?: boolean; [key: string]: unknown; } interface ProvidersResponse { connections?: ProviderConnection[]; } // ── Component ───────────────────────────────────────────────────────────────── interface CliCodePageClientProps { machineId: string; } export default function CliCodePageClient({ machineId: _machineId }: CliCodePageClientProps) { const t = useTranslations("cliCode"); const tCommon = useTranslations("cliCommon"); // ── Batch statuses ────────────────────────────────────────────────────────── const { statuses, loading, refetch } = useToolBatchStatuses(); // ── Providers ─────────────────────────────────────────────────────────────── const [hasActiveProviders, setHasActiveProviders] = useState(false); const [providersLoading, setProvidersLoading] = useState(true); useEffect(() => { let cancelled = false; fetch("/api/providers") .then((res) => (res.ok ? res.json() : Promise.resolve({ connections: [] }))) .then((data) => { if (cancelled) return; const active = (data.connections ?? []).filter((c) => c.isActive !== false); setHasActiveProviders(active.length > 0); }) .catch(() => { if (!cancelled) setHasActiveProviders(false); }) .finally(() => { if (!cancelled) setProvidersLoading(false); }); return () => { cancelled = true; }; }, []); // ── Filters ───────────────────────────────────────────────────────────────── const [search, setSearch] = useState(""); const [detectionFilter, setDetectionFilter] = useState("all"); const [baseUrlFilter, setBaseUrlFilter] = useState("all"); const handleSearchChange = useCallback((e: React.ChangeEvent) => { setSearch(e.target.value); }, []); const handleDetectionChange = useCallback((e: React.ChangeEvent) => { setDetectionFilter(e.target.value as DetectionFilter); }, []); const handleBaseUrlChange = useCallback((e: React.ChangeEvent) => { setBaseUrlFilter(e.target.value as BaseUrlFilter); }, []); // ── Filtered tools ────────────────────────────────────────────────────────── const filteredTools = useMemo<[string, CliCatalogEntry][]>(() => { const q = search.trim().toLowerCase(); return CODE_TOOLS.filter(([id, tool]) => { // Search filter if (q) { const haystack = `${tool.name} ${tool.vendor} ${tool.description}`.toLowerCase(); if (!haystack.includes(q)) return false; } // Detection filter if (detectionFilter !== "all") { const installed = statuses?.[id]?.detection.installed ?? false; if (detectionFilter === "installed" && !installed) return false; if (detectionFilter === "not_installed" && installed) return false; } // Base URL filter if (baseUrlFilter !== "all") { if (tool.baseUrlSupport !== baseUrlFilter) return false; } return true; }); }, [search, detectionFilter, baseUrlFilter, statuses]); // ── Render ─────────────────────────────────────────────────────────────────── const isLoadingOverall = loading || providersLoading; return (
{/* Concept card */} {/* Comparison card */} {/* Header bar */}
{/* Title + subtitle */}

{t("pageTitle")}

{t("pageSubtitle")}

{/* Refresh button */}
{/* Filter row */}
{/* Detection filter */}
{/* Base URL filter */}
{/* Empty state — no active providers */} {!providersLoading && !hasActiveProviders && (
warning

{tCommon("detail.noActiveProviders")}

{tCommon("detail.noActiveProvidersDesc")}

{tCommon("detail.openProviders")}
)} {/* Grid */} {isLoadingOverall ? (
{Array.from({ length: 6 }).map((_, i) => ( ))}
) : (
{filteredTools.map(([id, tool]) => ( ))}
)}
); }