// 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 axios from 'axios'; import type { ConfigRequest, BranchResponse, DiagramData, DiagramPatch, FlowDelta, AssetDelta, AvailableAction, SessionResult, VlInjection, FeederLabel } from './types'; // Same-origin by default when built with `VITE_API_BASE_URL=''` (the // frontend is served by the backend, e.g. on a HuggingFace Docker Space): // every request becomes a relative `/api/...` URL. When the variable is // unset — local dev (`npm run dev` on :5173) and the Vitest suite — it falls // back to the standalone backend at :8000. export const API_BASE_URL: string = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? 'http://127.0.0.1:8000'; export interface UserConfig { network_path: string; action_file_path: string; layout_path: string; output_folder_path: string; lines_monitoring_path: string; min_line_reconnections: number; min_close_coupling: number; min_open_coupling: number; min_line_disconnections: number; min_pst: number; min_load_shedding: number; min_renewable_curtailment_actions: number; min_redispatch: number; allowed_action_types?: string[]; n_prioritized_actions: number; monitoring_factor: number; pre_existing_overload_threshold: number; ignore_reconnections: boolean; pypowsybl_fast_mode: boolean; // Pluggable recommender selection (see GET /api/models). // Defaults to the expert system for backwards compatibility. model?: string; // Toggles the (expensive) step-2 overflow graph build. Only takes // effect when the chosen model declares requires_overflow_graph=true. compute_overflow_graph?: boolean; } // Mirrors expert_backend.recommenders.registry.list_models() output. // Consumed by SettingsModal to render the model dropdown and only the // parameter inputs the selected model actually consumes. export interface ModelParamSpec { name: string; label: string; kind: 'int' | 'float' | 'bool'; default: number | boolean | null; min?: number | null; max?: number | null; description?: string | null; group?: string | null; } export interface ModelDescriptor { name: string; label: string; requires_overflow_graph: boolean; is_default: boolean; params: ModelParamSpec[]; } export const api = { getUserConfig: async (): Promise => { const response = await axios.get(`${API_BASE_URL}/api/user-config`); return response.data; }, saveUserConfig: async (config: UserConfig): Promise => { await axios.post(`${API_BASE_URL}/api/user-config`, config); }, getConfigFilePath: async (): Promise => { const response = await axios.get<{ config_file_path: string }>(`${API_BASE_URL}/api/config-file-path`); return response.data.config_file_path; }, setConfigFilePath: async (path: string): Promise<{ config_file_path: string; config: UserConfig }> => { const response = await axios.post<{ status: string; config_file_path: string; config: UserConfig }>( `${API_BASE_URL}/api/config-file-path`, { path } ); return response.data; }, updateConfig: async (config: ConfigRequest) => { const response = await axios.post(`${API_BASE_URL}/api/config`, config); return response.data; }, /** * Lists the recommendation models registered on the backend. Called * once at startup by `useSettings`; the dropdown in the Settings * → Recommender tab is populated from the response and each model's * `params_spec` drives which parameter inputs are visible. */ getModels: async (): Promise<{ models: ModelDescriptor[] }> => { const response = await axios.get<{ models: ModelDescriptor[] }>(`${API_BASE_URL}/api/models`); return response.data; }, getBranches: async () => { const response = await axios.get(`${API_BASE_URL}/api/branches`); return response.data; }, getVoltageLevels: async (): Promise<{ voltage_levels: string[]; name_map?: Record }> => { const response = await axios.get<{ voltage_levels: string[]; name_map?: Record }>(`${API_BASE_URL}/api/voltage-levels`); return response.data; }, getNominalVoltages: async (): Promise<{ mapping: Record; unique_kv: number[] }> => { const response = await axios.get<{ mapping: Record; unique_kv: number[] }>( `${API_BASE_URL}/api/nominal-voltages` ); return response.data; }, getVoltageLevelSubstations: async (): Promise<{ mapping: Record }> => { const response = await axios.get<{ mapping: Record }>( `${API_BASE_URL}/api/voltage-level-substations` ); return response.data; }, getNetworkDiagram: async (): Promise => { const res = await fetch(`${API_BASE_URL}/api/network-diagram?format=text`); if (!res.ok) { const detail = await res.text().catch(() => ''); const err = new Error(`HTTP ${res.status}: ${detail || res.statusText}`) as Error & { response?: unknown }; err.response = { status: res.status, data: { detail } }; throw err; } const body = await res.text(); const nl = body.indexOf('\n'); if (nl < 0) { throw new Error('Invalid /api/network-diagram response: missing header/body separator'); } const header = JSON.parse(body.slice(0, nl)) as Omit; const svg = body.slice(nl + 1); return { ...header, svg } as DiagramData & { svg: string }; }, getContingencyDiagram: async (disconnectedElements: string[]): Promise => { const response = await axios.post( `${API_BASE_URL}/api/contingency-diagram`, { disconnected_elements: disconnectedElements } ); return response.data; }, getActionVariantDiagram: async (actionId: string): Promise => { const response = await axios.post( `${API_BASE_URL}/api/action-variant-diagram`, { action_id: actionId } ); return response.data; }, getContingencyDiagramPatch: async (disconnectedElements: string[]): Promise => { const response = await axios.post( `${API_BASE_URL}/api/contingency-diagram-patch`, { disconnected_elements: disconnectedElements } ); return response.data; }, getActionVariantDiagramPatch: async (actionId: string): Promise => { const response = await axios.post( `${API_BASE_URL}/api/action-variant-diagram-patch`, { action_id: actionId } ); return response.data; }, getAvailableActions: async (): Promise => { const response = await axios.get<{ actions: AvailableAction[] }>( `${API_BASE_URL}/api/actions` ); return response.data.actions; }, simulateManualAction: async (actionId: string, disconnectedElements: string[], actionContent?: Record | null, linesOverloaded?: string[] | null, targetMw?: number | null, targetTap?: number | null): Promise<{ action_id: string; description_unitaire: string; rho_before: number[] | null; rho_after: number[] | null; max_rho: number | null; max_rho_line: string; is_rho_reduction: boolean; is_islanded?: boolean; n_components?: number; disconnected_mw?: number; non_convergence: string | null; lines_overloaded: string[]; lines_overloaded_after?: string[]; half_open_overloads?: Record; action_topology?: import('./types').ActionTopology; load_shedding_details?: import('./types').LoadSheddingDetail[]; curtailment_details?: import('./types').CurtailmentDetail[]; redispatch_details?: import('./types').RedispatchDetail[]; pst_details?: import('./types').PstDetail[]; }> => { const response = await axios.post( `${API_BASE_URL}/api/simulate-manual-action`, { action_id: actionId, disconnected_elements: disconnectedElements, action_content: actionContent ?? null, lines_overloaded: linesOverloaded ?? null, target_mw: targetMw ?? null, target_tap: targetTap ?? null } ); return response.data; }, computeSuperposition: async (action1_id: string, action2_id: string, disconnectedElements: string[]): Promise => { const response = await axios.post( `${API_BASE_URL}/api/compute-superposition`, { action1_id, action2_id, disconnected_elements: disconnectedElements } ); return response.data; }, regenerateOverflowGraph: async (mode: 'hierarchical' | 'geo'): Promise<{ pdf_url: string | null; pdf_path: string | null; mode: string; cached: boolean; }> => { const response = await axios.post<{ pdf_url: string | null; pdf_path: string | null; mode: string; cached: boolean; }>(`${API_BASE_URL}/api/regenerate-overflow-graph`, { mode }); return response.data; }, pickPath: async (type: 'file' | 'dir'): Promise => { const response = await axios.get<{ path: string | null; error?: string }>( `${API_BASE_URL}/api/pick-path?type=${type}` ); if (response.data.error) { throw new Error(response.data.error); } return response.data.path; }, saveSession: async (params: { session_name: string; json_content: string; pdf_path: string | null; output_folder_path: string; interaction_log?: string; }): Promise<{ session_folder: string; pdf_copied: boolean }> => { const response = await axios.post<{ session_folder: string; pdf_copied: boolean }>( `${API_BASE_URL}/api/save-session`, params ); return response.data; }, getNSld: async (voltageLevelId: string): Promise<{ svg: string; sld_metadata: string | null; voltage_level_id: string; switch_states?: Record; injections?: Record; feeder_labels?: Record }> => { const response = await axios.post<{ svg: string; sld_metadata: string | null; voltage_level_id: string; switch_states?: Record; injections?: Record; feeder_labels?: Record }>( `${API_BASE_URL}/api/n-sld`, { voltage_level_id: voltageLevelId } ); return response.data; }, getContingencySld: async (disconnectedElements: string[], voltageLevelId: string): Promise<{ svg: string; sld_metadata: string | null; voltage_level_id: string; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; switch_states?: Record; injections?: Record; feeder_labels?: Record }> => { const response = await axios.post<{ svg: string; sld_metadata: string | null; voltage_level_id: string; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; switch_states?: Record; injections?: Record; feeder_labels?: Record }>( `${API_BASE_URL}/api/contingency-sld`, { disconnected_elements: disconnectedElements, voltage_level_id: voltageLevelId } ); return response.data; }, /** * Target-topology preview: re-render the VL SLD with the staged * switch overrides applied (topological colouring, no load flow). * `stale_flows` is always true — the caller greys the flow values. */ getSldTopologyPreview: async (params: { voltageLevelId: string; disconnectedElements: string[]; switches: Record; baseActionId?: string | null; }): Promise<{ svg: string; sld_metadata: string | null; voltage_level_id: string; switch_states?: Record; stale_flows?: boolean; feeder_labels?: Record }> => { const response = await axios.post<{ svg: string; sld_metadata: string | null; voltage_level_id: string; switch_states?: Record; stale_flows?: boolean; feeder_labels?: Record }>( `${API_BASE_URL}/api/sld-topology-preview`, { voltage_level_id: params.voltageLevelId, disconnected_elements: params.disconnectedElements, switches: params.switches, base_action_id: params.baseActionId ?? null, } ); return response.data; }, getActionVariantSld: async (actionId: string, voltageLevelId: string): Promise<{ svg: string; sld_metadata: string | null; action_id: string; voltage_level_id: string; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; changed_switches?: Record; switch_states?: Record; injections?: Record; feeder_labels?: Record }> => { const response = await axios.post<{ svg: string; sld_metadata: string | null; action_id: string; voltage_level_id: string; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; changed_switches?: Record; switch_states?: Record; injections?: Record; feeder_labels?: Record }>( `${API_BASE_URL}/api/action-variant-sld`, { action_id: actionId, voltage_level_id: voltageLevelId } ); return response.data; }, listSessions: async (folderPath: string): Promise<{ sessions: string[] }> => { const response = await axios.get<{ sessions: string[] }>( `${API_BASE_URL}/api/list-sessions`, { params: { folder_path: folderPath } } ); return response.data; }, loadSession: async (folderPath: string, sessionName: string): Promise => { const response = await axios.post( `${API_BASE_URL}/api/load-session`, { folder_path: folderPath, session_name: sessionName } ); return response.data; }, restoreAnalysisContext: async (params: { lines_we_care_about?: string[] | null; disconnected_elements?: string[] | null; lines_overloaded?: string[] | null; computed_pairs?: Record | null; }): Promise<{ status: string; lines_we_care_about_count: number; computed_pairs_count: number }> => { const response = await axios.post(`${API_BASE_URL}/api/restore-analysis-context`, params); return response.data; }, setRecommenderModel: async ( model: string, computeOverflowGraph: boolean, ): Promise<{ status: string; active_model: string; compute_overflow_graph: boolean }> => { const response = await axios.post(`${API_BASE_URL}/api/recommender-model`, { model, compute_overflow_graph: computeOverflowGraph, }); return response.data; }, runAnalysisStep1: async (disconnectedElements: string[], signal?: AbortSignal): Promise<{ lines_overloaded: string[]; message: string; can_proceed: boolean }> => { const response = await axios.post(`${API_BASE_URL}/api/run-analysis-step1`, { disconnected_elements: disconnectedElements }, { signal }); return response.data; }, runAnalysisStep2Stream: async (params: { selected_overloads: string[]; all_overloads: string[]; monitor_deselected: boolean; additional_lines_to_cut?: string[]; }, signal?: AbortSignal): Promise => { const response = await fetch(`${API_BASE_URL}/api/run-analysis-step2`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...params, additional_lines_to_cut: params.additional_lines_to_cut ?? [], }), signal, }); if (!response.ok) { throw new Error(`Analysis Resolution failed: ${response.statusText}`); } return response; }, simulateAndVariantDiagramStream: async (params: { action_id: string; disconnected_elements: string[]; action_content?: Record | null; lines_overloaded?: string[] | null; target_mw?: number | null; target_tap?: number | null; voltage_level_id?: string | null; mode?: 'network' | 'delta'; }): Promise => { const response = await fetch(`${API_BASE_URL}/api/simulate-and-variant-diagram`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action_id: params.action_id, disconnected_elements: params.disconnected_elements, action_content: params.action_content ?? null, lines_overloaded: params.lines_overloaded ?? null, target_mw: params.target_mw ?? null, target_tap: params.target_tap ?? null, voltage_level_id: params.voltage_level_id ?? null, mode: params.mode ?? 'network', }), }); if (!response.ok) { throw new Error(`Simulate-and-variant-diagram failed: ${response.statusText}`); } return response; }, };