arshenoy commited on
Commit
29085ee
·
verified ·
1 Parent(s): db97d58

updated with new features

Browse files
Files changed (1) hide show
  1. services/geminiService.ts +240 -237
services/geminiService.ts CHANGED
@@ -1,362 +1,365 @@
1
  import { GoogleGenAI, Type } from "@google/genai";
2
- import { PatientProfile, ClinicalVitals, AppMode, RiskAnalysisResult, ChatMessage } from "../types";
3
 
4
- // --- ROBUST API KEY RETRIEVAL ---
5
  const getApiKey = () => {
6
- let key = '';
7
-
8
- // 1. Try Vite Environment (Client-side / Build time)
9
  try {
10
  // @ts-ignore
11
- if (typeof import.meta !== 'undefined' && import.meta.env?.VITE_API_KEY) {
12
  // @ts-ignore
13
- key = import.meta.env.VITE_API_KEY;
14
  }
15
  } catch (e) {}
16
-
17
- // 2. Try Node Process Environment (Server-side / Docker Runtime)
18
- if (!key && typeof process !== 'undefined' && process.env) {
19
- key = process.env.API_KEY || process.env.VITE_API_KEY || '';
20
- }
21
-
22
- // 3. Debug Log (Masked)
23
- if (key) {
24
- console.log("[SomAI] API Key found:", key.substring(0, 4) + '...');
25
- } else {
26
- console.error("[SomAI] CRITICAL: No API Key found in environment variables.");
27
  }
28
-
29
- return key;
30
  };
31
 
32
  const API_KEY = getApiKey();
33
- // Initialize Client (Graceful handling if key is missing to prevent app crash on load)
34
- const ai = new GoogleGenAI({ apiKey: API_KEY || 'MISSING_KEY' });
35
 
36
  const MODEL_FAST = 'gemini-2.5-flash';
37
  const FALLBACK_API_BASE = 'https://arshenoy-somai-backend.hf.space';
38
 
