@@ -14,6 +14,7 @@ import githubBlack from "./assets/GitHub_Invertocat_Black.svg";
1414import githubWhite from "./assets/GitHub_Invertocat_White.svg" ;
1515import type { PlotlyFigurePayload } from "./types/plotly" ;
1616import type {
17+ BuildInspectPayload ,
1718 CellRef ,
1819 DetailMatrix ,
1920 EconomicReport ,
@@ -51,6 +52,51 @@ function toBackendResp(r: Response, body: unknown): { status: number; ok: boolea
5152 return { status : r . status , ok : r . ok , body : nextBody } ;
5253}
5354
55+ function asNumberArray ( x : unknown ) : number [ ] {
56+ if ( ! Array . isArray ( x ) ) return [ ] ;
57+ return x . map ( ( v ) => Number ( v ) ) ;
58+ }
59+
60+ function asNumberMatrix ( x : unknown ) : number [ ] [ ] {
61+ if ( ! Array . isArray ( x ) ) return [ ] ;
62+ return x . map ( ( row ) => ( Array . isArray ( row ) ? row . map ( ( v ) => Number ( v ) ) : [ ] ) ) ;
63+ }
64+
65+ function normalizeBuildInspect ( x : unknown ) : BuildInspectPayload | null {
66+ if ( ! x || typeof x !== "object" ) return null ;
67+ const raw = x as any ;
68+ const summary = raw . summary ;
69+ const problem = raw . problem_table ;
70+ const composite = raw . composite_curve ;
71+ if ( ! summary || typeof summary !== "object" ) return null ;
72+ if ( ! problem || typeof problem !== "object" ) return null ;
73+ if ( ! composite || typeof composite !== "object" ) return null ;
74+
75+ return {
76+ summary : {
77+ n_streams : Number ( summary . n_streams ?? 0 ) ,
78+ n_hot : Number ( summary . n_hot ?? 0 ) ,
79+ n_cold : Number ( summary . n_cold ?? 0 ) ,
80+ n_iso : Number ( summary . n_iso ?? 0 ) ,
81+ n_mvr : Number ( summary . n_mvr ?? 0 ) ,
82+ n_rk : Number ( summary . n_rk ?? 0 ) ,
83+ n_mhp : Number ( summary . n_mhp ?? 0 ) ,
84+ method_tgrid : String ( summary . method_tgrid ?? "" ) ,
85+ method_mvr : String ( summary . method_mvr ?? "" ) ,
86+ pinch_K : asNumberArray ( summary . pinch_K ) ,
87+ t_nodes_K : asNumberArray ( summary . t_nodes_K ) ,
88+ } ,
89+ problem_table : {
90+ columns : Array . isArray ( problem . columns ) ? problem . columns . map ( ( c : unknown ) => String ( c ) ) : [ ] ,
91+ rows : asNumberMatrix ( problem . rows ) ,
92+ } ,
93+ composite_curve : {
94+ columns : Array . isArray ( composite . columns ) ? composite . columns . map ( ( c : unknown ) => String ( c ) ) : [ ] ,
95+ rows : asNumberMatrix ( composite . rows ) ,
96+ } ,
97+ } ;
98+ }
99+
54100const API_KEY_STORAGE = "ei-api-key" ;
55101
56102function getStoredApiKey ( ) : string {
@@ -283,7 +329,10 @@ export default function App() {
283329 const [ uiMsg , setUiMsg ] = useState < string > ( "" ) ;
284330 const [ backendResp , setBackendResp ] = useState < { status : number ; ok : boolean ; body : unknown } | null > ( null ) ;
285331 const [ henPlot , setHenPlot ] = useState < PlotlyFigurePayload | null > ( null ) ;
332+ const [ buildInspect , setBuildInspect ] = useState < BuildInspectPayload | null > ( null ) ;
286333 const [ henReady , setHenReady ] = useState < boolean > ( false ) ;
334+ const [ isBuildingHEN , setIsBuildingHEN ] = useState < boolean > ( false ) ;
335+ const [ isSolving , setIsSolving ] = useState < boolean > ( false ) ;
287336 const [ resultsReady , setResultsReady ] = useState < boolean > ( false ) ;
288337 const [ henId , setHenId ] = useState < string | null > ( null ) ;
289338 const [ resultHotOrder , setResultHotOrder ] = useState < string [ ] > ( [ ] ) ;
@@ -316,6 +365,8 @@ export default function App() {
316365 const consoleBoxRef = useRef < HTMLDivElement > ( null ) ;
317366 const consoleEsRef = useRef < EventSource | null > ( null ) ;
318367 const streamsetFileRef = useRef < HTMLInputElement | null > ( null ) ;
368+ const buildHenPendingRef = useRef ( false ) ;
369+ const solvePendingRef = useRef ( false ) ;
319370 const dragRef = useRef < { axis : "hot" | "cold" ; index : number } | null > ( null ) ;
320371 const hoverKeyRef = useRef < string | null > ( null ) ;
321372 const hoverTimerRef = useRef < number | null > ( null ) ;
@@ -559,6 +610,8 @@ export default function App() {
559610 } , [ theme ] ) ;
560611
561612 useEffect ( ( ) => {
613+ setHenPlot ( null ) ;
614+ setBuildInspect ( null ) ;
562615 setHenReady ( false ) ;
563616 setResultsReady ( false ) ;
564617 setHenId ( null ) ;
@@ -639,14 +692,17 @@ export default function App() {
639692 } , [ tooltipCell , tooltipMatrix , tooltipLoading , tooltipError ] ) ;
640693
641694 async function buildHEN ( ) {
642- setUiMsg ( "" ) ;
643- setBackendResp ( null ) ;
644- setHenPlot ( null ) ;
645- setHenReady ( false ) ;
646- setHenId ( null ) ;
647- const payload = buildPayloadSI ( streams , intervalsConfig ) ;
648-
695+ if ( buildHenPendingRef . current ) return ;
696+ buildHenPendingRef . current = true ;
697+ setIsBuildingHEN ( true ) ;
649698 try {
699+ setUiMsg ( "" ) ;
700+ setBackendResp ( null ) ;
701+ setHenPlot ( null ) ;
702+ setBuildInspect ( null ) ;
703+ setHenReady ( false ) ;
704+ setHenId ( null ) ;
705+ const payload = buildPayloadSI ( streams , intervalsConfig ) ;
650706 const { r, body } = await apiFetch ( "/api/streams" , {
651707 method : "POST" ,
652708 headers : { "Content-Type" : "application/json" } ,
@@ -658,75 +714,87 @@ export default function App() {
658714 const nextHenId = String ( ( body as any ) ?. hen_id ?? "" ) ;
659715 setHenId ( nextHenId || null ) ;
660716 setHenPlot ( fig && typeof fig === "object" ? ( fig as PlotlyFigurePayload ) : null ) ;
717+ setBuildInspect ( normalizeBuildInspect ( ( body as any ) ?. build_inspect ) ) ;
661718 setHenReady ( true ) ;
662719 setStep ( "build" ) ;
663720 }
664721 } catch ( e : any ) {
665722 setBackendResp ( { status : 0 , ok : false , body : { message : "Request failed" , error : String ( e ) } } ) ;
723+ } finally {
724+ buildHenPendingRef . current = false ;
725+ setIsBuildingHEN ( false ) ;
666726 }
667727 }
668728
669729 async function solveMILP ( ) {
670- setUiMsg ( "" ) ;
671- setBackendResp ( null ) ;
730+ if ( solvePendingRef . current ) return ;
731+ solvePendingRef . current = true ;
732+ setIsSolving ( true ) ;
733+ try {
734+ setUiMsg ( "" ) ;
735+ setBackendResp ( null ) ;
672736
673- if ( ! henReady ) {
674- setUiMsg ( "Build HEN first." ) ;
675- return ;
676- }
677- if ( ! henId ) {
678- setBackendResp ( { status : 400 , ok : false , body : { message : "hen_id is required. Build HEN first to get hen_id." } } ) ;
679- return ;
680- }
737+ if ( ! henReady ) {
738+ setUiMsg ( "Build HEN first." ) ;
739+ return ;
740+ }
741+ if ( ! henId ) {
742+ setBackendResp ( { status : 400 , ok : false , body : { message : "hen_id is required. Build HEN first to get hen_id." } } ) ;
743+ return ;
744+ }
681745
682- try {
683- const { r, body } = await apiFetch ( "/api/solve" , {
684- method : "POST" ,
685- headers : { "Content-Type" : "application/json" } ,
686- body : JSON . stringify ( { hen_id : henId } ) ,
687- } , { promptOn401 : false } ) ;
688- setBackendResp ( toBackendResp ( r , body ) ) ;
689- if ( r . ok && body && typeof body === "object" && ( body as any ) . ok ) {
690- setUiMsg ( "Solve completed." ) ;
691- detailCacheRef . current . clear ( ) ;
692- streamDetailCacheRef . current . clear ( ) ;
693- const hot = Array . isArray ( ( body as any ) . hot_names ) ? ( body as any ) . hot_names . map ( ( x : any ) => String ( x ) ) : [ ] ;
694- const cold = Array . isArray ( ( body as any ) . cold_names ) ? ( body as any ) . cold_names . map ( ( x : any ) => String ( x ) ) : [ ] ;
695- const edgesRaw = Array . isArray ( ( body as any ) . edges ) ? ( body as any ) . edges : [ ] ;
696- const edges : ResultEdge [ ] = edgesRaw
697- . map ( ( x : any ) => ( {
698- hot : String ( x ?. hot ?? "" ) ,
699- cold : String ( x ?. cold ?? "" ) ,
700- q_total : Number ( x ?. q_total ?? 0 ) ,
701- } ) )
702- . filter ( ( x : ResultEdge ) => x . hot && x . cold ) ;
703-
704- setResultHotOrder ( hot ) ;
705- setResultColdOrder ( cold ) ;
706- setResultEdges ( edges ) ;
707- setResultObjValue ( Number ( ( body as any ) . obj_value ?? NaN ) ) ;
708- const reportRaw = ( body as any ) . solution_report ;
709- setSolutionReport ( reportRaw && typeof reportRaw === "object" ? ( reportRaw as SolutionReport ) : null ) ;
710- const econRaw = ( body as any ) . economic_report ;
711- if ( econRaw && typeof econRaw === "object" ) {
712- const column_labels = Array . isArray ( ( econRaw as any ) . column_labels )
713- ? ( econRaw as any ) . column_labels . map ( ( x : any ) => String ( x ) )
714- : [ ] ;
715- const row_labels = Array . isArray ( ( econRaw as any ) . row_labels )
716- ? ( econRaw as any ) . row_labels . map ( ( x : any ) => String ( x ) )
717- : [ ] ;
718- const data = Array . isArray ( ( econRaw as any ) . data )
719- ? ( econRaw as any ) . data . map ( ( row : any ) => ( Array . isArray ( row ) ? row . map ( ( v : any ) => String ( v ) ) : [ ] ) )
720- : [ ] ;
721- setEconomicReport ( { column_labels, row_labels, data } ) ;
722- } else {
723- setEconomicReport ( null ) ;
746+ try {
747+ const { r, body } = await apiFetch ( "/api/solve" , {
748+ method : "POST" ,
749+ headers : { "Content-Type" : "application/json" } ,
750+ body : JSON . stringify ( { hen_id : henId } ) ,
751+ } , { promptOn401 : false } ) ;
752+ setBackendResp ( toBackendResp ( r , body ) ) ;
753+ if ( r . ok && body && typeof body === "object" && ( body as any ) . ok ) {
754+ setUiMsg ( "Solve completed." ) ;
755+ detailCacheRef . current . clear ( ) ;
756+ streamDetailCacheRef . current . clear ( ) ;
757+ const hot = Array . isArray ( ( body as any ) . hot_names ) ? ( body as any ) . hot_names . map ( ( x : any ) => String ( x ) ) : [ ] ;
758+ const cold = Array . isArray ( ( body as any ) . cold_names ) ? ( body as any ) . cold_names . map ( ( x : any ) => String ( x ) ) : [ ] ;
759+ const edgesRaw = Array . isArray ( ( body as any ) . edges ) ? ( body as any ) . edges : [ ] ;
760+ const edges : ResultEdge [ ] = edgesRaw
761+ . map ( ( x : any ) => ( {
762+ hot : String ( x ?. hot ?? "" ) ,
763+ cold : String ( x ?. cold ?? "" ) ,
764+ q_total : Number ( x ?. q_total ?? 0 ) ,
765+ } ) )
766+ . filter ( ( x : ResultEdge ) => x . hot && x . cold ) ;
767+
768+ setResultHotOrder ( hot ) ;
769+ setResultColdOrder ( cold ) ;
770+ setResultEdges ( edges ) ;
771+ setResultObjValue ( Number ( ( body as any ) . obj_value ?? NaN ) ) ;
772+ const reportRaw = ( body as any ) . solution_report ;
773+ setSolutionReport ( reportRaw && typeof reportRaw === "object" ? ( reportRaw as SolutionReport ) : null ) ;
774+ const econRaw = ( body as any ) . economic_report ;
775+ if ( econRaw && typeof econRaw === "object" ) {
776+ const column_labels = Array . isArray ( ( econRaw as any ) . column_labels )
777+ ? ( econRaw as any ) . column_labels . map ( ( x : any ) => String ( x ) )
778+ : [ ] ;
779+ const row_labels = Array . isArray ( ( econRaw as any ) . row_labels )
780+ ? ( econRaw as any ) . row_labels . map ( ( x : any ) => String ( x ) )
781+ : [ ] ;
782+ const data = Array . isArray ( ( econRaw as any ) . data )
783+ ? ( econRaw as any ) . data . map ( ( row : any ) => ( Array . isArray ( row ) ? row . map ( ( v : any ) => String ( v ) ) : [ ] ) )
784+ : [ ] ;
785+ setEconomicReport ( { column_labels, row_labels, data } ) ;
786+ } else {
787+ setEconomicReport ( null ) ;
788+ }
789+ setResultsReady ( true ) ;
790+ setStep ( "results" ) ;
724791 }
725- setResultsReady ( true ) ;
726- setStep ( "results" ) ;
792+ } catch ( e : any ) {
793+ setBackendResp ( { status : 0 , ok : false , body : { message : "Request failed" , error : String ( e ) } } ) ;
727794 }
728- } catch ( e : any ) {
729- setBackendResp ( { status : 0 , ok : false , body : { message : "Request failed" , error : String ( e ) } } ) ;
795+ } finally {
796+ solvePendingRef . current = false ;
797+ setIsSolving ( false ) ;
730798 }
731799 }
732800
@@ -932,6 +1000,7 @@ export default function App() {
9321000 onResetIntervals = { ( ) => setIntervalsConfig ( defaultIntervalsConfigUI ( ) ) }
9331001 onAddStream = { addStream }
9341002 onBuildHEN = { buildHEN }
1003+ isBuildingHEN = { isBuildingHEN }
9351004 onUpdateStream = { updateStream }
9361005 onDeleteStream = { deleteStream }
9371006 onDuplicateStream = { duplicateStream }
@@ -944,14 +1013,17 @@ export default function App() {
9441013 < BuildStep
9451014 hasError = { hasError }
9461015 onBuildHEN = { buildHEN }
1016+ isBuildingHEN = { isBuildingHEN }
9471017 buttonTone = { buttonTone }
9481018 panelTone = { panelTone }
9491019 henPlot = { henPlot }
1020+ buildInspect = { buildInspect }
9501021 theme = { theme }
9511022 />
9521023 ) : step === "solve" ? (
9531024 < SolveStep
9541025 henReady = { henReady }
1026+ isSolving = { isSolving }
9551027 onSolve = { solveMILP }
9561028 buttonTone = { buttonTone }
9571029 panelTone = { panelTone }
0 commit comments