Samad14 Cursor commited on
Commit
bc59cdc
·
1 Parent(s): 35104cd

fix(frontend): clear stale results, prefill-only bridges, and results-ready banners

Browse files

Stop bridge pages from auto-running searches on navigation, clear result cards when inputs change, and add completion banners with auto-scroll on polling job pages.

Co-authored-by: Cursor <cursoragent@cursor.com>

bioai-platform/frontend/src/app/(dashboard)/analyze/admet/page.tsx CHANGED
@@ -79,21 +79,6 @@ export default function ADMETPage() {
79
  if (stored) {
80
  sessionStorage.removeItem('admet_smiles');
81
  setSmiles(stored);
82
- setTimeout(() => {
83
- (async () => {
84
- setLoading(true);
85
- setError("");
86
- setResult(null);
87
- try {
88
- const res = await computeADMET(stored);
89
- setResult(res.result);
90
- } catch (e: any) {
91
- setError(typeof e?.response?.data?.detail === "string" ? e.response.data.detail : e?.response?.data?.detail?.message || e.message || "Computation failed");
92
- } finally {
93
- setLoading(false);
94
- }
95
- })();
96
- }, 100);
97
  }
98
  }, []);
99
 
@@ -122,7 +107,7 @@ export default function ADMETPage() {
122
  <div className="data-card p-5">
123
  <label className="block text-sm text-text-secondary mb-2">SMILES String</label>
124
  <div className="flex gap-2">
125
- <FlatInput value={smiles} onChange={(e) => setSmiles(e.target.value)}
126
  placeholder="e.g. CC(=O)OC1=CC=CC=C1C(=O)O"
127
  className="flex-1 font-mono"
128
  onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
 
79
  if (stored) {
80
  sessionStorage.removeItem('admet_smiles');
81
  setSmiles(stored);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
  }, []);
84
 
 
107
  <div className="data-card p-5">
108
  <label className="block text-sm text-text-secondary mb-2">SMILES String</label>
109
  <div className="flex gap-2">
110
+ <FlatInput value={smiles} onChange={(e) => { setSmiles(e.target.value); setResult(null); setError(""); }}
111
  placeholder="e.g. CC(=O)OC1=CC=CC=C1C(=O)O"
112
  className="flex-1 font-mono"
113
  onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
