faisalAI27 commited on
Commit
d7b0e52
·
1 Parent(s): 3919fe2

added the AI layer for explanation

Browse files
backend/.env.example CHANGED
@@ -4,5 +4,13 @@ MODEL_MAX_LENGTH=512
4
  MODEL_NAME=DNABERT-2 ClinVar 20k
5
  DEVICE=auto
6
 
 
 
 
 
 
 
 
 
7
  # Comma-separated origins for browser clients
8
  ALLOWED_ORIGINS=http://localhost:3000
 
4
  MODEL_NAME=DNABERT-2 ClinVar 20k
5
  DEVICE=auto
6
 
7
+ # Optional OpenAI explanation layer.
8
+ # Keep USE_OPENAI_EXPLANATION=false for the local rule-based explanation.
9
+ # To enable it, copy this file to backend/.env and paste your real key there.
10
+ USE_OPENAI_EXPLANATION=false
11
+ OPENAI_API_KEY=
12
+ OPENAI_EXPLANATION_MODEL=gpt-4.1-mini
13
+ OPENAI_EXPLANATION_TIMEOUT=12
14
+
15
  # Comma-separated origins for browser clients
16
  ALLOWED_ORIGINS=http://localhost:3000
backend/README.md CHANGED
@@ -4,6 +4,9 @@ FastAPI backend for the Variant Risk Explainer research demo.
4
 
5
  The backend loads a fine-tuned DNABERT-2 sequence-classification model once at
6
  startup and exposes `POST /analyze` for DNA sequence risk prediction.
 
 
 
7
 
8
  This is for research/demo use only. It is not a clinical diagnostic system and
9
  must not be used for medical decisions.
@@ -48,6 +51,23 @@ DEVICE=auto
48
 
49
  `DEVICE=auto` selects CUDA, then MPS, then CPU.
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ## Run
52
 
53
  From `backend/`:
@@ -110,6 +130,21 @@ print(response.json())
110
  - `pathogenic_probability`: class 1 probability
111
  - `threshold`: decision threshold, currently `0.16`
112
  - `sequence_length_used`: sequence length after optional center crop
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  ## Safety Notice
115
 
 
4
 
5
  The backend loads a fine-tuned DNABERT-2 sequence-classification model once at
6
  startup and exposes `POST /analyze` for DNA sequence risk prediction.
7
+ The response also includes an explanation generated from the model
8
+ probabilities, selected threshold, and prediction label. By default this is
9
+ rule-based. You can optionally enable an OpenAI-powered explanation paragraph.
10
 
11
  This is for research/demo use only. It is not a clinical diagnostic system and
12
  must not be used for medical decisions.
 
51
 
52
  `DEVICE=auto` selects CUDA, then MPS, then CPU.
53
 
54
+ ## Optional OpenAI Explanation
55
+
56
+ Do not paste your OpenAI API key into source code or `.env.example`.
57
+
58
+ Paste it only into your local `backend/.env` file:
59
+
60
+ ```bash
61
+ USE_OPENAI_EXPLANATION=true
62
+ OPENAI_API_KEY=sk-your-real-key-here
63
+ OPENAI_EXPLANATION_MODEL=gpt-4.1-mini
64
+ OPENAI_EXPLANATION_TIMEOUT=12
65
+ ```
66
+
67
+ Then restart the backend. If the OpenAI key is missing, the package is not
68
+ installed, or the API call fails, the backend automatically falls back to the
69
+ local rule-based explanation.
70
+
71
  ## Run
72
 
73
  From `backend/`:
 
130
  - `pathogenic_probability`: class 1 probability
131
  - `threshold`: decision threshold, currently `0.16`
132
  - `sequence_length_used`: sequence length after optional center crop
133
+ - `explanation`: plain-language explanation of the model output
134
+ - `confidence_level`: rough confidence category based on model probability
135
+ - `recommendation`: safe research/demo recommendation
136
+ - `limitations`: important limitations to show users
137
+
138
+ ## Explanation Layer
139
+
140
+ The default explanation is generated by local backend rules. When
141
+ `USE_OPENAI_EXPLANATION=true`, the backend asks OpenAI to rewrite only the
142
+ explanation paragraph in beginner-friendly language. The prediction,
143
+ probabilities, threshold, confidence level, recommendation, and limitations stay
144
+ controlled by backend logic.
145
+
146
+ The wording is intentionally cautious because it is derived only from the model
147
+ output, not from clinical review.
148
 
