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:
- Structured path (preferred). When a card has any of the four ID fields populated,
entity-registry.tsresolves 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.
- Legacy regex path. Falls back when no IDs are emitted (older briefings, or claims the synthesizer left untyped).
entity-parser.tsparses 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,implicationprose blocks- A
<ConfidenceBadge>(high / medium / low) - A row of
<EntityChip>s forevidence_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¶
/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.