SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
4.2 KB · 112 lines typescript
Raw Blame History
1'use client';2import { useCallback, useEffect, useState } from 'react';34/**5 * Compare tray — the list of entity slugs the visitor is collecting for `/compare?ids=`.6 * Source of truth: `localStorage['aia-compare']` (mirrored across tabs through the `storage` event and a same-tab7 * custom event). The tray is type-homogeneous: the first item fixes the entity type; adding another type replaces8 * the tray (the API compares 2–6 entities of one type).9 */10export const COMPARE_KEY = 'aia-compare';11export const COMPARE_MAX = 6;12export const COMPARE_MIN = 2;13const EVENT = 'aia-compare-change';1415export type TrayItem = { slug: string; name: string; entity_type: string; organization?: string | null };1617function read(): TrayItem[] {18  if (typeof window === 'undefined') return [];19  try {20    const raw = window.localStorage.getItem(COMPARE_KEY);21    if (!raw) return [];22    const arr = JSON.parse(raw) as unknown;23    if (!Array.isArray(arr)) return [];24    return arr.filter((x): x is TrayItem => !!x && typeof x === 'object' && typeof (x as TrayItem).slug === 'string' && typeof (x as TrayItem).entity_type === 'string').slice(0, COMPARE_MAX);25  } catch {26    return [];27  }28}2930function write(items: TrayItem[]) {31  if (typeof window === 'undefined') return;32  try {33    window.localStorage.setItem(COMPARE_KEY, JSON.stringify(items.slice(0, COMPARE_MAX)));34  } catch {35    /* storage full / disabled */36  }37  window.dispatchEvent(new CustomEvent(EVENT));38}3940/** Normalises the API's type aliases so companies/labs/orgs compare together (the API accepts them under `company`). */41export function trayType(entityType: string): string {42  if (['company', 'organization', 'lab', 'university'].includes(entityType)) return 'company';43  if (['framework', 'library', 'runtime'].includes(entityType)) return 'framework';44  if (entityType === 'quantization') return 'model';45  return entityType;46}4748export function readTray(): TrayItem[] {49  return read();50}51export function setTray(items: TrayItem[]) {52  write(items);53}54export function clearTray() {55  write([]);56}5758/** Add one item. Returns the new tray; a type change replaces the tray. */59export function addToTray(item: TrayItem): TrayItem[] {60  const cur = read();61  const t = trayType(item.entity_type);62  const same = cur.filter((c) => trayType(c.entity_type) === t);63  if (same.some((c) => c.slug === item.slug)) return cur;64  const next = same.length === cur.length ? [...cur, item] : [item];65  const capped = next.slice(0, COMPARE_MAX);66  write(capped);67  return capped;68}69export function removeFromTray(slug: string): TrayItem[] {70  const next = read().filter((c) => c.slug !== slug);71  write(next);72  return next;73}74export function toggleTray(item: TrayItem): TrayItem[] {75  return read().some((c) => c.slug === item.slug) ? removeFromTray(item.slug) : addToTray(item);76}7778/** React binding: returns the tray and mutators, subscribed to storage changes. `ready` is false during SSR/hydration. */79export function useCompareTray() {80  const [items, setItems] = useState<TrayItem[]>([]);81  const [ready, setReady] = useState(false);82  useEffect(() => {83    const sync = () => setItems(read());84    sync();85    setReady(true);86    window.addEventListener(EVENT, sync);87    window.addEventListener('storage', sync);88    return () => {89      window.removeEventListener(EVENT, sync);90      window.removeEventListener('storage', sync);91    };92  }, []);93  const add = useCallback((item: TrayItem) => setItems(addToTray(item)), []);94  const remove = useCallback((slug: string) => setItems(removeFromTray(slug)), []);95  const toggle = useCallback((item: TrayItem) => setItems(toggleTray(item)), []);96  const clear = useCallback(() => {97    clearTray();98    setItems([]);99  }, []);100  const replace = useCallback((next: TrayItem[]) => {101    write(next);102    setItems(next.slice(0, COMPARE_MAX));103  }, []);104  const has = useCallback((slug: string) => items.some((c) => c.slug === slug), [items]);105  const type = items[0] ? trayType(items[0].entity_type) : null;106  return { items, ready, add, remove, toggle, clear, replace, has, type, full: items.length >= COMPARE_MAX, canCompare: items.length >= COMPARE_MIN };107}108109export function compareHref(items: { slug: string }[]): string {110  return items.length ? `/compare?ids=${items.map((i) => encodeURIComponent(i.slug)).join(',')}` : '/compare';111}112