149
  ## Safety Notice
150
 
backend/app/core/config.py CHANGED
@@ -18,6 +18,13 @@ def _default_model_dir() -> str:
18
  return str(_repo_root() / "training" / "training_model_files")
19
 
20
 
 
 
 
 
 
 
 
21
  @dataclass(frozen=True)
22
  class Settings:
23
  app_name: str = "variant-risk-explainer"
@@ -28,6 +35,12 @@ class Settings:
28
  model_name: str = os.getenv("MODEL_NAME", "DNABERT-2 ClinVar 20k").strip() or "DNABERT-2 ClinVar 20k"
29
  device: str = os.getenv("DEVICE", "auto").strip().lower() or "auto"
30
  max_sequence_context_length: int = int(os.getenv("MAX_SEQUENCE_CONTEXT_LENGTH", "2000"))
 
 
 
 
 
 
31
  allowed_origins: tuple[str, ...] = tuple(
32
  origin.strip()
33
  for origin in os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",")
@@ -57,6 +70,8 @@ class Settings:
57
  raise ValueError("MODEL_MAX_LENGTH must be positive")
58
  if self.device not in {"auto", "cuda", "mps", "cpu"}:
59
  raise ValueError("DEVICE must be one of: auto, cuda, mps, cpu")
 
 
60
  return self
61
 
62
 
 
18
  return str(_repo_root() / "training" / "training_model_files")
19
 
20
 
21
+ def _env_bool(name: str, default: bool = False) -> bool:
22
+ value = os.getenv(name)
23
+ if value is None:
24
+ return default
25
+ return value.strip().lower() in {"1", "true", "yes", "on"}
26
+
27
+
28
  @dataclass(frozen=True)
29
  class Settings:
30
  app_name: str = "variant-risk-explainer"
 
35
  model_name: str = os.getenv("MODEL_NAME", "DNABERT-2 ClinVar 20k").strip() or "DNABERT-2 ClinVar 20k"
36
  device: str = os.getenv("DEVICE", "auto").strip().lower() or "auto"
37
  max_sequence_context_length: int = int(os.getenv("MAX_SEQUENCE_CONTEXT_LENGTH", "2000"))
38
+ use_openai_explanation: bool = _env_bool("USE_OPENAI_EXPLANATION", False)
39
+ openai_api_key: str = os.getenv("OPENAI_API_KEY", "").strip()
40
+ openai_explanation_model: str = (
41
+ os.getenv("OPENAI_EXPLANATION_MODEL", "gpt-4.1-mini").strip() or "gpt-4.1-mini"
42
+ )
43
+ openai_explanation_timeout: float = float(os.getenv("OPENAI_EXPLANATION_TIMEOUT", "12"))
44
  allowed_origins: tuple[str, ...] = tuple(
45
  origin.strip()
46
  for origin in os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",")
 
70
  raise ValueError("MODEL_MAX_LENGTH must be positive")
71
  if self.device not in {"auto", "cuda", "mps", "cpu"}:
72
  raise ValueError("DEVICE must be one of: auto, cuda, mps, cpu")
73
+ if self.openai_explanation_timeout <= 0:
74
+ raise ValueError("OPENAI_EXPLANATION_TIMEOUT must be positive")
75
  return self
76
 
77
 
backend/app/main.py CHANGED
@@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
5
 
6
  from app.core.config import get_settings
7
  from app.schemas import AnalyzeRequest, AnalyzeResponse, HealthResponse
 
8
  from app.services.model_service import ModelService
9
 
10
 
@@ -59,6 +60,22 @@ def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
59
  except Exception as exc: # pragma: no cover - defensive boundary for inference failures.
60
  raise HTTPException(status_code=500, detail=f"Prediction failed: {type(exc).__name__}: {exc}") from exc
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  return AnalyzeResponse(
63
  variant_name=request.variant_name,
64
  gene=request.gene,
@@ -70,5 +87,9 @@ def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
70
  threshold=prediction.threshold,
71
  model_name=prediction.model_name,
72
  sequence_length_used=prediction.sequence_length_used,
 
 
 
 
73
  disclaimer=prediction.disclaimer,
74
  )
 
