Skip to content

Commit 205e314

Browse files
committed
feat(frontend): add structured Build inspect panels with tables, utility targets, and temperature-node track
1 parent 41c27d1 commit 205e314

16 files changed

Lines changed: 620 additions & 130 deletions

src/App.tsx

Lines changed: 135 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import githubBlack from "./assets/GitHub_Invertocat_Black.svg";
1414
import githubWhite from "./assets/GitHub_Invertocat_White.svg";
1515
import type { PlotlyFigurePayload } from "./types/plotly";
1616
import 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+
54100
const API_KEY_STORAGE = "ei-api-key";
55101

56102
function 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}
379 KB
Binary file not shown.
344 KB
Binary file not shown.

src/components/ScalarSpecInline.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@ export function ScalarSpecInline(props: {
3535
const fieldDisabledClass = disabled ? "cursor-not-allowed opacity-80" : "";
3636

3737
const icon = spec.mode === "fixed" ? "≡" : "↔";
38+
const switchHint = spec.mode === "fixed"
39+
? "Click to switch to range input."
40+
: "Click to switch to single-value input.";
3841
const modeTitle = disabled
3942
? spec.mode === "fixed" ? "Fixed" : "Range"
4043
: lockMode
4144
? `${spec.mode === "fixed" ? "Fixed" : "Range"} (locked)`
42-
: `${spec.mode === "fixed" ? "Fixed" : "Range"} (click to change)`;
45+
: switchHint;
4346

4447
const gridCols = spec.mode === "fixed"
4548
? (showModeToggle ? "grid-cols-[24px_minmax(0,1fr)_auto]" : "grid-cols-[minmax(0,1fr)_auto]")

src/components/StreamDetail.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function StreamDetail(props: {
130130

131131
{!stream.useAdvancedHcoeff ? (
132132
unitNumberInline(
133-
"Cp (used as Hcoeff=(0,Cp,0,0,0,0))",
133+
"Cp (molar heat capacity at constant pressure)",
134134
stream.Cp,
135135
cpUnits,
136136
(u) => onChange({ ...stream, Cp: u }),
@@ -139,7 +139,7 @@ export function StreamDetail(props: {
139139
)
140140
) : (
141141
<div className="space-y-2">
142-
<div className={`text-sm ${mutedText}`}>Hcoeff6 (SI assumed). a1 is typically Cp.</div>
142+
<div className={`text-sm ${mutedText}`}>Hcoeff6 (SI assumed). The second input is Cp.</div>
143143
<div className="grid grid-cols-3 gap-2">
144144
{stream.Hcoeff6.map((v, i) => (
145145
<input
@@ -165,7 +165,7 @@ export function StreamDetail(props: {
165165
<div className={`text-sm ${mutedText}`}>Hvap is used for isothermal streams only.</div>
166166
)}
167167

168-
{unitNumberInline("HTC", stream.HTC, htcUnits, (u) => onChange({ ...stream, HTC: u }), false, theme)}
168+
{unitNumberInline("HTC (heat transfer coefficient)", stream.HTC, htcUnits, (u) => onChange({ ...stream, HTC: u }), false, theme)}
169169
</section>
170170
</div>
171171

0 commit comments

Comments
 (0)