// 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 React, { useMemo, useRef, useState } from 'react'; import { colors, radius, space, text } from '../styles/tokens'; interface AdditionalLinesPickerProps { branches: string[]; /** Lines already detected as N-1 overloads — excluded from suggestions. */ n1Overloads: string[]; additionalLinesToCut: Set; onToggle: (line: string) => void; /** Resolve an element ID to its human-readable display name. Falls back to the ID. */ displayName?: (id: string) => string; } /** * Operator-controlled extra "lines to cut" tied to the recommender * analysis (ExpertAgent's `additionalLinesToCut`/`ltc` semantic). * Sits directly above the "Analyze & Suggest" button so the operator * configures the extra targets right where the analysis is launched. * * Lines selected here are simulated as disconnected during the * overflow analysis to prevent flow increase on them, but are NOT * rendered as overloads. Useful for highlighting alternate path * flows in double/triple-line corridors when only studying N-1. */ const AdditionalLinesPicker: React.FC = ({ branches, n1Overloads, additionalLinesToCut, onToggle, displayName = (id: string) => id, }) => { const [query, setQuery] = useState(''); const [focused, setFocused] = useState(false); const closeTimer = useRef(null); const detectedSet = useMemo(() => new Set(n1Overloads), [n1Overloads]); const suggestions = useMemo(() => { const q = query.trim().toUpperCase(); return branches .filter(b => !detectedSet.has(b) && !additionalLinesToCut.has(b)) .filter(b => q === '' || b.toUpperCase().includes(q) || displayName(b).toUpperCase().includes(q)) .slice(0, 50); }, [branches, detectedSet, additionalLinesToCut, query, displayName]); const commit = (line: string) => { if (!line) return; if (additionalLinesToCut.has(line)) return; if (detectedSet.has(line)) return; if (!branches.includes(line)) return; onToggle(line); setQuery(''); }; return (
Additional lines to prevent flow increase: ? {Array.from(additionalLinesToCut).map(line => ( {displayName(line)} ))} setQuery(e.target.value)} onFocus={() => { if (closeTimer.current !== null) { window.clearTimeout(closeTimer.current); closeTimer.current = null; } setFocused(true); }} onBlur={() => { closeTimer.current = window.setTimeout(() => { setFocused(false); closeTimer.current = null; }, 120); }} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); const exact = suggestions.find( s => s.toUpperCase() === query.trim().toUpperCase(), ); if (exact) commit(exact); else if (suggestions.length === 1) commit(suggestions[0]); } else if (e.key === 'Escape') { setQuery(''); } }} style={{ fontSize: text.xs, padding: '2px 6px', border: `1px solid ${colors.border}`, borderRadius: radius.sm, minWidth: '140px', }} /> {focused && query.length > 0 && suggestions.length > 0 && (
{suggestions.map(line => (
{ e.preventDefault(); commit(line); }} style={{ padding: '4px 8px', fontSize: text.xs, cursor: 'pointer', color: colors.textPrimary, }} onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.background = colors.surfaceMuted; }} onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.background = 'transparent'; }} > {displayName(line)}
))}
)}
); }; export default React.memo(AdditionalLinesPicker);