File size: 5,801 Bytes
86e2a29 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | <script lang="ts">
import { briefingState } from '$lib/stores/briefingState.svelte';
/** v0.4.5 — live status pill for the AppHeader.
*
* Tracks pipeline phase + active step + fired-count + reconcile
* attempt so a user staring at a half-rendered briefing knows what's
* actually being crunched. Reads from the briefingState rune store
* which q/[queryId]/+page.svelte writes into from SSE callbacks.
*
* Visible only during a live run (phase != idle && != done). On the
* prerendered /q/sample route the store stays at idle and the pill
* stays hidden.
*/
// Pretty short labels for FSM step names. Lifted from the legacy
// agent.js label set; only the short names land here — the trace
// panel still owns the long form. Anything not mapped just shows
// its raw step name, which is mono-cased and readable.
const SHORT: Record<string, string> = {
geocode: 'geocoding',
nta_resolve: 'resolving NTA',
sandy_inundation: 'Sandy 2012',
dep_stormwater: 'DEP scenarios',
floodnet: 'FloodNet sensors',
nyc311: 'NYC 311 history',
noaa_tides: 'NOAA tides',
nws_alerts: 'NWS alerts',
nws_obs: 'NWS hourly obs',
ttm_forecast: 'TTM r2 surge (zero-shot)',
ttm_311_forecast: 'TTM r2 weekly 311',
ttm_battery_surge: 'TTM Battery (NYC fine-tune)',
floodnet_forecast: 'FloodNet recurrence forecast',
ida_hwm_2021: 'Ida 2021 HWMs',
// Two distinct Prithvi specialists with different compute profiles:
// prithvi_eo_v2 — static spatial join against the baked Ida 2021
// polygons in data/prithvi_ida_2021.geojson. No
// model inference; sub-100ms even on cold cache.
// prithvi_eo_live — live Sentinel-2 fetch + Prithvi-NYC-Pluvial
// forward pass. The actual ML run.
prithvi_eo_v2: 'Ida 2021 polygons (baked lookup)',
prithvi_eo_live: 'Prithvi-NYC-Pluvial v2 segmentation',
microtopo_lidar: 'LiDAR microtopo',
mta_entrance_exposure: 'MTA entrances',
nycha_development_exposure: 'NYCHA developments',
doe_school_exposure: 'DOE schools',
doh_hospital_exposure: 'NYS DOH hospitals',
terramind_synthesis: 'TerraMind v1 synthesis',
terramind_lulc: 'TerraMind LULC',
terramind_buildings: 'TerraMind Buildings',
eo_chip_fetch: 'fetching S2/S1/DEM chip',
rag_granite_embedding: 'RAG retrieval',
gliner_extract: 'GLiNER typed extraction',
};
let visible = $derived(
briefingState.phase !== 'idle' && briefingState.phase !== 'done'
);
let phaseLabel = $derived.by(() => {
switch (briefingState.phase) {
case 'planning': return 'planning intent';
case 'specialists': return 'gathering evidence';
case 'reconciling': return 'reconciling';
case 'streaming': return briefingState.attempt > 1
? `writing (reroll ${briefingState.attempt - 1})`
: 'writing briefing';
case 'error': return 'error';
default: return '';
}
});
let stepLabel = $derived.by(() => {
const s = briefingState.activeStep;
if (!s) return null;
return SHORT[s] ?? s;
});
let progress = $derived.by(() => {
if (briefingState.phase !== 'specialists' && briefingState.phase !== 'reconciling') return null;
const fired = briefingState.firedCount;
const total = briefingState.totalSpecialists;
if (!total) return fired > 0 ? `${fired}` : null;
return `${fired}/${total}`;
});
let kind = $derived.by(() => {
// Drives the dot color — error red, otherwise the accent pulse.
if (briefingState.phase === 'error') return 'err';
return 'live';
});
</script>
{#if visible}
<span class="status" data-kind={kind} aria-live="polite" aria-atomic="true">
<span class="status-dot" aria-hidden="true"></span>
<span class="status-phase">{phaseLabel}</span>
{#if stepLabel}
<span class="status-sep">·</span>
<span class="status-step">{stepLabel}</span>
{/if}
{#if progress}
<span class="status-sep">·</span>
<span class="status-progress">{progress}</span>
{/if}
{#if briefingState.phase === 'error' && briefingState.errorMessage}
<span class="status-sep">·</span>
<span class="status-err">{briefingState.errorMessage}</span>
{/if}
</span>
{/if}
<style>
.status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
background: var(--paper-deep);
border: 1px solid var(--rule-soft);
font-family: var(--font-mono);
font-size: 11px;
color: var(--ink-secondary);
letter-spacing: 0.04em;
max-width: min(60ch, 50vw);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.status[data-kind='err'] {
border-color: #B91C1C;
color: #B91C1C;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex: none;
background: var(--accent-graphical);
animation: pulse 1.4s ease-in-out infinite;
}
.status[data-kind='err'] .status-dot {
background: #B91C1C;
animation: none;
}
.status-phase {
color: var(--ink);
text-transform: lowercase;
letter-spacing: 0.05em;
font-weight: 600;
}
.status-sep { color: var(--ink-tertiary); opacity: 0.6; }
.status-step {
color: var(--ink-secondary);
overflow: hidden;
text-overflow: ellipsis;
}
.status-progress { color: var(--ink); font-weight: 600; }
.status-err { color: #B91C1C; }
@keyframes pulse {
0%, 100% { opacity: 0.35; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.1); }
}
@media (prefers-reduced-motion: reduce) {
.status-dot { animation: none; opacity: 0.7; }
}
</style>
|