Spaces:
Sleeping
frontend: APK status bar + reactive connectivity + dev-view tool_calls
Browse filesApp.jsx changes that landed across the session 28 APK rebuild round:
- Status bar (Capacitor): overlay + Style.Light (= dark icons) on
Android. Was transparent-on-light = invisible airplane / clock icons.
Light naming in Capacitor is inverted; LIGHT style = dark icons.
Browser fallback wrapped in try/catch.
- Reactive online/offline: on 'online' event re-probe /api/health,
on 'offline' immediately flip apiReachable=false + update banner.
Was: one probe on mount, badge stayed stale through airplane toggles.
- Dev-view _raw: capture tool_calls + form + danger + timing from the
SSE response so the Developer view card on Voice/Text tabs can
render the Ollama tool_calls round-trip on screen — the same Ollama
function-calling proof that appears in the demo video.
Capacitor plugin config: register StatusBar plugin with the same
overlay + LIGHT defaults so the native shell starts in the right
state before JS runs.
@capacitor/status-bar dep added at ^8.0.2 (matches @capacitor/core 8.x).
- frontend/capacitor.config.json +6 -0
- frontend/package-lock.json +10 -0
- frontend/package.json +1 -0
- frontend/src/App.jsx +125 -9
|
@@ -5,5 +5,11 @@
|
|
| 5 |
"server": {
|
| 6 |
"androidScheme": "http",
|
| 7 |
"cleartext": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
}
|
|
|
|
| 5 |
"server": {
|
| 6 |
"androidScheme": "http",
|
| 7 |
"cleartext": true
|
| 8 |
+
},
|
| 9 |
+
"plugins": {
|
| 10 |
+
"StatusBar": {
|
| 11 |
+
"overlaysWebView": true,
|
| 12 |
+
"style": "LIGHT"
|
| 13 |
+
}
|
| 14 |
}
|
| 15 |
}
|
|
@@ -11,6 +11,7 @@
|
|
| 11 |
"@capacitor/android": "^8.3.1",
|
| 12 |
"@capacitor/cli": "^8.3.1",
|
| 13 |
"@capacitor/core": "^8.3.1",
|
|
|
|
| 14 |
"react": "^19.2.4",
|
| 15 |
"react-dom": "^19.2.4"
|
| 16 |
},
|
|
@@ -328,6 +329,15 @@
|
|
| 328 |
"tslib": "^2.1.0"
|
| 329 |
}
|
| 330 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
"node_modules/@emnapi/core": {
|
| 332 |
"version": "1.9.1",
|
| 333 |
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
|
|
|
| 11 |
"@capacitor/android": "^8.3.1",
|
| 12 |
"@capacitor/cli": "^8.3.1",
|
| 13 |
"@capacitor/core": "^8.3.1",
|
| 14 |
+
"@capacitor/status-bar": "^8.0.2",
|
| 15 |
"react": "^19.2.4",
|
| 16 |
"react-dom": "^19.2.4"
|
| 17 |
},
|
|
|
|
| 329 |
"tslib": "^2.1.0"
|
| 330 |
}
|
| 331 |
},
|
| 332 |
+
"node_modules/@capacitor/status-bar": {
|
| 333 |
+
"version": "8.0.2",
|
| 334 |
+
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",
|
| 335 |
+
"integrity": "sha512-WXs8YB8B9eEaPZz+bcdY6t2nForF1FLoj/JU0Dl9RRgQnddnS98FEEyDooQhaY7wivr000j4+SC1FyeJkrFO7A==",
|
| 336 |
+
"license": "MIT",
|
| 337 |
+
"peerDependencies": {
|
| 338 |
+
"@capacitor/core": ">=8.0.0"
|
| 339 |
+
}
|
| 340 |
+
},
|
| 341 |
"node_modules/@emnapi/core": {
|
| 342 |
"version": "1.9.1",
|
| 343 |
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
|
@@ -14,6 +14,7 @@
|
|
| 14 |
"@capacitor/android": "^8.3.1",
|
| 15 |
"@capacitor/cli": "^8.3.1",
|
| 16 |
"@capacitor/core": "^8.3.1",
|
|
|
|
| 17 |
"react": "^19.2.4",
|
| 18 |
"react-dom": "^19.2.4"
|
| 19 |
},
|
|
|
|
| 14 |
"@capacitor/android": "^8.3.1",
|
| 15 |
"@capacitor/cli": "^8.3.1",
|
| 16 |
"@capacitor/core": "^8.3.1",
|
| 17 |
+
"@capacitor/status-bar": "^8.0.2",
|
| 18 |
"react": "^19.2.4",
|
| 19 |
"react-dom": "^19.2.4"
|
| 20 |
},
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { saveRecording, getQueue, getRecording, removeRecording, clearQueue, updateRecordingStatus, appendChunk, assembleChunks, listOrphanedSessions, clearChunks } from './offlineQueue'
|
|
|
|
| 3 |
import Cactus from './lib/cactus'
|
| 4 |
import { runPipeline } from './lib/pipeline'
|
| 5 |
import './App.css'
|
|
@@ -181,6 +182,19 @@ function keyValueRows(data, prefix = '') {
|
|
| 181 |
}
|
| 182 |
|
| 183 |
function App() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
const [activeTab, setActiveTab] = useState('voice')
|
| 185 |
const [health, setHealth] = useState('Checking backend...')
|
| 186 |
const [apiReachable, setApiReachable] = useState(null) // null = unknown, true/false after probe
|
|
@@ -226,6 +240,7 @@ function App() {
|
|
| 226 |
form: null,
|
| 227 |
danger: null,
|
| 228 |
timing: null,
|
|
|
|
| 229 |
})
|
| 230 |
|
| 231 |
const [textState, setTextState] = useState({
|
|
@@ -235,6 +250,7 @@ function App() {
|
|
| 235 |
form: null,
|
| 236 |
danger: null,
|
| 237 |
timing: null,
|
|
|
|
| 238 |
})
|
| 239 |
|
| 240 |
const [pipelineStages, setPipelineStages] = useState([])
|
|
@@ -364,10 +380,35 @@ function App() {
|
|
| 364 |
if (metadata.asha_id) localStorage.setItem('sakhi_asha_id', metadata.asha_id)
|
| 365 |
}, [metadata.asha_id])
|
| 366 |
|
| 367 |
-
// Online/offline detection + queue loading
|
|
|
|
|
|
|
| 368 |
useEffect(() => {
|
| 369 |
-
const
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
window.addEventListener('online', goOnline)
|
| 372 |
window.addEventListener('offline', goOffline)
|
| 373 |
loadQueue()
|
|
@@ -779,6 +820,12 @@ function App() {
|
|
| 779 |
form: evt.form || {},
|
| 780 |
danger: evt.danger || {},
|
| 781 |
timing: evt.timing || {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
})
|
| 783 |
setPipelineStages((prev) => prev.map((s) => ({ ...s, status: 'done' })))
|
| 784 |
saveToHistory(source, evt.visit_type, evt.form, evt.danger, evt.transcript || null, evt.timing)
|
|
@@ -809,7 +856,7 @@ function App() {
|
|
| 809 |
setVoiceState((s) => ({ ...s, error: 'Upload or record audio first.' }))
|
| 810 |
return
|
| 811 |
}
|
| 812 |
-
setVoiceState({ loading: true, error: '', transcript: '', visitType: '', form: null, danger: null, timing: null })
|
| 813 |
setPipelineStages([])
|
| 814 |
|
| 815 |
const formData = new FormData()
|
|
@@ -849,7 +896,7 @@ function App() {
|
|
| 849 |
setTextState((s) => ({ ...s, error: 'Transcript is empty.' }))
|
| 850 |
return
|
| 851 |
}
|
| 852 |
-
setTextState({ loading: true, error: '', visitType: '', form: null, danger: null, timing: null })
|
| 853 |
setPipelineStages([])
|
| 854 |
|
| 855 |
fetch(`${API_BASE}/api/process-text-stream`, {
|
|
@@ -1089,6 +1136,14 @@ function App() {
|
|
| 1089 |
<pre className="transcript">{translation.english}</pre>
|
| 1090 |
</>
|
| 1091 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1092 |
</div>
|
| 1093 |
</section>
|
| 1094 |
)}
|
|
@@ -1123,6 +1178,14 @@ function App() {
|
|
| 1123 |
onChange={(e) => setTextInput(e.target.value)}
|
| 1124 |
placeholder="Paste Hindi conversation transcript here..."
|
| 1125 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
</div>
|
| 1127 |
</section>
|
| 1128 |
)}
|
|
@@ -1166,8 +1229,12 @@ function App() {
|
|
| 1166 |
</div>
|
| 1167 |
)}
|
| 1168 |
<div className="card">
|
| 1169 |
-
<div className={`connectivity-badge ${
|
| 1170 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1171 |
</div>
|
| 1172 |
<p className="field-desc">
|
| 1173 |
Record ASHA conversations during home visits. Audio is saved on your device
|
|
@@ -1198,6 +1265,21 @@ function App() {
|
|
| 1198 |
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
| 1199 |
))}
|
| 1200 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
<button
|
| 1202 |
className="btn primary"
|
| 1203 |
onClick={processFieldOnDevice}
|
|
@@ -1273,7 +1355,7 @@ function App() {
|
|
| 1273 |
<div className="queue-header">
|
| 1274 |
<h3>Saved Recordings ({offlineQueue.length})</h3>
|
| 1275 |
<div className="queue-actions">
|
| 1276 |
-
{
|
| 1277 |
<button className="btn primary" onClick={syncAll} disabled={syncingId != null}>
|
| 1278 |
Sync All
|
| 1279 |
</button>
|
|
@@ -1297,7 +1379,7 @@ function App() {
|
|
| 1297 |
<button className="btn secondary" onClick={() => playRecording(entry.id)}>
|
| 1298 |
{playingId === entry.id ? 'Stop' : 'Play'}
|
| 1299 |
</button>
|
| 1300 |
-
{
|
| 1301 |
<button className="btn secondary" onClick={() => syncRecording(entry.id)} disabled={syncingId != null}>
|
| 1302 |
Sync
|
| 1303 |
</button>
|
|
@@ -1591,6 +1673,40 @@ function App() {
|
|
| 1591 |
})}
|
| 1592 |
</div>
|
| 1593 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1594 |
</div>
|
| 1595 |
)
|
| 1596 |
}
|
|
|
|
| 1 |
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { saveRecording, getQueue, getRecording, removeRecording, clearQueue, updateRecordingStatus, appendChunk, assembleChunks, listOrphanedSessions, clearChunks } from './offlineQueue'
|
| 3 |
+
import { StatusBar, Style } from '@capacitor/status-bar'
|
| 4 |
import Cactus from './lib/cactus'
|
| 5 |
import { runPipeline } from './lib/pipeline'
|
| 6 |
import './App.css'
|
|
|
|
| 182 |
}
|
| 183 |
|
| 184 |
function App() {
|
| 185 |
+
// Native APK: dark status-bar icons (LinkedIn-style) so the airplane icon
|
| 186 |
+
// and clock render visibly against Sakhi's light content. No-op in browser.
|
| 187 |
+
useEffect(() => {
|
| 188 |
+
(async () => {
|
| 189 |
+
try {
|
| 190 |
+
await StatusBar.setOverlaysWebView({ overlay: true })
|
| 191 |
+
await StatusBar.setStyle({ style: Style.Light })
|
| 192 |
+
} catch {
|
| 193 |
+
/* not running in a native Capacitor context */
|
| 194 |
+
}
|
| 195 |
+
})()
|
| 196 |
+
}, [])
|
| 197 |
+
|
| 198 |
const [activeTab, setActiveTab] = useState('voice')
|
| 199 |
const [health, setHealth] = useState('Checking backend...')
|
| 200 |
const [apiReachable, setApiReachable] = useState(null) // null = unknown, true/false after probe
|
|
|
|
| 240 |
form: null,
|
| 241 |
danger: null,
|
| 242 |
timing: null,
|
| 243 |
+
_raw: null,
|
| 244 |
})
|
| 245 |
|
| 246 |
const [textState, setTextState] = useState({
|
|
|
|
| 250 |
form: null,
|
| 251 |
danger: null,
|
| 252 |
timing: null,
|
| 253 |
+
_raw: null,
|
| 254 |
})
|
| 255 |
|
| 256 |
const [pipelineStages, setPipelineStages] = useState([])
|
|
|
|
| 380 |
if (metadata.asha_id) localStorage.setItem('sakhi_asha_id', metadata.asha_id)
|
| 381 |
}, [metadata.asha_id])
|
| 382 |
|
| 383 |
+
// Online/offline detection + queue loading.
|
| 384 |
+
// On network flip, immediately reflect the new API reachability so the
|
| 385 |
+
// top banner + Field Mode badge stop showing a stale "Connected" state.
|
| 386 |
useEffect(() => {
|
| 387 |
+
const probeHealth = () => {
|
| 388 |
+
fetch(`${API_BASE}/api/health`)
|
| 389 |
+
.then((r) => r.json())
|
| 390 |
+
.then((d) => {
|
| 391 |
+
setHealth(
|
| 392 |
+
d.whisper
|
| 393 |
+
? `API: ${d.status} · LLM: ${d.model} · ASR: ${d.whisper}`
|
| 394 |
+
: `API: ${d.status} · Model: ${d.model}`
|
| 395 |
+
)
|
| 396 |
+
setApiReachable(true)
|
| 397 |
+
})
|
| 398 |
+
.catch(() => {
|
| 399 |
+
setHealth(`API not reachable at ${API_BASE || window.location.origin}`)
|
| 400 |
+
setApiReachable(false)
|
| 401 |
+
})
|
| 402 |
+
}
|
| 403 |
+
const goOnline = () => {
|
| 404 |
+
setIsOnline(true)
|
| 405 |
+
probeHealth()
|
| 406 |
+
}
|
| 407 |
+
const goOffline = () => {
|
| 408 |
+
setIsOnline(false)
|
| 409 |
+
setApiReachable(false)
|
| 410 |
+
setHealth('API not reachable — phone is offline')
|
| 411 |
+
}
|
| 412 |
window.addEventListener('online', goOnline)
|
| 413 |
window.addEventListener('offline', goOffline)
|
| 414 |
loadQueue()
|
|
|
|
| 820 |
form: evt.form || {},
|
| 821 |
danger: evt.danger || {},
|
| 822 |
timing: evt.timing || {},
|
| 823 |
+
_raw: {
|
| 824 |
+
tool_calls: evt.tool_calls || [],
|
| 825 |
+
form: evt.form || {},
|
| 826 |
+
danger: evt.danger || {},
|
| 827 |
+
metadata: evt.metadata || null,
|
| 828 |
+
},
|
| 829 |
})
|
| 830 |
setPipelineStages((prev) => prev.map((s) => ({ ...s, status: 'done' })))
|
| 831 |
saveToHistory(source, evt.visit_type, evt.form, evt.danger, evt.transcript || null, evt.timing)
|
|
|
|
| 856 |
setVoiceState((s) => ({ ...s, error: 'Upload or record audio first.' }))
|
| 857 |
return
|
| 858 |
}
|
| 859 |
+
setVoiceState({ loading: true, error: '', transcript: '', visitType: '', form: null, danger: null, timing: null, _raw: null })
|
| 860 |
setPipelineStages([])
|
| 861 |
|
| 862 |
const formData = new FormData()
|
|
|
|
| 896 |
setTextState((s) => ({ ...s, error: 'Transcript is empty.' }))
|
| 897 |
return
|
| 898 |
}
|
| 899 |
+
setTextState({ loading: true, error: '', visitType: '', form: null, danger: null, timing: null, _raw: null })
|
| 900 |
setPipelineStages([])
|
| 901 |
|
| 902 |
fetch(`${API_BASE}/api/process-text-stream`, {
|
|
|
|
| 1136 |
<pre className="transcript">{translation.english}</pre>
|
| 1137 |
</>
|
| 1138 |
)}
|
| 1139 |
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#555', cursor: 'pointer' }}>
|
| 1140 |
+
<input
|
| 1141 |
+
type="checkbox"
|
| 1142 |
+
checked={devViewEnabled}
|
| 1143 |
+
onChange={(e) => setDevViewEnabled(e.target.checked)}
|
| 1144 |
+
/>
|
| 1145 |
+
Developer view — show raw API response (tool_calls, parsed form, parsed danger)
|
| 1146 |
+
</label>
|
| 1147 |
</div>
|
| 1148 |
</section>
|
| 1149 |
)}
|
|
|
|
| 1178 |
onChange={(e) => setTextInput(e.target.value)}
|
| 1179 |
placeholder="Paste Hindi conversation transcript here..."
|
| 1180 |
/>
|
| 1181 |
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#555', cursor: 'pointer' }}>
|
| 1182 |
+
<input
|
| 1183 |
+
type="checkbox"
|
| 1184 |
+
checked={devViewEnabled}
|
| 1185 |
+
onChange={(e) => setDevViewEnabled(e.target.checked)}
|
| 1186 |
+
/>
|
| 1187 |
+
Developer view — show raw API response (tool_calls, parsed form, parsed danger)
|
| 1188 |
+
</label>
|
| 1189 |
</div>
|
| 1190 |
</section>
|
| 1191 |
)}
|
|
|
|
| 1229 |
</div>
|
| 1230 |
)}
|
| 1231 |
<div className="card">
|
| 1232 |
+
<div className={`connectivity-badge ${apiReachable === true ? 'online' : 'offline'}`}>
|
| 1233 |
+
{apiReachable === true
|
| 1234 |
+
? 'Connected — ready to sync'
|
| 1235 |
+
: isOnline
|
| 1236 |
+
? 'Workstation unreachable — recordings saved locally'
|
| 1237 |
+
: 'Offline — recordings saved locally'}
|
| 1238 |
</div>
|
| 1239 |
<p className="field-desc">
|
| 1240 |
Record ASHA conversations during home visits. Audio is saved on your device
|
|
|
|
| 1265 |
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
| 1266 |
))}
|
| 1267 |
</select>
|
| 1268 |
+
<button
|
| 1269 |
+
className="btn secondary"
|
| 1270 |
+
type="button"
|
| 1271 |
+
onClick={() => {
|
| 1272 |
+
const anc = examples.find((e) => (e.label || '').toLowerCase().includes('preeclampsia'))
|
| 1273 |
+
|| examples.find((e) => e.visit_type === 'anc')
|
| 1274 |
+
if (anc && anc.transcript) {
|
| 1275 |
+
setFieldOnDeviceText(anc.transcript)
|
| 1276 |
+
setFieldOnDeviceVisitType('anc')
|
| 1277 |
+
}
|
| 1278 |
+
}}
|
| 1279 |
+
disabled={fieldOnDeviceState.loading || examples.length === 0}
|
| 1280 |
+
>
|
| 1281 |
+
Load ANC example
|
| 1282 |
+
</button>
|
| 1283 |
<button
|
| 1284 |
className="btn primary"
|
| 1285 |
onClick={processFieldOnDevice}
|
|
|
|
| 1355 |
<div className="queue-header">
|
| 1356 |
<h3>Saved Recordings ({offlineQueue.length})</h3>
|
| 1357 |
<div className="queue-actions">
|
| 1358 |
+
{apiReachable === true && (
|
| 1359 |
<button className="btn primary" onClick={syncAll} disabled={syncingId != null}>
|
| 1360 |
Sync All
|
| 1361 |
</button>
|
|
|
|
| 1379 |
<button className="btn secondary" onClick={() => playRecording(entry.id)}>
|
| 1380 |
{playingId === entry.id ? 'Stop' : 'Play'}
|
| 1381 |
</button>
|
| 1382 |
+
{apiReachable === true && entry.status === 'pending' && (
|
| 1383 |
<button className="btn secondary" onClick={() => syncRecording(entry.id)} disabled={syncingId != null}>
|
| 1384 |
Sync
|
| 1385 |
</button>
|
|
|
|
| 1673 |
})}
|
| 1674 |
</div>
|
| 1675 |
)}
|
| 1676 |
+
|
| 1677 |
+
{devViewEnabled && activeTab !== 'field' && activeState._raw && (
|
| 1678 |
+
<div className="card" style={{ background: '#0f172a', color: '#e2e8f0', fontFamily: 'ui-monospace, Menlo, Consolas, monospace' }}>
|
| 1679 |
+
<h3 style={{ marginTop: 0, color: '#93c5fd' }}>Raw API response (workstation pipeline)</h3>
|
| 1680 |
+
{Array.isArray(activeState._raw.tool_calls) && activeState._raw.tool_calls.length > 0 && (
|
| 1681 |
+
<div style={{ marginBottom: 12 }}>
|
| 1682 |
+
<div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ tool_calls (Ollama function calling) →</div>
|
| 1683 |
+
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
|
| 1684 |
+
{JSON.stringify(activeState._raw.tool_calls, null, 2)}
|
| 1685 |
+
</pre>
|
| 1686 |
+
</div>
|
| 1687 |
+
)}
|
| 1688 |
+
<div style={{ marginBottom: 12 }}>
|
| 1689 |
+
<div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ form →</div>
|
| 1690 |
+
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
|
| 1691 |
+
{JSON.stringify(activeState._raw.form, null, 2)}
|
| 1692 |
+
</pre>
|
| 1693 |
+
</div>
|
| 1694 |
+
<div style={{ marginBottom: 12 }}>
|
| 1695 |
+
<div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ danger →</div>
|
| 1696 |
+
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
|
| 1697 |
+
{JSON.stringify(activeState._raw.danger, null, 2)}
|
| 1698 |
+
</pre>
|
| 1699 |
+
</div>
|
| 1700 |
+
{activeState._raw.metadata && (
|
| 1701 |
+
<div>
|
| 1702 |
+
<div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ metadata (header from ASHA) →</div>
|
| 1703 |
+
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 200, overflow: 'auto' }}>
|
| 1704 |
+
{JSON.stringify(activeState._raw.metadata, null, 2)}
|
| 1705 |
+
</pre>
|
| 1706 |
+
</div>
|
| 1707 |
+
)}
|
| 1708 |
+
</div>
|
| 1709 |
+
)}
|
| 1710 |
</div>
|
| 1711 |
)
|
| 1712 |
}
|