5
 
6
  from app.core.config import get_settings
7
  from app.schemas import AnalyzeRequest, AnalyzeResponse, HealthResponse
8
+ from app.services.explanation_service import generate_explanation
9
  from app.services.model_service import ModelService
10
 
11
 
 
60
  except Exception as exc: # pragma: no cover - defensive boundary for inference failures.
61
  raise HTTPException(status_code=500, detail=f"Prediction failed: {type(exc).__name__}: {exc}") from exc
62
 
63
+ explanation = generate_explanation(
64
+ prediction_class=prediction.prediction_class,
65
+ prediction_label=prediction.prediction_label,
66
+ risk_level=prediction.risk_level,
67
+ benign_probability=prediction.benign_probability,
68
+ pathogenic_probability=prediction.pathogenic_probability,
69
+ threshold=prediction.threshold,
70
+ variant_name=request.variant_name,
71
+ gene=request.gene,
72
+ sequence_length_used=prediction.sequence_length_used,
73
+ use_openai=settings.use_openai_explanation,
74
+ openai_api_key=settings.openai_api_key,
75
+ openai_model=settings.openai_explanation_model,
76
+ openai_timeout=settings.openai_explanation_timeout,
77
+ )
78
+
79
  return AnalyzeResponse(
80
  variant_name=request.variant_name,
81
  gene=request.gene,
 
87
  threshold=prediction.threshold,
88
  model_name=prediction.model_name,
89
  sequence_length_used=prediction.sequence_length_used,
90
+ explanation=explanation["explanation"],
91
+ confidence_level=explanation["confidence_level"],
92
+ recommendation=explanation["recommendation"],
93
+ limitations=explanation["limitations"],
94
  disclaimer=prediction.disclaimer,
95
  )
backend/app/schemas.py CHANGED
@@ -42,6 +42,10 @@ class AnalyzeResponse(BaseModel):
42
  threshold: float = Field(..., ge=0.0, le=1.0)
43
  model_name: str
44
  sequence_length_used: int
 
 
 
 
45
  disclaimer: str = DISCLAIMER
46
 
47
 
 
42
  threshold: float = Field(..., ge=0.0, le=1.0)
43
  model_name: str
44
  sequence_length_used: int
45
+ explanation: str
46
+ confidence_level: str
47
+ recommendation: str
48
+ limitations: list[str]
49
  disclaimer: str = DISCLAIMER
50
 
51
 