39
- // --- HELPER TO CLEAN MARKDOWN ---
40
  const cleanText = (text: string) => {
41
  if (!text) return "";
42
  return text.replace(/\*\*/g, '').replace(/###/g, '').replace(/\*/g, '-').trim();
43
  };
44
 
45
- // --- FALLBACK HANDLER ---
46
- const callFallbackAPI = async (endpoint: string, payload: any): Promise<string> => {
47
- console.warn(`[System] Switching to Fallback API (Phi-3): ${FALLBACK_API_BASE}${endpoint}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
 
 
 
49
  const makeRequest = async (retries = 2) => {
50
  const controller = new AbortController();
51
- // TIMEOUT FIX: Increased to 90 seconds for slow Cold Starts
52
  const timeoutId = setTimeout(() => controller.abort(), 90000);
53
-
54
  try {
55
  const response = await fetch(`${FALLBACK_API_BASE}${endpoint}`, {
56
  method: 'POST',
57
  mode: 'cors',
58
- credentials: 'omit', // CRITICAL: Fixes CORS on HF Spaces
59
- headers: {
60
- 'Content-Type': 'application/json',
61
- 'Accept': 'application/json'
62
- },
63
  body: JSON.stringify(payload),
64
  signal: controller.signal
65
  });
66
  clearTimeout(timeoutId);
67
 
68
- // Handle Cold Starts (503 Service Unavailable)
69
  if (!response.ok && (response.status === 503 || response.status === 504) && retries > 0) {
70
- console.warn(`Backend waking up (${response.status}). Retrying in 5s...`);
71
  await new Promise(r => setTimeout(r, 5000));
72
  return makeRequest(retries - 1);
73
  }
74
 
75
  if (!response.ok) {
76
- const errText = await response.text();
77
- // Handle HTML responses (common during HF building phase)
78
- if (errText.trim().startsWith('<')) {
79
- throw new Error(`Backend unavailable (Status ${response.status}). Space might be building/sleeping.`);
80
- }
81
- throw new Error(`API Error ${response.status}: ${errText.substring(0, 100)}`);
82
  }
83
 
84
  const data = await response.json();
85
- return data.text || data.response || data.generated_text || JSON.stringify(data);
 
 
 
 
 
 
 
86
  } catch (error: any) {
87
  clearTimeout(timeoutId);
88
- // Retry on network/timeout errors
89
- if (retries > 0 && (error.name === 'AbortError' || error.message.includes('Failed to fetch'))) {
90
- console.warn(`Network/Timeout error. Retrying in 5s...`);
91
  await new Promise(r => setTimeout(r, 5000));
92
  return makeRequest(retries - 1);
93
  }
94
  throw error;
95
  }
96
  };
97
-
98
- try {
99
- return await makeRequest();
100
- } catch (error) {
101
- console.error("Fallback API Connection Failed:", error);
102
- throw error;
103
- }
104
  };
105
 
106
- // --- HELPER TO MAP RESPONSE TO RESULT ---
107
  const parseRiskResponse = (text: string, calculatedScore: number): RiskAnalysisResult => {
108
  try {
109
- const jsonMatch = text.match(/\{[\s\S]*\}/);
110
- const jsonStr = jsonMatch ? jsonMatch[0] : text;
 
 
 
 
 
111
  const data = JSON.parse(jsonStr);
112
-
113
  const pipeline = [
114
  { code: data.primaryConditionCode?.code || "N/A", description: data.primaryConditionCode?.description || "Unknown", type: 'Primary' },
115
  ...(data.historyCodes || []).map((h: any) => ({ code: h.code, description: h.description, type: 'History' }))
116
  ];
117
-
118
- const legacyCodes = [data.primaryConditionCode?.code, ...(data.historyCodes || []).map((h: any) => h.code)].filter(Boolean);
119
-
120
  return {
121
  numericScore: calculatedScore,
122
  summary: cleanText(data.summary || "Analysis completed."),
123
  actionItems: (data.actionItems || []).map(cleanText),
124
- icd10Codes: legacyCodes,
125
  codingPipeline: pipeline as any,
126
  insuranceNote: cleanText(data.insuranceNote || "Review required."),
127
  timestamp: new Date().toISOString()
128
  };
129
  } catch (e) {
130
- throw new Error("Failed to parse analysis data");
 
 
 
 
 
 
 
 
131
  }
132
  };
133
 
134
- export const analyzeRisk = async (
135
- profile: PatientProfile,
136
- vitals: ClinicalVitals,
137
- calculatedScore: number
138
- ): Promise<RiskAnalysisResult> => {
139
- const prompt = `
140
- Act as a Senior Clinical Risk Assessor and Certified Medical Coder.
141
- Analyze the following patient data to generate a clinical report and an ICD-10 coding pipeline.
142
- Patient Profile:
143
- - Age: ${profile.age}
144
- - Primary Condition: ${profile.condition}
145
- - Patient History Text: "${profile.history}"
146
- - Allergies: ${profile.allergies}
147
-
148
- Current Vitals (Today):
149
- - Systolic BP: ${vitals.systolicBp} mmHg
150
- - Glucose: ${vitals.glucose} mg/dL
151
- - Sleep Quality: ${vitals.sleepQuality}/10
152
- - Adherence: ${vitals.missedDoses} missed doses in 7 days.
153
 
154
- Algo-Calculated Risk Score: ${calculatedScore}/100.
155
- Task:
156
- 1. Clinical Summary: 1-2 sentences explaining the risk level based on vitals.
157
- 2. Action Items: 3 specific lifestyle changes.
158
- 3. Coding Pipeline:
159
- - Extract the ICD-10-CM code for the Primary Condition.
160
- - Analyze the "Patient History Text" and extract ICD-10-CM codes for any mention of past diseases (e.g., "history of heart attack" -> Z86.74 or I25.2). If history is empty/none, ignore.
161
- 4. Insurance Justification: A professional one-sentence note justifying medical necessity for monitoring.
162
 
163
- Return strict JSON.
164
- `;
 
 
 
 
 
165
 
166
- // Default Error Object for complete failure
167
- const returnError = () => ({
168
- numericScore: calculatedScore,
169
- summary: "Clinical analysis currently unavailable.",
170
- actionItems: ["Monitor daily vitals", "Consult healthcare provider"],
171
- icd10Codes: ["R69"],
172
- codingPipeline: [{ code: "R69", description: "Unspecified illness", type: "Primary" } as any],
173
- insuranceNote: "Automated risk assessment pending professional review.",
174
- timestamp: new Date().toISOString()
175
- });
 
 
 
 
 
 
176
 
177
- try {
178
- if (!API_KEY || API_KEY === 'MISSING_KEY') throw new Error("API Key missing or invalid");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
 
 
180
  const response = await ai.models.generateContent({
181
  model: MODEL_FAST,
182
  contents: prompt,
183
  config: {
184
  responseMimeType: "application/json",
 
185
  responseSchema: {
186
  type: Type.OBJECT,
187
  properties: {
188
  summary: { type: Type.STRING },
189
  actionItems: { type: Type.ARRAY, items: { type: Type.STRING } },
190
- primaryConditionCode: {
191
- type: Type.OBJECT,
192
- properties: { code: {type: Type.STRING}, description: {type: Type.STRING} }
193
- },
194
- historyCodes: {
195
- type: Type.ARRAY,
196
- items: {
197
- type: Type.OBJECT,
198
- properties: { code: {type: Type.STRING}, description: {type: Type.STRING} }
199
- }
200
- },
201
  insuranceNote: { type: Type.STRING }
202
  },
203
  required: ["summary", "actionItems", "primaryConditionCode", "historyCodes", "insuranceNote"]
204
  }
205
  }
206
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
- if (!response.text) throw new Error("No response from AI");
209
- return parseRiskResponse(response.text, calculatedScore);
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- } catch (primaryError: any) {
212
- const errorStr = primaryError.toString();
213
- if (errorStr.includes('429') || errorStr.includes('Quota')) {
214
- console.warn("⚠️ GEMINI QUOTA EXCEEDED. Switching to Phi-3 Backend.");
215
- } else {
216
- console.warn("Gemini API Error. Switching to Backend.", primaryError);
217
- }
218
-
 
 
 
 
 
 
 
219
  try {
220
- const fallbackText = await callFallbackAPI('/analyze', { prompt });
221
- return parseRiskResponse(fallbackText, calculatedScore);
222
- } catch (fallbackError) {
223
- console.error("All APIs failed for analyzeRisk", fallbackError);
224
- return returnError();
225
  }
226
  }
227
  };
228
 
229
  export const generateChatResponse = async (
230
- history: ChatMessage[],
231
- currentMessage: string,
232
- currentImage: string | undefined,
233
- profile: PatientProfile,
234
- mode: AppMode
 
235
  ): Promise<string> => {
236
- const baseContext = `
237
- Context:
238
- Patient: ${profile.name} (${profile.age}y)
239
- Condition: ${profile.condition}
240
- History: ${profile.history}
241
-
242
- Formatting: Plain text only. No markdown.
 
243
  `;
244
-
245
- const systemInstruction = mode === AppMode.THERAPY
246
- ? `You are SomAI, a CBT Companion. ${baseContext} Use CBT techniques. Be empathetic.`
247
- : `You are SomAI, a Medical Education Assistant. ${baseContext} Explain concepts clearly. If image provided, analyze it.`;
248
 
249
  try {
250
- if (!API_KEY || API_KEY === 'MISSING_KEY') throw new Error("API Key missing");
251
-
252
- const contents: any[] = history.map(msg => {
253
- const parts: any[] = [{ text: msg.text }];
254
- if (msg.image) {
255
- const base64 = msg.image.includes('base64,') ? msg.image.split('base64,')[1] : msg.image;
256
- parts.push({ inlineData: { mimeType: 'image/jpeg', data: base64 } });
257
- }
258
- return { role: msg.role === 'user' ? 'user' : 'model', parts };
259
- });
260
-
261
- const currentParts: any[] = [{ text: currentMessage }];
262
- if (currentImage) {
263
- const base64 = currentImage.includes('base64,') ? currentImage.split('base64,')[1] : currentImage;
264
- currentParts.push({ inlineData: { mimeType: 'image/jpeg', data: base64 } });
265
- }
266
- contents.push({ role: 'user', parts: currentParts });
267
-
268
  const response = await ai.models.generateContent({
269
  model: MODEL_FAST,
270
  contents: contents,
271
  config: {
272
- systemInstruction: systemInstruction,
273
- temperature: 0.5,
274
- maxOutputTokens: 500,
275
  }
276
  });
277
 
278
- return cleanText(response.text || "I'm having trouble retrieving that information.");
279
-
280
- } catch (primaryError: any) {
281
- const errorStr = primaryError.toString();
282
- if (errorStr.includes('429') || errorStr.includes('Quota')) {
283
- console.warn("⚠️ GEMINI QUOTA EXCEEDED. Switching to Phi-3 Backend.");
284
- } else {
285
- console.warn("Gemini API Error. Switching to Backend.", primaryError);
286
- }
287
 
 
288
  try {
289
- // --- FALLBACK LOGIC ---
290
- const recentHistory = history.slice(-6);
291
- const historyText = recentHistory.map(m => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.text}`).join('\n');
292
-
293
- const fullPrompt = `${systemInstruction}\n\n[Recent Chat History]:\n${historyText}\nUser: ${currentMessage}\n${currentImage ? '[Image Context Provided]' : ''}\nAssistant:`;
294
-
295
- const fallbackResponse = await callFallbackAPI('/generate', { prompt: fullPrompt });
296
- return cleanText(fallbackResponse);
297
-
298
- } catch (fallbackError) {
299
- console.error("All APIs failed for chat", fallbackError);
300
- return "I apologize, but I am unable to connect at the moment. Please check your internet connection or try again later.";
301
  }
302
  }
