bshepp commited on
Commit
40a59f7
·
1 Parent(s): 7cdc1a2

feat: add retry/reset buttons, report .md download, improved warm-up indicator

Browse files
src/frontend/src/app/page.tsx CHANGED
@@ -1,17 +1,20 @@
1
  "use client";
2
 
3
- import { useState } from "react";
4
  import { PatientInput } from "@/components/PatientInput";
5
  import { AgentPipeline } from "@/components/AgentPipeline";
6
  import { CDSReport } from "@/components/CDSReport";
7
  import { useAgentWebSocket } from "@/hooks/useAgentWebSocket";
 
8
 
9
  export default function Home() {
10
- const { steps, report, isRunning, isWarmingUp, warmUpMessage, error, submitCase } = useAgentWebSocket();
11
  const [hasSubmitted, setHasSubmitted] = useState(false);
 
12
 
13
  const handleSubmit = (patientText: string) => {
14
  setHasSubmitted(true);
 
15
  submitCase({
16
  patient_text: patientText,
17
  include_drug_check: true,
@@ -19,6 +22,32 @@ export default function Home() {
19
  });
20
  };
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  return (
23
  <main className="min-h-screen">
24
  {/* Header */}
@@ -61,9 +90,46 @@ export default function Home() {
61
  {/* Agent Pipeline (left) */}
62
  <div className="lg:col-span-1">
63
  <AgentPipeline steps={steps} isRunning={isRunning} />
 
 
64
  {error && (
65
- <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
66
- {error}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  </div>
68
  )}
69
  </div>
@@ -73,17 +139,25 @@ export default function Home() {
73
  {report ? (
74
  <CDSReport report={report} />
75
  ) : isWarmingUp ? (
76
- <div className="flex items-center justify-center h-64 text-amber-600">
77
- <div className="text-center">
78
- <div className="animate-pulse w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center mx-auto mb-4">
79
- <span className="text-xl">&#9881;</span>
 
 
 
 
80
  </div>
81
- <p className="font-medium">Model Warming Up</p>
82
- <p className="text-sm text-amber-500 mt-1">
 
 
83
  {warmUpMessage || "Waiting for MedGemma endpoint..."}
84
  </p>
85
- <p className="text-xs text-gray-400 mt-2">
86
- This happens when the model scales from zero. Usually takes 1-2 minutes.
 
 
87
  </p>
88
  </div>
89
  </div>
@@ -94,6 +168,31 @@ export default function Home() {
94
  <p>Agent pipeline running...</p>
95
  </div>
96
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  ) : null}
98
  </div>
99
  </div>
 
1
  "use client";
2
 
3
+ import { useState, useCallback } from "react";
4
  import { PatientInput } from "@/components/PatientInput";
5
  import { AgentPipeline } from "@/components/AgentPipeline";
6
  import { CDSReport } from "@/components/CDSReport";
7
  import { useAgentWebSocket } from "@/hooks/useAgentWebSocket";
8
+ import { reportToMarkdown } from "@/lib/reportToMarkdown";
9
 
10
  export default function Home() {
11
+ const { steps, report, isRunning, isWarmingUp, warmUpMessage, error, submitCase, reset } = useAgentWebSocket();
12
  const [hasSubmitted, setHasSubmitted] = useState(false);
13
+ const [lastPatientText, setLastPatientText] = useState("");
14
 
15
  const handleSubmit = (patientText: string) => {
16
  setHasSubmitted(true);
17
+ setLastPatientText(patientText);
18
  submitCase({
19
  patient_text: patientText,
20
  include_drug_check: true,
 
22
  });
23
  };
24
 
25
+ const handleNewCase = useCallback(() => {
26
+ reset();
27
+ setHasSubmitted(false);
28
+ setLastPatientText("");
29
+ }, [reset]);
30
+
31
+ const handleRetry = useCallback(() => {
32
+ if (lastPatientText) {
33
+ handleSubmit(lastPatientText);
34
+ }
35
+ }, [lastPatientText]);
36
+
37
+ const handleDownload = useCallback(() => {
38
+ if (!report) return;
39
+ const md = reportToMarkdown(report);
40
+ const blob = new Blob([md], { type: "text/markdown;charset=utf-8" });
41
+ const url = URL.createObjectURL(blob);
42
+ const a = document.createElement("a");
43
+ a.href = url;
44
+ a.download = `cds-report-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.md`;
45
+ document.body.appendChild(a);
46
+ a.click();
47
+ document.body.removeChild(a);
48
+ URL.revokeObjectURL(url);
49
+ }, [report]);
50
+
51
  return (
52
  <main className="min-h-screen">
53
  {/* Header */}
 
90
  {/* Agent Pipeline (left) */}
91
  <div className="lg:col-span-1">
92
  <AgentPipeline steps={steps} isRunning={isRunning} />
93
+
94
+ {/* Error display with retry/reset */}
95
  {error && (
96
+ <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
97
+ <p className="text-red-700 text-sm mb-3">{error}</p>
98
+ <div className="flex gap-2">
99
+ <button
100
+ onClick={handleRetry}
101
+ className="px-3 py-1.5 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"
102
+ >
103
+ Try Again
104
+ </button>
105
+ <button
106
+ onClick={handleNewCase}
107
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors"
108
+ >
109
+ New Case
110
+ </button>
111
+ </div>
112
+ </div>
113
+ )}
114
+
115
+ {/* New Case button when pipeline finished (not running, no error) */}
116
+ {!isRunning && !error && steps.length > 0 && (
117
+ <div className="mt-4 flex gap-2">
118
+ <button
119
+ onClick={handleNewCase}
120
+ className="flex-1 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 border border-blue-200 rounded-lg transition-colors"
121
+ >
122
+ New Case
123
+ </button>
124
+ {report && (
125
+ <button
126
+ onClick={handleDownload}
127
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border border-gray-300 rounded-lg transition-colors"
128
+ title="Download report as Markdown"
129
+ >
130
+ Download .md
131
+ </button>
132
+ )}
133
  </div>
134
  )}
135
  </div>
 
139
  {report ? (
140
  <CDSReport report={report} />
141
  ) : isWarmingUp ? (
142
+ <div className="flex items-center justify-center h-64">
143
+ <div className="text-center max-w-md">
144
+ <div className="relative mx-auto mb-5 w-14 h-14">
145
+ <div className="absolute inset-0 rounded-full border-4 border-amber-200" />
146
+ <div className="absolute inset-0 rounded-full border-4 border-amber-500 border-t-transparent animate-spin" />
147
+ <div className="absolute inset-0 flex items-center justify-center text-xl text-amber-600">
148
+ &#9881;
149
+ </div>
150
  </div>
151
+ <p className="font-semibold text-amber-700 text-base">
152
+ Model Warming Up
153
+ </p>
154
+ <p className="text-sm text-amber-600 mt-1">
155
  {warmUpMessage || "Waiting for MedGemma endpoint..."}
156
  </p>
157
+ <p className="text-xs text-gray-400 mt-3 leading-relaxed">
158
+ The MedGemma model scales from zero when inactive.
159
+ This usually takes 1-2 minutes. The pipeline will start
160
+ automatically once the model is ready.
161
  </p>
162
  </div>
163
  </div>
 
168
  <p>Agent pipeline running...</p>
169
  </div>
170
  </div>
171
+ ) : error && steps.length === 0 ? (
172
+ /* Full-screen error when nothing has even started (e.g. WS connection failed) */
173
+ <div className="flex items-center justify-center h-64">
174
+ <div className="text-center max-w-sm">
175
+ <div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
176
+ <span className="text-red-500 text-xl">!</span>
177
+ </div>
178
+ <p className="font-medium text-gray-800 mb-1">Connection Failed</p>
179
+ <p className="text-sm text-gray-500 mb-4">{error}</p>
180
+ <div className="flex gap-2 justify-center">
181
+ <button
182
+ onClick={handleRetry}
183
+ className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
184
+ >
185
+ Try Again
186
+ </button>
187
+ <button
188
+ onClick={handleNewCase}
189
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors"
190
+ >
191
+ New Case
192
+ </button>
193
+ </div>
194
+ </div>
195
+ </div>
196
  ) : null}
197
  </div>
198
  </div>
src/frontend/src/hooks/useAgentWebSocket.ts CHANGED
@@ -26,6 +26,7 @@ interface UseAgentWebSocketReturn {
26
  warmUpMessage: string | null;
27
  error: string | null;
28
  submitCase: (submission: CaseSubmission) => void;
 
29
  }
30
 
31
  function getWsUrl(): string {
@@ -130,5 +131,18 @@ export function useAgentWebSocket(): UseAgentWebSocketReturn {
130
  };
131
  }, []);
132
 
133
- return { steps, report, isRunning, isWarmingUp, warmUpMessage, error, submitCase };
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
 
26
  warmUpMessage: string | null;
27
  error: string | null;
28
  submitCase: (submission: CaseSubmission) => void;
29
+ reset: () => void;
30
  }
31
 
32
  function getWsUrl(): string {
 
131
  };
132
  }, []);
133
 
134
+ const reset = useCallback(() => {
135
+ if (wsRef.current) {
136
+ wsRef.current.close();
137
+ wsRef.current = null;
138
+ }
139
+ setSteps([]);
140
+ setReport(null);
141
+ setError(null);
142
+ setIsRunning(false);
143
+ setIsWarmingUp(false);
144
+ setWarmUpMessage(null);
145
+ }, []);
146
+
147
+ return { steps, report, isRunning, isWarmingUp, warmUpMessage, error, submitCase, reset };
148
  }
src/frontend/src/lib/reportToMarkdown.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Convert a CDS report object to a well-formatted Markdown document.
3
+ */
4
+ export function reportToMarkdown(report: any): string {
5
+ const lines: string[] = [];
6
+
7
+ lines.push("# Clinical Decision Support Report");
8
+ lines.push("");
9
+ lines.push("*Generated by CDS Agent - MedGemma (HAI-DEF)*");
10
+ lines.push("");
11
+
12
+ // Patient Summary
13
+ if (report.patient_summary) {
14
+ lines.push("## Patient Summary");
15
+ lines.push("");
16
+ lines.push(report.patient_summary);
17
+ lines.push("");
18
+ }
19
+
20
+ // Differential Diagnosis
21
+ if (report.differential_diagnosis?.length) {
22
+ lines.push("## Differential Diagnosis");
23
+ lines.push("");
24
+ for (const dx of report.differential_diagnosis) {
25
+ lines.push(`### ${dx.diagnosis}`);
26
+ lines.push("");
27
+ lines.push(`- **Likelihood:** ${dx.likelihood}`);
28
+ lines.push(`- **Reasoning:** ${dx.reasoning}`);
29
+ if (dx.supporting_evidence?.length) {
30
+ lines.push(`- **Supporting Evidence:** ${dx.supporting_evidence.join("; ")}`);
31
+ }
32
+ lines.push("");
33
+ }
34
+ }
35
+
36
+ // Drug Interactions
37
+ if (report.drug_interaction_warnings?.length) {
38
+ lines.push("## Drug Interaction Warnings");
39
+ lines.push("");
40
+ for (const ix of report.drug_interaction_warnings) {
41
+ lines.push(`- **${ix.drug_a} + ${ix.drug_b}** (${ix.severity}): ${ix.description}`);
42
+ }
43
+ lines.push("");
44
+ }
45
+
46
+ // Conflicts
47
+ if (report.conflicts?.length) {
48
+ lines.push("## Conflicts & Gaps Detected");
49
+ lines.push("");
50
+ for (const c of report.conflicts) {
51
+ lines.push(`### ${c.conflict_type?.toUpperCase() || "CONFLICT"} - ${c.severity}`);
52
+ lines.push("");
53
+ lines.push(c.description);
54
+ lines.push("");
55
+ lines.push(`- **Guideline:** ${c.guideline_text} (${c.guideline_source})`);
56
+ lines.push(`- **Patient Data:** ${c.patient_data}`);
57
+ if (c.suggested_resolution) {
58
+ lines.push(`- **Suggested Resolution:** ${c.suggested_resolution}`);
59
+ }
60
+ lines.push("");
61
+ }
62
+ }
63
+
64
+ // Guideline Recommendations
65
+ if (report.guideline_recommendations?.length) {
66
+ lines.push("## Guideline-Concordant Recommendations");
67
+ lines.push("");
68
+ for (const rec of report.guideline_recommendations) {
69
+ lines.push(`- ${rec}`);
70
+ }
71
+ lines.push("");
72
+ }
73
+
74
+ // Suggested Next Steps
75
+ if (report.suggested_next_steps?.length) {
76
+ lines.push("## Suggested Next Steps");
77
+ lines.push("");
78
+ for (const step of report.suggested_next_steps) {
79
+ lines.push(`- **[${step.priority?.toUpperCase()}] ${step.action}** - ${step.rationale}`);
80
+ }
81
+ lines.push("");
82
+ }
83
+
84
+ // Caveats
85
+ if (report.caveats?.length) {
86
+ lines.push("## Caveats & Limitations");
87
+ lines.push("");
88
+ for (const caveat of report.caveats) {
89
+ lines.push(`- ${caveat}`);
90
+ }
91
+ lines.push("");
92
+ }
93
+
94
+ // Sources
95
+ if (report.sources_cited?.length) {
96
+ lines.push("## Sources");
97
+ lines.push("");
98
+ report.sources_cited.forEach((source: string, i: number) => {
99
+ lines.push(`${i + 1}. ${source}`);
100
+ });
101
+ lines.push("");
102
+ }
103
+
104
+ lines.push("---");
105
+ lines.push("");
106
+ lines.push("*AI-generated clinical decision support - for demonstration purposes only. Does not replace professional medical judgment.*");
107
+ lines.push("");
108
+
109
+ return lines.join("\n");
110
+ }