backend/app/services/explanation_service.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+
6
+
7
+ LIMITATIONS = [
8
+ "The model uses DNA sequence patterns and does not replace clinical interpretation.",
9
+ "The model performance is limited, with test AUC around 0.5928.",
10
+ "The prediction does not include full clinical evidence, family history, population frequency, or functional studies.",
11
+ "The result should not be used for diagnosis or treatment decisions.",
12
+ ]
13
+
14
+ RECOMMENDATION = (
15
+ "This result is for research/demo use only. For any real genetic or medical decision, "
16
+ "consult a qualified clinical genetics professional and use validated clinical databases/testing."
17
+ )
18
+
19
+
20
+ OPENAI_SYSTEM_INSTRUCTIONS = """
21
+ You explain a research-only DNABERT-2 variant risk demo to non-expert users.
22
+ Rules:
23
+ - Do not make clinical claims.
24
+ - Do not say a variant causes disease.
25
+ - Use cautious wording like "the model estimated" and "this may indicate".
26
+ - State that the result is not for diagnosis.
27
+ - Use only the model output values provided in the prompt.
28
+ - Keep the response to one short paragraph.
29
+ - Return JSON only with this shape: {"explanation": "..."}
30
+ """.strip()
31
+
32
+
33
+ def _confidence_level(prediction_class: int, benign_probability: float, pathogenic_probability: float, threshold: float) -> str:
34
+ if prediction_class == 1:
35
+ if pathogenic_probability >= 0.80:
36
+ return "High model confidence"
37
+ if pathogenic_probability >= 0.60:
38
+ return "Moderate model confidence"
39
+ if pathogenic_probability >= threshold:
40
+ return "Low model confidence"
41
+ return "Low model confidence"
42
+
43
+ if benign_probability >= 0.80:
44
+ return "High model confidence"
45
+ if benign_probability >= 0.60:
46
+ return "Moderate model confidence"
47
+ return "Low model confidence"
48
+
49
+
50
+ def _context_text(variant_name: str | None, gene: str | None, sequence_length_used: int | None) -> str:
51
+ details: list[str] = []
52
+ if variant_name:
53
+ details.append(f"variant {variant_name}")
54
+ if gene:
55
+ details.append(f"gene {gene}")
56
+ if sequence_length_used is not None:
57
+ details.append(f"{sequence_length_used} bases used by the model")
58
+
59
+ if not details:
60
+ return ""
61
+
62
+ return " Context: " + "; ".join(details) + "."
63
+
64
+
65
+ def _generate_rule_based_explanation(
66
+ prediction_class: int,
67
+ prediction_label: str,
68
+ risk_level: str,
69
+ benign_probability: float,
70
+ pathogenic_probability: float,
71
+ threshold: float,
72
+ variant_name: str | None = None,
73
+ gene: str | None = None,
74
+ sequence_length_used: int | None = None,
75
+ ) -> dict:
76
+ pathogenic_percent = pathogenic_probability * 100.0
77
+ confidence_level = _confidence_level(
78
+ prediction_class=prediction_class,
79
+ benign_probability=benign_probability,
80
+ pathogenic_probability=pathogenic_probability,
81
+ threshold=threshold,
82
+ )
83
+ context = _context_text(variant_name, gene, sequence_length_used)
84
+
85
+ if prediction_class == 1:
86
+ explanation = (
87
+ "The DNABERT-2 model estimated this sequence as more similar to pathogenic or likely pathogenic "
88
+ f"variants in the training data. The pathogenic probability is {pathogenic_percent:.1f}%. "
89
+ f"Because this is above the selected threshold of {threshold:.2f}, the model labels it as "
90
+ f"{prediction_label} with an {risk_level.lower()} research-demo risk level."
91
+ f"{context}"
92
+ )
93
+ else:
94
+ explanation = (
95
+ "The DNABERT-2 model estimated this sequence as more similar to benign or likely benign "
96
+ f"variants in the training data. The pathogenic probability is {pathogenic_percent:.1f}%, "
97
+ f"which is below the selected threshold of {threshold:.2f}. The model labels it as "
98
+ f"{prediction_label} with a {risk_level.lower()} research-demo risk level."
99
+ f"{context}"
100
+ )
101
+
102
+ return {
103
+ "explanation": explanation,
104
+ "confidence_level": confidence_level,
105
+ "recommendation": RECOMMENDATION,
106
+ "limitations": LIMITATIONS,
107
+ }
108
+
109
+
110
+ def _extract_json_object(text: str) -> dict:
111
+ try:
112
+ return json.loads(text)
113
+ except json.JSONDecodeError:
114
+ match = re.search(r"\{.*\}", text, flags=re.DOTALL)
115
+ if not match:
116
+ raise
117
+ return json.loads(match.group(0))
118
+
119
+
120
+ def _generate_openai_explanation(
121
+ fallback: dict,
122
+ prediction_class: int,
123
+ prediction_label: str,
124
+ risk_level: str,
125
+ benign_probability: float,
126
+ pathogenic_probability: float,
127
+ threshold: float,
128
+ openai_api_key: str,
129
+ openai_model: str,
130
+ openai_timeout: float,
131
+ variant_name: str | None,
132
+ gene: str | None,
133
+ sequence_length_used: int | None,
134
+ ) -> dict:
135
+ try:
136
+ from openai import OpenAI
137
+ except ImportError:
138
+ print("OpenAI package is not installed. Using rule-based explanation.")
139
+ return fallback
140
+
141
+ context = {
142
+ "variant_name": variant_name,
143
+ "gene": gene,
144
+ "prediction_class": prediction_class,
145
+ "prediction_label": prediction_label,
146
+ "risk_level": risk_level,
147
+ "benign_probability": round(benign_probability, 6),
148
+ "pathogenic_probability": round(pathogenic_probability, 6),
149
+ "pathogenic_probability_percent": round(pathogenic_probability * 100.0, 1),
150
+ "threshold": threshold,
151
+ "sequence_length_used": sequence_length_used,
152
+ "model": "DNABERT-2 fine-tuned on a ClinVar alternate-sequence research dataset",
153
+ "test_auc_roc": 0.5928,
154
+ }
155
+ user_prompt = (
156
+ "Write the explanation paragraph for this model output. "
157
+ "The explanation must be understandable to a beginner and must stay research/demo-only.\n\n"
158
+ f"Model output JSON:\n{json.dumps(context, indent=2)}"
159
+ )
160
+
161
+ try:
162
+ client = OpenAI(api_key=openai_api_key, timeout=openai_timeout)
163
+ response = client.responses.create(
164
+ model=openai_model,
165
+ instructions=OPENAI_SYSTEM_INSTRUCTIONS,
166
+ input=user_prompt,
167
+ max_output_tokens=300,
168
+ )
169
+ output_text = str(getattr(response, "output_text", "")).strip()
170
+ if not output_text:
171
+ print("OpenAI explanation response was empty. Using rule-based explanation.")
172
+ return fallback
173
+
174
+ parsed = _extract_json_object(output_text)
175
+ explanation = str(parsed.get("explanation", "")).strip()
176
+ if not explanation:
177
+ print("OpenAI explanation JSON did not include explanation. Using rule-based explanation.")
178
+ return fallback
179
+
180
+ enhanced = dict(fallback)
181
+ enhanced["explanation"] = explanation
182
+ return enhanced
183
+ except Exception as exc: # pragma: no cover - network/API failures vary.
184
+ print(f"OpenAI explanation failed: {type(exc).__name__}: {exc}. Using rule-based explanation.")
185
+ return fallback
186
+
187
+
188
+ def generate_explanation(
189
+ prediction_class: int,
190
+ prediction_label: str,
191
+ risk_level: str,
192
+ benign_probability: float,
193
+ pathogenic_probability: float,
194
+ threshold: float,
195
+ variant_name: str | None = None,
196
+ gene: str | None = None,
197
+ sequence_length_used: int | None = None,
198
+ use_openai: bool = False,
199
+ openai_api_key: str = "",
200
+ openai_model: str = "gpt-4.1-mini",
201
+ openai_timeout: float = 12.0,
202
+ ) -> dict:
203
+ fallback = _generate_rule_based_explanation(
204
+ prediction_class=prediction_class,
205
+ prediction_label=prediction_label,
206
+ risk_level=risk_level,
207
+ benign_probability=benign_probability,
208
+ pathogenic_probability=pathogenic_probability,
209
+ threshold=threshold,
210
+ variant_name=variant_name,
211
+ gene=gene,
212
+ sequence_length_used=sequence_length_used,
213
+ )
214
+
215
+ if not use_openai:
216
+ return fallback
217
+ if not openai_api_key:
218
+ print("USE_OPENAI_EXPLANATION is true, but OPENAI_API_KEY is missing. Using rule-based explanation.")
219
+ return fallback
220
+
221
+ return _generate_openai_explanation(
222
+ fallback=fallback,
223
+ prediction_class=prediction_class,
224
+ prediction_label=prediction_label,
225
+ risk_level=risk_level,
226
+ benign_probability=benign_probability,
227
+ pathogenic_probability=pathogenic_probability,
228
+ threshold=threshold,
229
+ openai_api_key=openai_api_key,
230
+ openai_model=openai_model,
231
+ openai_timeout=openai_timeout,
232
+ variant_name=variant_name,
233
+ gene=gene,
234
+ sequence_length_used=sequence_length_used,
235
+ )
backend/requirements.txt CHANGED
@@ -6,3 +6,4 @@ safetensors
6
  pydantic