bioai-platform/frontend/src/app/(dashboard)/analyze/blast/page.tsx CHANGED
@@ -210,7 +210,7 @@ export default function BlastWizardPage() {
210
  <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5">
211
  <FlatTextarea
212
  value={rawInput}
213
- onChange={(e) => setRawInput(e.target.value)}
214
  placeholder="Paste a FASTA or raw sequence here..."
215
  className="w-full h-40 font-mono text-sm text-text-primary"
216
  />
@@ -247,7 +247,7 @@ export default function BlastWizardPage() {
247
  <FlatInput
248
  type="text"
249
  value={rawInput}
250
- onChange={(e) => setRawInput(e.target.value)}
251
  onKeyDown={(e) => e.key === 'Enter' && handleFetchAccession()}
252
  placeholder="e.g. NP_000509.1, P04637, 1TIM"
253
  className="flex-1 font-mono"
 
210
  <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5">
211
  <FlatTextarea
212
  value={rawInput}
213
+ onChange={(e) => { setRawInput(e.target.value); setAccessionResult(null); }}
214
  placeholder="Paste a FASTA or raw sequence here..."
215
  className="w-full h-40 font-mono text-sm text-text-primary"
216
  />
 
247
  <FlatInput
248
  type="text"
249
  value={rawInput}
250
+ onChange={(e) => { setRawInput(e.target.value); setAccessionResult(null); }}
251
  onKeyDown={(e) => e.key === 'Enter' && handleFetchAccession()}
252
  placeholder="e.g. NP_000509.1, P04637, 1TIM"
253
  className="flex-1 font-mono"
bioai-platform/frontend/src/app/(dashboard)/analyze/compare/page.tsx CHANGED
@@ -23,7 +23,7 @@ export default function ComparePage() {
23
  <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5 mb-6 space-y-4">
24
  <div className="flex gap-3">
25
  <FlatInput type="text" value={pdbId}
26
- onChange={e => setPdbId(e.target.value.toUpperCase())}
27
  onKeyDown={e => e.key === "Enter" && setSubmitted(pdbId)}
28
  placeholder="e.g. 1TUP"
29
  className="flex-1 font-mono" />
 
23
  <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5 mb-6 space-y-4">
24
  <div className="flex gap-3">
25
  <FlatInput type="text" value={pdbId}
26
+ onChange={e => { setPdbId(e.target.value.toUpperCase()); setSubmitted(""); }}
27
  onKeyDown={e => e.key === "Enter" && setSubmitted(pdbId)}
28
  placeholder="e.g. 1TUP"
29
  className="flex-1 font-mono" />
bioai-platform/frontend/src/app/(dashboard)/analyze/docking/page.tsx CHANGED
@@ -13,7 +13,7 @@ import type { DockingResult } from '@/lib/api';
13
  import { useAuditTrail } from '@/hooks/useAuditTrail';
14
  import { DockingViewer } from '@/components/DockingViewer';
15
  import { InteractionPanel } from '@/components/InteractionPanel';
16
- import { BackButton, CriticalButton, FlatInput, PageHeader } from '@/components/ui';
17
 
18
  const PDB_EXAMPLES = ['1TIM', '4HHB', '1A42', '2XAB'];
19
  const SMILES_EXAMPLES = [
@@ -177,6 +177,11 @@ export default function DockingPage() {
177
  if (status.status === 'complete' || status.status === 'completed' || status.status === 'failed') {
178
  setPolling(false);
179
  }
 
 
 
 
 
180
  } catch {
181
  setPolling(false);
182
  setError('Failed to check docking status');
@@ -224,7 +229,7 @@ export default function DockingPage() {
224
  <FlatInput
225
  type="text"
226
  value={pdbId}
227
- onChange={(e) => setPdbId(e.target.value.toUpperCase())}
228
  onKeyDown={(e) => e.key === 'Enter' && startDocking()}
229
  placeholder="e.g. 1TIM"
230
  className="w-full px-4 py-3 rounded-xl text-sm font-mono"
@@ -260,7 +265,7 @@ export default function DockingPage() {
260
  <FlatInput
261
  type="text"
262
  value={smiles}
263
- onChange={(e) => setSmiles(e.target.value)}
264
  onKeyDown={(e) => e.key === 'Enter' && startDocking()}
265
  placeholder="e.g. CC(=O)Oc1ccccc1C(=O)O"
266
  className="w-full px-4 py-3 rounded-xl text-sm font-mono"
@@ -296,7 +301,13 @@ export default function DockingPage() {
296
  )}
297
 
298
  {result && (
299
- <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="space-y-4">
 
 
 
 
 
 
300
  <div className="glass-card p-5">
301
  <div className="flex items-center justify-between mb-3">
302
  <div className="flex items-center gap-2">
 
13
  import { useAuditTrail } from '@/hooks/useAuditTrail';
14
  import { DockingViewer } from '@/components/DockingViewer';
15
  import { InteractionPanel } from '@/components/InteractionPanel';
16
+ import { BackButton, CriticalButton, FlatInput, PageHeader, ResultsReadyBanner } from '@/components/ui';
17
 
18
  const PDB_EXAMPLES = ['1TIM', '4HHB', '1A42', '2XAB'];
19
  const SMILES_EXAMPLES = [
 
177
  if (status.status === 'complete' || status.status === 'completed' || status.status === 'failed') {
178
  setPolling(false);
179
  }
180
+ if (status.status === 'complete' || status.status === 'completed') {
181
+ requestAnimationFrame(() => {
182
+ document.getElementById('docking-results')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
183
+ });
184
+ }
185
  } catch {
186
  setPolling(false);
187
  setError('Failed to check docking status');
 
229
  <FlatInput
230
  type="text"
231
  value={pdbId}
232
+ onChange={(e) => { setPdbId(e.target.value.toUpperCase()); setResult(null); setError(null); }}
233
  onKeyDown={(e) => e.key === 'Enter' && startDocking()}
234
  placeholder="e.g. 1TIM"
235
  className="w-full px-4 py-3 rounded-xl text-sm font-mono"
 
265
  <FlatInput
266
  type="text"
267
  value={smiles}
268
+ onChange={(e) => { setSmiles(e.target.value); setResult(null); setError(null); }}
269
  onKeyDown={(e) => e.key === 'Enter' && startDocking()}
270
  placeholder="e.g. CC(=O)Oc1ccccc1C(=O)O"
271
  className="w-full px-4 py-3 rounded-xl text-sm font-mono"
 
301
  )}
302
 
303
  {result && (
304
+ <motion.div id="docking-results" variants={fadeUp} initial={{ y: 24 }} animate="show" className="space-y-4">
305
+ {(result.status === 'complete' || result.status === 'completed') && (
306
+ <ResultsReadyBanner
307
+ title="Docking complete"
308
+ subtitle={bestPose?.affinity != null ? `Best pose: ${bestPose.affinity.toFixed(2)} kcal/mol · ${result.result?.num_poses ?? ''} poses` : `${result.result?.num_poses ?? ''} poses generated`}
309
+ />
310
+ )}
311
  <div className="glass-card p-5">
312
  <div className="flex items-center justify-between mb-3">
313
  <div className="flex items-center gap-2">
bioai-platform/frontend/src/app/(dashboard)/analyze/domains/page.tsx CHANGED
@@ -25,7 +25,6 @@ export default function DomainsPage() {
25
  if (stored) {
26
  sessionStorage.removeItem("domains_accession");
27
  setAccession(stored.toUpperCase());
28
- setSubmitted(stored.toUpperCase());
29
  }
30
  }, []);
31
 
@@ -47,7 +46,7 @@ export default function DomainsPage() {
47
  <div className="flex-1 relative">
48
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
49
  <FlatInput type="text" value={accession}
50
- onChange={e => setAccession(e.target.value.toUpperCase())}
51
  onKeyDown={e => e.key === "Enter" && handleSubmit()}
52
  placeholder="UniProt accession, e.g. P04637, P00698, Q9Y261"
53
  className="w-full pl-10 pr-4 font-mono" />
 
25
  if (stored) {
26
  sessionStorage.removeItem("domains_accession");
27
  setAccession(stored.toUpperCase());
 
28
  }
29
  }, []);
30
 
 
46
  <div className="flex-1 relative">
47
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
48
  <FlatInput type="text" value={accession}
49
+ onChange={e => { setAccession(e.target.value.toUpperCase()); setSubmitted(""); }}
50
  onKeyDown={e => e.key === "Enter" && handleSubmit()}
51
  placeholder="UniProt accession, e.g. P04637, P00698, Q9Y261"
52
  className="w-full pl-10 pr-4 font-mono" />
bioai-platform/frontend/src/app/(dashboard)/analyze/function/page.tsx CHANGED
@@ -5,7 +5,7 @@ import { Brain, Loader2, ExternalLink, Dna, Layers, Target } from "lucide-react"
5
  import { fadeUp } from "@/lib/animations";
6
  import { useAuditTrail } from "@/hooks/useAuditTrail";
7
  import { predictFunction, getFunctionStatus, type FunctionPredictionResult } from "@/lib/api";
8
- import { BackButton, PageHeader, CriticalButton, FlatInput } from "@/components/ui";
9
 
10
  const NAMESPACE_COLORS: Record<string, { text: string; bg: string; label: string }> = {
11
  MF: { text: "text-accent-cyan", bg: "bg-accent-cyan/10", label: "Molecular Function" },
@@ -30,6 +30,9 @@ export default function FunctionPage() {
30
  if (res.status === "complete" && res.result) {
31
  setResult(res.result);
32
  setLoading(false);
 
 
 
33
  } else if (res.status === "failed") {
34
  setError(res.error || "Prediction failed");
35
  setLoading(false);
@@ -60,21 +63,6 @@ export default function FunctionPage() {
60
  if (stored) {
61
  sessionStorage.removeItem('function_pdb_id');
62
  setPdbId(stored);
63
- setTimeout(() => {
64
- (async () => {
65
- setLoading(true);
66
- setError("");
67
- setResult(null);
68
- try {
69
- const res = await predictFunction(stored);
70
- setJobId(res.job_id);
71
- setStatus(res.status);
72
- } catch (e: any) {
73
- setError(typeof e?.response?.data?.detail === "string" ? e.response.data.detail : e?.response?.data?.detail?.message || e.message || "Submission failed");
74
- setLoading(false);
75
- }
76
- })();
77
- }, 100);
78
  }
79
  }, []);
80
 
@@ -109,7 +97,7 @@ export default function FunctionPage() {
109
  <div className="data-card p-5">
110
  <label className="block text-sm text-text-secondary mb-2">PDB ID</label>
111
  <div className="flex gap-2">
112
- <FlatInput value={pdbId} onChange={(e) => setPdbId(e.target.value.toUpperCase())}
113
  placeholder="e.g. 1TIM" maxLength={4}
114
  className="w-32 font-mono uppercase"
115
  onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
@@ -128,7 +116,11 @@ export default function FunctionPage() {
128
  )}
129
 
130
  {result && (
131
- <motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} className="mt-6 space-y-4">
 
 
 
 
132
  {/* Header */}
133
  <div className="data-card p-4 flex flex-wrap items-center gap-4">
134
  <div>
 
5
  import { fadeUp } from "@/lib/animations";
6
  import { useAuditTrail } from "@/hooks/useAuditTrail";
7
  import { predictFunction, getFunctionStatus, type FunctionPredictionResult } from "@/lib/api";
8
+ import { BackButton, PageHeader, CriticalButton, FlatInput, ResultsReadyBanner } from "@/components/ui";
9
 
10
  const NAMESPACE_COLORS: Record<string, { text: string; bg: string; label: string }> = {
11
  MF: { text: "text-accent-cyan", bg: "bg-accent-cyan/10", label: "Molecular Function" },
 
30
  if (res.status === "complete" && res.result) {
31
  setResult(res.result);
32
  setLoading(false);
33
+ requestAnimationFrame(() => {
34
+ document.getElementById('function-results')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
35
+ });
36
  } else if (res.status === "failed") {
37
  setError(res.error || "Prediction failed");
38
  setLoading(false);
 
63
  if (stored) {
64
  sessionStorage.removeItem('function_pdb_id');
65
  setPdbId(stored);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
  }, []);
68
 
 
97
  <div className="data-card p-5">
98
  <label className="block text-sm text-text-secondary mb-2">PDB ID</label>
99
  <div className="flex gap-2">
100
+ <FlatInput value={pdbId} onChange={(e) => { setPdbId(e.target.value.toUpperCase()); setResult(null); setError(""); }}
101
  placeholder="e.g. 1TIM" maxLength={4}
102
  className="w-32 font-mono uppercase"
103
  onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
 
116
  )}
117
 
118
  {result && (
119
+ <motion.div id="function-results" initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} className="mt-6 space-y-4">
120
+ <ResultsReadyBanner
121
+ title={`Prediction complete · ${result.pdb_id}`}
122
+ subtitle={`${result.method.replace(/_/g, ' ')} · ${result.sequence_length} residues`}
123
+ />
124
  {/* Header */}
125
  <div className="data-card p-4 flex flex-wrap items-center gap-4">
126
  <div>
bioai-platform/frontend/src/app/(dashboard)/analyze/interactions/page.tsx CHANGED
@@ -25,9 +25,8 @@ export default function InteractionsPage() {
25
  if (stored) {
26
  sessionStorage.removeItem('interaction_gene');
27
  setGeneName(stored);
28
- setTimeout(() => submitGene(stored), 100);
29
  }
30
- }, [submitGene]);
31
 
32
  return (
33
  <div className="max-w-3xl">
@@ -41,7 +40,7 @@ export default function InteractionsPage() {
41
  <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5 mb-6 space-y-4">
42
  <div className="flex gap-3">
43
  <FlatInput type="text" value={geneName}
44
- onChange={e => setGeneName(e.target.value.toUpperCase())}
45
  onKeyDown={e => e.key === "Enter" && submitGene(geneName)}
46
  placeholder="e.g. TP53"
47
  className="flex-1 font-mono" />
 
25
  if (stored) {
26
  sessionStorage.removeItem('interaction_gene');
27
  setGeneName(stored);
 
28
  }
29
+ }, []);
30
 
31
  return (
32
  <div className="max-w-3xl">
 
40
  <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5 mb-6 space-y-4">
41
  <div className="flex gap-3">
42
  <FlatInput type="text" value={geneName}
43
+ onChange={e => { setGeneName(e.target.value.toUpperCase()); setSubmitted(""); }}
44
  onKeyDown={e => e.key === "Enter" && submitGene(geneName)}
45
  placeholder="e.g. TP53"
46
  className="flex-1 font-mono" />
bioai-platform/frontend/src/app/(dashboard)/analyze/md/page.tsx CHANGED
@@ -6,7 +6,7 @@ import { ArrowLeft, Play, Loader2, Activity, Zap, BarChart3, Info } from "lucide
6
  import { fadeUp } from "@/lib/animations";
7
  import { useAuditTrail } from "@/hooks/useAuditTrail";
8
  import { runMD, getMDStatus, type MDSimulationResult } from "@/lib/api";
9
- import { ClaySegmented, ClaySlider, CriticalButton, FlatInput } from "@/components/ui";
10
 
11
  const MODES = [
12
  { value: "minimize", label: "Minimization Only", short: "Minimize", desc: "500 steps, ~5 sec", detail: "Energy minimization using L-BFGS. Removes steric clashes and high-energy contacts." },
@@ -46,6 +46,9 @@ export default function MDPage() {
46
  if (res.status === "complete" && res.result) {
47
  setResult(res.result);
48
  setLoading(false);
 
 
 
49
  } else if (res.status === "failed") {
50
  setError(res.error || "Simulation failed");
51
  setLoading(false);
@@ -76,21 +79,6 @@ export default function MDPage() {
76
  if (stored) {
77
  sessionStorage.removeItem('md_pdb_id');
78
  setPdbId(stored);
79
- setTimeout(() => {
80
- (async () => {
81
- setLoading(true);
82
- setError("");
83
- setResult(null);
84
- try {
85
- const res = await runMD(stored, mode, { forcefield, solvent, run_length_ps: runLengthPs });
86
- setJobId(res.job_id);
87
- setStatus(res.status);
88
- } catch (e: any) {
89
- setError(typeof e?.response?.data?.detail === "string" ? e.response.data.detail : e?.response?.data?.detail?.message || e.message || "Submission failed");
90
- setLoading(false);
91
- }
92
- })();
93
- }, 100);
94
  }
95
  }, []);
96
 
@@ -127,7 +115,7 @@ export default function MDPage() {
127
  <div className="data-card p-5">
128
  <label className="block text-sm text-text-secondary mb-2">PDB ID</label>
129
  <div className="flex gap-2 mb-4">
130
- <FlatInput value={pdbId} onChange={(e) => setPdbId(e.target.value.toUpperCase())}
131
  placeholder="e.g. 1TIM" maxLength={4}
132
  className="w-32 font-mono uppercase"
133
  onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
@@ -169,7 +157,11 @@ export default function MDPage() {
169
  )}
170
 
171
  {result && (
172
- <motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} className="mt-6 space-y-4">
 
 
 
 
173
  {/* Simulation Parameters */}
174
  <div className="data-card p-5">
175
  <h3 className="text-sm font-semibold text-text-primary mb-3 flex items-center gap-2">
 
6
  import { fadeUp } from "@/lib/animations";
7
  import { useAuditTrail } from "@/hooks/useAuditTrail";
8
  import { runMD, getMDStatus, type MDSimulationResult } from "@/lib/api";
9
+ import { ClaySegmented, ClaySlider, CriticalButton, FlatInput, ResultsReadyBanner } from "@/components/ui";
10
 
11
  const MODES = [
12
  { value: "minimize", label: "Minimization Only", short: "Minimize", desc: "500 steps, ~5 sec", detail: "Energy minimization using L-BFGS. Removes steric clashes and high-energy contacts." },
 
46
  if (res.status === "complete" && res.result) {
47
  setResult(res.result);
48
  setLoading(false);
49
+ requestAnimationFrame(() => {
50
+ document.getElementById('md-results')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
51
+ });
52
  } else if (res.status === "failed") {
53
  setError(res.error || "Simulation failed");
54
  setLoading(false);
 
79
  if (stored) {
80
  sessionStorage.removeItem('md_pdb_id');
81
  setPdbId(stored);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
  }, []);
84
 
 
115
  <div className="data-card p-5">
116
  <label className="block text-sm text-text-secondary mb-2">PDB ID</label>
117
  <div className="flex gap-2 mb-4">
118
+ <FlatInput value={pdbId} onChange={(e) => { setPdbId(e.target.value.toUpperCase()); setResult(null); setError(""); }}
119
  placeholder="e.g. 1TIM" maxLength={4}
120
  className="w-32 font-mono uppercase"
121
  onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
 
157
  )}
158
 
159
  {result && (
160
+ <motion.div id="md-results" initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} className="mt-6 space-y-4">
161
+ <ResultsReadyBanner
162
+ title={`Simulation complete · ${result.mode}`}
163
+ subtitle={`${result.engine === 'openmm' ? 'OpenMM' : 'BioPython'} · ${result.elapsed_seconds}s${result.final_energy_kj_mol ? ` · ${result.final_energy_kj_mol} kJ/mol` : ''}`}
164
+ />
165
  {/* Simulation Parameters */}
166
  <div className="data-card p-5">
167
  <h3 className="text-sm font-semibold text-text-primary mb-3 flex items-center gap-2">
bioai-platform/frontend/src/app/(dashboard)/analyze/pathway/page.tsx CHANGED
@@ -31,20 +31,6 @@ export default function PathwayPage() {
31
  if (stored) {
32
  sessionStorage.removeItem('pathway_query');
33
  setQuery(stored);
34
- setTimeout(() => {
35
- (async () => {
36
- setLoading(true);
37
- setError(null);
38
- try {
39
- const res = await searchPathways(stored);
40
- setReactomeResults(res.results);
41
- } catch (err: unknown) {
42
- setError(extractErrorMessage(err, 'Search failed'));
43
- } finally {
44
- setLoading(false);
45
- }
46
- })();
47
- }, 100);
48
  }
49
  }, []);
50
 
@@ -120,7 +106,7 @@ export default function PathwayPage() {
120
  <FlatInput
121
  type="text"
122
  value={query}
123
- onChange={(e) => setQuery(e.target.value)}
124
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
125
  placeholder={tab === 'reactome' ? 'e.g. TP53, BRCA1, EGFR' : 'e.g. TP53, BRCA1'}
126
  className="flex-1"
@@ -143,7 +129,7 @@ export default function PathwayPage() {
143
  <p className="text-sm text-text-secondary mb-3">Paste gene or protein identifiers (one per line or comma-separated) to find over-represented pathways.</p>
144
  <FlatTextarea
145
  value={geneInput}
146
- onChange={(e) => setGeneInput(e.target.value)}
147
  placeholder={`TP53\nBRCA1\nEGFR\nMYC\nPTEN`}
148
  rows={6}
149
  className="w-full mb-3"
 
31
  if (stored) {
32
  sessionStorage.removeItem('pathway_query');
33
  setQuery(stored);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
  }, []);
36
 
 
106
  <FlatInput
107
  type="text"
108
  value={query}
109
+ onChange={(e) => { setQuery(e.target.value); setReactomeResults(null); setKeggResults(null); setError(null); }}
110
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
111
  placeholder={tab === 'reactome' ? 'e.g. TP53, BRCA1, EGFR' : 'e.g. TP53, BRCA1'}
112
  className="flex-1"
 
129
  <p className="text-sm text-text-secondary mb-3">Paste gene or protein identifiers (one per line or comma-separated) to find over-represented pathways.</p>
130
  <FlatTextarea
131
  value={geneInput}
132
+ onChange={(e) => { setGeneInput(e.target.value); setEnrichmentResult(null); setError(null); }}
133
  placeholder={`TP53\nBRCA1\nEGFR\nMYC\nPTEN`}
134
  rows={6}
135
  className="w-full mb-3"
bioai-platform/frontend/src/app/(dashboard)/analyze/phylo/page.tsx CHANGED
@@ -3,7 +3,7 @@
3
  import { useCallback, useEffect, useRef, useState } from 'react'
4
  import dynamic from 'next/dynamic'
5
  import { useAuditTrail } from '@/hooks/useAuditTrail'
6
- import { CriticalButton, FlatTextarea, PageHeader } from '@/components/ui'
7
 
8
  const PhyloTreeViewer = dynamic(
9
  () => import('@/components/phylo/PhyloTreeViewer'),
@@ -161,6 +161,11 @@ export default function PhyloPage() {
161
  const data: PhyloJobStatus = await res.json()
162
  setJob(data)
163
  if (data.phase === 'complete' || data.phase === 'error') stopPoll()
 
 
 
 
 
164
  } catch { }
165
  }, [stopPoll])
166
 
@@ -276,7 +281,7 @@ export default function PhyloPage() {
276
  ↑ Upload FASTA
277
  </button>
278
  </div>
279
- <FlatTextarea rows={8} value={fasta} onChange={e => setFasta(e.target.value)}
280
  placeholder={`>Sequence_1\nMVLSPADKTNVKAAWGK...\n>Sequence_2\nMVLSGEDKSNVKAAWGK...`}
281
  spellCheck={false}
282
  className="w-full px-3 py-2 text-sm" />
@@ -382,7 +387,11 @@ export default function PhyloPage() {
382
  )}
383
 
384
  {isDone && job && (
385
- <div className="space-y-5">
 
 
 
 
386
  <div className="data-card p-4 flex flex-wrap gap-4 items-center">
387
  <div className="flex gap-4 text-sm flex-wrap flex-1">
388
  <span className="text-text-secondary">
 
3
  import { useCallback, useEffect, useRef, useState } from 'react'
4
  import dynamic from 'next/dynamic'
5
  import { useAuditTrail } from '@/hooks/useAuditTrail'
6
+ import { CriticalButton, FlatTextarea, PageHeader, ResultsReadyBanner } from '@/components/ui'
7
 
8
  const PhyloTreeViewer = dynamic(
9
  () => import('@/components/phylo/PhyloTreeViewer'),
 
161
  const data: PhyloJobStatus = await res.json()
162
  setJob(data)
163
  if (data.phase === 'complete' || data.phase === 'error') stopPoll()
164
+ if (data.phase === 'complete') {
165
+ requestAnimationFrame(() => {
166
+ document.getElementById('phylo-results')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
167
+ })
168
+ }
169
  } catch { }
170
  }, [stopPoll])
171
 
 
281
  ↑ Upload FASTA
282
  </button>
283
  </div>
284
+ <FlatTextarea rows={8} value={fasta} onChange={e => { setFasta(e.target.value); stopPoll(); setJob(null); setJobId(null); setSubmitError('') }}
285
  placeholder={`>Sequence_1\nMVLSPADKTNVKAAWGK...\n>Sequence_2\nMVLSGEDKSNVKAAWGK...`}
286
  spellCheck={false}
287
  className="w-full px-3 py-2 text-sm" />
 
387
  )}
388
 
389
  {isDone && job && (
390
+ <div id="phylo-results" className="space-y-5">
391
+ <ResultsReadyBanner
392
+ title="Tree complete"
393
+ subtitle={`${METHOD_INFO[job.method].label}${job.model ? ` · ${job.model}` : ''}${job.done_at ? ` · ${Math.round(job.done_at - job.created_at)}s` : ''}`}
394
+ />
395
  <div className="data-card p-4 flex flex-wrap gap-4 items-center">
396
  <div className="flex gap-4 text-sm flex-wrap flex-1">
397
  <span className="text-text-secondary">
bioai-platform/frontend/src/app/(dashboard)/analyze/sequencing/page.tsx CHANGED
@@ -8,7 +8,7 @@ import { fadeUp } from '@/lib/animations';
8
  import { runSequencing, getSequencingStatus, listSequencingReferences } from '@/lib/api';
9
  import type { SequencingResult, SequencingReference } from '@/lib/api';
10
  import { useAuditTrail } from '@/hooks/useAuditTrail';
11
- import { BackButton, CriticalButton, FlatInput, PageHeader } from '@/components/ui';
12
 
13
  const DEFAULT_REFERENCES = [
14
  { id: 'sars-cov-2', name: 'Sars Cov 2' },
@@ -72,6 +72,11 @@ export default function SequencingPage() {
72
  if (status.status === 'complete' || status.status === 'failed') {
73
  setPolling(false);
74
  }
 
 
 
 
 
75
  } catch {
76
  setPolling(false);
77
  setError('Failed to check pipeline status');
@@ -124,7 +129,7 @@ export default function SequencingPage() {
124
  <FlatInput
125
  type="text"
126
  value={fastqUrl}
127
- onChange={(e) => setFastqUrl(e.target.value)}
128
  onKeyDown={(e) => e.key === 'Enter' && startPipeline()}
129
  placeholder="https://example.com/sample.fastq"
130
  className="w-full px-4 py-3 rounded-xl text-sm font-mono"
@@ -173,8 +178,13 @@ export default function SequencingPage() {
173
  )}
174
 
175
  {result && (
176
- <motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="space-y-4">
177
-
 
 
 
 
 
178
  <div className="glass-card p-5">
179
  <div className="flex items-center justify-between mb-4">
180
  <div className="flex items-center gap-2">
 
8
  import { runSequencing, getSequencingStatus, listSequencingReferences } from '@/lib/api';
9
  import type { SequencingResult, SequencingReference } from '@/lib/api';
10
  import { useAuditTrail } from '@/hooks/useAuditTrail';
11
+ import { BackButton, CriticalButton, FlatInput, PageHeader, ResultsReadyBanner } from '@/components/ui';
12
 
13
  const DEFAULT_REFERENCES = [
14
  { id: 'sars-cov-2', name: 'Sars Cov 2' },
 
72
  if (status.status === 'complete' || status.status === 'failed') {
73
  setPolling(false);
74
  }
75
+ if (status.status === 'complete') {
76
+ requestAnimationFrame(() => {
77
+ document.getElementById('sequencing-results')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
78
+ });
79
+ }
80
  } catch {
81
  setPolling(false);
82
  setError('Failed to check pipeline status');
 
129
  <FlatInput
130
  type="text"
131
  value={fastqUrl}
132
+ onChange={(e) => { setFastqUrl(e.target.value); setResult(null); setError(null); }}
133
  onKeyDown={(e) => e.key === 'Enter' && startPipeline()}
134
  placeholder="https://example.com/sample.fastq"
135
  className="w-full px-4 py-3 rounded-xl text-sm font-mono"
 
178
  )}
179
 
180
  {result && (
181
+ <motion.div id="sequencing-results" variants={fadeUp} initial={{ y: 24 }} animate="show" className="space-y-4">
182
+ {result.status === 'complete' && (
183
+ <ResultsReadyBanner
184
+ title="Pipeline complete"
185
+ subtitle={result.result?.consensus_sequence ? `Consensus sequence ready · ${result.result.reference ?? ''}` : 'All pipeline steps finished'}
186
+ />
187
+ )}
188
  <div className="glass-card p-5">
189
  <div className="flex items-center justify-between mb-4">
190
  <div className="flex items-center gap-2">
bioai-platform/frontend/src/app/(dashboard)/analyze/structure/page.tsx CHANGED
@@ -26,7 +26,6 @@ export default function StructurePage() {
26
  if (stored) {
27
  sessionStorage.removeItem('structure_query');
28
  setQuery(stored);
29
- setTimeout(() => handleSearch(), 100);
30
  }
31
  }, []);
32
 
@@ -67,7 +66,7 @@ export default function StructurePage() {
67
  <FlatInput
68
  type="text"
69
  value={query}
70
- onChange={(e) => setQuery(e.target.value)}
71
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
72
  placeholder="e.g. 1TIM, P04637, 4HHB"
73
  className="flex-1"
 
26
  if (stored) {
27
  sessionStorage.removeItem('structure_query');
28
  setQuery(stored);
 
29
  }
30
  }, []);
31
 
 
66
  <FlatInput
67
  type="text"
68
  value={query}
69
+ onChange={(e) => { setQuery(e.target.value); setResult(null); setError(null); setInventory(null); }}
70
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
71
  placeholder="e.g. 1TIM, P04637, 4HHB"
72
  className="flex-1"
bioai-platform/frontend/src/app/(dashboard)/analyze/uniprot/page.tsx CHANGED
@@ -38,26 +38,6 @@ export default function UniprotLookupPage() {
38
  if (stored) {
39
  sessionStorage.removeItem('uniprot_accession');
40
  setQuery(stored);
41
- setTimeout(() => {
42
- (async () => {
43
- setLoading(true);
44
- setError(null);
45
- setResults(null);
46
- setDetail(null);
47
- try {
48
- const res = await searchUniprot(stored);
49
- if (res.results.length === 1) {
50
- handleSelect(res.results[0].accession);
51
- } else {
52
- setResults(res.results);
53
- }
54
- } catch (err: unknown) {
55
- setError(extractErrorMessage(err, 'Search failed'));
56
- } finally {
57
- setLoading(false);
58
- }
59
- })();
60
- }, 100);
61
  }
62
  }, []);
63
 
@@ -132,7 +112,7 @@ export default function UniprotLookupPage() {
132
  <FlatInput
133
  type="text"
134
  value={query}
135
- onChange={(e) => setQuery(e.target.value)}
136
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
137
  placeholder="e.g. p53, BRCA1, TP53 human, kinase"
138
  className="flex-1"
 
38
  if (stored) {
39
  sessionStorage.removeItem('uniprot_accession');
40
  setQuery(stored);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
  }, []);
43
 
 
112
  <FlatInput
113
  type="text"
114
  value={query}
115
+ onChange={(e) => { setQuery(e.target.value); setResults(null); setDetail(null); setError(null); }}
116
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
117
  placeholder="e.g. p53, BRCA1, TP53 human, kinase"
118
  className="flex-1"
bioai-platform/frontend/src/components/SequenceRetrieval.tsx CHANGED
@@ -120,7 +120,7 @@ export function SequenceRetrieval() {
120
  <input
121
  type="text"
122
  value={input}
123
- onChange={(e) => setInput(e.target.value)}
124
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
125
  placeholder={
126
  mode === 'accession' ? 'e.g. NP_000509.1, P04637, 1TIM' :
 
120
  <input
121
  type="text"
122
  value={input}
123
+ onChange={(e) => { setInput(e.target.value); setResult(null); setValidation(null); setSearchResults(null); setError(null); }}
124
  onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
125
  placeholder={
126
  mode === 'accession' ? 'e.g. NP_000509.1, P04637, 1TIM' :
bioai-platform/frontend/src/components/ui/ResultsReadyBanner.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { CheckCircle2 } from 'lucide-react';
4
+
5
+ /**
6
+ * Distinct "results arrived" notification shown at the top of a tool's
7
+ * results section. Job pages poll in the background; this banner is the
8
+ * explicit signal that a run finished so users aren't left staring at
9
+ * the form wondering if anything happened.
10
+ */
11
+
12
+ interface ResultsReadyBannerProps {
13
+ title?: string;
14
+ subtitle?: string;
15
+ className?: string;
16
+ }
17
+
18
+ export function ResultsReadyBanner({ title = 'Results ready', subtitle, className = '' }: ResultsReadyBannerProps) {
19
+ return (
20
+ <div className={`rounded-xl bg-green-500/10 border border-green-500/25 px-4 py-3 flex items-center gap-3 ${className}`}>
21
+ <CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
22
+ <div className="min-w-0">
23
+ <p className="text-sm font-semibold text-text-primary">{title}</p>
24
+ {subtitle && <p className="text-xs text-text-muted mt-0.5 truncate">{subtitle}</p>}
25
+ </div>
26
+ </div>
27
+ );
28
+ }
bioai-platform/frontend/src/components/ui/index.ts CHANGED
@@ -9,3 +9,4 @@ export { HudLegend, LegendItem } from './HudLegend';
9
  export { FlatInput, FlatTextarea } from './FlatInput';
10
  export { BackButton } from './BackButton';
11
  export { PageHeader } from './PageHeader';
 
 
9
  export { FlatInput, FlatTextarea } from './FlatInput';
10
  export { BackButton } from './BackButton';
11
  export { PageHeader } from './PageHeader';
12
+ export { ResultsReadyBanner } from './ResultsReadyBanner';