Spaces:
Sleeping
Sleeping
File size: 8,514 Bytes
13d4e44 | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | // Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.
import { useState, useCallback, useMemo, type Dispatch, type SetStateAction } from 'react';
import type { ActionDetail, AnalysisResult } from '../types';
import { interactionLogger } from '../utils/interactionLogger';
export interface ActionsState {
selectedActionIds: Set<string>;
setSelectedActionIds: Dispatch<SetStateAction<Set<string>>>;
manuallyAddedIds: Set<string>;
setManuallyAddedIds: Dispatch<SetStateAction<Set<string>>>;
rejectedActionIds: Set<string>;
setRejectedActionIds: Dispatch<SetStateAction<Set<string>>>;
suggestedByRecommenderIds: Set<string>;
setSuggestedByRecommenderIds: Dispatch<SetStateAction<Set<string>>>;
handleActionFavorite: (actionId: string, setResult: Dispatch<SetStateAction<AnalysisResult | null>>) => void;
handleActionReject: (actionId: string) => void;
handleManualActionAdded: (
actionId: string,
detail: ActionDetail,
linesOverloaded: string[],
setResult: Dispatch<SetStateAction<AnalysisResult | null>>,
onSelectAction: (actionId: string) => void,
/**
* Provenance stamped on the new action card. Defaults to `"user"`
* (manual search dropdown / "Make a first guess"). The
* unsimulated-pin path passes the recommender model id instead,
* because that pin was scored by the model — the operator only
* triggered its materialisation.
*/
origin?: string,
) => void;
handleActionResimulated: (
actionId: string,
detail: ActionDetail,
linesOverloaded: string[],
setResult: Dispatch<SetStateAction<AnalysisResult | null>>,
onSelectAction: (actionId: string) => void,
) => void;
clearActionState: () => void;
}
export function useActions(): ActionsState {
const [selectedActionIds, setSelectedActionIds] = useState<Set<string>>(new Set());
const [manuallyAddedIds, setManuallyAddedIds] = useState<Set<string>>(new Set());
const [rejectedActionIds, setRejectedActionIds] = useState<Set<string>>(new Set());
const [suggestedByRecommenderIds, setSuggestedByRecommenderIds] = useState<Set<string>>(new Set());
const handleActionFavorite = useCallback((actionId: string, setResult: React.Dispatch<React.SetStateAction<AnalysisResult | null>>) => {
interactionLogger.record('action_favorited', { action_id: actionId });
setSelectedActionIds(prev => {
const next = new Set(prev);
next.add(actionId);
return next;
});
setResult(prev => {
if (!prev || !prev.actions[actionId]) return prev;
return {
...prev,
actions: {
...prev.actions,
[actionId]: { ...prev.actions[actionId], is_manual: true }
}
};
});
setRejectedActionIds(prev => {
const next = new Set(prev);
next.delete(actionId);
return next;
});
}, []);
const handleActionReject = useCallback((actionId: string) => {
interactionLogger.record('action_rejected', { action_id: actionId });
setRejectedActionIds(prev => {
const next = new Set(prev);
next.add(actionId);
return next;
});
setSelectedActionIds(prev => {
const next = new Set(prev);
next.delete(actionId);
return next;
});
setManuallyAddedIds(prev => {
const next = new Set(prev);
next.delete(actionId);
return next;
});
}, []);
const handleManualActionAdded = useCallback((
actionId: string,
detail: ActionDetail,
_linesOverloaded: string[],
setResult: React.Dispatch<React.SetStateAction<AnalysisResult | null>>,
onSelectAction: (actionId: string) => void,
origin: string = 'user',
) => {
interactionLogger.record('manual_action_simulated', { action_id: actionId });
setResult(prev => {
const base = prev || {
pdf_path: null,
pdf_url: null,
actions: {},
lines_overloaded: [],
message: '',
dc_fallback: false,
};
// ``_linesOverloaded`` is the manual-simulation backend response's
// ``lines_overloaded`` field. It carries grid2op's synthetic
// ``line_<i>`` names when the user hits "+ Manual Selection"
// before running analysis (no ``_analysis_context``; backend
// falls through to the vectorised obs-based path). Keeping
// ``lines_overloaded`` empty in that case lets ``App.tsx`` fall
// back to ``n1Diagram.lines_overloaded`` — the authoritative
// pypowsybl-style identifier list — for the card display.
// Step1 / session reload set it on their own setResult paths.
//
// ``origin`` is preserved if the action already exists with one
// (e.g. re-adding a recommender suggestion the operator pulled
// into the feed) — the first provenance wins.
const existing = base.actions[actionId];
return {
...base,
actions: {
...base.actions,
[actionId]: { ...detail, is_manual: true, origin: existing?.origin ?? origin },
},
};
});
setSelectedActionIds(prev => new Set(prev).add(actionId));
setManuallyAddedIds(prev => new Set(prev).add(actionId));
onSelectAction(actionId);
}, []);
// Re-simulating an existing action (e.g. editing the Target MW of a
// suggested load-shedding card and clicking "Re-simulate") must update
// the action's details in place WITHOUT promoting it into the Selected
// Actions list — otherwise a still-suggested action would silently jump
// into the selected bucket on every edit. We still trigger onSelectAction
// so the action-variant diagram is refetched for the new state.
const handleActionResimulated = useCallback((
actionId: string,
detail: ActionDetail,
_linesOverloaded: string[],
setResult: React.Dispatch<React.SetStateAction<AnalysisResult | null>>,
onSelectAction: (actionId: string) => void,
) => {
// NOTE: re-simulation events (action_mw_resimulated /
// pst_tap_resimulated) are logged at the call site in
// ActionFeed.tsx so the logger can capture the user-edited
// target value (MW or tap). This hook used to also log
// 'manual_action_simulated' which conflated the two flows
// and made replay impossible — the log entry now lives next
// to the actual button click instead.
//
// ``_linesOverloaded`` (the manual-sim response array) is
// intentionally NOT promoted to ``prev.lines_overloaded`` —
// see ``handleManualActionAdded`` for the rationale (the
// backend emits grid2op's synthetic ``line_<i>`` names when
// no analysis context has been set yet).
setResult(prev => {
if (!prev) return prev;
const existing = prev.actions[actionId];
return {
...prev,
actions: {
...prev.actions,
// Preserve the is_manual flag AND the origin from the
// existing entry — re-simulation changes the metrics, never
// the provenance: a recommender-suggested action stays
// recommender-suggested, a user action stays a user action.
[actionId]: {
...detail,
is_manual: existing?.is_manual ?? false,
origin: existing?.origin ?? detail.origin ?? 'user',
},
},
};
});
onSelectAction(actionId);
}, []);
const clearActionState = useCallback(() => {
setSelectedActionIds(new Set());
setManuallyAddedIds(new Set());
setRejectedActionIds(new Set());
setSuggestedByRecommenderIds(new Set());
}, []);
return useMemo(() => ({
selectedActionIds, setSelectedActionIds,
manuallyAddedIds, setManuallyAddedIds,
rejectedActionIds, setRejectedActionIds,
suggestedByRecommenderIds, setSuggestedByRecommenderIds,
handleActionFavorite,
handleActionReject,
handleManualActionAdded,
handleActionResimulated,
clearActionState,
}), [
selectedActionIds, manuallyAddedIds, rejectedActionIds, suggestedByRecommenderIds,
handleActionFavorite, handleActionReject, handleManualActionAdded, handleActionResimulated, clearActionState
]);
}
|