303
  };
304
 
305
- export const generateQuickReplies = async (
306
- lastAiMessage: string
307
- ): Promise<string[]> => {
308
- if (!API_KEY || API_KEY === 'MISSING_KEY') return [];
309
- const prompt = `Based on this AI response: "${lastAiMessage}", generate 3 short, relevant quick reply options. Return JSON array of strings.`;
 
 
310
 
311
  try {
312
- const response = await ai.models.generateContent({
313
- model: MODEL_FAST,
314
- contents: prompt,
315
- config: {
316
- responseMimeType: "application/json",
317
- responseSchema: {
318
- type: Type.ARRAY,
319
- items: { type: Type.STRING }
320
- }
321
- }
322
  });
323
- return JSON.parse(response.text || "[]").slice(0, 3);
324
- } catch {
325
- return [];
326
- }
327
  };
328
 
329
- export const summarizeConversation = async (
330
- history: ChatMessage[]
331
- ): Promise<string> => {
332
- if (history.length === 0) return "No conversation to summarize.";
333
-
334
- const historyText = history.map(msg =>
335
- `${msg.role === 'user' ? 'Patient' : 'AI'}: ${msg.text}`
336
- ).join('\n');
337
-
338
- const prompt = `Create a professional clinical note summarizing this conversation. Include: Chief Complaint, Topics Discussed, and Patient Sentiment. Format as a single paragraph plain text. No markdown.\n\n${historyText}`;
339
-
340
  try {
341
- if (!API_KEY || API_KEY === 'MISSING_KEY') throw new Error("API Key missing");
342
-
343
- const response = await ai.models.generateContent({
344
- model: MODEL_FAST,
345
- contents: prompt,
346
- });
347
- return cleanText(response.text || "Summary not available.");
348
- } catch (error) {
349
- try {
350
- const shortHistoryText = history.slice(-10).map(msg =>
351
- `${msg.role === 'user' ? 'Patient' : 'AI'}: ${msg.text}`
352
- ).join('\n');
353
- const shortPrompt = `Summarize conversation:\n${shortHistoryText}`;
354
-
355
- const fallbackResponse = await callFallbackAPI('/generate', { prompt: shortPrompt });
356
- return cleanText(fallbackResponse);
357
- } catch (fallbackError) {
358
- console.error("Summarization Error", fallbackError);
359
- return "Could not generate summary.";
360
- }
361
- }
362
  };
 
