Skip to content

Frontend

The frontend is a Vite + React 18 SPA that consumes the Ogur REST API. Source lives in frontend/.

This doc is a technical overview. For visual language, component contracts, badge system, confidence composites, and the "what goes on which page" authority, see design/ux-spec.md. If something below conflicts with the UX spec, the UX spec wins.


Stack

Layer Choice Why
Bundler Vite 5 Fast cold starts, first-class TS, /api dev proxy
Framework React 18 + TypeScript 5 Team familiarity
Routing react-router-dom 6 Client-side routing, nested routes for Asset Detail tabs
Server state TanStack Query 5 Cache, polling, background refetch, SSR-ready
Client state Zustand 4 Inspector panel state, UI-only — not persisted
Virtualization TanStack Virtual 3 Large signal lists (~1,500 rows)
Styling Tailwind 3 Design tokens → utility classes
Components shadcn/ui (Radix primitives) Dialog, Tabs, Tooltip, ScrollArea, Collapsible
Animation Framer Motion 11 Inspector slide-in, tab transitions
Charts Recharts 2 Trial timelines, phase distribution
Icons lucide-react Consistent stroke weight
Command palette cmdk ⌘K anywhere → command search
Dates date-fns Timezone-safe, tree-shakeable

Full manifest: frontend/package.json.


File layout

frontend/
  index.html
  vite.config.ts          ← /api/* → http://localhost:8000 proxy
  tailwind.config.ts
  tsconfig.json
  src/
    main.tsx              ← Entry, TanStack Query client, router
    App.tsx               ← Route tree + top-level layout
    index.css             ← Tailwind directives + design tokens
    api/                  ← One hook per endpoint (useBriefing, useSignals, useAsk, …)
      client.ts           ← fetch wrapper, shared error handling
      useSignals.ts
      useBriefing.ts
      useAssetBriefing.ts
      useAssetOverview.ts
      useAssetTrials.ts
      useAssetCompetitive.ts
      useAsk.ts
      useTriggerBriefing.ts
    components/
      ask/                ← AskPanel, CitationChip
      asset/              ← LandscapeTab (shared across tabs)
      briefing/           ← BriefingDocument, EntityChip, ParsedProse,
                            PredictionCard, SignalAnalysisCard, KIQAnswersSection
      portfolio/          ← AssetTable, FranchiseAssetTable
      shell/              ← CommandPalette, Inspector, LeftRail, TopBar
      signals/            ← SignalRow, SourceFilterChips, TimelineScrubber
    lib/                  ← cn, entity-parser (legacy regex), entity-registry,
                            entity-resolver, severity helpers
    mocks/                ← Static data for offline / demo mode
    store/
      inspector.ts        ← Zustand store for the right-rail Inspector
    types/
      index.ts            ← Shared TS types
    views/                ← One per top-level route
      Portfolio.tsx
      FranchisePortfolio.tsx
      AssetDetail.tsx     ← 6 tabs per ux-spec.md
      GlobalSignals.tsx
      GlobalAsk.tsx
      Deals.tsx

Top-level views

View Route (App.tsx) Purpose
Portfolio / Cross-landscape asset table — every tracked drug with phase, last signal, badges
Franchise portfolio /franchise/:id Filtered portfolio for a named franchise (e.g. "NSCLC franchise")
Asset Detail /asset/:drug 6 tabs: Overview, Trials, Competitive, Landscape, Evidence, Ask
Global Signals /signals Timeline-scrubbed feed of every signal across landscapes
Global Ask /ask Free-form Q&A against /api/ask
Deals /deals Licensing / M&A / investment signals filtered from SEC EDGAR

The Inspector pattern

A 380 px right rail that context-switches content based on the selected object (drug / trial / signal / company). Driven by a single Zustand store at store/inspector.ts:

type InspectorState =
  | { kind: "drug"; drugName: string }
  | { kind: "trial"; nctId: string }
  | { kind: "signal"; signalId: string }
  | { kind: "company"; normalizedName: string }
  | null;

Clicking any EntityChip in a briefing swaps the Inspector content without navigating away. This is the key interaction pattern in the UX spec — users stay in flow while drilling into provenance.


Entity rendering — structured refs vs. legacy regex

Briefings now carry structured entity references alongside prose. Each signal_analyses[] and predictions[] entry can include optional drug_id, company_id, target_id, trial_nct_id fields, and kiq_answers[].evidence_refs[] is a list of those same shapes. The synthesizer emits IDs from the ENTITY CATALOG (see architecture.md §4.4).

Two paths render entities, picked per claim:

  1. Structured path (preferred). When a card has any of the four ID fields populated, entity-registry.ts resolves them:
ID prefix Routes to
drug-<slug> Asset Detail page if the drug is in useAssetIndex(); else Inspector
co-<slug> Inspector → company panel
tgt-<slug> Inspector → target panel
NCT\d{8} External: https://clinicaltrials.gov/study/{nct}

The card renders a chip row above the prose — <EntityChip resolved={…} /> — and clicks dispatch via react-router-dom href, external <a>, or the Zustand inspector store depending on ResolvedEntity.type.

  1. Legacy regex path. Falls back when no IDs are emitted (older briefings, or claims the synthesizer left untyped). entity-parser.ts parses the prose itself, finds drug/trial mentions, and renders <EntityChip text={…} entity={…} />. This path is on the deprecation list — it's fragile (regex over prose) and the structured emitter has been live for several pipeline runs.

<EntityChip> (components/briefing/EntityChip.tsx) discriminates the two modes by prop shape. PredictionCard and SignalAnalysisCard resolve all four ID fields, render the chip row if any resolved, and fall back to ParsedProse for the body.

KIQ Answers

KIQAnswersSection.tsx renders the kiq_answers[] block. One card per active KIQ, each with:

  • The KIQ question as a header
  • finding, evidence, uncertainty, implication prose blocks
  • A <ConfidenceBadge> (high / medium / low)
  • A row of <EntityChip>s for evidence_refs[] — uses the same structured-path resolver as above

Drug briefings render only drug-specific KIQs (those keyed on the drug-composite landscape ID); class-level KIQs appear only in landscape briefings. See architecture.md §2 — Path C.


Data fetching

Every backend endpoint has a matching TanStack Query hook in src/api/. Convention:

export function useBriefing(landscapeId: string) {
  return useQuery({
    queryKey: ["briefing", landscapeId],
    queryFn: () => client.get<BriefingOut>(`/api/briefing/${landscapeId}`),
    staleTime: 60_000,  // 1 min
  });
}

POST endpoints (trigger briefing, trigger analyzer) use useMutation — see useTriggerBriefing.ts.

The client.ts wrapper: - Handles JSON parsing + error surfacing - Throws on non-2xx so TanStack Query populates error - No auth logic (single-tenant)


Dev workflow

make dev              # backend, terminal 1 → :8000
make frontend         # frontend, terminal 2 → :5173

/api/* requests from the frontend proxy to :8000 via frontend/vite.config.ts. There is no CORS setup in production because we assume same-origin deployment.

Running against mock data

For demos / offline work, each view can import from src/mocks/. This is done by swapping the hook inside the view file — not driven by an env var. Cleaner separation is on the backlog.


What's not built (yet)

  • No frontend tests. The backend has 418 tests; the frontend has zero. This is the biggest gap in the current codebase.
  • No auth. Single-tenant assumption.
  • No error boundary strategy beyond TanStack's default. A 500 from the backend surfaces as a toast; no retry UI.
  • No analytics instrumentation. Phase 5 work.