Spaces:
Running on Zero
Running on Zero
Drake commited on
Update Figment Gradio Server UI
Browse files- README.md +39 -21
- app.py +725 -220
- docs/assets/mockups/figment-intake-tab.png +0 -3
- docs/assets/mockups/figment-navigator-output-handoff-tab.png +0 -3
- docs/assets/mockups/figment-protocol-guidance-tab.png +0 -3
- docs/assets/mockups/figment-risk-check-tab.png +0 -3
- docs/assets/mockups/figment-trace-tab.png +0 -3
- figment/ui_theme.py +906 -196
- requirements.txt +2 -1
- tests/test_app_scaffold.py +5 -2
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 📉
|
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: red
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 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 5
|
| 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
|
| 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
|
| 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 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
--
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
```
|
| 147 |
|
| 148 |
-
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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 |
|
| 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 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 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 "
|
| 498 |
if config.audio_backend == "omni_native" and config.model_backend == "hosted_omni":
|
| 499 |
-
return "
|
| 500 |
if config.audio_backend == "parakeet_nemo":
|
| 501 |
-
return "
|
| 502 |
if config.audio_backend == "canned":
|
| 503 |
-
return "
|
| 504 |
-
return "
|
| 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 & Performance</h4>
|
| 953 |
<table class="figment-table">
|
|
@@ -1274,4 +1779,4 @@ def _navigate_ui(
|
|
| 1274 |
|
| 1275 |
|
| 1276 |
if __name__ == "__main__":
|
| 1277 |
-
build_app().
|
|
|
|
| 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 & 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
|
docs/assets/mockups/figment-navigator-output-handoff-tab.png
DELETED
Git LFS Details
|
docs/assets/mockups/figment-protocol-guidance-tab.png
DELETED
Git LFS Details
|
docs/assets/mockups/figment-risk-check-tab.png
DELETED
Git LFS Details
|
docs/assets/mockups/figment-trace-tab.png
DELETED
Git LFS Details
|
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: #
|
| 6 |
-
--figment-muted: #
|
| 7 |
-
--figment-
|
|
|
|
|
|
|
|
|
|
| 8 |
--figment-panel: #ffffff;
|
| 9 |
-
--figment-panel-soft: #
|
| 10 |
-
--figment-
|
| 11 |
-
--figment-blue
|
| 12 |
-
--figment-
|
| 13 |
-
--figment-green
|
| 14 |
-
--figment-
|
| 15 |
-
--figment-red
|
| 16 |
-
--figment-
|
| 17 |
-
--figment-amber
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
color: var(--figment-ink);
|
| 23 |
-
background:
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
.figment-topbar {
|
| 32 |
display: flex;
|
| 33 |
align-items: center;
|
| 34 |
justify-content: space-between;
|
| 35 |
-
gap:
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
border
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
.figment-brand {
|
| 48 |
display: flex;
|
| 49 |
align-items: baseline;
|
| 50 |
-
gap:
|
| 51 |
min-width: 0;
|
| 52 |
}
|
| 53 |
|
| 54 |
.figment-logo {
|
| 55 |
-
|
| 56 |
-
|
|
|
|
| 57 |
font-weight: 760;
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
.figment-positioning {
|
|
|
|
|
|
|
| 61 |
font-size: 15px;
|
| 62 |
-
|
| 63 |
-
white-space: nowrap;
|
| 64 |
}
|
| 65 |
|
| 66 |
.figment-safety {
|
| 67 |
display: flex;
|
| 68 |
align-items: center;
|
| 69 |
gap: 9px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
font-size: 14px;
|
| 71 |
-
|
| 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:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
border: 1px solid var(--figment-line);
|
| 93 |
-
border-radius:
|
| 94 |
-
background:
|
| 95 |
}
|
| 96 |
|
| 97 |
-
.figment-statusline strong
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
.figment-chip {
|
| 102 |
display: inline-flex;
|
| 103 |
align-items: center;
|
|
|
|
| 104 |
min-height: 26px;
|
| 105 |
-
padding: 4px
|
| 106 |
border: 1px solid var(--figment-line);
|
| 107 |
-
border-radius:
|
| 108 |
-
background: #
|
| 109 |
color: var(--figment-ink);
|
| 110 |
font-size: 13px;
|
| 111 |
font-weight: 650;
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
.figment-chip.blue {
|
| 115 |
-
border-color:
|
| 116 |
background: var(--figment-blue-soft);
|
| 117 |
-
color: #
|
| 118 |
}
|
| 119 |
|
| 120 |
.figment-chip.green {
|
| 121 |
-
border-color:
|
| 122 |
background: var(--figment-green-soft);
|
| 123 |
-
color:
|
| 124 |
}
|
| 125 |
|
| 126 |
.figment-chip.red {
|
| 127 |
-
border-color:
|
| 128 |
background: var(--figment-red-soft);
|
| 129 |
color: var(--figment-red);
|
| 130 |
}
|
| 131 |
|
| 132 |
.figment-chip.amber {
|
| 133 |
-
border-color:
|
| 134 |
background: var(--figment-amber-soft);
|
| 135 |
color: var(--figment-amber);
|
| 136 |
}
|
| 137 |
|
| 138 |
.figment-tabs {
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
|
| 142 |
-
.figment-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
border
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
-
.figment-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
}
|
| 153 |
|
| 154 |
-
.figment-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
-
.figment-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
|
| 163 |
-
.figment-
|
| 164 |
-
|
| 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-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
border: 1px solid var(--figment-line);
|
| 172 |
-
border-radius:
|
| 173 |
-
background: var(--figment-panel
|
| 174 |
-
|
|
|
|
| 175 |
}
|
| 176 |
|
| 177 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
display: flex;
|
| 179 |
-
align-items:
|
| 180 |
justify-content: space-between;
|
| 181 |
gap: 12px;
|
| 182 |
-
margin:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
color: var(--figment-ink);
|
| 184 |
font-size: 16px;
|
| 185 |
-
font-weight:
|
|
|
|
|
|
|
| 186 |
}
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
.figment-section-subtitle {
|
| 189 |
-
margin:
|
|
|
|
| 190 |
color: var(--figment-muted);
|
| 191 |
-
font-size:
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
display: grid;
|
| 196 |
-
grid-template-columns:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
padding: 8px 10px;
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
border-radius: 6px;
|
| 204 |
-
|
| 205 |
-
font-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
}
|
| 209 |
|
| 210 |
-
.figment-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
}
|
| 215 |
|
| 216 |
.figment-card-grid {
|
|
@@ -220,16 +486,28 @@ FIGMENT_CSS = """
|
|
| 220 |
}
|
| 221 |
|
| 222 |
.figment-mini-card {
|
| 223 |
-
min-
|
|
|
|
| 224 |
border: 1px solid var(--figment-line);
|
| 225 |
-
border-radius:
|
| 226 |
-
background: #
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
border-radius: 8px;
|
| 246 |
-
background: #fff;
|
| 247 |
font-size: 13px;
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
.figment-table th,
|
| 251 |
.figment-table td {
|
| 252 |
-
padding: 10px
|
| 253 |
border-bottom: 1px solid var(--figment-line);
|
| 254 |
text-align: left;
|
| 255 |
vertical-align: top;
|
| 256 |
}
|
| 257 |
|
| 258 |
.figment-table th {
|
| 259 |
-
|
| 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:
|
| 273 |
align-items: center;
|
| 274 |
-
padding: 14px
|
| 275 |
border: 1px solid var(--figment-line);
|
| 276 |
-
border-radius:
|
| 277 |
-
background:
|
| 278 |
}
|
| 279 |
|
| 280 |
.figment-urgency-word {
|
| 281 |
display: inline-flex;
|
| 282 |
align-items: center;
|
| 283 |
-
min-height:
|
| 284 |
-
padding: 0
|
| 285 |
border-radius: 6px;
|
| 286 |
-
font-size:
|
| 287 |
-
font-weight:
|
| 288 |
letter-spacing: 0;
|
| 289 |
}
|
| 290 |
|
| 291 |
.figment-urgency-word.routine {
|
| 292 |
-
background:
|
| 293 |
-
color: #
|
| 294 |
}
|
| 295 |
|
| 296 |
.figment-urgency-word.monitor {
|
|
@@ -304,144 +618,540 @@ FIGMENT_CSS = """
|
|
| 304 |
}
|
| 305 |
|
| 306 |
.figment-urgency-word.emergency {
|
| 307 |
-
background:
|
| 308 |
-
color: #
|
| 309 |
}
|
| 310 |
|
| 311 |
.figment-lockout {
|
| 312 |
-
border: 1px solid
|
| 313 |
-
border-radius:
|
| 314 |
-
padding:
|
| 315 |
background: var(--figment-red-soft);
|
| 316 |
-
color: #
|
| 317 |
font-size: 13px;
|
|
|
|
| 318 |
}
|
| 319 |
|
| 320 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
display: grid;
|
| 322 |
-
grid-template-columns:
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
margin: 0;
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
}
|
| 328 |
|
| 329 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
position: relative;
|
| 331 |
-
padding
|
| 332 |
-
|
| 333 |
-
|
|
|
|
| 334 |
}
|
| 335 |
|
| 336 |
-
.figment-
|
| 337 |
content: "";
|
| 338 |
position: absolute;
|
|
|
|
|
|
|
| 339 |
left: 0;
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
height: 13px;
|
| 343 |
-
border: 1px solid #9aa9bb;
|
| 344 |
-
border-radius: 3px;
|
| 345 |
-
background: #fff;
|
| 346 |
}
|
| 347 |
|
| 348 |
-
.figment-
|
| 349 |
-
|
| 350 |
-
background: var(--figment-blue);
|
| 351 |
-
box-shadow: inset 0 0 0 3px #fff;
|
| 352 |
}
|
| 353 |
|
| 354 |
-
.figment-
|
| 355 |
-
.figment-
|
| 356 |
-
|
| 357 |
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
|
| 358 |
}
|
| 359 |
|
| 360 |
-
.figment-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
gap: 10px;
|
| 364 |
}
|
| 365 |
|
| 366 |
-
.figment-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 380 |
-
|
| 381 |
-
align-items: center;
|
| 382 |
-
flex-wrap: wrap;
|
| 383 |
-
gap: 10px;
|
| 384 |
}
|
| 385 |
|
| 386 |
-
.figment-
|
| 387 |
-
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
-
.figment-
|
| 391 |
-
|
| 392 |
}
|
| 393 |
|
| 394 |
-
.figment-
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
}
|
| 397 |
|
| 398 |
-
.figment-
|
| 399 |
-
|
|
|
|
|
|
|
| 400 |
}
|
| 401 |
|
| 402 |
-
|
| 403 |
-
|
| 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-
|
| 413 |
-
min-height:
|
| 414 |
-
height: auto !important;
|
| 415 |
-
flex: 0 0 auto !important;
|
| 416 |
}
|
| 417 |
|
| 418 |
-
.figment-
|
| 419 |
-
|
|
|
|
|
|
|
| 420 |
}
|
| 421 |
|
| 422 |
-
.figment-
|
| 423 |
-
|
|
|
|
| 424 |
}
|
| 425 |
|
| 426 |
-
@media (max-width:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
.figment-topbar,
|
| 428 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
.figment-card-grid,
|
| 430 |
-
.figment-
|
|
|
|
| 431 |
grid-template-columns: 1fr;
|
| 432 |
}
|
| 433 |
|
| 434 |
-
.figment-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
display: grid;
|
|
|
|
|
|
|
|
|
|
| 436 |
}
|
| 437 |
|
| 438 |
-
.figment-
|
| 439 |
-
|
|
|
|
| 440 |
white-space: normal;
|
| 441 |
}
|
| 442 |
|
| 443 |
-
.figment-
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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",
|