1
  import { GoogleGenAI, Type } from "@google/genai";
2
+ import { PatientProfile, ClinicalVitals, AppMode, RiskAnalysisResult, ChatMessage, ExtractionResult, HealthInsights } from "../types";
3
 
4
+ // --- API KEY & CLIENT INITIALIZATION ---
5
  const getApiKey = () => {
 
 
 
6
  try {
7
  // @ts-ignore
8
+ if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_API_KEY) {
9
  // @ts-ignore
10
+ return import.meta.env.VITE_API_KEY;
11
  }
12
  } catch (e) {}
13
+
14
+ if (typeof process !== 'undefined' && process.env && process.env.API_KEY) {
15
+ return process.env.API_KEY;
 
 
 
 
 
 
 
 
16
  }
17
+ return '';
 
18
  };
19
 
20
  const API_KEY = getApiKey();
21
+ const ai = new GoogleGenAI({ apiKey: API_KEY });
 
22
 
23
  const MODEL_FAST = 'gemini-2.5-flash';
24
  const FALLBACK_API_BASE = 'https://arshenoy-somai-backend.hf.space';
25
 
26
+ // Cleaning for final blocks
27
  const cleanText = (text: string) => {
28
  if (!text) return "";
29
  return text.replace(/\*\*/g, '').replace(/###/g, '').replace(/\*/g, '-').trim();
30
  };
31
 
32
+ const compressImage = async (base64Str: string, maxWidth = 800): Promise<string> => {
33
+ return new Promise((resolve) => {
34
+ const img = new Image();
35
+ img.src = base64Str;
36
+ img.onload = () => {
37
+ const canvas = document.createElement('canvas');
38
+ let width = img.width;
39
+ let height = img.height;
40
+
41
+ if (width > maxWidth) {
42
+ height = (height * maxWidth) / width;
43
+ width = maxWidth;
44
+ }
45
+
46
+ canvas.width = width;
47
+ canvas.height = height;
48
+ const ctx = canvas.getContext('2d');
49
+ ctx?.drawImage(img, 0, 0, width, height);
50
+ resolve(canvas.toDataURL('image/jpeg', 0.7));
51
+ };
52
+ img.onerror = () => resolve(base64Str);
53
+ });
54
+ };
55
+
56
+ export const wakeUpBackend = async () => {
57
+ try {
58
+ await fetch(`${FALLBACK_API_BASE}/`, { method: 'GET', mode: 'cors' });
59
+ } catch (e) {}
60
+ };
61
 
62
+ const callFallbackAPI = async (endpoint: string, payload: any): Promise<string> => {
63
+ console.info(`[SomAI System] Switching to Fallback API: ${FALLBACK_API_BASE}${endpoint}`);
64
+
65
  const makeRequest = async (retries = 2) => {
66
  const controller = new AbortController();
 
67
  const timeoutId = setTimeout(() => controller.abort(), 90000);
68
+
69
  try {
70
  const response = await fetch(`${FALLBACK_API_BASE}${endpoint}`, {
71
  method: 'POST',
72
  mode: 'cors',
73
+ credentials: 'omit',
74
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
 
 
 
75
  body: JSON.stringify(payload),
76
  signal: controller.signal
77
  });
78
  clearTimeout(timeoutId);
79
 
 
80
  if (!response.ok && (response.status === 503 || response.status === 504) && retries > 0) {
 
81
  await new Promise(r => setTimeout(r, 5000));
82
  return makeRequest(retries - 1);
83
  }
84
 
85
  if (!response.ok) {
86
+ const err = await response.text().catch(() => "Unknown");
87
+ throw new Error(`API Error ${response.status}: ${err.substring(0, 50)}`);
 
 
 
 
88
  }
89
 
90
  const data = await response.json();
91
+
92
+ if (typeof data === 'string') return data;
93
+ if (data.text) return data.text;
94
+ if (data.response) return data.response;
95
+ if (data.generated_text) return data.generated_text;
96
+
97
+ return JSON.stringify(data);
98
+
99
  } catch (error: any) {
100
  clearTimeout(timeoutId);
101
+ if (retries > 0 && (error.name === 'AbortError' || error.message.includes('Failed'))) {
 
 
102
  await new Promise(r => setTimeout(r, 5000));
103
  return makeRequest(retries - 1);
104
  }
105
  throw error;
106
  }
107
  };
108
+
109
+ try { return await makeRequest(); } catch (error) { throw error; }
 
 
 
 
 
110
  };
111
 
 
112
  const parseRiskResponse = (text: string, calculatedScore: number): RiskAnalysisResult => {
113
  try {
114
+ let jsonStr = text;
115
+ const codeBlockMatch = text.match(/```json\s*(\{[\s\S]*?\})\s*```/);
116
+ if (codeBlockMatch) jsonStr = codeBlockMatch[1];
117
+ else {
118
+ const braceMatch = text.match(/\{[\s\S]*\}/);
119
+ if (braceMatch) jsonStr = braceMatch[0];
120
+ }
121
  const data = JSON.parse(jsonStr);
 
122
  const pipeline = [
123
  { code: data.primaryConditionCode?.code || "N/A", description: data.primaryConditionCode?.description || "Unknown", type: 'Primary' },
124
  ...(data.historyCodes || []).map((h: any) => ({ code: h.code, description: h.description, type: 'History' }))
125
  ];
 
 
 
126
  return {
127
  numericScore: calculatedScore,
128
  summary: cleanText(data.summary || "Analysis completed."),
129
  actionItems: (data.actionItems || []).map(cleanText),
130
+ icd10Codes: [],
131
  codingPipeline: pipeline as any,
132
  insuranceNote: cleanText(data.insuranceNote || "Review required."),
133
  timestamp: new Date().toISOString()
134
  };
135
  } catch (e) {
136
+ return {
137
+ numericScore: calculatedScore,
138
+ summary: cleanText(text).substring(0, 500) || "Analysis currently unavailable.",
139
+ actionItems: ["Review inputs", "Consult provider"],
140
+ icd10Codes: [],
141
+ codingPipeline: [],
142
+ insuranceNote: "Automated analysis fallback.",
143
+ timestamp: new Date().toISOString()
144
+ }
145
  }
146
  };
147
 
148
+ export const extractClinicalData = async (imageBase64: string): Promise<ExtractionResult> => {
149
+ const base64Data = imageBase64.includes('base64,') ? imageBase64.split('base64,')[1] : imageBase64;
150
+ const prompt = `Analyze medical image. Extract JSON: { name, age, condition, history, allergies, systolicBp, glucose, heartRate, weight, temperature, spo2, clinicalNote }. Return JSON only.`;
151
+
152
+ try {
153
+ if (!API_KEY) throw new Error("API Key missing");
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ const response = await ai.models.generateContent({
156
+ model: MODEL_FAST,
157
+ contents: [{ role: 'user', parts: [{ text: prompt }, { inlineData: { mimeType: 'image/jpeg', data: base64Data } }] }],
158
+ config: { responseMimeType: "application/json", maxOutputTokens: 2000 }
159
+ });
 
 
 
160
 
161
+ const text = response.text || "{}";
162
+ const data = JSON.parse(text);
163
+ return {
164
+ profile: { name: data.name, age: data.age, condition: data.condition, history: data.history, allergies: data.allergies },
165
+ vitals: { systolicBp: data.systolicBp, glucose: data.glucose, heartRate: data.heartRate, weight: data.weight, temperature: data.temperature, spo2: data.spo2, clinicalNote: data.clinicalNote },
166
+ confidence: 0.9
167
+ };
168
 
169
+ } catch (e: any) {
170
+ try {
171
+ const compressedBase64 = await compressImage(imageBase64);
172
+ const cleanBase64 = compressedBase64.includes('base64,') ? compressedBase64.split('base64,')[1] : compressedBase64;
173
+ const resText = await callFallbackAPI('/vision', { image: cleanBase64, prompt: "Describe this medical document in detail, listing any numbers or patient names found." });
174
+
175
+ return {
176
+ profile: {},
177
+ vitals: { clinicalNote: `[Auto-Scanned by SomAI Vision]: ${resText}` },
178
+ confidence: 0.6
179
+ }
180
+ } catch (fallbackError) {
181
+ throw new Error("Scan failed. Please type details manually.");
182
+ }
183
+ }
184
+ };
185
 
186
+ export const transcribeAudio = async (audioBlob: Blob): Promise<string> => {
187
+ const reader = new FileReader();
188
+ return new Promise((resolve, reject) => {
189
+ reader.onloadend = async () => {
190
+ const base64 = (reader.result as string).split(',')[1];
191
+ try {
192
+ const text = await callFallbackAPI('/transcribe', { audio: base64 });
193
+ resolve(text);
194
+ } catch (e) { reject("Voice transcription failed."); }
195
+ };
196
+ reader.readAsDataURL(audioBlob);
197
+ });
198
+ };
199
+
200
+ export const analyzeRisk = async (profile: PatientProfile, vitals: ClinicalVitals, calculatedScore: number): Promise<RiskAnalysisResult> => {
201
+ const prompt = `
202
+ Act as a Senior Clinical Risk Assessor.
203
+ Patient: ${profile.name} (${profile.age}, ${profile.gender}). Condition: ${profile.condition}.
204
+ History: ${profile.history}. Surgeries: ${profile.surgeries}. Family History: ${profile.familyHistory}.
205
+ Lifestyle: Diet-${profile.diet}, Exercise-${profile.exerciseFrequency}, Smoke-${profile.smokingStatus}, Alcohol-${profile.alcoholConsumption}.
206
+ Vitals: BP Morning ${vitals.systolicBpMorning} / Evening ${vitals.systolicBpEvening}. Glucose ${vitals.glucose}. HR ${vitals.heartRate}. SpO2 ${vitals.spo2}%. Temp ${vitals.temperature}F. Weight ${vitals.weight}kg.
207
+ Note: ${vitals.clinicalNote}.
208
+ Task: 1. Summary (Risk level). 2. 3 Action Items. 3. ICD-10 Pipeline (Condition, History, Symptoms). 4. Insurance Note.
209
+ Return JSON.
210
+ `;
211
 
212
+ try {
213
+ if (!API_KEY) throw new Error("API Key missing");
214
  const response = await ai.models.generateContent({
215
  model: MODEL_FAST,
216
  contents: prompt,
217
  config: {
218
  responseMimeType: "application/json",
219
+ maxOutputTokens: 4000,
220
  responseSchema: {
221
  type: Type.OBJECT,
222
  properties: {
223
  summary: { type: Type.STRING },
224
  actionItems: { type: Type.ARRAY, items: { type: Type.STRING } },
225
+ primaryConditionCode: { type: Type.OBJECT, properties: { code: {type: Type.STRING}, description: {type: Type.STRING} } },
226
+ historyCodes: { type: Type.ARRAY, items: { type: Type.OBJECT, properties: { code: {type: Type.STRING}, description: {type: Type.STRING} } } },
 
 
 
 
 
 
 
 
 
227
  insuranceNote: { type: Type.STRING }
228
  },
229
  required: ["summary", "actionItems", "primaryConditionCode", "historyCodes", "insuranceNote"]
230
  }
231
  }
232
  });
233
+ return {
234
+ ...parseRiskResponse(response.text || "{}", calculatedScore),
235
+ source: 'Gemini 2.5 Flash'
236
+ };
237
+ } catch (err: any) {
238
+ try {
239
+ const payload = { ...profile, ...vitals, riskScore: calculatedScore, prompt };
240
+ const fallback = await callFallbackAPI('/analyze', payload);
241
+ return {
242
+ ...parseRiskResponse(fallback, calculatedScore),
243
+ source: 'Phi-3 Mini (Fallback)'
244
+ };
245
+ } catch {
246
+ throw new Error("Analysis failed");
247
+ }
248
+ }
249
+ };
250
 
251
+ export const generateHealthInsights = async (profile: PatientProfile, vitals: ClinicalVitals): Promise<HealthInsights> => {
252
+ const prompt = `Based on Patient: ${profile.name}, ${profile.age}y, ${profile.condition}. Vitals: BP ${vitals.systolicBp}, SpO2 ${vitals.spo2}%. Generate JSON: { weeklySummary, progress, tips: [] }.`;
253
+ try {
254
+ if (!API_KEY) throw new Error("No Key");
255
+ const response = await ai.models.generateContent({
256
+ model: MODEL_FAST,
257
+ contents: prompt,
258
+ config: { responseMimeType: "application/json", maxOutputTokens: 2000 }
259
+ });
260
+ return JSON.parse(response.text || "{}");
261
+ } catch {
262
+ return { weeklySummary: "Keep tracking your vitals.", progress: "Data accumulated.", tips: ["Maintain a balanced diet.", "Stay hydrated."] };
263
+ }
264
+ };
265
 
266
+ export const generateSessionName = async (userText: string, aiText: string): Promise<string> => {
267
+ const prompt = `Generate a very short, specific title (max 4 words) for a medical chat session based on this context.
268
+ User: ${userText}
269
+ AI: ${aiText}
270
+ Title:`;
271
+
272
+ try {
273
+ if (!API_KEY) return "New Consultation";
274
+ const response = await ai.models.generateContent({
275
+ model: MODEL_FAST,
276
+ contents: prompt,
277
+ config: { maxOutputTokens: 20 }
278
+ });
279
+ return cleanText(response.text || "New Consultation").replace(/^["']|["']$/g, '');
280
+ } catch (e) {
281
  try {
282
+ const fallbackRes = await callFallbackAPI('/generate', { prompt: prompt });
283
+ return cleanText(fallbackRes).replace(/^["']|["']$/g, '');
284
+ } catch {
285
+ return "New Consultation";
 
286
  }
287
  }
288
  };
289
 
290
  export const generateChatResponse = async (
291
+ history: ChatMessage[],
292
+ currentMessage: string,
293
+ image: string | undefined,
294
+ profile: PatientProfile,
295
+ mode: AppMode,
296
+ onSource: (source: string) => void
297
  ): Promise<string> => {
298
+ const context = `
299
+ Patient: ${profile.name} (${profile.age}y).
300
+ Condition: ${profile.condition}. History: ${profile.history}.
301
+ Surgeries: ${profile.surgeries}. Family Hx: ${profile.familyHistory}.
302
+ Lifestyle: ${profile.diet}, ${profile.exerciseFrequency}, Smoke: ${profile.smokingStatus}.
303
+ Emergency Contact: ${profile.emergencyContactName} (${profile.emergencyContactPhone}).
304
+ Tone: ${mode === AppMode.THERAPY ? 'Empathetic, calm, therapeutic (CBT).' : 'Professional, educational, clear.'}
305
+ Format: Plain text. No markdown.
306
  `;
307
+
308
+ const contents = history.map(msg => ({ role: msg.role === 'user' ? 'user' : 'model', parts: [{ text: msg.text }, ...(msg.image ? [{ inlineData: { mimeType: 'image/jpeg', data: msg.image.split('base64,')[1] } }] : [])] }));
309
+ contents.push({ role: 'user', parts: [{ text: context + "\nUser: " + currentMessage }, ...(image ? [{ inlineData: { mimeType: 'image/jpeg', data: image.split('base64,')[1] } }] : [])] });
 
310
 
311
  try {
312
+ if (!API_KEY) throw new Error("No Key");
313
+
314
+ // 1. Try Gemini
315
+ onSource('Gemini 2.5 Flash');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  const response = await ai.models.generateContent({
317
  model: MODEL_FAST,
318
  contents: contents,
319
  config: {
320
+ maxOutputTokens: 4000,
321
+ temperature: 0.7
 
322
  }
323
  });
324
 
325
+ return cleanText(response.text || "I didn't catch that.");
 
 
 
 
 
 
 
 
326
 
327
+ } catch (e) {
328
  try {
329
+ // 2. Fallback
330
+ onSource('Phi-3 Mini (Fallback)');
331
+ const fallbackPrompt = `${context}\n\nChat History:\n${history.slice(-3).map(m => m.text).join('\n')}\nUser: ${currentMessage}`;
332
+ const responseText = await callFallbackAPI('/generate', { prompt: fallbackPrompt });
333
+ return cleanText(responseText);
334
+
335
+ } catch {
336
+ return "I'm having trouble connecting. Please check your internet.";
 
 
 
 
337
  }
338
  }
339
  };
340
 
341
+ // --- UPDATED: CONTEXT-AWARE QUICK REPLIES ---
342
+ export const generateQuickReplies = async (history: ChatMessage[]) => {
343
+ if (!API_KEY || history.length === 0) return [];
344
+
345
+ // Use last 3 messages for context
346
+ const recentContext = history.slice(-3).map(m => `${m.role}: ${m.text}`).join('\n');
347
+ const prompt = `Based on this conversation:\n${recentContext}\n\nSuggest 3 short, relevant follow-up questions the USER might want to ask next. Return ONLY a JSON array of strings.`;
348
 
349
  try {
350
+ const res = await ai.models.generateContent({
351
+ model: MODEL_FAST,
352
+ contents: prompt,
353
+ config: { responseMimeType: "application/json" }
 
 
 
 
 
 
354
  });
355
+ return JSON.parse(res.text || "[]");
356
+ } catch { return []; }
 
 
357
  };
358
 
359
+ export const summarizeConversation = async (history: ChatMessage[]) => {
360
+ if (!API_KEY) return "Summary unavailable.";
 
 
 
 
 
 
 
 
 
361
  try {
362
+ const res = await ai.models.generateContent({ model: MODEL_FAST, contents: `Summarize clinical conversation:\n${history.map(m=>m.text).join('\n')}` });
363
+ return cleanText(res.text || "");
364
+ } catch { return "Could not summarize."; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  };