7
  numpy
8
  python-dotenv
 
 
6
  pydantic
7
  numpy
8
  python-dotenv
9
+ openai
frontend/README.md CHANGED
@@ -4,7 +4,9 @@ Next.js frontend for the Variant Risk Explainer research demo.
4
 
5
  The app connects to the FastAPI backend, submits a DNA sequence to
6
  `POST /analyze`, shows the DNABERT-2 prediction result, and displays backend
7
- health/model status.
 
 
8
 
9
  This is for research/demo use only. It is not a clinical diagnostic system.
10
 
@@ -66,7 +68,13 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:8001
66
  5. Click `Analyze Variant`.
67
 
68
  The result card shows benign/pathogenic probabilities, threshold, model name,
69
- and sequence length used after center cropping.
 
 
 
 
 
 
70
 
71
  ## Safety Notice
72
 
 
4
 
5
  The app connects to the FastAPI backend, submits a DNA sequence to
6
  `POST /analyze`, shows the DNABERT-2 prediction result, and displays backend
7
+ health/model status. The result card also displays the backend's rule-based
8
+ or optional OpenAI explanation, confidence level, recommendation, and
9
+ limitations.
10
 
11
  This is for research/demo use only. It is not a clinical diagnostic system.
12
 
 
68
  5. Click `Analyze Variant`.
