udi-yac 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,16 +24,17 @@ Each is described once in [`src/app/envVars.ts`](src/app/envVars.ts); this table
24
24
 
25
25
  <!-- BEGIN generated: env vars (scripts/gen-chat-env-docs.mjs) -->
26
26
 
27
- | Variable | Default | Description |
28
- | -------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
29
- | `VITE_UDI_API_BASE_URL` | `http://localhost:8007` | UDIAgent FastAPI server URL. May also be a same-origin path (e.g. `/api/yac`) when the chat sits behind the host app’s reverse proxy. |
30
- | `VITE_UDI_DATA_PACKAGE` | `/data/hubmap/datapackage.json` | Path or URL to a `datapackage.json`. Defaults to the bundled HuBMAP snapshot synced from `sample-data/`. Ignored when `VITE_UDI_REMOTE_PACKAGE` is set. |
31
- | `VITE_UDI_REMOTE_PACKAGE` | — | Name of a server-side data package (configured on the agent via `UDI_QUERY_BACKENDS`). When set, no CSVs load in the browser: metadata comes from `/v1/yac/metadata` and queries go to `/v1/yac/query`. Takes precedence over `VITE_UDI_DATA_PACKAGE`. |
32
- | `VITE_UDI_REQUIRE_API_KEY` | `true` | Prompt the user for an OpenAI key in the UI. Defaults to `true`, but `.env.example` ships `false` because local dev runs against an agent that has its own `OPENAI_API_KEY`. |
33
- | `VITE_UDI_MODEL` | — | LLM model override. Sent **only when the user supplies their own OpenAI key** — the agent honors a requested model only alongside an `X-OpenAI-Key`, so whoever pays for the tokens picks the model. Otherwise the agent’s `GPT_MODEL_NAME` applies. |
34
- | `VITE_UDI_READ_ONLY` | `false` | Start in read-only mode: the chat pane and the dashboard’s top bar (counts, grid settings, session import/export, downloads) are hidden, as is every editing control on the cards, while brushing, filters and the table toggle stay. `true` leaves an Explore Data button over the dashboard that opens the chat; `locked` removes it. With the chat hidden there is nothing to build a dashboard from, so pair this with `VITE_UDI_INITIAL_SESSION`. |
35
- | `VITE_UDI_INITIAL_SESSION` | — | Path or URL to a session export — the JSON written by the dashboard’s `Session → Export session` action. Fetched at startup and used to seed the dashboard and the conversation behind it. Anything under `public/` is served from the site root, so `public/demo-session.json` is `/demo-session.json`. A malformed file fails loudly through the ErrorBoundary; a missing one only logs, and the dashboard starts empty. |
36
- | `VITE_BASE` | `/` | Public base path for the built SPA (read in `vite.config.ts`, build-time only). The library build always uses `./`. |
27
+ | Variable | Default | Description |
28
+ | -------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `VITE_UDI_API_BASE_URL` | `http://localhost:8007` | UDIAgent FastAPI server URL. May also be a same-origin path (e.g. `/api/yac`) when the chat sits behind the host app’s reverse proxy. |
30
+ | `VITE_UDI_DATA_PACKAGE` | `/data/hubmap/datapackage.json` | Path or URL to a `datapackage.json`. Defaults to the bundled HuBMAP snapshot synced from `sample-data/`. Ignored when `VITE_UDI_REMOTE_PACKAGE` is set. |
31
+ | `VITE_UDI_REMOTE_PACKAGE` | — | Name of a server-side data package (configured on the agent via `UDI_QUERY_BACKENDS`). When set, no CSVs load in the browser: metadata comes from `/v1/yac/metadata` and queries go to `/v1/yac/query`. Takes precedence over `VITE_UDI_DATA_PACKAGE`. |
32
+ | `VITE_UDI_REQUIRE_API_KEY` | `true` | Prompt the user for an OpenAI key in the UI. Defaults to `true`, but `.env.example` ships `false` because local dev runs against an agent that has its own `OPENAI_API_KEY`. |
33
+ | `VITE_UDI_MODEL` | — | LLM model override. Sent **only when the user supplies their own OpenAI key** — the agent honors a requested model only alongside an `X-OpenAI-Key`, so whoever pays for the tokens picks the model. Otherwise the agent’s `GPT_MODEL_NAME` applies. |
34
+ | `VITE_UDI_READ_ONLY` | `false` | Start in read-only mode: the chat pane and the dashboard’s top bar (counts, grid settings, session import/export, downloads) are hidden, as is every editing control on the cards, while brushing, filters and the table toggle stay. `true` leaves an Explore Data button over the dashboard that opens the chat; `locked` removes it. With the chat hidden there is nothing to build a dashboard from, so pair this with `VITE_UDI_INITIAL_SESSION`. |
35
+ | `VITE_UDI_INITIAL_SESSION` | — | Path or URL to a session export — the JSON written by the dashboard’s `Session → Export session` action. Fetched at startup and used to seed the dashboard and the conversation behind it. Anything under `public/` is served from the site root, so the pcx demo session (synced from `sample-data/`) is `/data/pcx/sessions/demo.json`. A malformed file fails loudly through the ErrorBoundary; a missing one only logs, and the dashboard starts empty. |
36
+ | `VITE_UDI_ISSUE_REPORTING` | `false` | Let users report a problem to the development team (Session menu, crash screen, and an offer from the agent). Reports go to the agent’s `POST /v1/yac/issue_report`, which records them in LangFuse, so turn this on only against an agent with LangFuse configured. |
37
+ | `VITE_BASE` | `/` | Public base path for the built SPA (read in `vite.config.ts`, build-time only). The library build always uses `./`. |
37
38
 
