diff --git a/web/src/components/AdvancedFields.vue b/web/src/components/AdvancedFields.vue index 9591f3b..4811496 100644 --- a/web/src/components/AdvancedFields.vue +++ b/web/src/components/AdvancedFields.vue @@ -1,6 +1,7 @@ + + + + + + + + + + + + {{ store.state.validationError }} + + + + Strategy + + + {{ label }} + + + + + + + {{ CONFIG_FIELD_LABELS[field] }} + base {{ store.state.base[field] }} + + + + + + + + + + + diff --git a/web/src/components/PanelMenu.vue b/web/src/components/PanelMenu.vue new file mode 100644 index 0000000..fc72e10 --- /dev/null +++ b/web/src/components/PanelMenu.vue @@ -0,0 +1,107 @@ + + + + + + {{ item.label }} + + + + + diff --git a/web/src/components/ResultsTable.vue b/web/src/components/ResultsTable.vue index 19bc416..5f7e08b 100644 --- a/web/src/components/ResultsTable.vue +++ b/web/src/components/ResultsTable.vue @@ -1,4 +1,5 @@ @@ -27,7 +33,7 @@ const refreshing = computed( type="button" :disabled="atPanelCap" :title="atPanelCap ? 'Maximum 6 scenarios' : undefined" - @click="store.addPanel()" + @click="addScenario()" > Add scenario diff --git a/web/src/composables/__tests__/useSimStore.spec.ts b/web/src/composables/__tests__/useSimStore.spec.ts index 8c219de..dea9086 100644 --- a/web/src/composables/__tests__/useSimStore.spec.ts +++ b/web/src/composables/__tests__/useSimStore.spec.ts @@ -152,11 +152,14 @@ describe('useSimStore runs', () => { expect(store.state.errorMessage).toBe('fetch failed') expect(store.state.validationError).toBeNull() expect(store.state.resultsByPanelId).toEqual(goodResults) + // Every panel in the failed run gets the rose kebab dot. + expect(store.state.failedPanelIds.size).toBe(3) runScenarioMock.mockImplementation(async (request) => fakeResponse(request)) await store.retry() expect(store.state.runState).toBe('idle') expect(store.state.errorMessage).toBeNull() + expect(store.state.failedPanelIds.size).toBe(0) }) it('routes a 400 to validationError instead of the banner', async () => { diff --git a/web/src/composables/useSimStore.ts b/web/src/composables/useSimStore.ts index 47168d2..28360af 100644 --- a/web/src/composables/useSimStore.ts +++ b/web/src/composables/useSimStore.ts @@ -28,6 +28,8 @@ export interface SimState { playing: boolean speed: PlaybackSpeed focusPanelId: string | null + editingPanelId: string | null // panel whose editor popover is open + failedPanelIds: Set // panels whose latest request failed (rose kebab dot) hoveredNode: number | null runState: RunState errorMessage: string | null // network/server failure, shown in the ErrorBanner @@ -54,6 +56,8 @@ export function createSimStore(preset: StudyPreset = deepfakeSchoolPreset) { playing: false, speed: 1, focusPanelId: null, + editingPanelId: null, + failedPanelIds: new Set(), hoveredNode: null, runState: 'idle', errorMessage: null, @@ -135,6 +139,10 @@ export function createSimStore(preset: StudyPreset = deepfakeSchoolPreset) { // Keep the last good results untouched; only the latest run may // surface its error. if (!isLatestRun) return + settled.forEach((outcome, index) => { + const panel = targets[index] + if (panel && outcome.status === 'rejected') state.failedPanelIds.add(panel.id) + }) const reason: unknown = firstFailure.reason state.runState = 'error' if (reason instanceof ApiError && reason.status === 400) { @@ -153,6 +161,7 @@ export function createSimStore(preset: StudyPreset = deepfakeSchoolPreset) { if (outcome?.status !== 'fulfilled') return state.resultsByPanelId[panel.id] = outcome.value.result state.edgesByGraphHash[graphKey(outcome.value.config)] = outcome.value.edges + state.failedPanelIds.delete(panel.id) }) if (!isLatestRun) return @@ -213,7 +222,9 @@ export function createSimStore(preset: StudyPreset = deepfakeSchoolPreset) { if (index < 0) return state.panels.splice(index, 1) delete state.resultsByPanelId[panelId] + state.failedPanelIds.delete(panelId) if (state.focusPanelId === panelId) state.focusPanelId = null + if (state.editingPanelId === panelId) state.editingPanelId = null syncToUrl() } diff --git a/web/src/lib/fieldLabels.ts b/web/src/lib/fieldLabels.ts new file mode 100644 index 0000000..153c82e --- /dev/null +++ b/web/src/lib/fieldLabels.ts @@ -0,0 +1,21 @@ +import type { Config, Strategy } from '@/types/engine' + +// One human name per Config field, shared by the controls card and the +// panel editor so the same lever never appears under two names. +export const CONFIG_FIELD_LABELS: Record = { + numStudents: 'Students', + edgesPerNode: 'Friends per student', + triangleProb: 'Clique tendency', + forwardProb: 'Chance to forward', + numEducated: 'Education budget', + origin: 'First poster', + graphSeed: 'Friendship network', + thresholdSeed: 'Who resists', + educationSeed: 'Random picks', +} + +export const STRATEGY_LABELS: Record = { + none: 'None', + random: 'Random', + 'most-connected': 'Most connected', +}
+ {{ store.state.validationError }} +