Lab Results
A lab panel with each value placed on the lab's reference range, plus trends from earlier tests.
Experience · Shares patterns with Data Table, Chart
≈ 1,594 tokens per use (tool 925 · guidance 365 · typical payload 304)How token costs are estimated
Example: Lab Results
Show my blood test results
Metabolic panel
3 within range · 1 above · 1 below
Northgate DiagnosticsOct 2, 2026| Marker | Value | Unit | Lab's range | Status |
|---|---|---|---|---|
| Sodium (Na) | 139 | mmol/L | 135–145 | Within range |
| Creatinine | 84 | µmol/L | 60–110 | Within range |
| eGFR | 92 | mL/min/1.73m² | > 90 | Within range |
| Alanine aminotransferase (ALT) | 58 | U/L | 7–40 | Above range |
| Vitamin D, 25-hydroxy (25-OH D) | 42 | nmol/L | 75–250 | Below range |
When to use
- The user asks to see lab results you've read from a report or health record: "Show my blood test results", "Is my vitamin D low?", "How has my cholesterol changed?"
- The report gives a reference range for each marker, so values can be placed on it.
When not to use
- You have no lab report or record. Never invent values or ranges.
- The user asks what a result means for their health or what to do about it. Say you can't interpret results and suggest they discuss them with their clinician.
- It's a reading the user tracks themselves, like weight or a blood pressure log. Use a Chart or a KPI Card.
How the agent should use it
- Copy values, units, ranges, flags, history and
clinicianNotefrom the report unchanged. They're all data-bound. Reference ranges come only from the lab report, never from general knowledge, because they differ between labs. - Set
flagonly when the lab printed one. Without it, the component works out the position from the range. A printed flag always wins. - Never diagnose, suggest causes or recommend treatment, in the component or in the text around it. Describe positions only: within, below or above the lab's range.
- Set
explainon the markers the user is likely to ask about. Onexplain, say in general terms what the marker measures (the Term Explainer fits), without interpreting the user's value. - Keep
groupnames as printed on the report. Up to 30 markers; split a larger report by panel. - Lifecycle: none needed. A lab report is historical, so it doesn't go stale or expire. When a newer report replaces it in the conversation, mark the old call superseded.
Reading the results
- Header. The panel, one quiet line with the date the sample was taken and the lab, and a count in words: "3 within range · 1 above · 1 below".
- Spectrum bar. The track covers the lab's range plus 25% of its span on each side (never below 0 for non-negative ranges), so values just outside stay visible. The within zone is a soft fill and the lab's optimal band, when printed, a stronger one; the zones outside the range are the plain track. The low and high bounds are labeled under the track, so the zones never rely on shade alone. A dot with a vertical tick marks the value; beyond the padded track, an arrow at the edge shows which way it lies.
- One-sided ranges. "< 5.2" runs from 0 to the bound; "> 90" shows the bound and as much again above it. A range with no numbers (say "Negative") is shown as text ("Range Negative"), and the status reads "See lab's range". Without any range the row says "No range given".
- Status. An icon plus text ("Above range"), and the value is bold when it's outside. A lab's critical flag adds a "Critical" badge.
- Optimal band. When the lab printed one, it's the stronger fill on the bar; whether the result is inside it is read to screen readers.
- Trend. With
history, the change since the last result ("↑ 14 since Mar") sits beside the value. The trend button (a chevron, named "Show trend") opens a line chart of every result with the range as a band. - Bottom. The clinician's note (verbatim, labeled "Clinician"), then one quiet line with the source and "Not medical advice." (always shown). Results are general information to discuss with a clinician; the card keeps that to the one short line.
Interaction
- Filter. When some results are outside the range, All and Out of range chips filter the list. Group headings stay.
- What is this? Each marker with
explaingets a help icon button, named "What is this?", that sends{ action: "explain", markerId }and leaves a short note under the results. The card never locks: it's a handoff, and the user may ask about several markers.
Accessibility
The bars and trend charts are hidden from screen readers. Each row states the value, unit and status in text, with the lab's range in hidden text; change arrows have hidden "Up 14 since Mar 12, 2026" text. A visually hidden table lists every marker with its value, unit, range and status, and each opened trend chart has its own hidden table of dates and values. Filter chips use aria-pressed, and trend toggles aria-expanded.
Installation
npx shadcn@latest add https://your-site.com/r/lab-results.json
Usage
import { tool } from "ai"
import { labResultsTool } from "@/components/genui/lab-results.schema"
const tools = {
[labResultsTool.name]: tool({
description: labResultsTool.description,
inputSchema: labResultsTool.inputSchema,
}),
}Without any explain markers the card is display-only, so give the tool an execute that returns immediately. With them, the agent receives:
{ action: "explain", markerId: "ferritin" }Render it through the Renderer under show_lab_results, or directly:
<LabResults
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) => {
if (part.state !== "output-available") {
addToolResult({ tool: "show_lab_results", toolCallId: part.toolCallId, output: result })
} else {
sendMessage({ text: `Lab results action: ${JSON.stringify(result)}` })
}
}}
/>While streaming, the header shows first and the markers appear one by one. The marker still being written is held back until it's complete, so values never jump. The counts, filter, clinician's note and source wait for the finished payload.
With LangGraph
Push it from a graph node with ui.push({ name: "show_lab_results", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored result back as result, or an array when the user asked about several markers. The notes show under the results and onResult isn't called. The buttons stay available, since asking is a handoff.
<LabResults {...part.input} result={part.state === "output-available" ? part.output : undefined} />Schema
| Prop | Type | Notes |
|---|---|---|
panel | string | Data-bound. e.g. "Lipid panel" |
takenAt | string | Data-bound. When the sample was taken (ISO date) |
lab | { name }? | Data-bound. |
markers | Marker[] | 1–30, see below |
clinicianNote | string? | Data-bound. Shown verbatim, up to 600 characters |
source | { label, url? }? | Data-bound. Linked when the URL is http(s) |
Marker
| Field | Type | Notes |
|---|---|---|
id | string | Returned in the result |
name | string | e.g. "Potassium" |
abbreviation | string? | e.g. "K" |
value | number | Data-bound. |
unit | string | Data-bound. e.g. "mmol/L" |
range | { low?, high?, optimalLow?, optimalHigh?, text? } | Data-bound. As printed by the lab; text for ranges like "< 5.2" or "Negative" |
flag | "low" | "high" | "critical-low" | "critical-high"? | Data-bound. As printed; computed from the range when absent |
history | { date, value }[]? | Data-bound. Up to 12 earlier results |
group | string? | Section heading, e.g. "Electrolytes" |
explain | boolean? | Adds "What is this?" |
Host props: status (streaming or ready), onResult, result and className.