Skip to content

Revamp UI: share ord-app's look-and-feel and stack; add landing page#210

Open
skearnes wants to merge 3 commits into
open-reaction-database:mainfrom
skearnes:ui-revamp-shared-look
Open

Revamp UI: share ord-app's look-and-feel and stack; add landing page#210
skearnes wants to merge 3 commits into
open-reaction-database:mainfrom
skearnes:ui-revamp-shared-look

Conversation

@skearnes

@skearnes skearnes commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Rebuilds the search/browse SPA on the same foundation as the ord-app contribute/editor frontend, so the two ORD frontends look and feel like one product — and adds a proper landing page to help drive adoption.

Shared look-and-feel + stack (enables extracting shared code later)

  • Mantine 7 with ord-app's theme.ts and CSS design tokens ported verbatim (annotated to keep in sync until extracted into a shared package), Tabler icons, SCSS modules, and direct ports of ord-app's PaperButton, Pagination, DataTable (mantine-react-table wrapper), and NotFound components.
  • ord-schema-protobufjs (same package + version range as ord-app) replaces the google-protobuf ord-schema bindings. Display components now type against the shared ord.I* interfaces — the same data currency as ord-app's converters — which is the key prerequisite for sharing reaction-display code.
  • wouter (replacing react-router-dom) and axios (replacing bare fetch), matching ord-app.
  • ketcher-react + ketcher-standalone from npm (lazy-loaded chunk, fetched only when the draw modal opens) replace the frozen Ketcher 2.5.1 static iframe bundle. The manual "download Ketcher into app/public/" dev step and the Docker copy step are gone; the legacy Flask editor keeps its own bundle and is untouched.
  • Bootstrap, jQuery, DataTables, and Material Icons CDN tags removed.
  • Deliberate deviation from ord-app: server-state fetching stays on TanStack Query rather than Redux Toolkit — ord-app's Redux holds editor-document state that this read-only app doesn't have, and the realistic sharing surface is props-based display components.

New landing page

Hero with the ORD logo and a natural-language search box that hands off to /ask (LLM-backed name resolution), live reaction/dataset counts from /api/datasets, ord-app-style action tiles (Browse / Search / Contribute), featured datasets and a news timeline curated via app/src/data/homeContent.ts (featured list falls back to largest datasets automatically), an "Open by design" strip, and a copyable citation card.

All views rebuilt in the shared language