69
 
70
  The result card shows benign/pathogenic probabilities, threshold, model name,
71
+ sequence length used after center cropping, and a beginner-friendly explanation.
72
+
73
+ The explanation is generated from the backend model output and threshold. It is
74
+ not medical advice and is not a clinical interpretation. If the backend has
75
+ `USE_OPENAI_EXPLANATION=true`, the backend may use OpenAI to improve the
76
+ explanation paragraph. Do not put the OpenAI API key in the frontend; keep it in
77
+ `backend/.env` only.
78
 
79
  ## Safety Notice
80
 
frontend/app/globals.css CHANGED
@@ -432,6 +432,24 @@ dd {
432
  line-height: 1.55;
433
  }
434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  .limitations {
436
  margin-top: 18px;
437
  border-top: 1px solid var(--border);
 
432
  line-height: 1.55;
433
  }
434
 
435
+ .explanation p {
436
+ margin-bottom: 8px;
437
+ }
438
+
439
+ .recommendationBlock {
440
+ margin-top: 16px;
441
+ border: 1px solid #d8c8b0;
442
+ border-radius: 8px;
443
+ padding: 12px;
444
+ background: #fffaf0;
445
+ color: #4e3c34;
446
+ line-height: 1.5;
447
+ }
448
+
449
+ .recommendationBlock p {
450
+ margin-bottom: 0;
451
+ }
452
+
453
  .limitations {
454
  margin-top: 18px;
455
  border-top: 1px solid var(--border);
frontend/components/ResultCard.tsx CHANGED
@@ -95,7 +95,33 @@ export function ResultCard({ error, isLoading, result }: ResultCardProps) {
95
  </div>
96
  </div>
97
 
98
- <p className="disclaimer">{result.disclaimer}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </section>
100
  );
101
  }
 
95
  </div>
96
  </div>
97
 
98
+ <div className="explanation">
99
+ <h3>Explanation</h3>
100
+ <p>{result.explanation}</p>
101
+ <p>
102
+ <strong>Confidence level:</strong> {result.confidence_level}
103
+ </p>
104
+ </div>
105
+
106
+ <div className="recommendationBlock">
107
+ <h3>Recommendation</h3>
108
+ <p>{result.recommendation}</p>
109
+ </div>
110
+
111
+ <div className="limitations">
112
+ <h3>Limitations</h3>
113
+ <ul>
114
+ {result.limitations.map((limitation) => (
115
+ <li key={limitation}>{limitation}</li>
116
+ ))}
117
+ </ul>
118
+ </div>
119
+
120
+ <p className="disclaimer">
121
+ <strong>Research/demo only. Not for clinical diagnosis.</strong>
122
+ <br />
123
+ {result.disclaimer}
124
+ </p>
125
  </section>
126
  );
127
  }
frontend/types.ts CHANGED
@@ -16,6 +16,10 @@ export type AnalyzeResponse = {
16
  threshold: number;
17
  model_name: string;
18
  sequence_length_used: number;
 
 
 
 
19
  disclaimer: string;
20
  };
21
 
 
16
  threshold: number;
17
  model_name: string;
18
  sequence_length_used: number;
19
+ explanation: string;
20
+ confidence_level: string;
21
+ recommendation: string;
22
+ limitations: string[];
23
  disclaimer: string;
24
  };
25