diff --git a/objectiveai-web/app/functions/page.tsx b/objectiveai-web/app/functions/page.tsx index 78b399f05..854d8c425 100644 --- a/objectiveai-web/app/functions/page.tsx +++ b/objectiveai-web/app/functions/page.tsx @@ -7,7 +7,7 @@ import { createPublicClient } from "../../lib/client"; import { deriveCategory, deriveDisplayName } from "../../lib/objectiveai"; import { NAV_HEIGHT_CALCULATION_DELAY_MS, STICKY_BAR_HEIGHT, STICKY_SEARCH_OVERLAP } from "../../lib/constants"; import { useResponsive } from "../../hooks/useResponsive"; -import { ErrorAlert, EmptyState, SkeletonCard } from "../../components/ui"; +import { ErrorAlert, EmptyState } from "../../components/ui"; // Function item type for UI interface FunctionItem { @@ -28,7 +28,8 @@ const LOAD_MORE_COUNT = 6; export default function FunctionsPage() { const [functions, setFunctions] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [isListLoading, setIsListLoading] = useState(true); + const [detailsLoaded, setDetailsLoaded] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("All"); @@ -40,12 +41,16 @@ export default function FunctionsPage() { const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT); const searchRef = useRef(null); - // Fetch functions from API + // Fetch functions from API — progressive loading useEffect(() => { + let cancelled = false; + async function fetchFunctions() { try { - setIsLoading(true); + setIsListLoading(true); + setDetailsLoaded(false); setError(null); + setFunctions([]); // Get all functions via SDK const client = createPublicClient(); @@ -60,46 +65,67 @@ export default function FunctionsPage() { } } - // Fetch details for each unique function (gracefully skip any that 404) - const results = await Promise.all( - Array.from(uniqueFunctions.values()).map(async (fn): Promise => { - try { - const slug = `${fn.owner}/${fn.repository}`; + if (cancelled) return; + setIsListLoading(false); - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); + // Fire all detail fetches concurrently — update state as each resolves + const entries = Array.from(uniqueFunctions.values()); + let settled = 0; - const category = deriveCategory(details); - const name = deriveDisplayName(fn.repository); + entries.forEach((fn) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }) + .then((details) => { + clearTimeout(timeout); + if (cancelled) return; + const name = deriveDisplayName(fn.repository); const tags = fn.repository.split("-").filter((t: string) => t.length > 2); if (details.type === "vector.function") tags.push("ranking"); else tags.push("scoring"); - return { - slug, + const item: FunctionItem = { + slug: `${fn.owner}/${fn.repository}`, owner: fn.owner, repository: fn.repository, commit: fn.commit, name, description: details.description || `${name} function`, - category, + category: deriveCategory(details), tags, }; - } catch { - return null; - } - }) - ); - setFunctions(results.filter((item): item is FunctionItem => item !== null)); + setFunctions(prev => [...prev, item]); + }) + .catch(() => { + clearTimeout(timeout); + // Skip functions that fail to load + }) + .finally(() => { + settled++; + if (settled === entries.length && !cancelled) { + setDetailsLoaded(true); + } + }); + }); + + // Edge case: no functions to fetch details for + if (entries.length === 0) { + setDetailsLoaded(true); + } } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load functions"); - } finally { - setIsLoading(false); + if (!cancelled) { + setError(err instanceof Error ? err.message : "Failed to load functions"); + setIsListLoading(false); + setDetailsLoaded(true); + } } } fetchFunctions(); + return () => { cancelled = true; }; }, []); // Load pinned functions from localStorage @@ -287,8 +313,37 @@ export default function FunctionsPage() { flexDirection: 'column', width: '100%', }}> - {/* Only render grid when we have results */} - {!isLoading && !error && visibleFunctions.length > 0 && ( + {/* Skeleton grid while list is loading */} + {isListLoading && ( +
+ {Array.from({ length: INITIAL_VISIBLE_COUNT }).map((_, i) => ( +
+
+
+
+
+ ))} +
+ )} + + {/* Function cards — render progressively as details arrive */} + {!isListLoading && !error && visibleFunctions.length > 0 && ( <>
)} - {isLoading && ( -
- {Array.from({ length: 9 }).map((_, i) => ( - - ))} -
- )} - - {error && !isLoading && ( + {error && !isListLoading && ( )} - {!isLoading && !error && filteredFunctions.length === 0 && ( + {!isListLoading && !error && detailsLoaded && filteredFunctions.length === 0 && ( )}
diff --git a/objectiveai-web/app/page.tsx b/objectiveai-web/app/page.tsx index a98aa6ff2..d14489d84 100644 --- a/objectiveai-web/app/page.tsx +++ b/objectiveai-web/app/page.tsx @@ -27,20 +27,23 @@ interface FeaturedFunction { export default function Home() { const { isMobile } = useResponsive(); - const [functions, setFunctions] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [slots, setSlots] = useState<(FeaturedFunction | null)[]>( + Array.from({ length: FEATURED_COUNT }, () => null) + ); + const [isListLoading, setIsListLoading] = useState(true); - // Fetch functions from API + // Fetch functions from API — progressive loading useEffect(() => { + let cancelled = false; + async function fetchFunctions() { try { - setIsLoading(true); + setIsListLoading(true); - // Fetch functions list via SDK const client = createPublicClient(); const result = await Functions.list(client); - // Deduplicate by owner/repository (same function may have multiple commits) + // Deduplicate by owner/repository const uniqueFunctions = new Map(); for (const fn of result.data) { const key = `${fn.owner}/${fn.repository}`; @@ -49,44 +52,55 @@ export default function Home() { } } - // Limit to FEATURED_COUNT - const limitedFunctions = Array.from(uniqueFunctions.values()).slice(0, FEATURED_COUNT); + const entries = Array.from(uniqueFunctions.values()).slice(0, FEATURED_COUNT); + if (cancelled) return; - const results = await Promise.all( - limitedFunctions.map(async (fn): Promise => { - try { - const slug = `${fn.owner}/${fn.repository}`; - const details = await Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit); + // Initialize slots to match actual count + setSlots(Array.from({ length: entries.length }, () => null)); + setIsListLoading(false); - const category = deriveCategory(details); - const name = deriveDisplayName(fn.repository); + // Fire all detail fetches — fill slots as each resolves + entries.forEach((fn, index) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + Functions.retrieve(client, "github", fn.owner, fn.repository, fn.commit, { signal: controller.signal }) + .then((details) => { + clearTimeout(timeout); + if (cancelled) return; + + const name = deriveDisplayName(fn.repository); const tags = fn.repository.split("-").filter((t: string) => t.length > 2); if (details.type === "vector.function") tags.push("ranking"); else tags.push("scoring"); - return { - slug, + const item: FeaturedFunction = { + slug: `${fn.owner}/${fn.repository}`, name, description: details.description || `${name} function`, - category, + category: deriveCategory(details), tags, }; - } catch { - return null; - } - }) - ); - setFunctions(results.filter((item): item is FeaturedFunction => item !== null)); + setSlots(prev => { + const next = [...prev]; + next[index] = item; + return next; + }); + }) + .catch(() => { + clearTimeout(timeout); + }); + }); } catch { - // Silent failure - page still renders, just without featured functions - } finally { - setIsLoading(false); + if (!cancelled) { + setIsListLoading(false); + } } } fetchFunctions(); + return () => { cancelled = true; }; }, []); return ( @@ -181,122 +195,99 @@ export default function Home() {
- {/* Function Cards Grid */} + {/* Function Cards Grid — slots fill progressively */}
- {isLoading ? ( - // Loading skeleton - Array.from({ length: FEATURED_COUNT }).map((_, i) => ( -
fn ? ( + +
+ + {fn.category} + +

+ {fn.name} +

+

+ {fn.description} +

-
+ display: 'flex', + flexWrap: 'wrap', + gap: '4px', + marginBottom: '10px', + }}> + {fn.tags.slice(0, 2).map(tag => ( + + {tag} + + ))} + {fn.tags.length > 2 && ( + + +{fn.tags.length - 2} + + )} +
-
- )) - ) : functions.length > 0 ? ( - functions.map(fn => ( - -
- - {fn.category} - -

- {fn.name} -

-

- {fn.description} -

-
- {fn.tags.slice(0, 2).map(tag => ( - - {tag} - - ))} - {fn.tags.length > 2 && ( - - +{fn.tags.length - 2} - - )} -
-
- Open -
+ Open
- - )) +
+ ) : ( - // Empty state +
+
+
+
+
+ ))} + {!isListLoading && slots.length === 0 && (