Drake commited on
Commit
133d478
·
unverified ·
1 Parent(s): 5dcfc5c

Update Figment Gradio Server UI

Browse files
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 📉
4
  colorFrom: indigo
5
  colorTo: red
6
  sdk: gradio
7
- sdk_version: 5.50.0
8
  app_file: app.py
9
  pinned: false
10
  python_version: 3.11
@@ -18,7 +18,7 @@ Figment uses deterministic rules for danger signs and an AI protocol navigator f
18
 
19
  > ⚠️ **Figment is not a medical device.** It does not diagnose, prescribe, or replace a clinician. It is a prototype for protocol navigation, escalation support, and documentation in low-connectivity environments, for use by trained responders. See [Safety & non-goals](#safety--non-goals).
20
 
21
- - **Status:** In active development for the [Build Small Hackathon](docs/build-small-hackathon-org-card.md) (build window **June 515, 2026**). The Gradio scaffold, deterministic rules, hosted NVIDIA Omni client, local OpenAI-compatible client, canned fallback, traces, and tests run locally; the hosted NVIDIA API smoke test is green. The 50-case hosted Omni eval has run: baseline whole-output model competence was **28/50**, and the load-bearing follow-up reached **31/50** with **480/650** model-retained fields, **170/650** deterministic patches, **8/50** full fallback, and **50/50** final validation. Public Space cold-boot evidence is still missing; the last known public Space API state reported `runtime.stage=NO_APP_FILE`, so local health must not be described as a runnable public Space. Local 4B runtime evidence, Parakeet ASR evidence, demo video, social post, and user-test notes are still proof-needed items tracked in the [adversarial review action items](docs/adversarial-review-action-items.md), [hosted eval results](docs/hosted_omni_eval_results.md), [parameter/evidence ledger](docs/model_parameter_evidence_ledger.md), and [submission checklist](docs/submission_checklist.md).
22
  - **Track target:** Backyard AI (solve a real problem for a specific, real person you know). Final evidence still needs a real trained responder using synthetic or de-identified scenarios; see [user test notes](docs/user_test_notes.md).
23
  - **Built for:** a real disaster-response volunteer trained in disaster-response first aid and local protocol use; name withheld for privacy.
24
  - **Model:** NVIDIA **Nemotron 3 Nano Omni 30B-A3B Reasoning** as the v1 default. The model-card body reports 31B total parameters; the workback plan and [parameter/evidence ledger](docs/model_parameter_evidence_ledger.md) track the HF-sidebar count ambiguity, local 4B + Parakeet story, adapter count status, and organizer-confirmation status.
@@ -37,7 +37,7 @@ The design goal is restraint. Figment is **a field protocol binder that can talk
37
 
38
  ## What it does
39
 
40
- Figment is a [Gradio](https://www.gradio.app/) app with five frozen tabs:
41
 
42
  1. **Intake** — structured capture of setting, patient age, pregnancy status, chief concern, symptoms, vitals, allergies, medications, available supplies, and a free-text responder note. Optional audio intake drafts fields only; typed/edited values must be confirmed before rules or navigation run.
43
  2. **Risk Check** — deterministic red-flag rules fire **before** the LLM and set the minimum urgency floor (e.g. altered mental status, severe respiratory distress, chest pain, stroke signs, pregnancy bleeding, pediatric lethargy, severe dehydration signs, fever escalation criteria, wound infection escalation criteria).
@@ -50,7 +50,7 @@ Figment is a [Gradio](https://www.gradio.app/) app with five frozen tabs:
50
  ## How it works
51
 
52
  ```text
53
- Gradio Blocks UI
54
  → structured intake schema
55
  → rules.py (deterministic red-flag engine)
56
  → retrieval.py (SQLite FTS protocol search)
@@ -86,7 +86,7 @@ Omni can satisfy an off-grid claim if it is self-hosted on sufficient local hard
86
  | `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` | local text-navigation and first fine-tuning target |
87
  | `nvidia/parakeet-rnnt-1.1b` | local/offline ASR target, enabled only after the local ASR gate passes |
88
 
89
- Reference dev/demo machine: an M4 Pro MacBook Pro with 48 GB RAM. Hosted Omni is the intended public Space story; local audio is Parakeet-only after verification, and the safe local proof may use typed intake or a canned transcript if ASR is not stable.
90
 
91
  ---
92
 
@@ -127,25 +127,43 @@ If the hosted model is unavailable or returns invalid JSON, Figment falls back t
127
 
128
  ### 2. Run against a local OpenAI-compatible server
129
 
130
- To target a local OpenAI-compatible server after the Nemotron 3 Nano 4B path is verified:
131
 
132
  ```bash
133
  vllm serve nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16 \
134
  --served-model-name nemotron3-nano-4b-bf16 \
135
  --trust-remote-code \
136
  --max-model-len 16384
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- # Or, on the Mac/off-grid path, use a verified 4B llama.cpp-compatible quantization:
139
- llama-server \
140
- -hf <verified-nemotron-3-nano-4b-gguf> \
141
- --ctx-size 16384 \
142
- --port 8001 \
143
- --host 127.0.0.1 \
144
- --temp 0.4 \
145
- --top-p 0.9
 
 
 
 
 
 
 
146
  ```
147
 
148
- Set `MODEL_BACKEND=llama_cpp`, `MODEL_STACK=local_4b_parakeet`, `LLAMA_BASE_URL=http://127.0.0.1:8001/v1`, and `LOCAL_MODEL_ID=nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` in `.env`.
149
 
150
  ### 3. Canned fallback
151
 
@@ -161,7 +179,7 @@ The submission Space target is under the **build-small-hackathon** Hugging Face
161
 
162
  [build-small-hackathon/figment](https://huggingface.co/spaces/build-small-hackathon/figment)
163
 
164
- The submission target is a live Gradio demo powered by a hosted or self-hosted Nemotron Omni endpoint. Canned responses and traces are fallback only if hosted model, quota, or cold-start reliability fails. The public Space is **not yet claimed runnable**: the last known public Space API evidence reported `runtime.stage=NO_APP_FILE` with only metadata files present. Public Space cold-boot evidence, app-file presence, typed intake, and trace labeling remain proof-needed in the [submission checklist](docs/submission_checklist.md).
165
 
166
  ---
167
 
@@ -171,7 +189,7 @@ This repo now holds the runnable scaffold plus the planning docs. Current struct
171
 
172
  ```text
173
  figment/
174
- app.py # Gradio Blocks UI
175
  figment/ # config, schemas, rules, retrieval, model_client,
176
  # prompt_builder, validators, trace, sbar
177
  data/
@@ -198,7 +216,7 @@ docs/superpowers/specs/ docs/superpowers/plans/ # design spec + implementation
198
  requirements.txt / requirements-dev.txt / .env.example
199
  ```
200
 
201
- Key docs: [workback plan](docs/figment-workback-plan.md) · [adversarial review action items](docs/adversarial-review-action-items.md) · [hosted eval results](docs/hosted_omni_eval_results.md) · [parameter/evidence ledger](docs/model_parameter_evidence_ledger.md) · [submission checklist](docs/submission_checklist.md) · [safety statement](docs/safety_statement.md) · [user test notes](docs/user_test_notes.md) · [prerequisites](docs/prerequisites.md) · [hackathon rules](docs/build-small-hackathon-org-card.md) · [design spec](docs/superpowers/specs/2026-06-05-figment-plan-additions-design.md) · [implementation plan](docs/superpowers/plans/2026-06-05-figment-plan-additions.md).
202
 
203
  ---
204
 
@@ -236,7 +254,7 @@ Figment is deliberately scoped. **It will not:**
236
  - **replace a clinician** — the trained responder remains the decision-maker;
237
  - **serve untrained users** — it is a tool for trained responders;
238
  - **store PHI or raw audio** — traces scrub raw audio-like payloads and uploaded filenames;
239
- - **hide hosted-mode data flow** — hosted Space mode may send synthetic or de-identified text/audio to the configured Omni endpoint, while local mode keeps runtime inputs on-device;
240
  - **act autonomously** — every output is advisory and requires human judgment.
241
 
242
  This posture reflects real risk: the WHO has warned that authoritative-sounding health AI can create automation bias, and the FDA regulates clinical-decision-support software depending on its claims and users. Figment makes no clinical claims. See the fuller [safety statement](docs/safety_statement.md).
@@ -251,7 +269,7 @@ This posture reflects real risk: the WHO has warned that authoritative-sounding
251
  | Synthetic dataset | CC-BY-4.0 |
252
  | Code | [Apache-2.0](LICENSE) |
253
 
254
- Data handling: local mode keeps runtime inputs on-device; hosted mode is for synthetic or de-identified demo inputs only; traces do not retain raw audio.
255
 
256
  ---
257
 
@@ -275,7 +293,7 @@ Submission claims are evidence-gated:
275
 
276
  | Claim / badge area | Current status | Proof needed before claiming achieved |
277
  | ------------------ | -------------- | ------------------------------------- |
278
- | Hosted Gradio Space | Targeted / proof-needed; not currently claimed runnable | Public Space app files present, cold boot, typed intake run, trace showing actual route and fallback status |
279
  | Backyard AI | Targeted / proof-needed | A real trained responder using synthetic or de-identified scenarios, recorded in [user test notes](docs/user_test_notes.md) |
280
  | Off the Grid | Targeted, not yet proven | Recorded no-cloud run using either self-hosted Omni on adequate local hardware or the smaller verified local stack |
281
  | Llama Champion | Targeted, not yet proven | Working eligible local model route with trace/eval evidence |
 
4
  colorFrom: indigo
5
  colorTo: red
6
  sdk: gradio
7
+ sdk_version: 6.17.3
8
  app_file: app.py
9
  pinned: false
10
  python_version: 3.11
 
18
 
19
  > ⚠️ **Figment is not a medical device.** It does not diagnose, prescribe, or replace a clinician. It is a prototype for protocol navigation, escalation support, and documentation in low-connectivity environments, for use by trained responders. See [Safety & non-goals](#safety--non-goals).
20
 
21
+ - **Status:** In active development for the [Build Small Hackathon](docs/build-small-hackathon-org-card.md) (build window **June 5-15, 2026**). The Gradio scaffold, deterministic rules, hosted NVIDIA Omni client, local OpenAI-compatible client, canned fallback, traces, and tests run locally; the hosted NVIDIA API smoke test is green. The public Hugging Face Space now boots with app files present at commit `5dcfc5c830de7331eca9020b17e1c571a8619654`; a no-secret public workflow smoke loaded a demo case, confirmed typed intake, fired deterministic pediatric-dehydration escalation, retrieved protocol cards, and produced an honestly labeled `canned_backend` trace with `validation_status=passed`, `raw_audio_stored=false`, and zero model-retained fields. The 50-case hosted Omni eval has run: baseline whole-output model competence was **28/50**, and the load-bearing follow-up reached **31/50** with **480/650** model-retained fields, **170/650** deterministic patches, **8/50** full fallback, and **50/50** final validation. Local 4B runtime evidence, Parakeet ASR evidence, demo video, social post, and user-test notes are still proof-needed items tracked in the [adversarial review action items](docs/adversarial-review-action-items.md), [hosted eval results](docs/hosted_omni_eval_results.md), [parameter/evidence ledger](docs/model_parameter_evidence_ledger.md), and [submission checklist](docs/submission_checklist.md).
22
  - **Track target:** Backyard AI (solve a real problem for a specific, real person you know). Final evidence still needs a real trained responder using synthetic or de-identified scenarios; see [user test notes](docs/user_test_notes.md).
23
  - **Built for:** a real disaster-response volunteer trained in disaster-response first aid and local protocol use; name withheld for privacy.
24
  - **Model:** NVIDIA **Nemotron 3 Nano Omni 30B-A3B Reasoning** as the v1 default. The model-card body reports 31B total parameters; the workback plan and [parameter/evidence ledger](docs/model_parameter_evidence_ledger.md) track the HF-sidebar count ambiguity, local 4B + Parakeet story, adapter count status, and organizer-confirmation status.
 
37
 
38
  ## What it does
39
 
40
+ Figment is a [Gradio Server](https://www.gradio.app/guides/server-mode) app with five frozen workflow views:
41
 
42
  1. **Intake** — structured capture of setting, patient age, pregnancy status, chief concern, symptoms, vitals, allergies, medications, available supplies, and a free-text responder note. Optional audio intake drafts fields only; typed/edited values must be confirmed before rules or navigation run.
43
  2. **Risk Check** — deterministic red-flag rules fire **before** the LLM and set the minimum urgency floor (e.g. altered mental status, severe respiratory distress, chest pain, stroke signs, pregnancy bleeding, pediatric lethargy, severe dehydration signs, fever escalation criteria, wound infection escalation criteria).
 
50
  ## How it works
51
 
52
  ```text
53
+ Gradio Server custom frontend + Gradio API endpoints
54
  → structured intake schema
55
  → rules.py (deterministic red-flag engine)
56
  → retrieval.py (SQLite FTS protocol search)
 
86
  | `nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` | local text-navigation and first fine-tuning target |
87
  | `nvidia/parakeet-rnnt-1.1b` | local/offline ASR target, enabled only after the local ASR gate passes |
88
 
89
+ Reference dev/demo machine: an M4 Pro MacBook Pro with 48 GB RAM. Hosted Omni is the intended public Space story; local audio is Parakeet-only after verification, and the safe local proof may use typed intake or a canned transcript if ASR is not stable. The full 4B BF16 artifact and Parakeet `.nemo` artifact are present locally, but the local 50-case text eval and local ASR provider proof still need to be run before any local badge claim is upgraded.
90
 
91
  ---
92
 
 
127
 
128
  ### 2. Run against a local OpenAI-compatible server
129
 
130
+ To target a local OpenAI-compatible server after the Nemotron 3 Nano 4B path is verified, use the full BF16 4B model as the canonical local text artifact:
131
 
132
  ```bash
133
  vllm serve nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16 \
134
  --served-model-name nemotron3-nano-4b-bf16 \
135
  --trust-remote-code \
136
  --max-model-len 16384
137
+ ```
138
+
139
+ The full-weight snapshot has been downloaded locally at:
140
+
141
+ ```text
142
+ /Users/drake.thomsen/.cache/huggingface/hub/models--nvidia--NVIDIA-Nemotron-3-Nano-4B-BF16/snapshots/dfaf35de3e30f1867dd8dbc38a7fc9fb52d3914f
143
+ ```
144
+
145
+ For a local run, expose that full-weight runtime through an OpenAI-compatible `/v1/chat/completions` endpoint and point Figment at it. Set `MODEL_BACKEND=llama_cpp`, `MODEL_STACK=local_4b_parakeet`, `LLAMA_BASE_URL=<local-openai-compatible-endpoint>/v1`, and `LOCAL_MODEL_ID=nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16` in `.env`. Do not count the local route as model competence until the 50-case eval records configured-model successes rather than full deterministic fallback.
146
+
147
+ To capture the local evidence bundle once the endpoint is live:
148
 
149
+ ```bash
150
+ PYTHON_DOTENV_DISABLED=true \
151
+ python3 scripts/run_local_4b_evidence.py \
152
+ --base-url http://127.0.0.1:8001/v1
153
+ ```
154
+
155
+ The helper writes `/v1/models` metadata, one-case route smoke, eval JSONL, eval summary, and `eval_evidence_manifest.json` under `traces/local_4b_evidence_*`. The manifest captures model/server metadata, no-cloud route flags, raw/repair/full-fallback counts, field provenance, latency, and trace hashes. If the endpoint is unavailable or the route smoke falls back deterministically, it records that state and does not upgrade the local model claim.
156
+
157
+ To capture the local Parakeet ASR evidence bundle once a real local ASR adapter or device runtime produces provider output:
158
+
159
+ ```bash
160
+ PYTHON_DOTENV_DISABLED=true \
161
+ python3 scripts/run_local_asr_evidence.py \
162
+ --provider-payload <local-parakeet-provider-output.json> \
163
+ --audio <optional-source-audio.wav>
164
  ```
165
 
166
+ The ASR helper records Parakeet artifact metadata, optional audio metadata without copying raw audio, provider-output hash, Figment draft checks, and `asr_evidence_manifest.json`. Artifact presence alone does not count as local ASR proof.
167
 
168
  ### 3. Canned fallback
169
 
 
179
 
180
  [build-small-hackathon/figment](https://huggingface.co/spaces/build-small-hackathon/figment)
181
 
182
+ The submission target is a live Gradio demo powered by a hosted or self-hosted Nemotron Omni endpoint. Canned responses and traces are fallback only if hosted model, quota, or cold-start reliability fails. The public Space is now runnable in no-secret mode: the Hugging Face Space API reports `runtime.stage=RUNNING`, `app.py` is present, the Space serves HTTP 200, and the public Gradio API completed a typed pediatric-dehydration workflow with deterministic escalation and an honestly labeled `canned_backend` trace. Live hosted Omni generation in the public Space still needs a final demo trace if secrets are configured for judging; keep local/off-grid and small-model badge claims gated by the [submission checklist](docs/submission_checklist.md).
183
 
184
  ---
185
 
 
189
 
190
  ```text
191
  figment/
192
+ app.py # Gradio Server app and custom frontend
193
  figment/ # config, schemas, rules, retrieval, model_client,
194
  # prompt_builder, validators, trace, sbar
195
  data/
 
216
  requirements.txt / requirements-dev.txt / .env.example
217
  ```
218
 
219
+ Key docs: [workback plan](docs/figment-workback-plan.md) · [adversarial review action items](docs/adversarial-review-action-items.md) · [hosted eval results](docs/hosted_omni_eval_results.md) · [parameter/evidence ledger](docs/model_parameter_evidence_ledger.md) · [local llama evidence](docs/local_llama_eval_evidence.md) · [local Parakeet ASR evidence](docs/local_parakeet_asr_evidence.md) · [submission checklist](docs/submission_checklist.md) · [safety statement](docs/safety_statement.md) · [user test notes](docs/user_test_notes.md) · [prerequisites](docs/prerequisites.md) · [hackathon rules](docs/build-small-hackathon-org-card.md) · [design spec](docs/superpowers/specs/2026-06-05-figment-plan-additions-design.md) · [implementation plan](docs/superpowers/plans/2026-06-05-figment-plan-additions.md).
220
 
221
  ---
222
 
 
254
  - **replace a clinician** — the trained responder remains the decision-maker;
255
  - **serve untrained users** — it is a tool for trained responders;
256
  - **store PHI or raw audio** — traces scrub raw audio-like payloads and uploaded filenames;
257
+ - **hide hosted-mode data flow** — hosted Space mode may send synthetic or de-identified text/audio to the configured Omni endpoint, while local mode keeps runtime inputs on the local machine;
258
  - **act autonomously** — every output is advisory and requires human judgment.
259
 
260
  This posture reflects real risk: the WHO has warned that authoritative-sounding health AI can create automation bias, and the FDA regulates clinical-decision-support software depending on its claims and users. Figment makes no clinical claims. See the fuller [safety statement](docs/safety_statement.md).
 
269
  | Synthetic dataset | CC-BY-4.0 |
270
  | Code | [Apache-2.0](LICENSE) |
271
 
272
+ Data handling: local mode keeps runtime inputs on the local machine; hosted mode is for synthetic or de-identified demo inputs only; traces do not retain raw audio.
273
 
274
  ---
275
 
 
293
 
294
  | Claim / badge area | Current status | Proof needed before claiming achieved |
295
  | ------------------ | -------------- | ------------------------------------- |
296
+ | Hosted Gradio Space | Runnable in no-secret canned-fallback mode; live hosted-model demo trace still proof-needed | Public Space app files present, cold boot, typed intake run, trace showing `raw_route=canned`, `final_route=canned_backend`, `validation_status=passed`, and `raw_audio_stored=false` |
297
  | Backyard AI | Targeted / proof-needed | A real trained responder using synthetic or de-identified scenarios, recorded in [user test notes](docs/user_test_notes.md) |
298
  | Off the Grid | Targeted, not yet proven | Recorded no-cloud run using either self-hosted Omni on adequate local hardware or the smaller verified local stack |
299
  | Llama Champion | Targeted, not yet proven | Working eligible local model route with trace/eval evidence |
app.py CHANGED
@@ -3,9 +3,12 @@
3
  from __future__ import annotations
4
 
5
  import html
 
6
  from pathlib import Path
7
  from typing import Any
8
 
 
 
9
  from figment.audio_intake import confirm_audio_draft as _confirm_audio_draft
10
  from figment.audio_intake import draft_audio_intake as _draft_audio_intake
11
  from figment.config import FigmentConfig, load_config
@@ -20,8 +23,10 @@ from figment.validators import urgency_floor_from_rules, validate_audio_ready
20
 
21
  try:
22
  import gradio as gr
 
23
  except (ImportError, OSError): # pragma: no cover - lets unit tests import without gradio installed
24
  gr = None
 
25
 
26
 
27
  TAB_TITLES = [
@@ -39,6 +44,19 @@ DEMO_AUDIO_FILENAMES = (
39
  "case_3_dictated_intake.wav",
40
  )
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  DEMO_CASES: dict[str, dict[str, str]] = {
44
  "Disaster clinic: pediatric dehydration": {
@@ -220,215 +238,89 @@ def build_app(config: FigmentConfig | None = None):
220
  if gr is None:
221
  return _FallbackDemo()
222
 
223
- with gr.Blocks(title="Figment", css=FIGMENT_CSS, theme=gr.themes.Base(), fill_width=True) as demo:
224
- gr.HTML(_app_header_html())
225
- gr.HTML(_statusline_html(config))
226
- intake_state = gr.State({})
227
- audio_state = gr.State(None)
228
- trace_state = gr.State({})
229
-
230
- with gr.Tabs(elem_classes=["figment-tabs"]):
231
- with gr.Tab(TAB_TITLES[0]):
232
- with gr.Column(elem_classes=["figment-tab-body"]):
233
- with gr.Row():
234
- with gr.Column(scale=11, elem_classes=["figment-panel"]):
235
- gr.HTML(_section_header_html("1. Quick start", "Load a frozen synthetic demo case, or type directly into the confirmed intake form."))
236
- with gr.Row():
237
- demo_case = gr.Dropdown(list(DEMO_CASES), label="Demo case", scale=4)
238
- load_demo = gr.Button("Load", scale=1)
239
- gr.HTML(_demo_case_pills_html())
240
-
241
- gr.HTML(
242
- _section_header_html(
243
- _audio_section_title(config),
244
- _audio_section_subtitle(config),
245
- )
246
- )
247
- with gr.Row():
248
- with gr.Column(scale=3):
249
- audio_clip = gr.Audio(
250
- label=_audio_clip_label(config),
251
- sources=["microphone", "upload"],
252
- type="filepath",
253
- interactive=config.enable_audio_intake,
254
- )
255
- with gr.Column(scale=2):
256
- draft_btn = gr.Button(
257
- "Draft Audio Fields",
258
- elem_classes=["primary"],
259
- interactive=config.enable_audio_intake,
260
- )
261
- apply_audio = gr.Button("Apply Audio Draft", interactive=config.enable_audio_intake)
262
- gr.HTML(_audio_runtime_chips_html(config))
263
- transcript = gr.Textbox(
264
- label=_transcript_label(config),
265
- lines=3,
266
- interactive=config.enable_audio_intake,
267
- )
268
- if examples := _demo_audio_examples():
269
- with gr.Accordion("Backup: upload/test audio clips", open=False):
270
- gr.HTML(
271
- '<div class="figment-section-subtitle">'
272
- "Use these only for testing, browser microphone failures, or repeatable demo fallback."
273
- "</div>"
274
- )
275
- gr.Examples(examples=examples, inputs=[audio_clip, transcript], label="Backup demo clips")
276
-
277
- gr.HTML(_section_header_html("3. Confirmed intake", "Protocol rules and navigation run only after this intake is confirmed."))
278
- with gr.Row():
279
- setting = gr.Textbox(label="Setting")
280
- patient_age = gr.Textbox(label="Patient age")
281
- pregnancy_status = gr.Textbox(label="Pregnancy status")
282
- chief_concern = gr.Textbox(label="Chief concern")
283
- symptoms = gr.Textbox(label="Symptoms")
284
- vitals = gr.Textbox(label="Vitals")
285
- with gr.Row():
286
- allergies = gr.Textbox(label="Allergies")
287
- medications = gr.Textbox(label="Medications")
288
- supplies = gr.Textbox(label="Available supplies")
289
- note = gr.Textbox(label="Responder note", lines=4)
290
-
291
- with gr.Column(scale=9, elem_classes=["figment-panel"]):
292
- gr.HTML(_section_header_html("Audio draft field suggestions", "Review timecoded suggestions before applying them to the editable intake."))
293
- audio_json = gr.JSON(label="Audio draft", elem_classes=["figment-json-compact"])
294
- gr.HTML(_section_header_html("Live confirmed intake preview", "This is the only source allowed to feed deterministic rules and navigation."))
295
- intake_json = gr.JSON(label="Confirmed intake", elem_classes=["figment-json-compact"])
296
- confirm_btn = gr.Button("Confirm Intake", elem_classes=["primary"])
297
-
298
- with gr.Tab(TAB_TITLES[1]):
299
- with gr.Column(elem_classes=["figment-tab-body"]):
300
- with gr.Row():
301
- with gr.Column(scale=8, elem_classes=["figment-panel"]):
302
- gr.HTML(_section_header_html("Deterministic Red-Flag Checklist", "Reference checklist for the frozen safety floor. These rules are deterministic."))
303
- gr.HTML(_red_flag_checklist_html())
304
- with gr.Column(scale=10, elem_classes=["figment-panel"]):
305
- gr.HTML(_section_header_html("Rule Output", "The model cannot lower the deterministic protocol_urgency floor."))
306
- risk_btn = gr.Button("Run Risk Check", elem_classes=["primary"])
307
- risk_html = gr.HTML(_risk_summary_html(_empty_risk_result()))
308
- with gr.Accordion("Raw deterministic red flags JSON", open=False):
309
- risk_json = gr.JSON(label="Deterministic red flags", elem_classes=["figment-json-compact"])
310
-
311
- with gr.Tab(TAB_TITLES[2]):
312
- with gr.Column(elem_classes=["figment-tab-body"]):
313
- with gr.Row():
314
- with gr.Column(scale=8, elem_classes=["figment-panel"]):
315
- gr.HTML(_section_header_html("Protocol Card Browser", "Local protocol cards retrieved from the confirmed intake."))
316
- gr.HTML(_protocol_library_html())
317
- retrieve_btn = gr.Button("Retrieve Protocol Cards", elem_classes=["primary"])
318
- with gr.Column(scale=10, elem_classes=["figment-panel"]):
319
- guidance_html = gr.HTML(_protocol_results_html([]))
320
- guidance_evidence = gr.Textbox(label="Protocol evidence panel", lines=8, interactive=False)
321
- with gr.Accordion("Retrieved protocol cards JSON", open=False):
322
- guidance_json = gr.JSON(label="Retrieved protocol cards", elem_classes=["figment-json-compact"])
323
-
324
- with gr.Tab(TAB_TITLES[3]):
325
- with gr.Column(elem_classes=["figment-tab-body"]):
326
- with gr.Row():
327
- with gr.Column(scale=8, elem_classes=["figment-panel"]):
328
- gr.HTML(_section_header_html("Navigator Output JSON", "Machine-readable protocol navigation output."))
329
- nav_btn = gr.Button("Run Navigator", elem_classes=["primary"])
330
- output_json = gr.JSON(label="Navigator output", elem_classes=["figment-json-tall"])
331
- with gr.Column(scale=10, elem_classes=["figment-panel"]):
332
- navigator_html = gr.HTML(_navigator_summary_html({}))
333
- sbar_text = gr.Textbox(label="SBAR handoff", lines=8)
334
-
335
- with gr.Tab(TAB_TITLES[4]):
336
- with gr.Column(elem_classes=["figment-tab-body"]):
337
- with gr.Row():
338
- with gr.Column(scale=8, elem_classes=["figment-panel"]):
339
- gr.HTML(_section_header_html("Run Steps (Timeline)", "Audit trail from intake through validation."))
340
- trace_audit_html = gr.HTML(_trace_audit_html({}))
341
- export_trace = gr.Button("Export Trace")
342
- trace_file = gr.File(label="Trace download", interactive=False)
343
- with gr.Column(scale=10, elem_classes=["figment-panel"]):
344
- gr.HTML(_section_header_html("Trace JSON", "Raw audit object for review and export."))
345
- trace_json = gr.JSON(label="Trace", elem_classes=["figment-json-tall"])
346
-
347
- gr.HTML(_footer_rail_html(config))
348
-
349
- fields = [setting, patient_age, pregnancy_status, chief_concern, symptoms, vitals, allergies, medications, supplies, note]
350
- source_outputs = [
351
- intake_json,
352
- risk_json,
353
- risk_html,
354
- guidance_json,
355
- guidance_evidence,
356
- guidance_html,
357
- output_json,
358
- sbar_text,
359
- navigator_html,
360
- trace_json,
361
- trace_file,
362
- trace_audit_html,
363
- intake_state,
364
- trace_state,
365
- ]
366
- audio_source_outputs = [
367
- audio_json,
368
- intake_json,
369
- risk_json,
370
- risk_html,
371
- guidance_json,
372
- guidance_evidence,
373
- guidance_html,
374
- output_json,
375
- sbar_text,
376
- navigator_html,
377
- trace_json,
378
- trace_file,
379
- trace_audit_html,
380
- intake_state,
381
- audio_state,
382
- trace_state,
383
- ]
384
- load_demo.click(
385
- _load_demo_case_and_reset,
386
- inputs=[demo_case],
387
- outputs=[
388
- *fields,
389
- audio_clip,
390
- transcript,
391
- audio_json,
392
- intake_json,
393
- risk_json,
394
- risk_html,
395
- guidance_json,
396
- guidance_evidence,
397
- guidance_html,
398
- output_json,
399
- sbar_text,
400
- navigator_html,
401
- trace_json,
402
- trace_file,
403
- trace_audit_html,
404
- intake_state,
405
- audio_state,
406
- trace_state,
407
- ],
408
- )
409
- draft_btn.click(
410
- lambda audio_file, transcript_text: _draft_audio_ui(audio_file, transcript_text, config=config),
411
- inputs=[audio_clip, transcript],
412
- outputs=[audio_json],
413
- ).then(lambda x: x, inputs=[audio_json], outputs=[audio_state]).then(_clear_source_outputs, outputs=source_outputs)
414
- apply_audio.click(_apply_audio_draft_ui, inputs=[*fields, audio_state], outputs=[*fields, audio_json, audio_state]).then(
415
- _clear_source_outputs,
416
- outputs=source_outputs,
417
- )
418
- for source in fields:
419
- source.change(_clear_source_outputs, outputs=source_outputs)
420
- audio_clip.change(_clear_audio_outputs, outputs=audio_source_outputs)
421
- transcript.change(_clear_audio_outputs, outputs=audio_source_outputs)
422
- confirm_btn.click(_confirm_ui_intake, inputs=[*fields, audio_state], outputs=[intake_json, intake_state, audio_state])
423
- risk_btn.click(_risk_ui_with_summary, inputs=[intake_state], outputs=[risk_json, risk_html])
424
- retrieve_btn.click(_retrieve_with_evidence_and_summary_ui, inputs=[intake_state], outputs=[guidance_json, guidance_evidence, guidance_html])
425
- nav_btn.click(
426
- lambda intake, audio_draft: _navigate_ui_with_summary(intake, audio_draft, config=config),
427
- inputs=[intake_state, audio_state],
428
- outputs=[output_json, sbar_text, trace_json, trace_state, navigator_html, trace_audit_html],
429
- )
430
- export_trace.click(lambda trace: trace_download_path(trace, config=config) if trace else None, inputs=[trace_state], outputs=[trace_file])
431
- return demo
432
 
433
 
434
  def _h(value: Any) -> str:
@@ -484,6 +376,582 @@ def _footer_rail_html(config: FigmentConfig) -> str:
484
  """
485
 
486
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  def _model_mode_label(config: FigmentConfig) -> str:
488
  if config.model_backend == "hosted_omni":
489
  return "Configured backend: hosted_omni"
@@ -494,14 +962,14 @@ def _model_mode_label(config: FigmentConfig) -> str:
494
 
495
  def _audio_section_title(config: FigmentConfig) -> str:
496
  if not config.enable_audio_intake or config.audio_backend == "none":
497
- return "2. Audio draft intake disabled"
498
  if config.audio_backend == "omni_native" and config.model_backend == "hosted_omni":
499
- return "2. Hosted Omni audio draft"
500
  if config.audio_backend == "parakeet_nemo":
501
- return "2. Local Parakeet ASR draft"
502
  if config.audio_backend == "canned":
503
- return "2. Canned audio demo draft"
504
- return "2. Audio draft intake"
505
 
506
 
507
  def _audio_section_subtitle(config: FigmentConfig) -> str:
@@ -556,11 +1024,6 @@ def _section_header_html(title: str, subtitle: str = "") -> str:
556
  return f'<div class="figment-section-title">{_h(title)}</div>{subtitle_html}'
557
 
558
 
559
- def _demo_case_pills_html() -> str:
560
- pills = "".join(f'<div class="figment-demo-pill">{_h(name)}</div>' for name in DEMO_CASES)
561
- return f'<div class="figment-quick-grid">{pills}</div>'
562
-
563
-
564
  def _red_flag_checklist_html() -> str:
565
  categories = {
566
  "Airway / Breathing": [
@@ -802,6 +1265,7 @@ def _navigator_summary_html(output: dict[str, Any], trace: dict[str, Any] | None
802
  urgency = "routine"
803
  handoff = output.get("handoff_note_sbar") if isinstance(output.get("handoff_note_sbar"), dict) else {}
804
  runtime_card = _runtime_contribution_card_html(trace)
 
805
  return f"""
806
  <div class="figment-urgency-banner">
807
  <div>
@@ -816,6 +1280,8 @@ def _navigator_summary_html(output: dict[str, Any], trace: dict[str, Any] | None
816
  <div style="height:12px"></div>
817
  {runtime_card}
818
  <div style="height:12px"></div>
 
 
819
  <div class="figment-card-grid">
820
  {_navigator_list_card_html("Missing Observations", output.get("missing_info_to_collect"))}
821
  {_navigator_list_card_html("Responder Checklist", output.get("responder_checklist"), checked=True)}
@@ -865,6 +1331,42 @@ def _runtime_contribution_card_html(trace: dict[str, Any] | None) -> str:
865
  """
866
 
867
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  def _navigator_list_card_html(title: str, values: Any, *, checked: bool = False) -> str:
869
  items = _as_list(values) or ["No items generated yet."]
870
  cls = "figment-checklist checked" if checked else "figment-checklist"
@@ -933,6 +1435,7 @@ def _trace_audit_html(trace: dict[str, Any]) -> str:
933
  route = trace.get("model_route") if isinstance(trace.get("model_route"), dict) else {}
934
  retrieval = trace.get("retrieval") if isinstance(trace.get("retrieval"), dict) else {}
935
  provenance_summary = trace.get("field_provenance_summary") if isinstance(trace.get("field_provenance_summary"), dict) else {}
 
936
  final_route = str(route.get("final_route") or "unknown")
937
  return f"""
938
  <table class="figment-table">
@@ -947,7 +1450,9 @@ def _trace_audit_html(trace: dict[str, Any]) -> str:
947
  <span class="figment-chip green">Schema valid: {_h(validator.get('passed'))}</span>
948
  <span class="figment-chip {_route_chip_class(final_route)}">{_h(runtime_route_label(route))}</span>
949
  <span class="figment-chip">Retrieval: {_h(retrieval.get('primary_source') or 'not traced')}</span>
 
950
  </div>
 
951
  <div class="figment-mini-card">
952
  <h4>Model &amp; Performance</h4>
953
  <table class="figment-table">
@@ -1274,4 +1779,4 @@ def _navigate_ui(
1274
 
1275
 
1276
  if __name__ == "__main__":
1277
- build_app().queue().launch()
 
3
  from __future__ import annotations
4
 
5
  import html
6
+ import json
7
  from pathlib import Path
8
  from typing import Any
9
 
10
+ from fastapi.responses import HTMLResponse
11
+
12
  from figment.audio_intake import confirm_audio_draft as _confirm_audio_draft
13
  from figment.audio_intake import draft_audio_intake as _draft_audio_intake
14
  from figment.config import FigmentConfig, load_config
 
23
 
24
  try:
25
  import gradio as gr
26
+ from gradio.data_classes import FileData
27
  except (ImportError, OSError): # pragma: no cover - lets unit tests import without gradio installed
28
  gr = None
29
+ FileData = Any # type: ignore[misc, assignment]
30
 
31
 
32
  TAB_TITLES = [
 
44
  "case_3_dictated_intake.wav",
45
  )
46
 
47
+ INTAKE_FIELD_KEYS = (
48
+ "setting",
49
+ "patient_age",
50
+ "pregnancy_status",
51
+ "chief_concern",
52
+ "symptoms",
53
+ "vitals",
54
+ "allergies",
55
+ "medications",
56
+ "available_supplies",
57
+ "responder_note",
58
+ )
59
+
60
 
61
  DEMO_CASES: dict[str, dict[str, str]] = {
62
  "Disaster clinic: pediatric dehydration": {
 
238
  if gr is None:
239
  return _FallbackDemo()
240
 
241
+ if not hasattr(gr, "Server"):
242
+ raise RuntimeError("Figment Server mode requires gradio>=6.0 so gradio.Server is available.")
243
+
244
+ server = gr.Server(
245
+ title="Figment",
246
+ summary="Protocol navigator for field clinics and disaster response.",
247
+ version="1.0.0",
248
+ )
249
+
250
+ @server.api(name="runtime", concurrency_limit=None)
251
+ def runtime_api() -> dict[str, Any]:
252
+ return _runtime_payload(config)
253
+
254
+ @server.api(name="load_demo_case", concurrency_limit=None)
255
+ def load_demo_case_api(name: str) -> dict[str, Any]:
256
+ fields = _fields_dict_from_values(_load_demo_case(name))
257
+ return {
258
+ "fields": fields,
259
+ "intake": collect_intake(*_field_values(fields)),
260
+ "risk": _empty_risk_result(),
261
+ "risk_html": _risk_summary_html(_empty_risk_result()),
262
+ "guidance_html": _protocol_results_html([]),
263
+ "navigator_html": _navigator_summary_html({}),
264
+ "trace_audit_html": _trace_audit_html({}),
265
+ }
266
+
267
+ @server.api(name="draft_audio", concurrency_limit=1)
268
+ def draft_audio_api(audio_file: FileData | None = None, transcript: str = "") -> dict[str, Any]:
269
+ path = _file_data_path(audio_file)
270
+ return draft_audio_intake(transcript=transcript or "", config=config, audio_file=path)
271
+
272
+ @server.api(name="apply_audio_draft", concurrency_limit=None)
273
+ def apply_audio_draft_api(fields: dict[str, Any], audio_draft: dict[str, Any] | None = None) -> dict[str, Any]:
274
+ values = _field_values(fields)
275
+ updated = _apply_audio_draft_ui(*values, audio_draft)
276
+ updated_fields = _fields_dict_from_values(updated[: len(INTAKE_FIELD_KEYS)])
277
+ return {
278
+ "fields": updated_fields,
279
+ "audio_draft": updated[-1],
280
+ "intake": collect_intake(*_field_values(updated_fields)),
281
+ "risk": _empty_risk_result(),
282
+ "risk_html": _risk_summary_html(_empty_risk_result()),
283
+ "guidance_html": _protocol_results_html([]),
284
+ "navigator_html": _navigator_summary_html({}),
285
+ "trace_audit_html": _trace_audit_html({}),
286
+ }
287
+
288
+ @server.api(name="confirm_intake", concurrency_limit=None)
289
+ def confirm_intake_api(fields: dict[str, Any], audio_draft: dict[str, Any] | None = None) -> dict[str, Any]:
290
+ confirmed, intake_state, updated_audio = _confirm_ui_intake(*_field_values(fields), audio_draft)
291
+ return {"intake": confirmed, "intake_state": intake_state, "audio_draft": updated_audio}
292
+
293
+ @server.api(name="risk_check", concurrency_limit=None)
294
+ def risk_check_api(intake: dict[str, Any]) -> dict[str, Any]:
295
+ risk, summary = _risk_ui_with_summary(intake)
296
+ return {"risk": risk, "risk_html": summary}
297
+
298
+ @server.api(name="retrieve_protocol_cards", concurrency_limit=None)
299
+ def retrieve_protocol_cards_api(intake: dict[str, Any]) -> dict[str, Any]:
300
+ cards, evidence, summary = _retrieve_with_evidence_and_summary_ui(intake)
301
+ return {"cards": cards, "evidence": evidence, "guidance_html": summary}
302
+
303
+ @server.api(name="run_navigator", concurrency_limit=1)
304
+ def run_navigator_api(intake: dict[str, Any], audio_draft: dict[str, Any] | None = None) -> dict[str, Any]:
305
+ output, sbar, trace, trace_state, summary, audit = _navigate_ui_with_summary(intake, audio_draft, config=config)
306
+ return {
307
+ "navigator_output": output,
308
+ "sbar": sbar,
309
+ "trace": trace,
310
+ "trace_state": trace_state,
311
+ "navigator_html": summary,
312
+ "trace_audit_html": audit,
313
+ }
314
+
315
+ @server.get("/", response_class=HTMLResponse)
316
+ async def homepage() -> str:
317
+ return _server_homepage_html(config)
318
+
319
+ @server.get("/health")
320
+ async def health() -> dict[str, str]:
321
+ return {"status": "ok", "mode": "gradio.Server"}
322
+
323
+ return server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
 
326
  def _h(value: Any) -> str:
 
376
  """
377
 
378
 
379
+ def _runtime_payload(config: FigmentConfig) -> dict[str, Any]:
380
+ return {
381
+ "model_mode_label": _model_mode_label(config),
382
+ "model_stack": config.model_stack,
383
+ "model_backend": config.model_backend,
384
+ "audio_backend": config.audio_backend,
385
+ "enable_audio_intake": config.enable_audio_intake,
386
+ "audio_section_title": _audio_section_title(config),
387
+ "audio_section_subtitle": _audio_section_subtitle(config),
388
+ "audio_clip_label": _audio_clip_label(config),
389
+ "transcript_label": _transcript_label(config),
390
+ "audio_chips_html": _audio_runtime_chips_html(config),
391
+ "demo_audio_examples": _demo_audio_examples(),
392
+ "status_text": _status_text(config),
393
+ }
394
+
395
+
396
+ def _fields_dict_from_values(values: list[Any] | tuple[Any, ...]) -> dict[str, str]:
397
+ return {key: str(value or "") for key, value in zip(INTAKE_FIELD_KEYS, values, strict=True)}
398
+
399
+
400
+ def _field_values(fields: dict[str, Any] | None) -> list[str]:
401
+ fields = fields or {}
402
+ return [str(fields.get(key, "") or "") for key in INTAKE_FIELD_KEYS]
403
+
404
+
405
+ def _file_data_path(file_data: Any) -> str | None:
406
+ if not file_data:
407
+ return None
408
+ if isinstance(file_data, dict):
409
+ path = file_data.get("path")
410
+ return str(path) if path else None
411
+ path = getattr(file_data, "path", None)
412
+ return str(path) if path else None
413
+
414
+
415
+ def _json_for_script(value: Any) -> str:
416
+ return json.dumps(value, ensure_ascii=True).replace("</", "<\\/")
417
+
418
+
419
+ def _server_homepage_html(config: FigmentConfig) -> str:
420
+ initial_data = {
421
+ "tabTitles": TAB_TITLES,
422
+ "fieldKeys": INTAKE_FIELD_KEYS,
423
+ "runtime": _runtime_payload(config),
424
+ "emptyRisk": _empty_risk_result(),
425
+ "riskHtml": _risk_summary_html(_empty_risk_result()),
426
+ "protocolLibraryHtml": _protocol_library_html(),
427
+ "guidanceHtml": _protocol_results_html([]),
428
+ "navigatorHtml": _navigator_summary_html({}),
429
+ "traceAuditHtml": _trace_audit_html({}),
430
+ }
431
+ html_doc = """
432
+ <!doctype html>
433
+ <html lang="en">
434
+ <head>
435
+ <meta charset="utf-8">
436
+ <meta name="viewport" content="width=device-width, initial-scale=1">
437
+ <title>Figment</title>
438
+ <style>
439
+ __FIGMENT_CSS__
440
+ </style>
441
+ </head>
442
+ <body>
443
+ <main class="figment-app-shell">
444
+ <aside class="figment-mission-rail" aria-label="Mission context">
445
+ <header class="figment-topbar">
446
+ <div class="figment-brand">
447
+ <a class="figment-logo" href="/" aria-label="Homepage">Figment</a>
448
+ <div class="figment-positioning">Offline protocol support for field clinics and disaster response.</div>
449
+ </div>
450
+ <div class="figment-safety">
451
+ <span class="figment-safety-mark">!</span>
452
+ <span>For trained responders only. Not a substitute for clinical judgment.</span>
453
+ </div>
454
+ </header>
455
+
456
+ <section class="figment-statusline" aria-label="Runtime status">
457
+ <strong>Runtime</strong>
458
+ <span class="figment-chip blue" id="runtime-mode"></span>
459
+ <span class="figment-chip" id="runtime-stack"></span>
460
+ <span class="figment-chip" id="runtime-backend"></span>
461
+ <span class="figment-chip" id="runtime-audio"></span>
462
+ <span class="figment-chip green">Privacy: no raw audio retained in traces</span>
463
+ </section>
464
+
465
+ <nav class="figment-tabs" aria-label="Workflow">
466
+ <button class="figment-tab-button" type="button" data-view="intake">Intake</button>
467
+ <button class="figment-tab-button" type="button" data-view="risk">Risk Check</button>
468
+ <button class="figment-tab-button" type="button" data-view="protocol">Protocol Guidance</button>
469
+ <button class="figment-tab-button" type="button" data-view="navigator">Navigator Output + Handoff</button>
470
+ <button class="figment-tab-button" type="button" data-view="trace">Trace</button>
471
+ </nav>
472
+
473
+ <p class="figment-live-status" id="live-status" aria-live="polite">Server mode ready. Gradio queue endpoints are connected.</p>
474
+
475
+ <footer class="figment-footer-rail">
476
+ <div class="figment-footer-cluster">
477
+ <strong>Server mode</strong>
478
+ <span class="figment-chip blue">gradio.Server</span>
479
+ <span class="figment-chip">Custom HTML/CSS/JS frontend</span>
480
+ <span class="figment-chip">Gradio API queue retained</span>
481
+ </div>
482
+ <div class="figment-footer-cluster">
483
+ <strong>Harness</strong>
484
+ <span class="figment-chip green">v1.0.0 schema</span>
485
+ <span class="figment-chip green">Deterministic red-flag floor enabled</span>
486
+ </div>
487
+ </footer>
488
+ </aside>
489
+
490
+ <section class="figment-operation-board" aria-label="Case workspace">
491
+
492
+ <section class="figment-view" id="view-intake" data-view-panel="intake">
493
+ <div class="figment-workspace figment-workspace-intake">
494
+ <section class="figment-panel figment-intake-panel">
495
+ <div class="figment-panel-heading">
496
+ <div>
497
+ <span class="figment-kicker">Intake</span>
498
+ <h2>Case intake</h2>
499
+ <p>Responder-entered facts for the protocol run.</p>
500
+ </div>
501
+ </div>
502
+ <form id="intake-form" class="figment-field-grid">
503
+ <label class="figment-control" for="field-setting">
504
+ <span>Setting</span>
505
+ <input id="field-setting" name="setting" type="text">
506
+ </label>
507
+ <label class="figment-control" for="field-patient_age">
508
+ <span>Patient age</span>
509
+ <input id="field-patient_age" name="patient_age" type="text">
510
+ </label>
511
+ <label class="figment-control" for="field-pregnancy_status">
512
+ <span>Pregnancy status</span>
513
+ <input id="field-pregnancy_status" name="pregnancy_status" type="text">
514
+ </label>
515
+ <label class="figment-control figment-control-wide" for="field-chief_concern">
516
+ <span>Chief concern</span>
517
+ <input id="field-chief_concern" name="chief_concern" type="text">
518
+ </label>
519
+ <label class="figment-control figment-control-wide" for="field-symptoms">
520
+ <span>Symptoms</span>
521
+ <input id="field-symptoms" name="symptoms" type="text">
522
+ </label>
523
+ <label class="figment-control figment-control-wide" for="field-vitals">
524
+ <span>Vitals</span>
525
+ <input id="field-vitals" name="vitals" type="text">
526
+ </label>
527
+ <label class="figment-control" for="field-allergies">
528
+ <span>Allergies</span>
529
+ <input id="field-allergies" name="allergies" type="text">
530
+ </label>
531
+ <label class="figment-control" for="field-medications">
532
+ <span>Medications</span>
533
+ <input id="field-medications" name="medications" type="text">
534
+ </label>
535
+ <label class="figment-control figment-control-wide" for="field-available_supplies">
536
+ <span>Available supplies</span>
537
+ <input id="field-available_supplies" name="available_supplies" type="text">
538
+ </label>
539
+ <label class="figment-control figment-control-wide" for="field-responder_note">
540
+ <span>Responder note</span>
541
+ <textarea id="field-responder_note" name="responder_note" rows="4"></textarea>
542
+ </label>
543
+ </form>
544
+
545
+ <div class="figment-section-divider"></div>
546
+
547
+ <section class="figment-audio-draft" aria-labelledby="audio-title">
548
+ <div class="figment-panel-heading figment-panel-heading-action">
549
+ <div>
550
+ <span class="figment-kicker">Audio draft</span>
551
+ <h2 id="audio-title"></h2>
552
+ <p id="audio-subtitle"></p>
553
+ </div>
554
+ <div id="audio-runtime-chips" class="figment-chip-row"></div>
555
+ </div>
556
+ <div class="figment-audio-grid">
557
+ <label class="figment-control" for="audio-file">
558
+ <span id="audio-file-label"></span>
559
+ <input id="audio-file" name="audio_file" type="file" accept="audio/*">
560
+ </label>
561
+ <div class="figment-audio-actions">
562
+ <button class="figment-button figment-button-secondary" id="draft-audio" type="button">Draft audio fields</button>
563
+ <button class="figment-button figment-button-secondary" id="apply-audio" type="button">Apply audio draft</button>
564
+ </div>
565
+ </div>
566
+ <label class="figment-control" for="transcript">
567
+ <span id="transcript-label"></span>
568
+ <textarea id="transcript" name="transcript" rows="3"></textarea>
569
+ </label>
570
+ </section>
571
+ </section>
572
+
573
+ <aside class="figment-panel figment-sticky-panel figment-review-panel">
574
+ <div class="figment-panel-heading figment-panel-heading-action figment-review-heading">
575
+ <div>
576
+ <span class="figment-kicker">Review</span>
577
+ <h2>Confirmed intake</h2>
578
+ <p>Protocol rules and navigation use this payload.</p>
579
+ </div>
580
+ <button class="figment-button figment-button-primary" id="confirm-intake" type="button">Confirm intake</button>
581
+ </div>
582
+ <pre class="figment-json" id="intake-json">{}</pre>
583
+ <div class="figment-section-divider"></div>
584
+ <div class="figment-panel-heading">
585
+ <div>
586
+ <span class="figment-kicker">Draft</span>
587
+ <h2>Audio suggestions</h2>
588
+ <p>Timecoded field suggestions before apply.</p>
589
+ </div>
590
+ </div>
591
+ <pre class="figment-json" id="audio-json">{}</pre>
592
+ </aside>
593
+ </div>
594
+ </section>
595
+
596
+ <section class="figment-view" id="view-risk" data-view-panel="risk" hidden>
597
+ <div class="figment-workspace">
598
+ <section class="figment-panel">
599
+ <div class="figment-panel-heading">
600
+ <div>
601
+ <h2>Deterministic red-flag checklist</h2>
602
+ <p>Reference checklist for the frozen safety floor. These rules are deterministic.</p>
603
+ </div>
604
+ </div>
605
+ __RED_FLAG_CHECKLIST__
606
+ </section>
607
+ <section class="figment-panel">
608
+ <div class="figment-panel-heading figment-panel-heading-action">
609
+ <div>
610
+ <h2>Rule output</h2>
611
+ <p>The model cannot lower the deterministic protocol_urgency floor.</p>
612
+ </div>
613
+ <button class="figment-button figment-button-primary" id="run-risk" type="button">Run risk check</button>
614
+ </div>
615
+ <div id="risk-html"></div>
616
+ <details class="figment-disclosure">
617
+ <summary>Raw deterministic red flags JSON</summary>
618
+ <pre class="figment-json" id="risk-json">{}</pre>
619
+ </details>
620
+ </section>
621
+ </div>
622
+ </section>
623
+
624
+ <section class="figment-view" id="view-protocol" data-view-panel="protocol" hidden>
625
+ <div class="figment-workspace">
626
+ <section class="figment-panel">
627
+ <div class="figment-panel-heading figment-panel-heading-action">
628
+ <div>
629
+ <h2>Protocol card browser</h2>
630
+ <p>Local protocol cards retrieved from the confirmed intake.</p>
631
+ </div>
632
+ <button class="figment-button figment-button-primary" id="retrieve-cards" type="button">Retrieve protocol cards</button>
633
+ </div>
634
+ <div id="protocol-library"></div>
635
+ </section>
636
+ <section class="figment-panel">
637
+ <div id="guidance-html"></div>
638
+ <div class="figment-section-divider"></div>
639
+ <label class="figment-control" for="guidance-evidence">
640
+ <span>Protocol evidence panel</span>
641
+ <textarea id="guidance-evidence" name="guidance_evidence" rows="8" readonly></textarea>
642
+ </label>
643
+ <details class="figment-disclosure">
644
+ <summary>Retrieved protocol cards JSON</summary>
645
+ <pre class="figment-json" id="guidance-json">[]</pre>
646
+ </details>
647
+ </section>
648
+ </div>
649
+ </section>
650
+
651
+ <section class="figment-view" id="view-navigator" data-view-panel="navigator" hidden>
652
+ <div class="figment-workspace">
653
+ <section class="figment-panel">
654
+ <div class="figment-panel-heading figment-panel-heading-action">
655
+ <div>
656
+ <h2>Navigator output JSON</h2>
657
+ <p>Machine-readable protocol navigation output.</p>
658
+ </div>
659
+ <button class="figment-button figment-button-primary" id="run-navigator" type="button">Run navigator</button>
660
+ </div>
661
+ <pre class="figment-json figment-json-tall" id="output-json">{}</pre>
662
+ </section>
663
+ <section class="figment-panel">
664
+ <div id="navigator-html"></div>
665
+ <label class="figment-control" for="sbar-text">
666
+ <span>SBAR handoff</span>
667
+ <textarea id="sbar-text" name="sbar_text" rows="8" readonly></textarea>
668
+ </label>
669
+ </section>
670
+ </div>
671
+ </section>
672
+
673
+ <section class="figment-view" id="view-trace" data-view-panel="trace" hidden>
674
+ <div class="figment-workspace">
675
+ <section class="figment-panel">
676
+ <div class="figment-panel-heading figment-panel-heading-action">
677
+ <div>
678
+ <h2>Run steps timeline</h2>
679
+ <p>Audit trail from intake through validation.</p>
680
+ </div>
681
+ <button class="figment-button figment-button-secondary" id="export-trace" type="button">Export trace</button>
682
+ </div>
683
+ <div id="trace-audit-html"></div>
684
+ </section>
685
+ <section class="figment-panel">
686
+ <div class="figment-panel-heading">
687
+ <div>
688
+ <h2>Trace JSON</h2>
689
+ <p>Raw audit object for review and export.</p>
690
+ </div>
691
+ </div>
692
+ <pre class="figment-json figment-json-tall" id="trace-json">{}</pre>
693
+ </section>
694
+ </div>
695
+ </section>
696
+
697
+ </section>
698
+ </main>
699
+
700
+ <script id="figment-data" type="application/json">__FIGMENT_DATA__</script>
701
+ <script type="module">
702
+ import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
703
+
704
+ const initial = JSON.parse(document.getElementById("figment-data").textContent);
705
+ const fieldKeys = initial.fieldKeys;
706
+ const clientPromise = Client.connect(window.location.origin);
707
+ const state = {
708
+ fields: emptyFields(),
709
+ intake: {},
710
+ audioDraft: null,
711
+ risk: initial.emptyRisk,
712
+ cards: [],
713
+ evidence: "",
714
+ navigatorOutput: {},
715
+ sbar: "",
716
+ trace: {},
717
+ riskHtml: initial.riskHtml,
718
+ guidanceHtml: initial.guidanceHtml,
719
+ navigatorHtml: initial.navigatorHtml,
720
+ traceAuditHtml: initial.traceAuditHtml,
721
+ };
722
+
723
+ const $ = (selector) => document.querySelector(selector);
724
+ const $$ = (selector) => Array.from(document.querySelectorAll(selector));
725
+
726
+ function emptyFields() {
727
+ return Object.fromEntries(initial.fieldKeys.map((key) => [key, ""]));
728
+ }
729
+
730
+ async function predict(name, args = []) {
731
+ const client = await clientPromise;
732
+ const result = await client.predict(`/${name}`, args);
733
+ return result.data[0];
734
+ }
735
+
736
+ function setLiveStatus(message) {
737
+ $("#live-status").textContent = message;
738
+ }
739
+
740
+ function setBusy(button, busy) {
741
+ if (!button) return;
742
+ button.disabled = busy;
743
+ button.classList.toggle("figment-button-loading", busy);
744
+ }
745
+
746
+ async function runAction(button, pendingMessage, doneMessage, action) {
747
+ try {
748
+ setBusy(button, true);
749
+ setLiveStatus(pendingMessage);
750
+ await action();
751
+ setLiveStatus(doneMessage);
752
+ } catch (error) {
753
+ console.error(error);
754
+ setLiveStatus(error?.message || "Action failed. Check the browser console.");
755
+ } finally {
756
+ setBusy(button, false);
757
+ }
758
+ }
759
+
760
+ function readFields() {
761
+ const fields = {};
762
+ for (const key of fieldKeys) {
763
+ fields[key] = $(`#field-${key}`).value;
764
+ }
765
+ return fields;
766
+ }
767
+
768
+ function writeFields(fields) {
769
+ for (const key of fieldKeys) {
770
+ const input = $(`#field-${key}`);
771
+ if (input) input.value = fields?.[key] || "";
772
+ }
773
+ state.fields = { ...emptyFields(), ...(fields || {}) };
774
+ }
775
+
776
+ function resetDownstream() {
777
+ state.intake = {};
778
+ state.risk = initial.emptyRisk;
779
+ state.cards = [];
780
+ state.evidence = "";
781
+ state.navigatorOutput = {};
782
+ state.sbar = "";
783
+ state.trace = {};
784
+ state.riskHtml = initial.riskHtml;
785
+ state.guidanceHtml = initial.guidanceHtml;
786
+ state.navigatorHtml = initial.navigatorHtml;
787
+ state.traceAuditHtml = initial.traceAuditHtml;
788
+ }
789
+
790
+ function renderJson(selector, value) {
791
+ $(selector).textContent = JSON.stringify(value ?? {}, null, 2);
792
+ }
793
+
794
+ function render() {
795
+ renderJson("#audio-json", state.audioDraft || {});
796
+ renderJson("#intake-json", state.intake || {});
797
+ renderJson("#risk-json", state.risk || {});
798
+ renderJson("#guidance-json", state.cards || []);
799
+ renderJson("#output-json", state.navigatorOutput || {});
800
+ renderJson("#trace-json", state.trace || {});
801
+ $("#risk-html").innerHTML = state.riskHtml || initial.riskHtml;
802
+ $("#guidance-html").innerHTML = state.guidanceHtml || initial.guidanceHtml;
803
+ $("#guidance-evidence").value = state.evidence || "";
804
+ $("#navigator-html").innerHTML = state.navigatorHtml || initial.navigatorHtml;
805
+ $("#sbar-text").value = state.sbar || "";
806
+ $("#trace-audit-html").innerHTML = state.traceAuditHtml || initial.traceAuditHtml;
807
+ }
808
+
809
+ function setView(name) {
810
+ for (const panel of $$("[data-view-panel]")) {
811
+ panel.hidden = panel.dataset.viewPanel !== name;
812
+ }
813
+ for (const button of $$(".figment-tab-button")) {
814
+ const active = button.dataset.view === name;
815
+ button.classList.toggle("is-active", active);
816
+ button.setAttribute("aria-current", active ? "page" : "false");
817
+ }
818
+ }
819
+
820
+ async function ensureConfirmed() {
821
+ if (state.intake?.confirmed) return;
822
+ const payload = await predict("confirm_intake", [readFields(), state.audioDraft]);
823
+ state.intake = payload.intake || {};
824
+ state.audioDraft = payload.audio_draft || state.audioDraft;
825
+ render();
826
+ }
827
+
828
+ async function draftAudio() {
829
+ const file = $("#audio-file").files[0];
830
+ const transcript = $("#transcript").value;
831
+ const args = file ? [handle_file(file), transcript] : [null, transcript];
832
+ state.audioDraft = await predict("draft_audio", args);
833
+ resetDownstream();
834
+ render();
835
+ }
836
+
837
+ async function applyAudioDraft() {
838
+ const payload = await predict("apply_audio_draft", [readFields(), state.audioDraft]);
839
+ writeFields(payload.fields || {});
840
+ state.audioDraft = payload.audio_draft || null;
841
+ resetDownstream();
842
+ state.risk = payload.risk || initial.emptyRisk;
843
+ state.riskHtml = payload.risk_html || initial.riskHtml;
844
+ state.guidanceHtml = payload.guidance_html || initial.guidanceHtml;
845
+ state.navigatorHtml = payload.navigator_html || initial.navigatorHtml;
846
+ state.traceAuditHtml = payload.trace_audit_html || initial.traceAuditHtml;
847
+ render();
848
+ }
849
+
850
+ async function confirmIntake() {
851
+ const payload = await predict("confirm_intake", [readFields(), state.audioDraft]);
852
+ state.intake = payload.intake || {};
853
+ state.audioDraft = payload.audio_draft || state.audioDraft;
854
+ render();
855
+ }
856
+
857
+ async function runRisk() {
858
+ await ensureConfirmed();
859
+ const payload = await predict("risk_check", [state.intake]);
860
+ state.risk = payload.risk || initial.emptyRisk;
861
+ state.riskHtml = payload.risk_html || initial.riskHtml;
862
+ render();
863
+ }
864
+
865
+ async function retrieveCards() {
866
+ await ensureConfirmed();
867
+ const payload = await predict("retrieve_protocol_cards", [state.intake]);
868
+ state.cards = payload.cards || [];
869
+ state.evidence = payload.evidence || "";
870
+ state.guidanceHtml = payload.guidance_html || initial.guidanceHtml;
871
+ render();
872
+ }
873
+
874
+ async function runNavigator() {
875
+ await ensureConfirmed();
876
+ const payload = await predict("run_navigator", [state.intake, state.audioDraft]);
877
+ state.navigatorOutput = payload.navigator_output || {};
878
+ state.sbar = payload.sbar || "";
879
+ state.trace = payload.trace || {};
880
+ state.navigatorHtml = payload.navigator_html || initial.navigatorHtml;
881
+ state.traceAuditHtml = payload.trace_audit_html || initial.traceAuditHtml;
882
+ render();
883
+ }
884
+
885
+ function exportTrace() {
886
+ if (!state.trace || Object.keys(state.trace).length === 0) {
887
+ setLiveStatus("Run the navigator before exporting a trace.");
888
+ return;
889
+ }
890
+ const blob = new Blob([JSON.stringify(state.trace, null, 2)], { type: "application/json" });
891
+ const url = URL.createObjectURL(blob);
892
+ const link = document.createElement("a");
893
+ link.href = url;
894
+ link.download = "figment-trace.json";
895
+ link.click();
896
+ URL.revokeObjectURL(url);
897
+ setLiveStatus("Trace export prepared in the browser.");
898
+ }
899
+
900
+ function initialize() {
901
+ const runtime = initial.runtime;
902
+ $("#runtime-mode").textContent = runtime.model_mode_label;
903
+ $("#runtime-stack").textContent = `MODEL_STACK=${runtime.model_stack}`;
904
+ $("#runtime-backend").textContent = `MODEL_BACKEND=${runtime.model_backend}`;
905
+ $("#runtime-audio").textContent = `ENABLE_AUDIO_INTAKE=${runtime.enable_audio_intake ? "ON" : "OFF"}`;
906
+ $("#runtime-audio").classList.add(runtime.enable_audio_intake ? "green" : "amber");
907
+ $("#audio-title").textContent = runtime.audio_section_title;
908
+ $("#audio-subtitle").textContent = runtime.audio_section_subtitle;
909
+ $("#audio-file-label").textContent = runtime.audio_clip_label;
910
+ $("#transcript-label").textContent = runtime.transcript_label;
911
+ $("#audio-runtime-chips").innerHTML = runtime.audio_chips_html;
912
+ $("#protocol-library").innerHTML = initial.protocolLibraryHtml;
913
+
914
+ for (const button of $$(".figment-tab-button")) {
915
+ button.addEventListener("click", () => setView(button.dataset.view));
916
+ }
917
+ for (const input of $$("#intake-form input, #intake-form textarea")) {
918
+ input.addEventListener("input", () => {
919
+ state.fields = readFields();
920
+ resetDownstream();
921
+ render();
922
+ });
923
+ }
924
+
925
+ $("#draft-audio").addEventListener("click", () => runAction($("#draft-audio"), "Drafting audio fields through Gradio Server.", "Audio draft updated.", draftAudio));
926
+ $("#apply-audio").addEventListener("click", () => runAction($("#apply-audio"), "Applying audio draft.", "Audio draft applied to editable intake.", applyAudioDraft));
927
+ $("#confirm-intake").addEventListener("click", () => runAction($("#confirm-intake"), "Confirming intake.", "Confirmed intake is ready for rules and navigation.", confirmIntake));
928
+ $("#run-risk").addEventListener("click", () => runAction($("#run-risk"), "Running deterministic red-flag checks.", "Risk check complete.", runRisk));
929
+ $("#retrieve-cards").addEventListener("click", () => runAction($("#retrieve-cards"), "Retrieving local protocol cards.", "Protocol cards retrieved.", retrieveCards));
930
+ $("#run-navigator").addEventListener("click", () => runAction($("#run-navigator"), "Running navigator through Gradio Server.", "Navigator output and trace complete.", runNavigator));
931
+ $("#export-trace").addEventListener("click", exportTrace);
932
+
933
+ const audioEnabled = Boolean(runtime.enable_audio_intake);
934
+ $("#audio-file").disabled = !audioEnabled;
935
+ $("#transcript").disabled = !audioEnabled;
936
+ $("#draft-audio").disabled = !audioEnabled;
937
+ $("#apply-audio").disabled = !audioEnabled;
938
+
939
+ setView("intake");
940
+ render();
941
+ }
942
+
943
+ initialize();
944
+ </script>
945
+ </body>
946
+ </html>
947
+ """
948
+ return (
949
+ html_doc.replace("__FIGMENT_CSS__", FIGMENT_CSS)
950
+ .replace("__FIGMENT_DATA__", _json_for_script(initial_data))
951
+ .replace("__RED_FLAG_CHECKLIST__", _red_flag_checklist_html())
952
+ )
953
+
954
+
955
  def _model_mode_label(config: FigmentConfig) -> str:
956
  if config.model_backend == "hosted_omni":
957
  return "Configured backend: hosted_omni"
 
962
 
963
  def _audio_section_title(config: FigmentConfig) -> str:
964
  if not config.enable_audio_intake or config.audio_backend == "none":
965
+ return "Audio draft intake disabled"
966
  if config.audio_backend == "omni_native" and config.model_backend == "hosted_omni":
967
+ return "Hosted Omni audio draft"
968
  if config.audio_backend == "parakeet_nemo":
969
+ return "Local Parakeet ASR draft"
970
  if config.audio_backend == "canned":
971
+ return "Canned audio demo draft"
972
+ return "Audio draft intake"
973
 
974
 
975
  def _audio_section_subtitle(config: FigmentConfig) -> str:
 
1024
  return f'<div class="figment-section-title">{_h(title)}</div>{subtitle_html}'
1025
 
1026
 
 
 
 
 
 
1027
  def _red_flag_checklist_html() -> str:
1028
  categories = {
1029
  "Airway / Breathing": [
 
1265
  urgency = "routine"
1266
  handoff = output.get("handoff_note_sbar") if isinstance(output.get("handoff_note_sbar"), dict) else {}
1267
  runtime_card = _runtime_contribution_card_html(trace)
1268
+ evidence_card = _harness_evidence_card_html(output, trace)
1269
  return f"""
1270
  <div class="figment-urgency-banner">
1271
  <div>
 
1280
  <div style="height:12px"></div>
1281
  {runtime_card}
1282
  <div style="height:12px"></div>
1283
+ {evidence_card}
1284
+ <div style="height:12px"></div>
1285
  <div class="figment-card-grid">
1286
  {_navigator_list_card_html("Missing Observations", output.get("missing_info_to_collect"))}
1287
  {_navigator_list_card_html("Responder Checklist", output.get("responder_checklist"), checked=True)}
 
1331
  """
1332
 
1333
 
1334
+ def _harness_evidence_card_html(output: dict[str, Any] | None, trace: dict[str, Any] | None = None) -> str:
1335
+ evidence = _harness_evidence_from(output, trace)
1336
+ if not evidence:
1337
+ return ""
1338
+ retrieved_count = len(_as_list(evidence.get("retrieved_card_ids")))
1339
+ rule_count = len(_as_list(evidence.get("deterministic_rule_ids")))
1340
+ source_count = len(_as_list(evidence.get("source_card_ids")))
1341
+ final_route = str(evidence.get("final_route") or "unknown")
1342
+ return f"""
1343
+ <div class="figment-mini-card">
1344
+ <h4>Harness Evidence</h4>
1345
+ <span class="figment-chip green">Intake confirmed: {_h(evidence.get('confirmed_intake'))}</span>
1346
+ <span class="figment-chip green">Validation: {_h(evidence.get('validator_status'))}</span>
1347
+ <span class="figment-chip">Retrieved cards: {_h(retrieved_count)}</span>
1348
+ <span class="figment-chip">Rule results: {_h(rule_count)}</span>
1349
+ <span class="figment-chip">Source cards: {_h(source_count)}</span>
1350
+ <span class="figment-chip">Urgency floor: {_h(evidence.get('urgency_floor'))}</span>
1351
+ <span class="figment-chip {_route_chip_class(final_route)}">Route: {_h(runtime_route_label(final_route))}</span>
1352
+ <span class="figment-chip">Audio correction: {_h(evidence.get('audio_correction_status'))}</span>
1353
+ </div>
1354
+ """
1355
+
1356
+
1357
+ def _harness_evidence_from(output: dict[str, Any] | None, trace: dict[str, Any] | None = None) -> dict[str, Any]:
1358
+ if isinstance(output, dict) and isinstance(output.get("harness_evidence"), dict):
1359
+ return output["harness_evidence"]
1360
+ if isinstance(trace, dict):
1361
+ normalized = normalize_trace_payload(trace)
1362
+ if isinstance(normalized.get("harness_evidence"), dict):
1363
+ return normalized["harness_evidence"]
1364
+ navigator_output = normalized.get("navigator_output")
1365
+ if isinstance(navigator_output, dict) and isinstance(navigator_output.get("harness_evidence"), dict):
1366
+ return navigator_output["harness_evidence"]
1367
+ return {}
1368
+
1369
+
1370
  def _navigator_list_card_html(title: str, values: Any, *, checked: bool = False) -> str:
1371
  items = _as_list(values) or ["No items generated yet."]
1372
  cls = "figment-checklist checked" if checked else "figment-checklist"
 
1435
  route = trace.get("model_route") if isinstance(trace.get("model_route"), dict) else {}
1436
  retrieval = trace.get("retrieval") if isinstance(trace.get("retrieval"), dict) else {}
1437
  provenance_summary = trace.get("field_provenance_summary") if isinstance(trace.get("field_provenance_summary"), dict) else {}
1438
+ evidence = _harness_evidence_from(None, trace)
1439
  final_route = str(route.get("final_route") or "unknown")
1440
  return f"""
1441
  <table class="figment-table">
 
1450
  <span class="figment-chip green">Schema valid: {_h(validator.get('passed'))}</span>
1451
  <span class="figment-chip {_route_chip_class(final_route)}">{_h(runtime_route_label(route))}</span>
1452
  <span class="figment-chip">Retrieval: {_h(retrieval.get('primary_source') or 'not traced')}</span>
1453
+ <span class="figment-chip">Harness evidence: {_h('visible' if evidence else 'not traced')}</span>
1454
  </div>
1455
+ {_harness_evidence_card_html(None, trace)}
1456
  <div class="figment-mini-card">
1457
  <h4>Model &amp; Performance</h4>
1458
  <table class="figment-table">
 
1779
 
1780
 
1781
  if __name__ == "__main__":
1782
+ build_app().launch()
docs/assets/mockups/figment-intake-tab.png DELETED

Git LFS Details

  • SHA256: b2c33264d41f9f7c46dccbb2729f9a32b164943141f2910d2d94f6765d8682a5
  • Pointer size: 132 Bytes
  • Size of remote file: 1.42 MB
docs/assets/mockups/figment-navigator-output-handoff-tab.png DELETED

Git LFS Details

  • SHA256: 09e2093fa5e82657656e396d484a0fa5c47d3d37118b14b68ecc95f1ca1b1019
  • Pointer size: 132 Bytes
  • Size of remote file: 1.46 MB
docs/assets/mockups/figment-protocol-guidance-tab.png DELETED

Git LFS Details

  • SHA256: a1543b07d38673d1ed568da0a4005ac0c5c0c656ffdaaf6fa6c7a01b8bd51403
  • Pointer size: 132 Bytes
  • Size of remote file: 1.38 MB
docs/assets/mockups/figment-risk-check-tab.png DELETED

Git LFS Details

  • SHA256: 64d610f3d49f00511346a3b0c65611ed0604b7bfb939043a3d6b4e35f09a4fec
  • Pointer size: 132 Bytes
  • Size of remote file: 1.55 MB
docs/assets/mockups/figment-trace-tab.png DELETED

Git LFS Details

  • SHA256: aefbcf4e43762eea1ca4a8260a75815f95b8434dec1e088f6454412510abd4be
  • Pointer size: 132 Bytes
  • Size of remote file: 1.68 MB
figment/ui_theme.py CHANGED
@@ -1,216 +1,482 @@
1
- """Presentation tokens for the Figment Gradio interface."""
2
 
3
  FIGMENT_CSS = """
 
 
4
  :root {
5
- --figment-ink: #0f172a;
6
- --figment-muted: #526174;
7
- --figment-line: #d8e0ea;
 
 
 
8
  --figment-panel: #ffffff;
9
- --figment-panel-soft: #f8fafc;
10
- --figment-blue: #0b57d0;
11
- --figment-blue-soft: #eaf2ff;
12
- --figment-green: #17803d;
13
- --figment-green-soft: #eaf8ef;
14
- --figment-red: #c91515;
15
- --figment-red-soft: #fff0f0;
16
- --figment-amber: #b15b00;
17
- --figment-amber-soft: #fff7e8;
18
- }
19
-
20
- .gradio-container {
21
- max-width: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  color: var(--figment-ink);
23
- background: #f5f7fb !important;
24
- padding: 0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
 
27
- .figment-shell {
 
 
 
 
 
28
  min-height: 100vh;
 
 
 
 
 
 
 
29
  }
30
 
31
  .figment-topbar {
32
  display: flex;
33
  align-items: center;
34
  justify-content: space-between;
35
- gap: 24px;
36
- padding: 16px 22px;
37
- color: #fff;
38
- background: linear-gradient(90deg, #061425 0%, #0b1d32 56%, #07111f 100%);
39
- border-radius: 0;
40
- }
41
-
42
- .figment-topbar,
43
- .figment-topbar * {
44
- color: #fff !important;
 
45
  }
46
 
47
  .figment-brand {
48
  display: flex;
49
  align-items: baseline;
50
- gap: 18px;
51
  min-width: 0;
52
  }
53
 
54
  .figment-logo {
55
- font-size: 30px;
56
- line-height: 1;
 
57
  font-weight: 760;
 
 
 
58
  }
59
 
60
  .figment-positioning {
 
 
61
  font-size: 15px;
62
- opacity: 0.92;
63
- white-space: nowrap;
64
  }
65
 
66
  .figment-safety {
67
  display: flex;
68
  align-items: center;
69
  gap: 9px;
 
 
 
 
70
  font-size: 14px;
71
- opacity: 0.95;
72
- white-space: nowrap;
73
  }
74
 
75
  .figment-safety-mark {
76
  display: grid;
77
  place-items: center;
 
78
  width: 22px;
79
  height: 22px;
80
  border: 1px solid rgba(255, 255, 255, 0.7);
81
  border-radius: 999px;
 
82
  font-weight: 800;
83
  }
84
 
 
 
 
 
 
 
 
 
 
 
85
  .figment-statusline {
86
  display: flex;
87
  flex-wrap: wrap;
88
  align-items: center;
89
- gap: 10px 14px;
90
- padding: 12px 16px;
91
- margin: 14px 18px 0;
92
  border: 1px solid var(--figment-line);
93
- border-radius: 8px;
94
- background: #fff;
95
  }
96
 
97
- .figment-statusline strong {
98
- font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
  .figment-chip {
102
  display: inline-flex;
103
  align-items: center;
 
104
  min-height: 26px;
105
- padding: 4px 10px;
106
  border: 1px solid var(--figment-line);
107
- border-radius: 6px;
108
- background: #fff;
109
  color: var(--figment-ink);
110
  font-size: 13px;
111
  font-weight: 650;
 
 
112
  }
113
 
114
  .figment-chip.blue {
115
- border-color: #93b7ff;
116
  background: var(--figment-blue-soft);
117
- color: #0749b2;
118
  }
119
 
120
  .figment-chip.green {
121
- border-color: #9ed7ad;
122
  background: var(--figment-green-soft);
123
- color: #106a30;
124
  }
125
 
126
  .figment-chip.red {
127
- border-color: #ffabab;
128
  background: var(--figment-red-soft);
129
  color: var(--figment-red);
130
  }
131
 
132
  .figment-chip.amber {
133
- border-color: #ffd58d;
134
  background: var(--figment-amber-soft);
135
  color: var(--figment-amber);
136
  }
137
 
138
  .figment-tabs {
139
- margin: 14px 18px 0;
 
 
 
 
 
 
 
140
  }
141
 
142
- .figment-tabs > .tab-nav,
143
- .figment-tabs [role="tablist"] {
144
- background: #fff;
145
- border: 1px solid var(--figment-line);
146
- border-radius: 8px 8px 0 0;
 
 
 
 
 
 
147
  }
148
 
149
- .figment-tabs button[role="tab"] {
150
- min-height: 54px;
151
- font-weight: 680;
152
  }
153
 
154
- .figment-tabs button[aria-selected="true"] {
155
- color: var(--figment-blue) !important;
156
- border-bottom: 3px solid var(--figment-blue) !important;
 
 
 
 
 
157
  }
158
 
159
- .figment-tab-body {
160
- padding: 14px 0 0;
 
 
 
 
161
  }
162
 
163
- .figment-panel {
164
- border: 1px solid var(--figment-line) !important;
165
- border-radius: 8px !important;
166
- background: var(--figment-panel) !important;
167
- box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
168
  }
169
 
170
- .figment-panel-soft {
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  border: 1px solid var(--figment-line);
172
- border-radius: 8px;
173
- background: var(--figment-panel-soft);
174
- padding: 12px;
 
175
  }
176
 
177
- .figment-section-title {
 
 
 
 
 
178
  display: flex;
179
- align-items: center;
180
  justify-content: space-between;
181
  gap: 12px;
182
- margin: 0 0 10px;
 
 
 
 
 
183
  color: var(--figment-ink);
184
  font-size: 16px;
185
- font-weight: 760;
 
 
186
  }
187
 
 
 
 
 
 
 
 
 
 
 
188
  .figment-section-subtitle {
189
- margin: -4px 0 12px;
 
190
  color: var(--figment-muted);
191
- font-size: 13px;
 
192
  }
193
 
194
- .figment-quick-grid {
 
 
 
 
 
 
 
 
 
 
 
195
  display: grid;
196
- grid-template-columns: repeat(4, minmax(0, 1fr));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  gap: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
198
  }
199
 
200
- .figment-demo-pill {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  padding: 8px 10px;
202
- border: 1px solid var(--figment-line);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  border-radius: 6px;
204
- background: #fff;
205
- font-size: 12px;
206
- font-weight: 650;
207
- text-align: center;
208
  }
209
 
210
- .figment-output-grid {
211
- display: grid;
212
- grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
213
- gap: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  }
215
 
216
  .figment-card-grid {
@@ -220,16 +486,28 @@ FIGMENT_CSS = """
220
  }
221
 
222
  .figment-mini-card {
223
- min-height: 120px;
 
224
  border: 1px solid var(--figment-line);
225
- border-radius: 8px;
226
- background: #fff;
227
  padding: 12px;
 
228
  }
229
 
230
  .figment-mini-card h4 {
231
  margin: 0 0 8px;
 
 
 
 
 
 
 
 
 
232
  font-size: 14px;
 
233
  }
234
 
235
  .figment-mini-card ul {
@@ -237,29 +515,65 @@ FIGMENT_CSS = """
237
  padding-left: 18px;
238
  }
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  .figment-table {
241
  width: 100%;
 
242
  border-collapse: collapse;
243
- overflow: hidden;
244
- border: 1px solid var(--figment-line);
245
- border-radius: 8px;
246
- background: #fff;
247
  font-size: 13px;
 
248
  }
249
 
250
  .figment-table th,
251
  .figment-table td {
252
- padding: 10px 12px;
253
  border-bottom: 1px solid var(--figment-line);
254
  text-align: left;
255
  vertical-align: top;
256
  }
257
 
258
  .figment-table th {
259
- background: #f7f9fc;
260
- color: #243044;
261
  font-size: 12px;
262
  font-weight: 760;
 
263
  }
264
 
265
  .figment-table tr:last-child td {
@@ -268,29 +582,29 @@ FIGMENT_CSS = """
268
 
269
  .figment-urgency-banner {
270
  display: grid;
271
- grid-template-columns: 1fr auto;
272
- gap: 14px;
273
  align-items: center;
274
- padding: 14px 16px;
275
  border: 1px solid var(--figment-line);
276
- border-radius: 8px;
277
- background: #fff;
278
  }
279
 
280
  .figment-urgency-word {
281
  display: inline-flex;
282
  align-items: center;
283
- min-height: 44px;
284
- padding: 0 18px;
285
  border-radius: 6px;
286
- font-size: 26px;
287
- font-weight: 850;
288
  letter-spacing: 0;
289
  }
290
 
291
  .figment-urgency-word.routine {
292
- background: #eef6ff;
293
- color: #0b57d0;
294
  }
295
 
296
  .figment-urgency-word.monitor {
@@ -304,144 +618,540 @@ FIGMENT_CSS = """
304
  }
305
 
306
  .figment-urgency-word.emergency {
307
- background: #d41111;
308
- color: #fff;
309
  }
310
 
311
  .figment-lockout {
312
- border: 1px solid #ef7b7b;
313
- border-radius: 8px;
314
- padding: 12px;
315
  background: var(--figment-red-soft);
316
- color: #7c1010;
317
  font-size: 13px;
 
318
  }
319
 
320
- .figment-checklist {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  display: grid;
322
- grid-template-columns: repeat(2, minmax(0, 1fr));
323
- gap: 8px 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  margin: 0;
325
- padding: 0;
326
- list-style: none;
 
 
 
 
 
 
327
  }
328
 
329
- .figment-checklist li {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  position: relative;
331
- padding-left: 22px;
332
- color: #172033;
333
- font-size: 13px;
 
334
  }
335
 
336
- .figment-checklist li::before {
337
  content: "";
338
  position: absolute;
 
 
339
  left: 0;
340
- top: 2px;
341
- width: 13px;
342
- height: 13px;
343
- border: 1px solid #9aa9bb;
344
- border-radius: 3px;
345
- background: #fff;
346
  }
347
 
348
- .figment-checklist.checked li::before {
349
- border-color: var(--figment-blue);
350
- background: var(--figment-blue);
351
- box-shadow: inset 0 0 0 3px #fff;
352
  }
353
 
354
- .figment-code-panel textarea,
355
- .figment-code-panel .json-viewer,
356
- .figment-code-panel pre {
357
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
358
  }
359
 
360
- .figment-action-row {
361
- display: grid;
362
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
363
- gap: 10px;
364
  }
365
 
366
- .figment-footer-rail {
367
- display: flex;
368
- flex-wrap: wrap;
369
- align-items: center;
370
- justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  gap: 12px;
372
- margin: 14px 18px 18px;
373
- padding: 12px 16px;
374
- border: 1px solid var(--figment-line);
375
- border-radius: 8px;
376
- background: #fff;
377
  }
378
 
379
- .figment-footer-cluster {
380
- display: flex;
381
- align-items: center;
382
- flex-wrap: wrap;
383
- gap: 10px;
384
  }
385
 
386
- .figment-json-compact {
387
- min-height: 220px;
 
388
  }
389
 
390
- .figment-json-tall {
391
- min-height: 520px;
392
  }
393
 
394
- .figment-muted {
395
- color: var(--figment-muted);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  }
397
 
398
- .figment-hide-label > label {
399
- display: none !important;
 
 
400
  }
401
 
402
- button.primary,
403
- .primary button {
404
- background: var(--figment-blue) !important;
405
- border-color: var(--figment-blue) !important;
406
- color: #fff !important;
407
- min-height: 42px !important;
408
- height: 42px !important;
409
- flex: 0 0 auto !important;
410
  }
411
 
412
- .figment-panel button {
413
- min-height: 38px !important;
414
- height: auto !important;
415
- flex: 0 0 auto !important;
416
  }
417
 
418
- .figment-panel textarea {
419
- min-height: 38px !important;
 
 
420
  }
421
 
422
- .figment-panel .form {
423
- gap: 8px !important;
 
424
  }
425
 
426
- @media (max-width: 1000px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  .figment-topbar,
428
- .figment-output-grid,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  .figment-card-grid,
430
- .figment-action-row {
 
431
  grid-template-columns: 1fr;
432
  }
433
 
434
- .figment-topbar {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  display: grid;
 
 
 
436
  }
437
 
438
- .figment-positioning,
439
- .figment-safety {
 
440
  white-space: normal;
441
  }
442
 
443
- .figment-quick-grid {
444
- grid-template-columns: repeat(2, minmax(0, 1fr));
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  }
446
  }
447
  """
 
1
+ """Presentation tokens for the Figment Gradio Server interface."""
2
 
3
  FIGMENT_CSS = """
4
+ @import url("https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..850&display=swap");
5
+
6
  :root {
7
+ --figment-ink: #18181b;
8
+ --figment-muted: #60606a;
9
+ --figment-subtle: #8a8a95;
10
+ --figment-line: rgba(24, 24, 27, 0.12);
11
+ --figment-line-strong: rgba(24, 24, 27, 0.18);
12
+ --figment-canvas: #f7f7f4;
13
  --figment-panel: #ffffff;
14
+ --figment-panel-soft: #f2f6f5;
15
+ --figment-control: #fbfbfa;
16
+ --figment-blue: #155eef;
17
+ --figment-blue-soft: #e8f0ff;
18
+ --figment-green: #087443;
19
+ --figment-green-soft: #e5f6ed;
20
+ --figment-red: #c81e1e;
21
+ --figment-red-soft: #fff0ee;
22
+ --figment-amber: #b45f06;
23
+ --figment-amber-soft: #fff5df;
24
+ --figment-radius: 8px;
25
+ --figment-shadow: 0 18px 50px rgba(24, 24, 27, 0.08);
26
+ color-scheme: light;
27
+ font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
28
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11", "ss01", "ss03";
29
+ }
30
+
31
+ * {
32
+ box-sizing: border-box;
33
+ }
34
+
35
+ html {
36
+ background: var(--figment-canvas);
37
+ }
38
+
39
+ body {
40
+ margin: 0;
41
+ min-width: 320px;
42
  color: var(--figment-ink);
43
+ background:
44
+ linear-gradient(180deg, rgba(242, 246, 245, 0.85), rgba(247, 247, 244, 0.98) 420px),
45
+ var(--figment-canvas);
46
+ -webkit-font-smoothing: antialiased;
47
+ text-rendering: optimizeLegibility;
48
+ }
49
+
50
+ button,
51
+ input,
52
+ select,
53
+ textarea {
54
+ font: inherit;
55
+ }
56
+
57
+ button {
58
+ cursor: pointer;
59
+ }
60
+
61
+ button:disabled,
62
+ input:disabled,
63
+ textarea:disabled {
64
+ cursor: not-allowed;
65
+ opacity: 0.58;
66
  }
67
 
68
+ a {
69
+ color: inherit;
70
+ }
71
+
72
+ .figment-app-shell {
73
+ isolate: isolate;
74
  min-height: 100vh;
75
+ padding: 16px;
76
+ background: var(--figment-canvas);
77
+ }
78
+
79
+ .figment-mission-rail,
80
+ .figment-operation-board {
81
+ display: contents;
82
  }
83
 
84
  .figment-topbar {
85
  display: flex;
86
  align-items: center;
87
  justify-content: space-between;
88
+ gap: 20px;
89
+ max-width: 1480px;
90
+ margin: 0 auto;
91
+ padding: 18px 20px;
92
+ border: 1px solid rgba(255, 255, 255, 0.14);
93
+ border-radius: var(--figment-radius);
94
+ background:
95
+ linear-gradient(135deg, rgba(8, 116, 67, 0.24), transparent 38%),
96
+ linear-gradient(90deg, #18181b, #202022);
97
+ box-shadow: var(--figment-shadow);
98
+ color: #ffffff;
99
  }
100
 
101
  .figment-brand {
102
  display: flex;
103
  align-items: baseline;
104
+ gap: 14px;
105
  min-width: 0;
106
  }
107
 
108
  .figment-logo {
109
+ flex: 0 0 auto;
110
+ color: #ffffff;
111
+ font-size: 28px;
112
  font-weight: 760;
113
+ letter-spacing: 0;
114
+ line-height: 1;
115
+ text-decoration: none;
116
  }
117
 
118
  .figment-positioning {
119
+ min-width: 0;
120
+ color: rgba(255, 255, 255, 0.82);
121
  font-size: 15px;
122
+ line-height: 1.4;
 
123
  }
124
 
125
  .figment-safety {
126
  display: flex;
127
  align-items: center;
128
  gap: 9px;
129
+ flex: 0 1 auto;
130
+ min-width: 260px;
131
+ justify-content: flex-end;
132
+ color: rgba(255, 255, 255, 0.9);
133
  font-size: 14px;
134
+ line-height: 1.4;
 
135
  }
136
 
137
  .figment-safety-mark {
138
  display: grid;
139
  place-items: center;
140
+ flex: 0 0 auto;
141
  width: 22px;
142
  height: 22px;
143
  border: 1px solid rgba(255, 255, 255, 0.7);
144
  border-radius: 999px;
145
+ font-size: 14px;
146
  font-weight: 800;
147
  }
148
 
149
+ .figment-statusline,
150
+ .figment-tabs,
151
+ .figment-live-status,
152
+ .figment-view,
153
+ .figment-footer-rail {
154
+ max-width: 1480px;
155
+ margin-right: auto;
156
+ margin-left: auto;
157
+ }
158
+
159
  .figment-statusline {
160
  display: flex;
161
  flex-wrap: wrap;
162
  align-items: center;
163
+ gap: 9px;
164
+ margin-top: 12px;
165
+ padding: 11px 12px;
166
  border: 1px solid var(--figment-line);
167
+ border-radius: var(--figment-radius);
168
+ background: rgba(255, 255, 255, 0.84);
169
  }
170
 
171
+ .figment-statusline strong,
172
+ .figment-footer-cluster strong {
173
+ color: var(--figment-muted);
174
+ font-size: 13px;
175
+ font-weight: 720;
176
+ }
177
+
178
+ .figment-chip-row,
179
+ .figment-footer-cluster {
180
+ display: flex;
181
+ flex-wrap: wrap;
182
+ align-items: center;
183
+ gap: 8px;
184
  }
185
 
186
  .figment-chip {
187
  display: inline-flex;
188
  align-items: center;
189
+ max-width: 100%;
190
  min-height: 26px;
191
+ padding: 4px 9px;
192
  border: 1px solid var(--figment-line);
193
+ border-radius: 999px;
194
+ background: #ffffff;
195
  color: var(--figment-ink);
196
  font-size: 13px;
197
  font-weight: 650;
198
+ line-height: 1.25;
199
+ white-space: nowrap;
200
  }
201
 
202
  .figment-chip.blue {
203
+ border-color: rgba(21, 94, 239, 0.22);
204
  background: var(--figment-blue-soft);
205
+ color: #1149b8;
206
  }
207
 
208
  .figment-chip.green {
209
+ border-color: rgba(8, 116, 67, 0.2);
210
  background: var(--figment-green-soft);
211
+ color: var(--figment-green);
212
  }
213
 
214
  .figment-chip.red {
215
+ border-color: rgba(200, 30, 30, 0.2);
216
  background: var(--figment-red-soft);
217
  color: var(--figment-red);
218
  }
219
 
220
  .figment-chip.amber {
221
+ border-color: rgba(180, 95, 6, 0.24);
222
  background: var(--figment-amber-soft);
223
  color: var(--figment-amber);
224
  }
225
 
226
  .figment-tabs {
227
+ display: flex;
228
+ gap: 6px;
229
+ margin-top: 12px;
230
+ padding: 6px;
231
+ border: 1px solid var(--figment-line);
232
+ border-radius: var(--figment-radius);
233
+ background: rgba(255, 255, 255, 0.82);
234
+ overflow-x: auto;
235
  }
236
 
237
+ .figment-tab-button {
238
+ flex: 0 0 auto;
239
+ min-height: 38px;
240
+ padding: 8px 12px;
241
+ border: 0;
242
+ border-radius: 6px;
243
+ background: transparent;
244
+ color: var(--figment-muted);
245
+ font-size: 14px;
246
+ font-weight: 650;
247
+ white-space: nowrap;
248
  }
249
 
250
+ .figment-tab-button.is-active {
251
+ background: #f0f3f1;
252
+ color: var(--figment-ink);
253
  }
254
 
255
+ .figment-tab-button:focus-visible,
256
+ .figment-button:focus-visible,
257
+ input:focus-visible,
258
+ select:focus-visible,
259
+ textarea:focus-visible,
260
+ summary:focus-visible {
261
+ outline: 2px solid var(--figment-blue);
262
+ outline-offset: 2px;
263
  }
264
 
265
+ .figment-live-status {
266
+ margin-top: 10px;
267
+ margin-bottom: 0;
268
+ color: var(--figment-muted);
269
+ font-size: 14px;
270
+ line-height: 1.5;
271
  }
272
 
273
+ .figment-view {
274
+ margin-top: 12px;
 
 
 
275
  }
276
 
277
+ .figment-workspace {
278
+ display: grid;
279
+ grid-template-columns: minmax(0, 9fr) minmax(360px, 7fr);
280
+ gap: 12px;
281
+ align-items: start;
282
+ }
283
+
284
+ .figment-workspace-intake {
285
+ grid-template-columns: minmax(0, 10fr) minmax(340px, 6fr);
286
+ }
287
+
288
+ .figment-panel {
289
+ min-width: 0;
290
+ padding: 16px;
291
  border: 1px solid var(--figment-line);
292
+ border-radius: var(--figment-radius);
293
+ background: var(--figment-panel);
294
+ box-shadow: 0 1px 0 rgba(24, 24, 27, 0.03);
295
+ overflow-x: auto;
296
  }
297
 
298
+ .figment-sticky-panel {
299
+ position: sticky;
300
+ top: 12px;
301
+ }
302
+
303
+ .figment-panel-heading {
304
  display: flex;
305
+ align-items: flex-start;
306
  justify-content: space-between;
307
  gap: 12px;
308
+ margin-bottom: 12px;
309
+ }
310
+
311
+ .figment-panel-heading h2,
312
+ .figment-section-title {
313
+ margin: 0;
314
  color: var(--figment-ink);
315
  font-size: 16px;
316
+ font-weight: 720;
317
+ letter-spacing: 0;
318
+ line-height: 1.35;
319
  }
320
 
321
+ .figment-kicker {
322
+ display: block;
323
+ margin-bottom: 6px;
324
+ color: var(--figment-green);
325
+ font-size: 13px;
326
+ font-weight: 720;
327
+ line-height: 1.25;
328
+ }
329
+
330
+ .figment-panel-heading p,
331
  .figment-section-subtitle {
332
+ margin: 4px 0 0;
333
+ max-width: 72ch;
334
  color: var(--figment-muted);
335
+ font-size: 14px;
336
+ line-height: 1.5;
337
  }
338
 
339
+ .figment-panel-heading-action {
340
+ align-items: center;
341
+ }
342
+
343
+ .figment-section-divider {
344
+ height: 1px;
345
+ margin: 16px 0;
346
+ background: var(--figment-line);
347
+ }
348
+
349
+ .figment-inline-controls,
350
+ .figment-audio-grid {
351
  display: grid;
352
+ grid-template-columns: minmax(0, 1fr) auto;
353
+ gap: 10px;
354
+ align-items: end;
355
+ }
356
+
357
+ .figment-audio-grid {
358
+ grid-template-columns: minmax(0, 3fr) minmax(260px, 2fr);
359
+ align-items: stretch;
360
+ }
361
+
362
+ .figment-audio-actions {
363
+ display: flex;
364
+ flex-direction: column;
365
+ align-items: stretch;
366
+ justify-content: end;
367
  gap: 8px;
368
+ min-width: 0;
369
+ }
370
+
371
+ .figment-audio-draft {
372
+ display: grid;
373
+ gap: 12px;
374
+ min-width: 0;
375
+ }
376
+
377
+ .figment-review-panel {
378
+ display: grid;
379
+ align-content: start;
380
  }
381
 
382
+ .figment-review-heading {
383
+ align-items: start;
384
+ }
385
+
386
+ .figment-field-grid {
387
+ display: grid;
388
+ grid-template-columns: repeat(3, minmax(0, 1fr));
389
+ gap: 10px;
390
+ }
391
+
392
+ .figment-control {
393
+ display: grid;
394
+ gap: 6px;
395
+ min-width: 0;
396
+ }
397
+
398
+ .figment-control-wide {
399
+ grid-column: 1 / -1;
400
+ }
401
+
402
+ .figment-control span {
403
+ color: var(--figment-muted);
404
+ font-size: 13px;
405
+ font-weight: 680;
406
+ line-height: 1.35;
407
+ }
408
+
409
+ .figment-control input,
410
+ .figment-control select,
411
+ .figment-control textarea {
412
+ width: 100%;
413
+ min-width: 0;
414
+ border: 1px solid var(--figment-line-strong);
415
+ border-radius: 6px;
416
+ background: var(--figment-control);
417
+ color: var(--figment-ink);
418
+ font-size: 15px;
419
+ line-height: 1.45;
420
+ }
421
+
422
+ .figment-control input,
423
+ .figment-control select {
424
+ min-height: 40px;
425
  padding: 8px 10px;
426
+ }
427
+
428
+ .figment-control textarea {
429
+ min-height: 88px;
430
+ padding: 9px 10px;
431
+ resize: vertical;
432
+ }
433
+
434
+ .figment-button {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ min-height: 38px;
439
+ padding: 8px 12px;
440
+ border: 1px solid transparent;
441
  border-radius: 6px;
442
+ font-size: 14px;
443
+ font-weight: 720;
444
+ line-height: 1.25;
445
+ white-space: nowrap;
446
  }
447
 
448
+ .figment-button-primary {
449
+ border-color: var(--figment-blue);
450
+ background: var(--figment-blue);
451
+ color: #ffffff;
452
+ }
453
+
454
+ .figment-button-primary:disabled {
455
+ border-color: var(--figment-line-strong);
456
+ background: #eef0f3;
457
+ color: var(--figment-muted);
458
+ }
459
+
460
+ .figment-button-secondary {
461
+ border-color: var(--figment-line-strong);
462
+ background: #ffffff;
463
+ color: var(--figment-ink);
464
+ }
465
+
466
+ .figment-button-loading {
467
+ color: rgba(255, 255, 255, 0.78);
468
+ }
469
+
470
+ .figment-full-button {
471
+ width: 100%;
472
+ margin-top: 12px;
473
+ }
474
+
475
+ .figment-panel-soft {
476
+ border: 1px solid var(--figment-line);
477
+ border-radius: var(--figment-radius);
478
+ background: var(--figment-panel-soft);
479
+ padding: 12px;
480
  }
481
 
482
  .figment-card-grid {
 
486
  }
487
 
488
  .figment-mini-card {
489
+ min-width: 0;
490
+ min-height: 112px;
491
  border: 1px solid var(--figment-line);
492
+ border-radius: var(--figment-radius);
493
+ background: #ffffff;
494
  padding: 12px;
495
+ overflow-x: auto;
496
  }
497
 
498
  .figment-mini-card h4 {
499
  margin: 0 0 8px;
500
+ color: var(--figment-ink);
501
+ font-size: 14px;
502
+ font-weight: 720;
503
+ line-height: 1.35;
504
+ }
505
+
506
+ .figment-mini-card p {
507
+ margin: 0;
508
+ color: var(--figment-muted);
509
  font-size: 14px;
510
+ line-height: 1.5;
511
  }
512
 
513
  .figment-mini-card ul {
 
515
  padding-left: 18px;
516
  }
517
 
518
+ .figment-checklist {
519
+ display: grid;
520
+ grid-template-columns: repeat(2, minmax(0, 1fr));
521
+ gap: 8px 16px;
522
+ margin: 0;
523
+ padding: 0;
524
+ list-style: none;
525
+ }
526
+
527
+ .figment-checklist li {
528
+ position: relative;
529
+ min-width: 0;
530
+ padding-left: 22px;
531
+ color: var(--figment-ink);
532
+ font-size: 14px;
533
+ line-height: 1.45;
534
+ }
535
+
536
+ .figment-checklist li::before {
537
+ content: "";
538
+ position: absolute;
539
+ left: 0;
540
+ top: 4px;
541
+ width: 13px;
542
+ height: 13px;
543
+ border: 1px solid rgba(24, 24, 27, 0.34);
544
+ border-radius: 3px;
545
+ background: #ffffff;
546
+ }
547
+
548
+ .figment-checklist.checked li::before {
549
+ border-color: var(--figment-blue);
550
+ background: var(--figment-blue);
551
+ box-shadow: inset 0 0 0 3px #ffffff;
552
+ }
553
+
554
  .figment-table {
555
  width: 100%;
556
+ min-width: 560px;
557
  border-collapse: collapse;
558
+ background: transparent;
559
+ color: var(--figment-ink);
 
 
560
  font-size: 13px;
561
+ line-height: 1.45;
562
  }
563
 
564
  .figment-table th,
565
  .figment-table td {
566
+ padding: 10px 8px;
567
  border-bottom: 1px solid var(--figment-line);
568
  text-align: left;
569
  vertical-align: top;
570
  }
571
 
572
  .figment-table th {
573
+ color: var(--figment-muted);
 
574
  font-size: 12px;
575
  font-weight: 760;
576
+ white-space: nowrap;
577
  }
578
 
579
  .figment-table tr:last-child td {
 
582
 
583
  .figment-urgency-banner {
584
  display: grid;
585
+ grid-template-columns: minmax(0, 1fr) minmax(240px, auto);
586
+ gap: 12px;
587
  align-items: center;
588
+ padding: 14px;
589
  border: 1px solid var(--figment-line);
590
+ border-radius: var(--figment-radius);
591
+ background: var(--figment-panel-soft);
592
  }
593
 
594
  .figment-urgency-word {
595
  display: inline-flex;
596
  align-items: center;
597
+ min-height: 42px;
598
+ padding: 0 16px;
599
  border-radius: 6px;
600
+ font-size: 24px;
601
+ font-weight: 820;
602
  letter-spacing: 0;
603
  }
604
 
605
  .figment-urgency-word.routine {
606
+ background: var(--figment-blue-soft);
607
+ color: #1149b8;
608
  }
609
 
610
  .figment-urgency-word.monitor {
 
618
  }
619
 
620
  .figment-urgency-word.emergency {
621
+ background: var(--figment-red);
622
+ color: #ffffff;
623
  }
624
 
625
  .figment-lockout {
626
+ border: 1px solid rgba(200, 30, 30, 0.22);
627
+ border-radius: 6px;
628
+ padding: 11px;
629
  background: var(--figment-red-soft);
630
+ color: #8f1616;
631
  font-size: 13px;
632
+ line-height: 1.45;
633
  }
634
 
635
+ .figment-json {
636
+ display: block;
637
+ min-height: 210px;
638
+ max-height: 420px;
639
+ margin: 0;
640
+ padding: 12px;
641
+ overflow: auto;
642
+ border: 1px solid var(--figment-line);
643
+ border-radius: 6px;
644
+ background: #111114;
645
+ color: #edfdf5;
646
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
647
+ font-size: 12px;
648
+ line-height: 1.55;
649
+ white-space: pre-wrap;
650
+ overflow-wrap: anywhere;
651
+ }
652
+
653
+ .figment-json-tall {
654
+ min-height: 520px;
655
+ max-height: 720px;
656
+ }
657
+
658
+ .figment-disclosure {
659
+ margin-top: 12px;
660
+ border-top: 1px solid var(--figment-line);
661
+ padding-top: 10px;
662
+ }
663
+
664
+ .figment-disclosure summary {
665
+ color: var(--figment-muted);
666
+ font-size: 14px;
667
+ font-weight: 680;
668
+ cursor: pointer;
669
+ }
670
+
671
+ .figment-disclosure .figment-json {
672
+ margin-top: 10px;
673
+ }
674
+
675
+ .figment-footer-rail {
676
+ display: flex;
677
+ align-items: center;
678
+ justify-content: space-between;
679
+ flex-wrap: wrap;
680
+ gap: 12px;
681
+ margin-top: 12px;
682
+ padding: 12px;
683
+ border: 1px solid var(--figment-line);
684
+ border-radius: var(--figment-radius);
685
+ background: rgba(255, 255, 255, 0.84);
686
+ }
687
+
688
+ .figment-muted {
689
+ color: var(--figment-muted);
690
+ }
691
+ .figment-app-shell {
692
+ --figment-ink: #13201b;
693
+ --figment-muted: #5f6d68;
694
+ --figment-subtle: #87938f;
695
+ --figment-line: rgba(19, 32, 27, 0.12);
696
+ --figment-line-strong: rgba(19, 32, 27, 0.2);
697
+ --figment-canvas: #edf3ee;
698
+ --figment-panel: #fffefa;
699
+ --figment-panel-soft: #eef8f3;
700
+ --figment-control: #ffffff;
701
+ --figment-blue: #1065d8;
702
+ --figment-blue-soft: #e7f0ff;
703
+ --figment-green: #08734f;
704
+ --figment-green-soft: #dff5eb;
705
+ --figment-red: #c63a2b;
706
+ --figment-red-soft: #fff0ec;
707
+ --figment-amber: #a85f00;
708
+ --figment-amber-soft: #fff2d2;
709
+ --figment-radius: 8px;
710
+ --figment-shadow: none;
711
  display: grid;
712
+ grid-template-columns: minmax(260px, 300px) minmax(0, 1fr);
713
+ grid-template-areas:
714
+ "rail board";
715
+ align-items: start;
716
+ gap: 16px;
717
+ max-width: 1620px;
718
+ margin: 0 auto;
719
+ padding: 18px;
720
+ }
721
+
722
+ .figment-app-shell .figment-mission-rail {
723
+ grid-area: rail;
724
+ display: grid;
725
+ gap: 16px;
726
+ align-content: start;
727
+ min-width: 0;
728
+ max-height: none;
729
+ overflow: visible;
730
+ position: static;
731
+ }
732
+
733
+ .figment-app-shell .figment-operation-board {
734
+ grid-area: board;
735
+ display: block;
736
+ min-width: 0;
737
+ }
738
+
739
+ .figment-app-shell .figment-topbar {
740
+ grid-area: auto;
741
+ display: grid;
742
+ gap: 18px;
743
+ align-content: start;
744
  margin: 0;
745
+ min-height: 236px;
746
+ padding: 22px;
747
+ color: #f8fff9;
748
+ background:
749
+ linear-gradient(180deg, rgba(255, 255, 255, 0.08), transparent 42%),
750
+ #15241e;
751
+ border: 1px solid rgba(255, 255, 255, 0.14);
752
+ box-shadow: none;
753
  }
754
 
755
+ .figment-app-shell .figment-brand {
756
+ display: grid;
757
+ gap: 12px;
758
+ }
759
+
760
+ .figment-app-shell .figment-logo {
761
+ color: #ffffff;
762
+ font-size: 34px;
763
+ }
764
+
765
+ .figment-app-shell .figment-positioning {
766
+ max-width: 23ch;
767
+ color: rgba(248, 255, 249, 0.82);
768
+ font-size: 17px;
769
+ line-height: 1.45;
770
+ }
771
+
772
+ .figment-app-shell .figment-safety {
773
+ align-self: end;
774
+ min-width: 0;
775
+ justify-content: flex-start;
776
+ padding-top: 16px;
777
+ border-top: 1px solid rgba(255, 255, 255, 0.16);
778
+ color: rgba(248, 255, 249, 0.9);
779
+ font-size: 14px;
780
+ }
781
+
782
+ .figment-app-shell .figment-statusline {
783
+ grid-area: auto;
784
+ align-self: stretch;
785
+ display: grid;
786
+ margin: 0;
787
+ padding: 14px;
788
+ background: #fffefa;
789
+ border-color: rgba(19, 32, 27, 0.12);
790
+ }
791
+
792
+ .figment-app-shell .figment-statusline strong {
793
+ flex-basis: 100%;
794
+ color: #08734f;
795
+ font-size: 14px;
796
+ }
797
+
798
+ .figment-app-shell .figment-tabs {
799
+ grid-area: auto;
800
+ counter-reset: figment-step;
801
+ display: grid;
802
+ gap: 8px;
803
+ align-content: start;
804
+ margin: 0;
805
+ padding: 12px;
806
+ background: #fffefa;
807
+ border-color: rgba(19, 32, 27, 0.12);
808
+ overflow: visible;
809
+ }
810
+
811
+ .figment-app-shell .figment-tab-button {
812
+ counter-increment: figment-step;
813
+ justify-content: flex-start;
814
+ gap: 10px;
815
+ width: 100%;
816
+ min-height: 48px;
817
+ padding: 10px 11px;
818
+ border: 1px solid transparent;
819
+ background: transparent;
820
+ color: var(--figment-muted);
821
+ text-align: left;
822
+ }
823
+
824
+ .figment-app-shell .figment-tab-button::before {
825
+ content: "0" counter(figment-step);
826
+ display: inline-grid;
827
+ place-items: center;
828
+ flex: 0 0 auto;
829
+ width: 30px;
830
+ height: 30px;
831
+ border: 1px solid var(--figment-line);
832
+ border-radius: 999px;
833
+ color: #08734f;
834
+ font-size: 12px;
835
+ font-weight: 760;
836
+ }
837
+
838
+ .figment-app-shell .figment-tab-button.is-active {
839
+ border-color: rgba(8, 115, 79, 0.18);
840
+ background: var(--figment-green-soft);
841
+ color: var(--figment-ink);
842
+ }
843
+
844
+ .figment-app-shell .figment-live-status {
845
+ grid-area: auto;
846
+ margin: 0;
847
+ padding: 12px;
848
+ background: #fffefa;
849
+ border: 1px solid rgba(19, 32, 27, 0.12);
850
+ border-radius: var(--figment-radius);
851
+ color: #50635b;
852
+ font-size: 15px;
853
+ }
854
+
855
+ .figment-app-shell .figment-view {
856
+ margin: 0;
857
+ }
858
+
859
+ .figment-app-shell .figment-footer-rail {
860
+ grid-area: auto;
861
+ display: grid;
862
+ margin: 0;
863
+ padding: 12px;
864
+ background: #fffefa;
865
+ border-color: rgba(19, 32, 27, 0.12);
866
+ }
867
+
868
+ .figment-app-shell .figment-footer-cluster {
869
+ align-items: flex-start;
870
+ }
871
+
872
+ .figment-app-shell .figment-workspace {
873
+ grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr);
874
+ gap: 18px;
875
+ }
876
+
877
+ .figment-app-shell .figment-workspace-intake {
878
+ grid-template-columns: minmax(0, 1.25fr) minmax(340px, 0.75fr);
879
+ }
880
+
881
+ .figment-app-shell .figment-panel {
882
  position: relative;
883
+ padding: 22px;
884
+ background: #fffefa;
885
+ border-color: rgba(19, 32, 27, 0.12);
886
+ overflow: hidden;
887
  }
888
 
889
+ .figment-app-shell .figment-panel::before {
890
  content: "";
891
  position: absolute;
892
+ top: 0;
893
+ right: 0;
894
  left: 0;
895
+ height: 5px;
896
+ background: linear-gradient(90deg, #08734f, #1065d8 54%, #c63a2b);
 
 
 
 
897
  }
898
 
899
+ .figment-app-shell .figment-panel-heading h2 {
900
+ font-size: 18px;
 
 
901
  }
902
 
903
+ .figment-app-shell .figment-panel-heading p,
904
+ .figment-app-shell .figment-section-subtitle {
905
+ font-size: 15px;
 
906
  }
907
 
908
+ .figment-app-shell .figment-section-divider {
909
+ margin: 22px 0;
910
+ background: rgba(19, 32, 27, 0.1);
 
911
  }
912
 
913
+ .figment-app-shell .figment-intake-panel {
914
+ padding: 24px;
915
+ }
916
+
917
+ .figment-app-shell .figment-audio-draft {
918
+ padding-top: 2px;
919
+ }
920
+
921
+ .figment-app-shell .figment-audio-draft .figment-panel-heading {
922
+ margin-bottom: 0;
923
+ }
924
+
925
+ .figment-app-shell .figment-review-panel {
926
+ gap: 0;
927
+ }
928
+
929
+ .figment-app-shell .figment-review-heading {
930
+ display: grid;
931
  gap: 12px;
 
 
 
 
 
932
  }
933
 
934
+ .figment-app-shell .figment-review-heading .figment-button {
935
+ width: 100%;
 
 
 
936
  }
937
 
938
+ .figment-app-shell .figment-field-grid {
939
+ grid-template-columns: repeat(2, minmax(0, 1fr));
940
+ gap: 14px;
941
  }
942
 
943
+ .figment-app-shell .figment-inline-controls {
944
+ grid-template-columns: minmax(0, 1fr) minmax(120px, auto);
945
  }
946
 
947
+ .figment-app-shell .figment-control input,
948
+ .figment-app-shell .figment-control select,
949
+ .figment-app-shell .figment-control textarea {
950
+ border-color: rgba(19, 32, 27, 0.16);
951
+ background: #ffffff;
952
+ font-size: 16px;
953
+ }
954
+
955
+ .figment-app-shell .figment-control input,
956
+ .figment-app-shell .figment-control select {
957
+ min-height: 46px;
958
+ }
959
+
960
+ .figment-app-shell .figment-button {
961
+ min-height: 42px;
962
+ }
963
+
964
+ .figment-app-shell .figment-audio-draft .figment-button {
965
+ min-height: 38px;
966
+ }
967
+
968
+ .figment-app-shell .figment-button-primary {
969
+ border-color: #08734f;
970
+ background: #08734f;
971
+ }
972
+
973
+ .figment-app-shell .figment-button-primary:disabled {
974
+ border-color: rgba(19, 32, 27, 0.14);
975
+ background: #e9eee9;
976
+ }
977
+
978
+ .figment-app-shell .figment-sticky-panel {
979
+ top: 18px;
980
  }
981
 
982
+ .figment-app-shell .figment-json {
983
+ background: #16342b;
984
+ border-color: rgba(8, 115, 79, 0.2);
985
+ color: #f0fff7;
986
  }
987
 
988
+ .figment-app-shell #intake-json {
989
+ min-height: 320px;
 
 
 
 
 
 
990
  }
991
 
992
+ .figment-app-shell #audio-json {
993
+ min-height: 180px;
 
 
994
  }
995
 
996
+ .figment-app-shell .figment-mini-card,
997
+ .figment-app-shell .figment-panel-soft {
998
+ background: #f8fbf7;
999
+ border-color: rgba(19, 32, 27, 0.1);
1000
  }
1001
 
1002
+ .figment-app-shell .figment-urgency-banner {
1003
+ background: #f7faf6;
1004
+ border-color: rgba(19, 32, 27, 0.12);
1005
  }
1006
 
1007
+ @media (max-width: 1100px) {
1008
+ .figment-workspace,
1009
+ .figment-workspace-intake,
1010
+ .figment-audio-grid {
1011
+ grid-template-columns: 1fr;
1012
+ }
1013
+
1014
+ .figment-sticky-panel {
1015
+ position: static;
1016
+ }
1017
+ }
1018
+
1019
+ @media (max-width: 760px) {
1020
+ .figment-app-shell {
1021
+ padding: 10px;
1022
+ }
1023
+
1024
  .figment-topbar,
1025
+ .figment-brand,
1026
+ .figment-safety,
1027
+ .figment-panel-heading,
1028
+ .figment-panel-heading-action,
1029
+ .figment-footer-rail {
1030
+ display: grid;
1031
+ justify-content: stretch;
1032
+ }
1033
+
1034
+ .figment-safety {
1035
+ min-width: 0;
1036
+ justify-content: start;
1037
+ }
1038
+
1039
+ .figment-logo {
1040
+ font-size: 26px;
1041
+ }
1042
+
1043
+ .figment-positioning,
1044
+ .figment-safety,
1045
+ .figment-control input,
1046
+ .figment-control select,
1047
+ .figment-control textarea,
1048
+ .figment-button,
1049
+ .figment-live-status {
1050
+ font-size: 16px;
1051
+ }
1052
+
1053
+ .figment-statusline,
1054
+ .figment-tabs,
1055
+ .figment-panel,
1056
+ .figment-footer-rail {
1057
+ padding: 10px;
1058
+ }
1059
+
1060
+ .figment-inline-controls,
1061
+ .figment-field-grid,
1062
  .figment-card-grid,
1063
+ .figment-checklist,
1064
+ .figment-urgency-banner {
1065
  grid-template-columns: 1fr;
1066
  }
1067
 
1068
+ .figment-button {
1069
+ min-height: 44px;
1070
+ width: 100%;
1071
+ }
1072
+
1073
+ .figment-json {
1074
+ min-height: 180px;
1075
+ }
1076
+ }
1077
+
1078
+ @media (max-width: 1100px) {
1079
+ .figment-app-shell {
1080
+ display: flex;
1081
+ flex-direction: column;
1082
+ padding: 10px;
1083
+ }
1084
+
1085
+ .figment-app-shell .figment-mission-rail,
1086
+ .figment-app-shell .figment-operation-board {
1087
+ max-height: none;
1088
+ overflow: visible;
1089
+ position: static;
1090
+ }
1091
+
1092
+ .figment-app-shell .figment-mission-rail {
1093
+ display: contents;
1094
+ }
1095
+
1096
+ .figment-app-shell .figment-operation-board {
1097
+ display: block;
1098
+ order: 3;
1099
+ }
1100
+
1101
+ .figment-app-shell .figment-topbar,
1102
+ .figment-app-shell .figment-statusline,
1103
+ .figment-app-shell .figment-tabs,
1104
+ .figment-app-shell .figment-live-status,
1105
+ .figment-app-shell .figment-view,
1106
+ .figment-app-shell .figment-footer-rail {
1107
+ margin-top: 12px;
1108
+ min-width: 0;
1109
+ width: 100%;
1110
+ }
1111
+
1112
+ .figment-app-shell .figment-topbar {
1113
+ order: 1;
1114
+ min-height: 0;
1115
+ padding: 18px;
1116
+ }
1117
+
1118
+ .figment-app-shell .figment-positioning {
1119
+ max-width: 34ch;
1120
+ }
1121
+
1122
+ .figment-app-shell .figment-safety {
1123
+ padding-top: 12px;
1124
+ }
1125
+
1126
+ .figment-app-shell .figment-tabs {
1127
+ order: 2;
1128
  display: grid;
1129
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1130
+ gap: 8px;
1131
+ overflow: visible;
1132
  }
1133
 
1134
+ .figment-app-shell .figment-tab-button {
1135
+ width: 100%;
1136
+ min-height: 52px;
1137
  white-space: normal;
1138
  }
1139
 
1140
+ .figment-app-shell .figment-live-status {
1141
+ order: 4;
1142
+ }
1143
+
1144
+ .figment-app-shell .figment-statusline {
1145
+ order: 5;
1146
+ }
1147
+
1148
+ .figment-app-shell .figment-footer-rail {
1149
+ order: 6;
1150
+ }
1151
+
1152
+ .figment-app-shell .figment-workspace,
1153
+ .figment-app-shell .figment-workspace-intake {
1154
+ grid-template-columns: 1fr;
1155
  }
1156
  }
1157
  """
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
- gradio>=5.0,<6
 
2
  huggingface_hub>=1.0,<2
3
  httpx>=0.27,<1
4
  pydantic>=2.7,<3
 
1
+ fastapi>=0.115,<1
2
+ gradio>=6.0,<7
3
  huggingface_hub>=1.0,<2
4
  httpx>=0.27,<1
5
  pydantic>=2.7,<3
tests/test_app_scaffold.py CHANGED
@@ -553,13 +553,16 @@ def test_run_case_uses_environment_config_when_not_supplied(monkeypatch: pytest.
553
  assert result["trace"]["model_route"]["model_backend"] == "hosted_omni"
554
 
555
 
556
- def test_app_import_and_blocks_smoke() -> None:
557
  app = importlib.import_module("app")
558
  config = FigmentConfig(model_backend="canned", audio_backend="none").validated()
559
 
560
  demo = app.build_app(config=config)
561
 
562
- assert hasattr(demo, "queue")
 
 
 
563
  assert app.TAB_TITLES == [
564
  "Intake",
565
  "Risk Check",
 
553
  assert result["trace"]["model_route"]["model_backend"] == "hosted_omni"
554
 
555
 
556
+ def test_app_import_and_server_smoke() -> None:
557
  app = importlib.import_module("app")
558
  config = FigmentConfig(model_backend="canned", audio_backend="none").validated()
559
 
560
  demo = app.build_app(config=config)
561
 
562
+ assert demo.__class__.__name__ == "Server"
563
+ assert hasattr(demo, "api")
564
+ assert hasattr(demo, "launch")
565
+ assert {route.path for route in demo.routes} >= {"/", "/health"}
566
  assert app.TAB_TITLES == [
567
  "Intake",
568
  "Risk Check",