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%
3.8 KB · 110 lines typescript
Raw Blame History
1'use client';2/**3 * Local watchlist (no account): `localStorage['aia-watchlist']` = `WatchItem[]` (slug, name, entity_type, org).4 * Same-tab `aia-watchlist-change` event + cross-tab `storage`. Capped at WATCH_MAX items (the `/watchlist` page5 * fetches events per item, so the cap keeps the page honest and fast).6 */7import { useCallback, useEffect, useState } from 'react';8import type { WatchItem } from './types';910export const WATCH_KEY = 'aia-watchlist';11export const WATCH_MAX = 12;12const EVENT = 'aia-watchlist-change';1314/** Event types that matter for a watched entity (new version / price / deprecation / benchmark / provider / licence). */15export const WATCH_EVENT_TYPES = new Set([16  'NEW_MODEL',17  'RELEASE',18  'VERSION_RELEASED',19  'MODEL_UPDATED',20  'PRICE_CHANGED',21  'PROVIDER_LISTED',22  'DEPRECATION_ANNOUNCED',23  'RETIREMENT_ANNOUNCED',24  'STATUS_CHANGED',25  'BENCHMARK_RESULT',26  'BENCHMARK_UPDATED',27  'NEW_PROVIDER',28  'LICENSE_CHANGED',29  'OPENNESS_CHANGED',30  'CONTEXT_CHANGED',31]);3233function read(): WatchItem[] {34  if (typeof window === 'undefined') return [];35  try {36    const raw = window.localStorage.getItem(WATCH_KEY);37    if (!raw) return [];38    const arr = JSON.parse(raw) as unknown;39    if (!Array.isArray(arr)) return [];40    return arr.filter((x): x is WatchItem => !!x && typeof x === 'object' && typeof (x as WatchItem).slug === 'string' && typeof (x as WatchItem).entity_type === 'string').slice(0, WATCH_MAX);41  } catch {42    return [];43  }44}45function write(items: WatchItem[]) {46  if (typeof window === 'undefined') return;47  try {48    window.localStorage.setItem(WATCH_KEY, JSON.stringify(items.slice(0, WATCH_MAX)));49  } catch {50    /* storage full / disabled */51  }52  window.dispatchEvent(new CustomEvent(EVENT));53}5455export function readWatchlist(): WatchItem[] {56  return read();57}58export function isWatched(slug: string): boolean {59  return read().some((i) => i.slug === slug);60}61export function addWatch(item: WatchItem): WatchItem[] {62  const cur = read();63  if (cur.some((i) => i.slug === item.slug)) return cur;64  const next = [{ ...item, added_at: new Date().toISOString() }, ...cur].slice(0, WATCH_MAX);65  write(next);66  return next;67}68export function removeWatch(slug: string): WatchItem[] {69  const next = read().filter((i) => i.slug !== slug);70  write(next);71  return next;72}73export function toggleWatch(item: WatchItem): WatchItem[] {74  return isWatched(item.slug) ? removeWatch(item.slug) : addWatch(item);75}76export function clearWatchlist() {77  write([]);78}7980/** Normalise an entity summary/detail into a WatchItem. */81export function toWatchItem(e: { slug: string; name: string; entity_type: string; organization?: { name: string } | string | null }): WatchItem {82  return { slug: e.slug, name: e.name, entity_type: e.entity_type, org: typeof e.organization === 'string' ? e.organization : e.organization?.name ?? null };83}8485/** React binding; `ready` is false during SSR/hydration so server and client markup match. */86export function useWatchlist() {87  const [items, setItems] = useState<WatchItem[]>([]);88  const [ready, setReady] = useState(false);89  useEffect(() => {90    const sync = () => setItems(read());91    sync();92    setReady(true);93    window.addEventListener(EVENT, sync);94    window.addEventListener('storage', sync);95    return () => {96      window.removeEventListener(EVENT, sync);97      window.removeEventListener('storage', sync);98    };99  }, []);100  const add = useCallback((i: WatchItem) => setItems(addWatch(i)), []);101  const remove = useCallback((slug: string) => setItems(removeWatch(slug)), []);102  const toggle = useCallback((i: WatchItem) => setItems(toggleWatch(i)), []);103  const clear = useCallback(() => {104    clearWatchlist();105    setItems([]);106  }, []);107  const has = useCallback((slug: string) => items.some((i) => i.slug === slug), [items]);108  return { items, ready, add, remove, toggle, clear, has, full: items.length >= WATCH_MAX };109}110