38
39
  <!-- END generated: env vars -->
39
40
 
@@ -114,31 +115,39 @@ Embedding in another app? Two things worth knowing up front:
114
115
 
115
116
  ### Config Props
116
117
 
117
- | Prop | Type | Description |
118
- | --------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
119
- | `apiBaseUrl` | `string` | Base URL for the UDIAgent API. Absolute (`https://agent.example.org`) or a same-origin path (`/api/yac`). |
120
- | `remotePackage` | `string?` | Server-side data package name. Schema/domains come from `GET /v1/yac/metadata` and queries from `POST /v1/yac/query`; no CSVs load in the browser and cross-filtering becomes commit-on-mouse-up. Takes precedence over both data-package props. |
121
- | `dataPackagePath` | `string?` | URL/path to `datapackage_udi.json`. Ignored when `dataPackage` is provided. |
122
- | `dataPackage` | `DataPackage?` | Provide a data package object directly instead of fetching from a URL. Takes precedence over `dataPackagePath`. |
123
- | `dataFieldDomains` | `DataFieldDomain[]?` | Pre-computed field domains. Skips CSV loading for domain computation when provided with `dataPackage`. |
124
- | `fetchOptions` | `RequestInit?` | Custom fetch options (headers, credentials, etc.) forwarded to all data-loading fetch calls. |
125
- | `authToken` | `string?` | JWT bearer token for API auth. Omit it when a backend proxy attaches the header instead; no `Authorization` header is then sent. |
126
- | `requireApiKey` | `boolean?` | Show API key input before chatting |
127
- | `model` | `string?` | LLM model name override |
128
- | `downloadActions` | `DownloadAction[]?` | Extra items appended to the Download Data dropdown. See [Custom download actions](#custom-download-actions). |
129
- | `downloadButtonLabel` | `string?` | Override the Download dropdown's trigger label. Defaults to `Download Data`. |
130
- | `entityIcons` | `EntityIconMap?` | Icon overrides for entity count chips. See [Custom entity icons](#custom-entity-icons). |
131
- | `mascot` | `ReactNode \| null?` | Replace or hide the welcome mascot. See [Custom mascot](#custom-mascot). |
132
- | `splashMessages` | `readonly string[]?` | Override or hide the randomised prompt above the mascot. See [Custom splash messages](#custom-splash-messages). |
133
- | `onEvent` | `TrackerFn?` | Analytics callback invoked on key user actions. See [Analytics events](#analytics-events). |
134
- | `palette` | `UDIPalette?` | Default color palette for every chart and table. See [Custom color palette](#custom-color-palette). |
135
- | `fontScale` | `number?` | Text size multiplier (1–2) for every chart, table and label. Overrides and hides the viewer's own Text size setting; omit it to leave text size to the viewer. |
136
- | `readOnly` | `boolean \| 'locked'?` | Start with the chat collapsed to a rail and every editing control hidden. `'locked'` removes the way back out. See [Read-only dashboard embed](#read-only-dashboard-embed). |
137
- | `initialSession` | `unknown?` | A session export to seed the dashboard and conversation with. See [Read-only dashboard embed](#read-only-dashboard-embed). |
138
- | `filters` | `UDIFilter[]?` | Filters set by the embedding page. See [Filters from the host page](#filters-from-the-host-page). |
139
- | `onFiltersChange` | `(filters) => void` | Called with every filter in place whenever one changes. See [Filters from the host page](#filters-from-the-host-page). |
140
- | `className` | `string?` | CSS class for the root element |
141
- | `style` | `CSSProperties?` | Inline styles for the root element |
118
+ | Prop | Type | Description |
119
+ | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
120
+ | `apiBaseUrl` | `string` | Base URL for the UDIAgent API. Absolute (`https://agent.example.org`) or a same-origin path (`/api/yac`). |
121
+ | `remotePackage` | `string?` | Server-side data package name. Schema/domains come from `GET /v1/yac/metadata` and queries from `POST /v1/yac/query`; no CSVs load in the browser and cross-filtering becomes commit-on-mouse-up. Takes precedence over both data-package props. |
122
+ | `dataPackagePath` | `string?` | URL/path to `datapackage_udi.json`. Ignored when `dataPackage` is provided. |
123
+ | `dataPackage` | `DataPackage?` | Provide a data package object directly instead of fetching from a URL. Takes precedence over `dataPackagePath`. |
124
+ | `dataFieldDomains` | `DataFieldDomain[]?` | Pre-computed field domains. Skips CSV loading for domain computation when provided with `dataPackage`. |
125
+ | `fetchOptions` | `RequestInit?` | Custom fetch options (headers, credentials, etc.) forwarded to all data-loading fetch calls. |
126
+ | `authToken` | `string?` | JWT bearer token for API auth. Omit it when a backend proxy attaches the header instead; no `Authorization` header is then sent. |
127
+ | `requireApiKey` | `boolean?` | Show API key input before chatting |
128
+ | `model` | `string?` | LLM model name override |
129
+ | `downloadActions` | `DownloadAction[]?` | Extra items appended to the Download Data dropdown. See [Custom download actions](#custom-download-actions). |
130
+ | `downloadButtonLabel` | `string?` | Override the Download dropdown's trigger label. Defaults to `Download Data`. |
131
+ | `entityIcons` | `EntityIconMap?` | Icon overrides for entity count chips. See [Custom entity icons](#custom-entity-icons). |
132
+ | `mascot` | `ReactNode \| null?` | Replace or hide the welcome mascot. See [Custom mascot](#custom-mascot). |
133
+ | `splashMessages` | `readonly string[]?` | Override or hide the randomised prompt above the mascot. See [Custom splash messages](#custom-splash-messages). |
134
+ | `onEvent` | `TrackerFn?` | Analytics callback invoked on key user actions. See [Analytics events](#analytics-events). |
135
+ | `palette` | `UDIPalette?` | Default color palette for every chart and table. See [Custom color palette](#custom-color-palette). |
136
+ | `recordUrl` | `RecordUrlFn?` | Link individual records (table rows, scatter points) to their own pages. See [Record links](#record-links). |
137
+ | `fontScale` | `number?` | Text size multiplier (1–2) for every chart, table and label. Overrides and hides the viewer's own Text size setting; omit it to leave text size to the viewer. |
138
+ | `readOnly` | `boolean \| 'locked'?` | Hide the chat pane and every editing control. `'locked'` removes the user's way back out. Followed on change. See [Read-only dashboard embed](#read-only-dashboard-embed). |
139
+ | `onReadOnlyChange` | `(readOnly) => void` | Called when the user leaves read-only via Explore Data. See [Switching modes from the host](#switching-modes-from-the-host). |
140
+ | `refitOnReadOnlyChange` | `boolean?` | Re-derive the dashboard's column count for its new width when read-only changes. Defaults to `true`. See [Switching modes from the host](#switching-modes-from-the-host). |
141
+ | `initialSession` | `unknown?` | A session export to seed the dashboard and conversation with. See [Read-only dashboard embed](#read-only-dashboard-embed). |
142
+ | `savedSessionsKey` | `string?` | Adds **Saved in browser…** to the dashboard's Session menu: save the current session by name, then load or delete it later. Stored in `localStorage` under this key, so use one key per data package. |
143
+ | `filters` | `UDIFilter[]?` | Filters set by the embedding page. See [Filters from the host page](#filters-from-the-host-page). |
144
+ | `onFiltersChange` | `(filters) => void` | Called with every filter in place whenever one changes. See [Filters from the host page](#filters-from-the-host-page). |
145
+ | `filterMode` | `'separate' \| 'shared'?` | `'shared'` makes a host filter on one field that column's filter, which chat and chart clicks edit too. See [Filters from the host page](#filters-from-the-host-page). |
146
+ | `cohort` | `{ entity, fields? }?` | The table whose filtered rows to report, and their key fields (default: its `primaryKey`). See [Reporting the filtered cohort](#reporting-the-filtered-cohort). |
147
+ | `onCohortChange` | `(cohort) => void` | Called with the key fields of every `cohort.entity` row all filters leave, or `null` while none reaches it. See [Reporting the filtered cohort](#reporting-the-filtered-cohort). |
148
+ | `issueReporting` | `boolean?` | Let users report a problem to the development team: a Report an issue item in the Session menu, a button on the crash screen, and an offer from the agent. Needs LangFuse on the agent, which records reports via `POST /v1/yac/issue_report`. |
149
+ | `className` | `string?` | CSS class for the root element |
150
+ | `style` | `CSSProperties?` | Inline styles for the root element |
142
151
 
143
152
  ### Read-only dashboard embed
144
153
 
@@ -159,12 +168,46 @@ import 'udi-yac/style.css';
159
168
  </div>;
160
169
  ```
161
170
 
162
- **Read-only is the starting mode, not a lock.** A floating "Explore Data" button at the dashboard's
171
+ **Read-only is a mode, not a lock.** A floating "Explore Data" button at the dashboard's
163
172
  bottom right leaves read-only, at which point the full chat and every editing control appear. The
164
173
  chat opens fresh: the conversation `initialSession` carried is hidden, though the model still has
165
174
  it as context for the charts on the dashboard. If your users must not be able to chat at all,
166
175
  pass `readOnly="locked"` to `UDIChat` instead: the button is dropped, leaving no way out.
167
176
 
177
+ #### Switching modes from the host
178
+
179
+ `readOnly` is followed on change, so a host can show the dashboard inline and read-only, then give
180
+ it a bigger stage — a modal, say — with the chat, and back again. Charts, filters, brushes, the
181
+ conversation and a draft in the chat input all carry over, as long as it stays the **same mounted
182
+ `UDIChat`**: restyle its container in place rather than rendering it somewhere else. A
183
+ portal-based dialog remounts it and starts over; tabs beside it should hide it with CSS, not
184
+ unmount it.
185
+
186
+ ```tsx
187
+ const [expanded, setExpanded] = useState(false);
188
+
189
+ <div className={expanded ? 'fixed inset-6 z-50' : 'h-[800px]'}>
190
+ <UDIChat
191
+ {...config}
192
+ initialSession={savedSession}
193
+ readOnly={!expanded}
194
+ onReadOnlyChange={(readOnly) => setExpanded(!readOnly)}
195
+ refitOnReadOnlyChange={false}
196
+ />
197
+ </div>;
198
+ ```
199
+
200
+ `onReadOnlyChange` fires when the user presses Explore Data, not when the prop changes. Passing it
201
+ makes the mode controlled: the button no longer leaves read-only by itself, your state setter
202
+ does, by changing `readOnly` in the same update that restyles the container — so the chat appears
203
+ in the new space with no in-between frame, and you can animate the move (a view transition, say)
204
+ from the read-only view still on screen. The seeded transcript is hidden the first time the chat
205
+ opens only; later, the conversation is the user's.
206
+
207
+ By default the dashboard re-derives its column count for its new width on each switch. Pass
208
+ `refitOnReadOnlyChange={false}` to keep the count — the one `initialSession` asked for, say — and
209
+ let the cards stretch or narrow instead.
210
+
168
211
  | Removed in read-only | Kept in read-only |
169
212
  | -------------------------------------------------------------------------- | --------------------------------------- |
170
213
  | The chat pane and the data overview | Brushing and cross-filtering |
@@ -196,9 +239,10 @@ silently empty dashboard. It is applied once, after the data package finishes lo
196
239
  and `initialSession` are independent — seeding a normal, chat-enabled session works too.
197
240
 
198
241
  To try the same thing in the standalone app without writing any code, point the two env vars at
199
- an exported session — `VITE_UDI_READ_ONLY=locked` and `VITE_UDI_INITIAL_SESSION=/demo-session.json`
200
- in `.env.local`, with the file dropped in `public/` (served from the site root). The app fetches it
201
- at startup and hands it to `initialSession`.
242
+ an exported session — `VITE_UDI_READ_ONLY=locked` and
243
+ `VITE_UDI_INITIAL_SESSION=/data/pcx/sessions/demo.json` in `.env.local` (the pcx demo session,
244
+ synced from `sample-data/` and served from the site root; pair it with the pcx package). The app
245
+ fetches it at startup and hands it to `initialSession`.
202
246
 
203
247
  ### Filters from the host page
204
248
 
@@ -227,6 +271,38 @@ const [filters, setFilters] = useState<UDIFilter[]>([
227
271
  A `point` filter's `selection` maps each field to the values to keep; an `interval` filter's maps
228
272
  each field to `[min, max]`.
229
273
 
274
+ **Sharing columns with the chat.** By default a host filter is its own chip, read-only to the agent,
275
+ and applies on top of any chat or chart filter on the same column. Pass `filterMode="shared"` to
276
+ keep one filter per column instead: a host filter on a single field becomes that column's filter,
277
+ the same one the agent and chart clicks write. Whoever edits it last sets it, and
278
+ `onFiltersChange` reports it as `col:<entity>.<field>` with that editor's `origin`. A host can
279
+ then mirror chart clicks and chat requests into its own controls, matching on `dataSourceKey` and
280
+ field. Dropping a host entry removes the filter only while the host was its last editor. Filters
281
+ on several fields stay separate. Don't scope a filter to its own table: it is treated as
282
+ unscoped, since a plain filter already reaches every related table through its foreign keys.
283
+
284
+ ### Reporting the filtered cohort
285
+
286
+ A host can follow which subjects the filters leave, to narrow its own lists to the same people.
287
+ Name the table and, if it has no `primaryKey`, the fields that identify its rows:
288
+
289
+ ```tsx
290
+ <UDIChat
291
+ {...config}
292
+ cohort={{ entity: 'patient', fields: ['patient_id'] }}
293
+ onCohortChange={(cohort) => setPatientIds(cohort?.keys.map((k) => k.patient_id) ?? null)}
294
+ />
295
+ ```
296
+
297
+ - Every filter counts: chat, chart and host, on any table that reaches `entity` (directly, or
298
+ through a subject scope), exactly as a chart of `entity` would be filtered.
299
+ - `onCohortChange` gets `{ entity, keys }`, one object of key fields per row, once the filters have
300
+ settled (300 ms after the last change, so a brush drag runs one query). It gets `null` while no
301
+ filter reaches `entity`: everyone is in, and no list of every subject is sent.
302
+ - In server-side data mode the keys come back grouped, so the list is complete: it is not cut at
303
+ the package's row cap.
304
+ - A filter on another table reaches `entity` through a single key column, as charts do.
305
+
230
306
  ### Data Source Configuration
231
307
 
232
308
  There are three ways to provide data to `UDIChat`, from simplest to most flexible:
@@ -466,6 +542,28 @@ Supply any of these to take over a channel; each one you set wins, and the rest
466
542
  | `text` | Tick labels, axis and legend titles, table text. |
467
543
  | `mutedText` | Empty cells and truncation notices. |
468
544
 
545
+ ### Record links
546
+
547
+ Pass `recordUrl` to link individual records out to their own pages: a dataset, a donor, a patient. It is called with a row and its entity (the spec's first source) and returns that record's URL, or nothing for a row with no page.
548
+
549
+ ```tsx
550
+ <UDIChat
551
+ apiBaseUrl="http://localhost:8007"
552
+ recordUrl={(record, entity) =>
553
+ typeof record.uuid === 'string'
554
+ ? `https://portal.hubmapconsortium.org/browse/${entity.replace(/s$/, '')}/${record.uuid}`
555
+ : undefined
556
+ }
557
+ />
558
+ ```
559
+
560
+ - **Tables** gain a leading ↗ column that opens the row's page in a new tab. A table of aggregates (counts by sex, say) has no record ids, so the function returns nothing and the column is left out.
561
+ - **Point marks** open their record on a plain click; the cursor turns to a pointer over them. A Shift, Ctrl or ⌘ click still picks, and clicking a point keeps an existing brush. Bars, lines and areas never link.
562
+ - The function sees the whole row, not just the columns on screen, so it can key on a column the table doesn't show.
563
+ - Only `http(s)` URLs are followed. A relative URL resolves against the host page.
564
+
565
+ Independently of `recordUrl`, a table cell whose value is an `http(s)` URL — a protocol DOI, say — is always shown as a link.
566
+
469
567
  ### Custom mascot
470
568
 
471
569
  The empty-dashboard welcome splash renders a YAC mascot by default. Consumers can replace it or hide it via the `mascot` prop on `UDIChatConfig`:
@@ -562,6 +660,9 @@ Event names are stable, snake_case strings. **Properties carry metadata only —
562
660
  | `filter_field_changed` | User changes the target field on a tweakable filter card | `filterType: 'interval' \| 'point'`<br>`entity: string`<br>`field: string` — the newly selected field |
563
661
  | `visualization_tweaked` | User changes the field a `VizTweakComponent` dropdown controls | `encoding: string` — the channel, or the template parameter for a re-bind<br>`kind: 'field' \| 'dimension' \| 'measure' \| 'binding'` — `binding` means the agent re-instantiated the template; the rest are local spec rewrites |
564
662
  | `visualization_tweak_failed` | The agent refused a template re-bind (invalid binding, unknown template, transport failure) | `encoding: string`<br>`kind: 'binding'` |
663
+ | `issue_report_opened` | The Report an issue dialog opens (from the Session menu or an agent offer) | `source: 'menu' \| 'chat'` |
664
+ | `issue_report_submitted` | The agent accepts an issue report | `source: 'menu' \| 'chat'`<br>`hasContact: boolean` |
665
+ | `issue_report_failed` | Sending an issue report fails (network error, HTTP !ok — e.g. no LangFuse on the agent) | `source: 'menu' \| 'chat'` |
565
666
 
566
667
  Every row in the table above also includes `sessionId: string`; it's omitted from the per-event cells to keep the table scannable.
567
668
 
@@ -609,7 +710,7 @@ src/
609
710
  ChatInput.tsx # Message input
610
711
  MessageList.tsx # Message history with auto-scroll
611
712
  MessageBubble.tsx # Single message + tool call tabs
612
- ConversationList.tsx # Sidebar with saved session files
713
+ ConversationList.tsx # Debug sidebar (`//admin`): new conversation
613
714
  ApiKeyInput.tsx # OpenAI API key input
614
715
  hooks/
615
716
  useChatApi.ts # LLM API integration hook