Browse (ord-app's table look incl. orange row-hover), search filters (Accordion / SegmentedControl / RangeSlider), result cards, dataset view (header card + themed composition charts), reaction detail (sticky section nav + paper sections), selected set, NL search, and About.

Bugs found and fixed during the migration

  • axios transmitted protobufjs's whole shared pool buffer for /api/compound_svg bodies → HTTP 500 (fixed with an exact-size copy in encodeCompound).
  • Peak wavelengths were formatted with the Length unit enum (nanometers would have been mislabeled); added a wavelengthStr formatter.
  • Yield/conversion precision displayed raw float32→double noise (± 4.800000190734863); now rounded.

Testing

  • tsc -b, vite build, eslint, and prettier all green; pre-commit hooks pass.
  • Verified end-to-end against the in-process test backend (ORD_INTERFACE_TESTING=TRUE + redis): every route screenshot at 1440px with zero console errors; substructure search (URL-driven state restoration included), dataset composition charts, reaction detail (protobuf decode + server-rendered summaries + compound SVGs), and the landing→/ask flow all work.
  • Not click-tested: the Ketcher draw modal (ported verbatim from ord-app's ComponentsKetcherEditor), file downloads, and clipboard copies — a manual pass is recommended.

🤖 Generated with Claude Code

Greptile Summary

This PR rebuilds the search and browse SPA around the shared ORD app frontend stack. The main changes are:

  • Mantine-based layout, theme, navigation, tables, and shared UI components.
  • Protobufjs ORD schema data flow with axios API helpers.
  • Wouter routing across search, browse, dataset, reaction, and natural-language search views.
  • Lazy-loaded npm Ketcher integration instead of the static iframe bundle.
  • A new landing page with search handoff, dataset stats, featured content, and citation copy.

Confidence Score: 4/5

This is close, but the failed search polling path should be fixed before merging.

  • Terminal poll responses can reject before clearing the stored task id.
  • The search page can keep polling a dead task while the URL query remains active.
  • The issue is contained to the structured search polling flow.

app/src/hooks/useSearchTask.ts

Important Files Changed

Filename Overview
app/src/hooks/useSearchTask.ts Search task polling moved to axios, but terminal poll errors can still leave the stored task id active.
app/src/views/search/MainSearch.tsx The search page now uses wouter and Mantine while continuing to drive polling from URL search params.

Reviews (3): Last reviewed commit: "Rebuild the reaction page as a port of o..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Rebuild the search/browse SPA on the same foundation as the ord-app
contribute/editor frontend so the two ORD frontends look and feel like
one product, and shared code can be extracted later:

- Mantine 7 with ord-app's theme and design tokens ported verbatim
  (marked to keep in sync until extracted to a shared package), Tabler
  icons, SCSS modules, and ports of ord-app's PaperButton, Pagination,
  DataTable, and NotFound components.
- ord-schema-protobufjs bindings (same package and range as ord-app)
  replacing the google-protobuf ord-schema bindings; display components
  now type against the shared ord.I* interfaces.
- wouter routing (replacing react-router-dom) and axios (replacing
  bare fetch), matching ord-app.
- ketcher-react + ketcher-standalone from npm (lazy-loaded chunk)
  replacing the static Ketcher 2.5.1 iframe bundle; the manual
  "download Ketcher into app/public" dev step and the Docker copy step
  are gone (the legacy Flask editor keeps its own bundle).
- New landing page: hero with natural-language search (routes to /ask),
  live reaction/dataset stats, action tiles, featured datasets and news
  curated via src/data/homeContent.ts, and a citation card.
- All views rebuilt in the shared visual language: browse (DataTable),
  search filters (Accordion/SegmentedControl/RangeSlider), result
  cards, dataset view, reaction detail, selected set, NL search, About.
- Drop Bootstrap, jQuery, DataTables, and Material Icons CDN tags.

Fixes found during the migration: axios was transmitting protobufjs's
whole shared pool buffer for /api/compound_svg (HTTP 500); peak
wavelengths were formatted with Length units; yield precision displayed
raw float32 noise.

Verified against the in-process test backend: all routes render with no
console errors; substructure search, dataset charts, and reaction
detail work end-to-end. tsc -b, vite build, eslint, and prettier are
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +105 to +109
// 200 = done, 202 = still running; anything else is an error.
const res = await api.get<Omit<SearchResult, 'data'>[]>('/fetch_query_result', {
params: { task_id: taskRef.current.taskId },
validateStatus: status => status === 200 || status === 202,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale Search Task Persists

When /fetch_query_result returns an unexpected status such as 404 for an expired task id, axios rejects before the hook clears taskRef.current.taskId. The same query then keeps polling the dead task id instead of resetting the task state, so the search can stay wedged until the user changes the query.

Suggested change
// 200 = done, 202 = still running; anything else is an error.
const res = await api.get<Omit<SearchResult, 'data'>[]>('/fetch_query_result', {
params: { task_id: taskRef.current.taskId },
validateStatus: status => status === 200 || status === 202,
});
// 200 = done, 202 = still running; anything else is an error.
const res = await api.get<Omit<SearchResult, 'data'>[]>('/fetch_query_result', {
params: { task_id: taskRef.current.taskId },
validateStatus: () => true,
});
if (res.status !== 200 && res.status !== 202) {
const id = taskRef.current.taskId;
taskRef.current.taskId = null;
throw new Error(`Search task ${id} failed (HTTP ${res.status})`);
}

Findings from a screenshot sweep of every route, including modal, selection,
empty, 404, and narrow-viewport states:

- Reaction view: fix null guards that rendered blank rows (Workups target pH,
  stirring RPM, conditions pH accepted null via `!== undefined`); size
  Outcomes' internal headings below the section heading instead of equal to
  it; restyle the raw-data "<>" chips and CUSTOM link from dark gray blocks to
  flat bordered chips with the orange hover accent; soften the compound table
  header rule from black to the standard border color; scroll the full-record
  JSON inside its modal instead of past the viewport.
- Search panel: match the add/draw button heights to their inputs.
- Landing page: stack the action tiles before their subtitles truncate; place
  the featured-card count badge uniformly beneath the dataset name so long
  names never squeeze or split mid-word.

Verified by re-screenshotting all routes and states; no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// 200 = done, 202 = still running; anything else is an error.
const res = await api.get<Omit<SearchResult, 'data'>[]>('/fetch_query_result', {
params: { task_id: taskRef.current.taskId },
validateStatus: status => status === 200 || status === 202,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Clear failed tasks When /fetch_query_result returns a terminal status such as 404 or 500 after an earlier 202, axios rejects here before taskRef.current.taskId is cleared. The query still has the last pending result, so the polling interval keeps firing with the same dead task id until the timeout or a URL change. Clear the stored task id on this rejected poll path before rethrowing so failed or expired tasks can reset instead of repeatedly polling stale state.

Replace the scroll-spy layout and legacy label/value sub-views with faithful
ports of ord-app's ReactionPage in view-only mode, verified side-by-side
against a live ord-app instance rendering the same reaction:

- Page shell: breadcrumbs (Datasets / dataset / reaction), a Download
  Reaction action, a header card with the reaction id + copy button and the
  reaction scheme preview (input cards with name pills, molecule images,
  amounts/roles/limiting badges, arrow, outcome card with the Desired badge
  and yield), a Tabs/List SegmentedControl, and the section content card.
- Nine section components ported from ord-app's ReactionView (Inputs,
  Outcomes, Conditions, Identifiers, Setup, Notes, Observations, Workups,
  Provenance) with its uppercase tab bar and mandatory-section asterisks,
  separated accordions, the shared identifiers/preview/role/details component
  grid, analysis cards, and KeyValueDisplay/RequiredOptionalFields
  primitives. Sections read the decoded ord.Reaction through context.
- Molecule images come from the existing /api/compound_svg RDKit endpoint
  (react-query cached) in place of ord-app's Redux-fed Indigo previews; the
  ported ReactionComponentPreview states (loading / no-preview) carry over.
- New shared pieces: Breadcrumbs, Counter, KeyValueDisplay,
  RequiredOptionalFields, typography helpers, and value/unit formatters
  (renderValuePrecisionUnit + the unit-symbol dictionary + ord-app's date
  format), all ported from ord-app and annotated as kept-in-sync copies.
- Edit-only affordances (validation pill, View buttons/drawer, rename,
  Save as Template, Copy reaction image) are intentionally dropped; the
  server-rendered reaction_summary HTML is no longer used on this page.
- Remove the superseded sub-views and now-unused legacy utils/styles
  (conditions/amount formatters, tabs/table/transition/vars partials).

tsc -b, vite build, eslint, and prettier are green; every route re-verified
headless with no console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@skearnes

Copy link
Copy Markdown
Member Author

Two follow-up commits since the PR opened:

  • 7338345 Design QA pass — screenshot sweep of every route and state (modals, selection, empty, 404, narrow viewports) with alignment/size/style fixes, including three null-guard display bugs in the reaction sub-views.
  • 874ec26 Reaction page rebuilt as a port of ord-app's read-only reaction view — the biggest look-and-feel item. The scroll-spy layout is gone; the page now mirrors ord-app's view-only ReactionPage: breadcrumbs, Download Reaction action, header card with the reaction scheme preview (input cards with name pills, molecules, amounts/roles/limiting badges → outcome card with the ✨ Desired badge and yield), the Tabs/List SegmentedControl, and the nine ReactionView section components (uppercase tab bar with mandatory-section asterisks, separated accordions, the shared identifiers/preview/role/details component grid, analysis cards). Verified side-by-side against a live ord-app instance rendering the same seeded reaction in view-only mode. Molecule images use the existing /api/compound_svg RDKit endpoint in place of ord-app's Indigo previews; edit-only affordances (validation pill, View drawer, rename, Save-as-Template) are deliberately omitted.

The ported primitives (Breadcrumbs, Counter, KeyValueDisplay, RequiredOptionalFields, value/unit formatters, preview components) are annotated as keep-in-sync copies of their ord-app counterparts — natural candidates for the shared package extraction discussed in the PR description.

🤖 Generated with Claude Code

Comment on lines +106 to +109
const res = await api.get<Omit<SearchResult, 'data'>[]>('/fetch_query_result', {
params: { task_id: taskRef.current.taskId },
validateStatus: status => status === 200 || status === 202,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Clear failed polls When /fetch_query_result returns a terminal status like 404 or 500, this validateStatus makes axios reject before the hook clears taskRef.current.taskId. The last successful query data can still be pending, so the interval can keep polling the same dead task id instead of resetting and submitting a fresh task. Clear the stored task id on rejected poll responses before rethrowing, or handle non-200/202 responses after the request returns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant