> ## Documentation Index
> Fetch the complete documentation index at: https://omniloy.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Insertion preview modal

> Let the doctor review, curate, and edit a generated report inside the SDK before it reaches your application

The **insertion preview** is an in-SDK modal that lets the doctor review, curate, and edit a generated report **before** it is handed back to your application. When enabled, an arriving report opens a preview where each field can be selected, edited, or filled; the doctor then clicks **Apply** and your app receives the curated payload via the `onReportApply` callback.

This page explains how to **enable**, **disable**, and **use** it.

<Info>
  The same modal is reused to curate **clinical actions (extras)** — petitions, appointments, tests, referrals — through a parallel channel. There the gate is `showInsertionPreview` **and** the presence of `templateExtras` (instead of `template`), and on **Apply** the items go to `handleExtras`. See [Clinical Actions (Extras)](/sofia/en/sdk/extras).
</Info>

## How it fits into the flow

<Steps>
  <Step title="Recording → Stop">
    After the doctor stops recording, a background auto-review drafts a report. The draft is **not shown yet** — it waits, exactly like the **Generate** button.
  </Step>

  <Step title="User clicks Generate (or Regenerate)">
    The modal **never opens on its own** from the background auto-review. It mirrors the Generate button and waits for a **user click**. Every user-initiated result — including after a failed automatic generation — opens the modal.
  </Step>

  <Step title="Curate in the modal">
    The insertion preview opens with the report fields. The doctor can select, edit, or fill each one. **Mandatory** fields block **Apply** until they are filled.
  </Step>

  <Step title="Apply → onReportApply(curated)">
    On **Apply**, your app receives the curated payload — only the fields the doctor kept or edited — via `onReportApply`. Your app then inserts it into the EHR/HIS.
  </Step>
</Steps>

<Info>
  If the feature is **disabled**, an arriving report is passed straight through to your `handleReport` callback instead (legacy behavior) — the modal never renders.
</Info>

## Enabling the modal

The modal has **two independent channels**: one for reports and one for [clinical actions (extras)](/sofia/en/sdk/extras). Each is enabled only when **its two** conditions are true. Both share the same backend flag, but each has its own schema:

| Channel     | Backend flag                 | Required schema                            | Curated on **Apply** by |
| ----------- | ---------------------------- | ------------------------------------------ | ----------------------- |
| **Reports** | `showInsertionPreview: true` | `template` (or the deprecated `toolsargs`) | `onReportApply`         |
| **Extras**  | `showInsertionPreview: true` | `templateExtras`                           | `handleExtras`          |

```
reports enabled = showInsertionPreview (backend, per API key)  AND  template is present
extras enabled  = showInsertionPreview (backend, per API key)  AND  templateExtras is present
```

The `showInsertionPreview` flag is **per API key** and shared by both channels. The schemas are independent: you can enable the modal for reports only (`template`), extras only (`templateExtras`), or both.

### Turn on the backend flag

The flag is **per API key** and lives in your Omniloy profile settings (`showInsertionPreview`). You **cannot** enable it from the frontend — contact [support@omniloy.com](mailto:support@omniloy.com) to have it set for your key. Until it is `true`, the SDK ignores the modal entirely and falls back to `handleReport`.

### Pass a template

The `template` is the JSON Schema that describes the report structure (the same schema used to generate the report). It **must** be present for the modal to open. See [Clinical data schemas](/sofia/en/sdk/templates).

## Disabling the modal (opting out)

The modal is **off by default** — it only appears when *both* enable conditions above are met. There is no special "off" switch: making **either** condition false disables it. You have two independent levers:

| Lever             | How                                                                                                       | Effect                                                                                                                                                                                             |
| ----------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Backend flag**  | Leave `showInsertionPreview` as `false` for your API key (the default), or ask Omniloy to set it `false`. | Authoritative, per-API-key switch. Report generation still works; reports go straight to `handleReport`.                                                                                           |
| **No `template`** | Do not pass the `template` prop (nor the deprecated `toolsargs`).                                         | Client-side opt-out. **Caveat:** the template also drives report generation — without it the Generate/Regenerate buttons are hidden too. Use this only if you don't want report generation either. |

When disabled, generated reports are delivered to your **`handleReport`** callback as before — no modal, no curation step. This is the legacy behavior.

<Warning>
  **Keep `handleReport` wired even when you enable the modal.** It is the fallback path: if the backend flag is off (or you drop the template), your integration keeps receiving reports with zero code changes.
</Warning>

### Common mistakes when enabling/disabling

<AccordionGroup>
  <Accordion title="There is no prop or HTML attribute that toggles the feature">
    A `show-insertion-preview` attribute appears in some demo code — it is a **no-op**: the SDK never reads it. The only enable/disable levers are the backend `showInsertionPreview` flag and the presence of `template`.
  </Accordion>

  <Accordion title="Omitting onReportApply does not disable the modal">
    If the feature is enabled, the modal still opens; on **Apply** it simply falls back to `handleReport` (and warns if neither is wired). To not show the modal, use one of the two levers above.
  </Accordion>

  <Accordion title="An empty template still counts as present">
    The gate only checks that `template` is truthy, so passing `{}` (or an object with no `properties`) can open a modal with **no fields**. Pass a real JSON Schema, or omit `template` entirely.
  </Accordion>
</AccordionGroup>

## Using it

<CodeGroup>
  ```tsx React theme={null}
  import { Omniscribe } from '@omniloy/sofia-sdk/react';
  import '@omniloy/sofia-sdk/react/index.css';

  <Omniscribe
    apikey={API_KEY}
    userid={USER_ID}
    patientid={PATIENT_ID}
    // Required for the modal to render (JSON Schema of the report):
    template={REPORT_TEMPLATE}

    // Called when the doctor clicks Apply — receives the curated payload:
    onReportApply={(curated) => {
      // curated: Record<string, unknown> — only the fields the doctor kept/edited
      myEhr.insertNote(curated);
    }}

    // Fallback: used when the modal is disabled (backend flag off / no template):
    handleReport={(report) => {
      myEhr.insertNote(report);
    }}

    // Optional: scope your CSS to the modal (it lives in the SDK shadow DOM):
    insertionPreviewClassNames={{
      panel: 'my-preview-panel',
      applyButton: 'my-apply-btn',
    }}
  />
  ```

  ```html Web Component theme={null}
  <sofia-sdk id="sofia" apikey="your-api-key" userid="doctor-123" patientid="patient-456"></sofia-sdk>

  <script>
    const el = document.querySelector('#sofia');

    // JSON props
    el.template = REPORT_TEMPLATE;
    el.insertionPreviewClassNames = { panel: 'my-preview-panel' };

    // Function props (assign as JS properties, never as HTML attributes)
    el.onReportApply = (curated) => myEhr.insertNote(curated);
    el.handleReport = (report) => myEhr.insertNote(report); // fallback
  </script>
  ```
</CodeGroup>

<Info>
  On the web component, `template` and `insertionPreviewClassNames` are **JSON props** and `onReportApply`/`handleReport` are **function props** — assign them as **JS properties** on the element, not as HTML attributes. Functions cannot be HTML attributes, and the `template` attribute is stripped from the DOM after it is read (it is sensitive data).
</Info>

### Props reference

| Prop                         | Type                               | Purpose                                                                                                    |
| ---------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `template`                   | JSON Schema (`object`)             | **Required to enable.** Describes the report structure and per-field rules. Also drives report generation. |
| `onReportApply`              | `(curated: CuratedReport) => void` | Fires when the doctor clicks **Apply**. Receives only the fields the doctor kept/edited.                   |
| `handleReport`               | `(report: object) => void`         | Fallback delivery when the modal is disabled. Keep it wired.                                               |
| `insertionPreviewClassNames` | `InsertionPreviewClassNames`       | Class-name overrides for the modal's built-in classes.                                                     |
| `toolsargs`                  | JSON Schema (`object`)             | **Deprecated** alias for `template`. Prefer `template`.                                                    |

`InsertionPreviewClassNames` keys (all optional): `backdrop`, `panel`, `header`, `body`, `footer`, `group`, `row`, `gapRow`, `applyButton`, `cancelButton`.

<Tip>
  The modal renders inside the SDK **shadow DOM**, so global page CSS won't reach it. Use `insertionPreviewClassNames` to attach your own classes, then style them.
</Tip>

The `CuratedReport` and `InsertionPreviewClassNames` types are structural (`Record<string, unknown>` and a flat map of optional class-name strings) — type your handlers against those shapes directly.

### What the doctor can do in the modal

* **Select / deselect** fields — only checked fields end up in the curated payload.
* **Edit** values inline (text, prose, number, boolean, date, enum, multi-enum, and arrays of objects).
* **Fill gaps** — empty fields the template expects are surfaced as gaps.
* **Edit with voice** — dictate to refine fields while the modal stays open.
* **Apply** — emits `onReportApply(curated)`. Disabled while any **mandatory** gap is unfilled.
* **Cancel** — closes without emitting.

## Template schema

The `template` is a standard **JSON Schema** object. Beyond the standard keywords, the modal understands:

* **`required`** (standard) — the agent should produce the field when it can.
* **`mandatory`** (custom keyword) — stronger than `required`: the doctor **must** review/fill it before Apply is allowed. Empty mandatory fields render as **blocking** gaps and disable Apply.
* **Field kinds** are inferred from the schema: `string`, `prose` (long text), `number`, `boolean`, `date` (via `pattern`), `enum`, `multi-enum` (array of enums), and `array-of-objects` (with optional `oneOf` + `discriminator` for per-row variants).

```json theme={null}
{
  "type": "object",
  "required": ["chief_complaint"],
  "properties": {
    "chief_complaint": { "type": "string" },
    "diagnosis":       { "type": "string", "mandatory": true },
    "follow_up_date":  { "type": "string", "pattern": "^(\\d{4}-\\d{2}-\\d{2})?$" },
    "medications": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": { "type": "string" },
          "dose": { "type": "number" },
          "unit": { "type": "string", "enum": ["mg", "g", "mcg"] }
        }
      }
    }
  }
}
```

<Warning>
  `mandatory` is a non-standard hint the SDK reads to gate Apply (`"diagnosis"` above blocks Apply until filled). Also instruct your generation prompt **not to fabricate** mandatory values — the doctor is expected to confirm them.
</Warning>

## Quick checklist

* [ ] Backend flag `showInsertionPreview` is `true` for your API key (contact support to enable). Shared by both channels.
* [ ] **For reports:** you pass a `template` (JSON Schema), with `onReportApply` wired for the curated payload and `handleReport` as the fallback.
* [ ] **For extras:** you pass a `templateExtras` (JSON Schema), with `handleExtras` wired to receive the curated items. See [Clinical Actions (Extras)](/sofia/en/sdk/extras).
* [ ] (Optional) `insertionPreviewClassNames` set for host-side styling.

<Tip>
  If the modal doesn't appear on a Generate click, verify both enable conditions. With `debug={true}` the SDK logs the gate state: `[InsertionPreview] gate { backend, templatePresent, isEnabled }`.
</Tip>

## Next steps

* **[Clinical data schemas](/sofia/en/sdk/templates)** — design the `template` and use the `mandatory` keyword
* **[Pre-fill from your EMR](/sofia/en/sdk/update-template)** — feed existing form content into generation with `updateTemplate`
* **[Optional properties](/sofia/en/sdk/optional-properties)** — full callback reference
