abhikamuni commited on
Commit
e9a3ab1
·
1 Parent(s): c4a671a
app.py CHANGED
@@ -1,6 +1,4 @@
1
- #
2
- # app.py (Modified to use LLM for descriptions and a robust model path)
3
- #
4
  import os
5
  import sys
6
 
@@ -17,10 +15,12 @@ from langchain_openai import ChatOpenAI
17
  from langchain_pinecone import PineconeVectorStore
18
  from werkzeug.utils import secure_filename
19
 
 
 
 
20
  app = Flask(__name__)
21
  load_dotenv()
22
 
23
-
24
  # --- Configuration for Chatbot (Existing) ---
25
  PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
26
  GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
@@ -28,77 +28,89 @@ GITHUB_AI_ENDPOINT = "https://models.github.ai/inference"
28
  GITHUB_AI_MODEL_NAME = "Phi-3-small-8k-instruct"
29
  os.environ["PINECONE_API_KEY"] = PINECONE_API_KEY
30
 
31
-
32
  # --- Configuration for Eye Disease Model (Newly Added) ---
33
  app.config['UPLOAD_FOLDER'] = 'static/uploads/'
34
  # --- FIX: Use an absolute path to ensure the model is found ---
35
  MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'abhi_model.h5')
36
 
37
-
38
  # --- Load Models and Setup Chains (Existing and New) ---
39
-
40
  # 1. Load Chatbot RAG Chain (Existing)
41
- from src.helpers import download_hugging_face_embeddings
42
- embeddings = download_hugging_face_embeddings()
43
- index_name = "medicalbot"
44
- docsearch = PineconeVectorStore.from_existing_index(
45
- index_name=index_name,
46
- embedding=embeddings
47
- )
48
- retriever = docsearch.as_retriever(search_type="similarity", search_kwargs={"k": 3})
49
- llm = ChatOpenAI(
50
- temperature=0.4,
51
- max_tokens=500,
52
- model=GITHUB_AI_MODEL_NAME,
53
- openai_api_key=GITHUB_TOKEN,
54
- openai_api_base=GITHUB_AI_ENDPOINT
55
- )
56
- system_prompt = (
57
- "You are a helpful medical assistant. Use the retrieved information to answer the question concisely and accurately. "
58
- "If you are asked to describe a medical condition, explain what it is, its common symptoms, and general causes in a way that is easy for a non-medical person to understand. "
59
- "Always include a disclaimer that the user should consult a qualified healthcare professional for a real diagnosis."
60
- "\n\n"
61
- "{context}"
62
- )
63
- prompt = ChatPromptTemplate.from_messages(
64
- [
65
- ("system", system_prompt),
66
- ("human", "{input}"),
67
- ]
68
- )
69
- Youtube_chain = create_stuff_documents_chain(llm, prompt)
70
- rag_chain = create_retrieval_chain(retriever, Youtube_chain)
 
 
 
 
 
71
 
72
 
73
  # 2. Load Eye Disease Prediction Model (Newly Added)
74
  try:
 
75
  eye_disease_model = load_model(MODEL_PATH)
76
  print("Successfully loaded eye disease model.")
77
  except Exception as e:
78
- print(f"Error loading model from path: {MODEL_PATH}")
79
- print(f"Details: {e}")
80
  sys.exit(1) # Exit if the model cannot be loaded
81
 
82
 
83
  # --- Prediction Helper Function (Newly Added) ---
84
  def predict_eye_disease(img_path):
85
  """Predicts the eye disease from an image path."""
86
- img = image.load_img(img_path, target_size=(256, 256))
87
- img_array = image.img_to_array(img)
88
- img_array = img_array / 255.0 # Normalize
89
- img_array = np.expand_dims(img_array, axis=0)
90
-
91
- # Assuming 'rnn_input' is not actually used by your model, as it's a zero array.
92
- # If it is, you'll need to define it correctly.
93
- rnn_input = np.zeros((img_array.shape[0], 512))
94
-
95
- prediction = eye_disease_model.predict([img_array, rnn_input])
96
-
97
- class_names = ['cataract', 'diabetic_retinopathy', 'glaucoma', 'normal']
98
- predicted_class_index = np.argmax(prediction)
99
- predicted_class = class_names[predicted_class_index]
100
-
101
- return predicted_class
 
 
 
 
 
 
 
 
 
102
 
103
 
104
  # --- Flask Routes ---
@@ -125,18 +137,26 @@ def upload_file():
125
  to generate a description for the predicted disease.
126
  """
127
  if 'file' not in request.files:
 
128
  return jsonify({'error': 'No file part in the request'}), 400
129
 
130
  file = request.files['file']
131
  if file.filename == '':
 
132
  return jsonify({'error': 'No file selected for uploading'}), 400
133
 
134
  if file:
135
  filename = secure_filename(file.filename)
 
 
 
 
136
  file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
137
- file.save(file_path)
138
-
139
  try:
 
 
 
140
  # 1. Get prediction from the vision model
141
  predicted_class_name = predict_eye_disease(file_path)
142
 
@@ -154,18 +174,17 @@ def upload_file():
154
  'description': description
155
  })
156
  except Exception as e:
157
- print(f"Error during prediction: {e}")
158
- return jsonify({'error': 'Failed to process image'}), 500
159
  finally:
160
  # Clean up the uploaded file
161
  if os.path.exists(file_path):
 
162
  os.remove(file_path)
163
 
 
164
  return jsonify({'error': 'Unknown error occurred'}), 500
165
 
166
 
167
  if __name__ == '__main__':
168
- # Make sure the upload folder exists
169
- if not os.path.exists(app.config['UPLOAD_FOLDER']):
170
- os.makedirs(app.config['UPLOAD_FOLDER'])
171
- app.run()
 
1
+
 
 
2
  import os
3
  import sys
4
 
 
15
  from langchain_pinecone import PineconeVectorStore
16
  from werkzeug.utils import secure_filename
17
 
18
+ # --- Imports for Chatbot (Existing) ---
19
+ from src.helpers import download_hugging_face_embeddings
20
+
21
  app = Flask(__name__)
22
  load_dotenv()
23
 
 
24
  # --- Configuration for Chatbot (Existing) ---
25
  PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY")
26
  GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
 
28
  GITHUB_AI_MODEL_NAME = "Phi-3-small-8k-instruct"
29
  os.environ["PINECONE_API_KEY"] = PINECONE_API_KEY
30
 
 
31
  # --- Configuration for Eye Disease Model (Newly Added) ---
32
  app.config['UPLOAD_FOLDER'] = 'static/uploads/'
33
  # --- FIX: Use an absolute path to ensure the model is found ---
34
  MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'abhi_model.h5')
35
 
 
36
  # --- Load Models and Setup Chains (Existing and New) ---
 
37
  # 1. Load Chatbot RAG Chain (Existing)
38
+ try:
39
+ print("Initializing chatbot components...")
40
+ embeddings = download_hugging_face_embeddings()
41
+ index_name = "medicalbot"
42
+ docsearch = PineconeVectorStore.from_existing_index(
43
+ index_name=index_name,
44
+ embedding=embeddings
45
+ )
46
+ retriever = docsearch.as_retriever(search_type="similarity", search_kwargs={"k": 3})
47
+ llm = ChatOpenAI(
48
+ temperature=0.4,
49
+ max_tokens=500,
50
+ model=GITHUB_AI_MODEL_NAME,
51
+ openai_api_key=GITHUB_TOKEN,
52
+ openai_api_base=GITHUB_AI_ENDPOINT
53
+ )
54
+ system_prompt = (
55
+ "You are a helpful medical assistant. Use the retrieved information to answer the question concisely and accurately. "
56
+ "If you are asked to describe a medical condition, explain what it is, its common symptoms, and general causes in a way that is easy for a non-medical person to understand. "
57
+ "Always include a disclaimer that the user should consult a qualified healthcare professional for a real diagnosis."
58
+ "\n\n"
59
+ "{context}"
60
+ )
61
+ prompt = ChatPromptTemplate.from_messages(
62
+ [
63
+ ("system", system_prompt),
64
+ ("human", "{input}"),
65
+ ]
66
+ )
67
+ Youtube_chain = create_stuff_documents_chain(llm, prompt)
68
+ rag_chain = create_retrieval_chain(retriever, Youtube_chain)
69
+ print("Chatbot components initialized successfully.")
70
+ except Exception as e:
71
+ print(f"Error initializing chatbot components: {e}", file=sys.stderr)
72
+ sys.exit(1)
73
 
74
 
75
  # 2. Load Eye Disease Prediction Model (Newly Added)
76
  try:
77
+ print(f"Attempting to load eye disease model from path: {MODEL_PATH}")
78
  eye_disease_model = load_model(MODEL_PATH)
79
  print("Successfully loaded eye disease model.")
80
  except Exception as e:
81
+ print(f"Error loading model from path: {MODEL_PATH}", file=sys.stderr)
82
+ print(f"Details: {e}", file=sys.stderr)
83
  sys.exit(1) # Exit if the model cannot be loaded
84
 
85
 
86
  # --- Prediction Helper Function (Newly Added) ---
87
  def predict_eye_disease(img_path):
88
  """Predicts the eye disease from an image path."""
89
+ print(f"Starting prediction for image: {img_path}")
90
+ try:
91
+ img = image.load_img(img_path, target_size=(256, 256))
92
+ img_array = image.img_to_array(img)
93
+ img_array = img_array / 255.0 # Normalize
94
+ img_array = np.expand_dims(img_array, axis=0)
95
+
96
+ # Check the input shape before prediction
97
+ print(f"Input image shape for model: {img_array.shape}")
98
+
99
+ rnn_input = np.zeros((img_array.shape[0], 512))
100
+
101
+ # Make the prediction
102
+ prediction = eye_disease_model.predict([img_array, rnn_input])
103
+ print("Prediction successful.")
104
+
105
+ class_names = ['cataract', 'diabetic_retinopathy', 'glaucoma', 'normal']
106
+ predicted_class_index = np.argmax(prediction)
107
+ predicted_class = class_names[predicted_class_index]
108
+
109
+ print(f"Predicted class: {predicted_class}")
110
+ return predicted_class
111
+ except Exception as e:
112
+ print(f"Error in predict_eye_disease function: {e}", file=sys.stderr)
113
+ raise # Re-raise the exception to be caught in the route handler
114
 
115
 
116
  # --- Flask Routes ---
 
137
  to generate a description for the predicted disease.
138
  """
139
  if 'file' not in request.files:
140
+ print("Error: No file part in the request.")
141
  return jsonify({'error': 'No file part in the request'}), 400
142
 
143
  file = request.files['file']
144
  if file.filename == '':
145
+ print("Error: No file selected for uploading.")
146
  return jsonify({'error': 'No file selected for uploading'}), 400
147
 
148
  if file:
149
  filename = secure_filename(file.filename)
150
+ # Create the uploads directory if it doesn't exist
151
+ if not os.path.exists(app.config['UPLOAD_FOLDER']):
152
+ os.makedirs(app.config['UPLOAD_FOLDER'])
153
+
154
  file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
155
+
 
156
  try:
157
+ print(f"Saving uploaded file to: {file_path}")
158
+ file.save(file_path)
159
+
160
  # 1. Get prediction from the vision model
161
  predicted_class_name = predict_eye_disease(file_path)
162
 
 
174
  'description': description
175
  })
176
  except Exception as e:
177
+ print(f"Uncaught exception in '/predict_disease' route: {e}", file=sys.stderr)
178
+ return jsonify({'error': 'Failed to process image due to a server error.'}), 500
179
  finally:
180
  # Clean up the uploaded file
181
  if os.path.exists(file_path):
182
+ print(f"Removing temporary file: {file_path}")
183
  os.remove(file_path)
184
 
185
+ print("Error: Unknown error occurred.")
186
  return jsonify({'error': 'Unknown error occurred'}), 500
187
 
188
 
189
  if __name__ == '__main__':
190
+ app.run(host='0.0.0.0', port=7860)
 
 
 
static/src/__init__.py DELETED
File without changes
static/src/__pycache__/__init__.cpython-311.pyc DELETED
Binary file (179 Bytes)
 
static/src/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (167 Bytes)
 
static/src/__pycache__/helpers.cpython-311.pyc DELETED
Binary file (1.37 kB)
 
static/src/__pycache__/helpers.cpython-313.pyc DELETED
Binary file (1.19 kB)
 
static/src/__pycache__/prompt.cpython-311.pyc DELETED
Binary file (493 Bytes)
 
static/src/__pycache__/prompt.cpython-313.pyc DELETED
Binary file (487 Bytes)
 
static/src/helpers.py DELETED
@@ -1,23 +0,0 @@
1
- from langchain.document_loaders import PyPDFLoader,DirectoryLoader
2
- from langchain.text_splitter import RecursiveCharacterTextSplitter
3
- from langchain.embeddings import HuggingFaceEmbeddings
4
-
5
- #Extract text from a PDF file
6
- def load_pdf_file(data):
7
- loader = DirectoryLoader(data,
8
- glob = "*.pdf", #LOAD ALL PDF FILES IN THE DIRECTORY
9
- loader_cls=PyPDFLoader) # EXTRACT TEXT FROM PDF FILES
10
- documents = loader.load()
11
- return documents
12
-
13
-
14
- #split the Data into smaller chunks
15
- def text_split(extracted_data):
16
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=200)
17
- texts_chunks = text_splitter.split_documents(extracted_data)
18
- return texts_chunks
19
-
20
- #download the embeddings model from HuggingFace
21
- def download_hugging_face_embeddings():
22
- embeddngs = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
23
- return embeddngs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/src/prompt.py DELETED
@@ -1,10 +0,0 @@
1
-
2
-
3
- sytem_prompt = (
4
- "You are a assistant that answers questions-answers tasks"
5
- "Use the following pieces of retrieved information to answer the question. "
6
- "If you don't know the answer, just say that you don't know, don't try to make up an answer."
7
- "Use the three sentences maximum and keep the answer concise."
8
- "\n\n"
9
- "{context}"
10
- )
 
 
 
 
 
 
 
 
 
 
 
templates/chat.html CHANGED
@@ -111,274 +111,266 @@
111
  </div>
112
  </div>
113
 
114
- <script>
115
- // --- DOM Element Caching ---
116
- const sidebar = document.querySelector('.sidebar');
117
- const menuButton = document.getElementById('menu-button');
118
- const newChatButton = document.getElementById('new-chat-button');
119
- const textarea = document.getElementById('message-input');
120
- const sendButton = document.getElementById('send-button');
121
- const messagesDiv = document.getElementById('messages');
122
- const welcomeMessageDiv = document.getElementById('welcome-message');
123
- const chatContainer = document.getElementById('chat-container');
124
- const inputModeButton = document.getElementById('input-mode-button');
125
- const inputModePanel = document.getElementById('input-mode-panel');
126
- const currentModeText = document.getElementById('current-mode-text');
127
-
128
- // Image-specific elements
129
- const imageUploadButton = document.getElementById('image-upload-button');
130
- const imageUploadInput = document.getElementById('image-upload-input');
131
-
132
- // --- State Management ---
133
- let currentInputMode = 'Symptom Analysis';
 
134
 
135
- // --- Core Functions ---
136
- const createMessageElement = (role, content) => {
137
- const roleClasses = {
138
- user: {
139
- container: 'message-user',
140
- icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-gray-600"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>`,
141
- },
142
- assistant: {
143
- container: 'message-assistant',
144
- icon: `<svg xmlns="http://www.w3.org/2d00/svg" viewBox="0 0 24 24" fill="white" class="w-5 h-5"><path d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /></svg>`,
145
- },
146
- };
147
-
148
- const isUser = role === 'user';
149
- const div = document.createElement('div');
150
- div.className = `${roleClasses[role].container} border-b border-gray-200/50`;
151
- div.dataset.role = role;
152
-
153
- const contentDiv = document.createElement('div');
154
- if (isUser) {
155
- // If the user content is an image, render it as HTML
156
- if (content.startsWith('<img')) {
157
- contentDiv.innerHTML = content;
158
- } else {
159
- contentDiv.textContent = content;
160
- }
161
- } else if (role === 'assistant') {
162
- contentDiv.innerHTML = marked.parse(content);
163
- contentDiv.classList.add('markdown-container');
164
- } else { // Typing indicator
165
- contentDiv.innerHTML = `<div class="animate-pulse text-gray-500">typing...</div>`;
166
- }
167
 
168
- const iconContainerClass = isUser ? 'bg-gray-200' : 'bg-blue-500';
169
- const mainAlignmentClass = isUser ? 'justify-end' : '';
170
- const flexDirectionClass = isUser ? 'flex-row-reverse' : '';
171
- const textAlignmentClass = isUser ? 'text-right' : '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- div.innerHTML = `
174
- <div class="mx-auto max-w-3xl px-4 py-8">
175
- <div class="flex ${mainAlignmentClass}">
176
- <div class="flex items-start gap-4 max-w-[80%] ${flexDirectionClass}">
177
- <div class="w-8 h-8 rounded-full ${iconContainerClass} flex-shrink-0 flex items-center justify-center">
178
- ${roleClasses[role].icon}
179
- </div>
180
- <div class="mt-1 text-sm ${textAlignmentClass}">
181
- ${contentDiv.outerHTML}
182
- </div>
183
- </div>
184
- </div>
185
- </div>
186
- `;
187
- return div;
188
- };
189
 
190
- const appendMessage = (role, content) => {
191
- const elementRole = role === 'typing' ? 'assistant' : role;
192
- const messageElement = createMessageElement(elementRole, content);
193
- if(role === 'typing') messageElement.dataset.role = 'typing';
194
-
195
- messagesDiv.appendChild(messageElement);
196
- chatContainer.scrollTop = chatContainer.scrollHeight;
197
- };
198
-
199
- const removeTypingIndicator = () => {
200
- const indicator = messagesDiv.querySelector('[data-role="typing"]');
201
- if (indicator) indicator.remove();
202
- };
203
 
204
- const sendMessage = async () => {
205
- const message = textarea.value.trim();
206
- if (!message) return;
207
 
208
- if (welcomeMessageDiv.style.display !== 'none') {
209
- welcomeMessageDiv.style.display = 'none';
210
- }
211
 
212
- appendMessage('user', message);
213
- textarea.value = '';
214
- textarea.style.height = 'auto';
215
 
216
- textarea.disabled = true;
217
- sendButton.disabled = true;
218
- imageUploadButton.disabled = true;
219
- appendMessage('typing', '');
220
 
221
- const messageToSend = `[${currentInputMode}] ${message}`;
222
 
223
- try {
224
- const response = await fetch('/get', {
225
- method: 'POST',
226
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
227
- body: `msg=${encodeURIComponent(messageToSend)}`
228
- });
229
 
230
- if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
231
-
232
- const botResponse = await response.text();
233
- removeTypingIndicator();
234
- appendMessage('assistant', botResponse);
235
- } catch (error) {
236
- console.error('Error fetching bot response:', error);
237
- removeTypingIndicator();
238
- appendMessage('assistant', "Sorry, I'm having trouble connecting right now. Please try again later.");
239
- } finally {
240
- textarea.disabled = false;
241
- sendButton.disabled = false;
242
- imageUploadButton.disabled = false;
243
- textarea.focus();
244
- }
245
- };
246
 
247
- const sendImage = async (file) => {
248
- if (!file) return;
249
 
250
- if (welcomeMessageDiv.style.display !== 'none') {
251
- welcomeMessageDiv.style.display = 'none';
252
- }
253
 
254
- // Read the file to display a thumbnail
255
- const reader = new FileReader();
256
- reader.onload = (e) => {
257
- const imgHtml = `<img src="${e.target.result}" class="max-h-48 rounded-md mx-auto my-2 border border-gray-200">`;
258
- appendMessage('user', imgHtml);
259
- };
260
- reader.readAsDataURL(file);
261
 
262
- // Disable inputs and show typing indicator
263
- textarea.disabled = true;
264
- sendButton.disabled = true;
265
- imageUploadButton.disabled = true;
266
- appendMessage('typing', '');
267
 
268
- const formData = new FormData();
269
- formData.append('file', file);
270
-
271
- try {
272
- const response = await fetch('/predict_disease', {
273
- method: 'POST',
274
- body: formData,
275
- });
276
 
277
- removeTypingIndicator();
278
 
279
- if (response.ok) {
280
- const data = await response.json();
281
- const predictionMessage = `**Prediction:** ${data.prediction}<br><br>${data.description}`;
282
- appendMessage('assistant', predictionMessage);
283
- } else {
284
- const errorData = await response.json();
285
- console.error('Server error:', errorData.error);
286
- appendMessage('assistant', `Error: ${errorData.error}`);
287
- }
288
- } catch (error) {
289
- console.error('Network error:', error);
290
- removeTypingIndicator();
291
- appendMessage('assistant', 'Sorry, I\'m having trouble connecting to the image classification service.');
292
- } finally {
293
- textarea.disabled = false;
294
- sendButton.disabled = false;
295
- imageUploadButton.disabled = false;
296
- textarea.focus();
297
- }
298
- chatContainer.scrollTop = chatContainer.scrollHeight;
299
- };
300
-
301
- // --- Event Listeners ---
302
- const toggleMenu = () => {
303
- sidebar.classList.toggle('open');
304
- };
305
 
306
- menuButton.addEventListener('click', toggleMenu);
307
-
308
- newChatButton.addEventListener('click', () => {
309
- messagesDiv.innerHTML = '';
310
- welcomeMessageDiv.style.display = 'block';
311
- textarea.value = '';
312
- textarea.style.height = 'auto';
313
- if (window.innerWidth < 769) {
314
- sidebar.classList.remove('open');
315
- }
316
- currentInputMode = 'Symptom Analysis';
317
- currentModeText.textContent = 'Symptom Analysis';
318
- });
319
 
320
- textarea.addEventListener('input', () => {
321
- textarea.style.height = 'auto';
322
- textarea.style.height = `${textarea.scrollHeight}px`;
323
- });
324
-
325
- sendButton.addEventListener('click', sendMessage);
326
 
327
- textarea.addEventListener('keydown', (e) => {
328
- if (e.key === 'Enter' && !e.shiftKey) {
329
- e.preventDefault();
330
- sendMessage();
331
- }
332
- });
333
-
334
- // Image upload button handler
335
- imageUploadButton.addEventListener('click', () => {
336
- imageUploadInput.click();
337
- });
338
 
339
- // Handle file selection from the hidden input
340
- imageUploadInput.addEventListener('change', (e) => {
341
- const file = e.target.files[0];
342
- if (!file) return;
343
 
344
- // Check if the current mode allows image uploads
345
- if (currentInputMode === 'Eye Scan Analysis') {
346
- // If it's the correct mode, send the image
347
- sendImage(file);
348
- } else {
349
- // If not, show a message in the chat interface
350
- appendMessage('assistant', "Image upload is only available in **Eye Scan Analysis** mode. Please switch modes to upload an eye scan image.");
351
- }
352
-
353
- // Clear the input so the same file can be uploaded again if needed
354
- e.target.value = '';
355
- });
356
 
357
 
358
- // --- Input Mode Dropdown Logic ---
359
- inputModeButton.addEventListener('click', (e) => {
360
- e.stopPropagation();
361
- inputModePanel.classList.toggle('hidden');
362
- });
363
 
364
- const modeOptions = document.querySelectorAll('.input-mode-option');
365
- modeOptions.forEach(option => {
366
- option.addEventListener('click', (e) => {
367
- e.preventDefault();
368
- currentInputMode = e.target.dataset.mode;
369
- currentModeText.textContent = currentInputMode;
370
- inputModePanel.classList.add('hidden');
371
- });
372
- });
373
 
374
- window.addEventListener('click', (e) => {
375
- if (!inputModePanel.classList.contains('hidden') && !inputModePanel.contains(e.target) && !inputModeButton.contains(e.target)) {
376
- inputModePanel.classList.add('hidden');
377
- }
378
- if (window.innerWidth < 769 && sidebar.classList.contains('open') && !sidebar.contains(e.target) && !menuButton.contains(e.target)) {
379
- toggleMenu();
380
- }
381
- });
382
- </script>
383
  </body>
384
  </html>
 
111
  </div>
112
  </div>
113
 
114
+
115
+     <script>
116
+         // --- DOM Element Caching ---
117
+         const sidebar = document.querySelector('.sidebar');
118
+         const menuButton = document.getElementById('menu-button');
119
+         const newChatButton = document.getElementById('new-chat-button');
120
+         const textarea = document.getElementById('message-input');
121
+         const sendButton = document.getElementById('send-button');
122
+         const messagesDiv = document.getElementById('messages');
123
+         const welcomeMessageDiv = document.getElementById('welcome-message');
124
+         const chatContainer = document.getElementById('chat-container');
125
+         const inputModeButton = document.getElementById('input-mode-button');
126
+         const inputModePanel = document.getElementById('input-mode-panel');
127
+         const currentModeText = document.getElementById('current-mode-text');
128
+        
129
+         // Image-specific elements
130
+         const imageUploadButton = document.getElementById('image-upload-button');
131
+         const imageUploadInput = document.getElementById('image-upload-input');
132
+        
133
+         // --- State Management ---
134
+         let currentInputMode = 'Symptom Analysis';
135
 
136
+         // --- Core Functions ---
137
+         const createMessageElement = (role, content) => {
138
+             const roleClasses = {
139
+                 user: {
140
+                     container: 'message-user',
141
+                     icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-gray-600"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>`,
142
+                 },
143
+                 assistant: {
144
+                     container: 'message-assistant',
145
+                     icon: `<svg xmlns="http://www.w3.org/2d00/svg" viewBox="0 0 24 24" fill="white" class="w-5 h-5"><path d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" /></svg>`,
146
+                 },
147
+             };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+             const isUser = role === 'user';
150
+             const div = document.createElement('div');
151
+             div.className = `${roleClasses[role].container} border-b border-gray-200/50`;
152
+             div.dataset.role = role;
153
+            
154
+             const contentDiv = document.createElement('div');
155
+             if (isUser) {
156
+                 if (content.startsWith('<img')) {
157
+                     contentDiv.innerHTML = content;
158
+                 } else {
159
+                     contentDiv.textContent = content;
160
+                 }
161
+             } else if (role === 'assistant') {
162
+                 contentDiv.innerHTML = marked.parse(content);
163
+                 contentDiv.classList.add('markdown-container');
164
+             } else if (role === 'typing') { // Added new 'typing' role
165
+                 contentDiv.innerHTML = `<div class="text-gray-500 italic">Processing image...</div>`;
166
+             }
167
+        
168
+             const iconContainerClass = isUser ? 'bg-gray-200' : 'bg-blue-500';
169
+             const mainAlignmentClass = isUser ? 'justify-end' : '';
170
+             const flexDirectionClass = isUser ? 'flex-row-reverse' : '';
171
+             const textAlignmentClass = isUser ? 'text-right' : '';
172
 
173
+             div.innerHTML = `
174
+                 <div class="mx-auto max-w-3xl px-4 py-8">
175
+                     <div class="flex ${mainAlignmentClass}">
176
+                         <div class="flex items-start gap-4 max-w-[80%] ${flexDirectionClass}">
177
+                             <div class="w-8 h-8 rounded-full ${iconContainerClass} flex-shrink-0 flex items-center justify-center">
178
+                                 ${roleClasses[role].icon}
179
+                             </div>
180
+                             <div class="mt-1 text-sm ${textAlignmentClass}">
181
+                                 ${contentDiv.outerHTML}
182
+                             </div>
183
+                     </div>
184
+                     </div>
185
+                 </div>
186
+             `;
187
+             return div;
188
+         };
189
 
190
+         const appendMessage = (role, content) => {
191
+             const elementRole = role === 'typing' ? 'assistant' : role;
192
+             const messageElement = createMessageElement(elementRole, content);
193
+             if(role === 'typing') messageElement.dataset.role = 'typing';
194
+            
195
+             messagesDiv.appendChild(messageElement);
196
+             chatContainer.scrollTop = chatContainer.scrollHeight;
197
+         };
198
+        
199
+         const removeTypingIndicator = () => {
200
+             const indicator = messagesDiv.querySelector('[data-role="typing"]');
201
+             if (indicator) indicator.remove();
202
+         };
203
 
204
+         const sendMessage = async () => {
205
+             const message = textarea.value.trim();
206
+             if (!message) return;
207
 
208
+             if (welcomeMessageDiv.style.display !== 'none') {
209
+                 welcomeMessageDiv.style.display = 'none';
210
+             }
211
 
212
+             appendMessage('user', message);
213
+             textarea.value = '';
214
+             textarea.style.height = 'auto';
215
 
216
+             textarea.disabled = true;
217
+             sendButton.disabled = true;
218
+             imageUploadButton.disabled = true;
219
+             appendMessage('typing', '');
220
 
221
+             const messageToSend = `[${currentInputMode}] ${message}`;
222
 
223
+             try {
224
+                 const response = await fetch('/get', {
225
+                     method: 'POST',
226
+                     headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
227
+                     body: `msg=${encodeURIComponent(messageToSend)}`
228
+                 });
229
 
230
+                 if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
231
+                
232
+                 const botResponse = await response.text();
233
+                 removeTypingIndicator();
234
+                 appendMessage('assistant', botResponse);
235
+             } catch (error) {
236
+                 console.error('Error fetching bot response:', error);
237
+                 removeTypingIndicator();
238
+                 appendMessage('assistant', "Sorry, I'm having trouble connecting right now. Please try again later.");
239
+             } finally {
240
+                 textarea.disabled = false;
241
+                 sendButton.disabled = false;
242
+                 imageUploadButton.disabled = false;
243
+                 textarea.focus();
244
+             }
245
+         };
246
 
247
+         const sendImage = async (file) => {
248
+             if (!file) return;
249
 
250
+             if (welcomeMessageDiv.style.display !== 'none') {
251
+                 welcomeMessageDiv.style.display = 'none';
252
+             }
253
 
254
+             const reader = new FileReader();
255
+             reader.onload = (e) => {
256
+                 const imgHtml = `<img src="${e.target.result}" class="max-h-48 rounded-md mx-auto my-2 border border-gray-200">`;
257
+                 appendMessage('user', imgHtml);
258
+             };
259
+             reader.readAsDataURL(file);
 
260
 
261
+             textarea.disabled = true;
262
+             sendButton.disabled = true;
263
+             imageUploadButton.disabled = true;
264
+             appendMessage('typing', ''); // Use the new 'typing' role
 
265
 
266
+             const formData = new FormData();
267
+             formData.append('file', file);
268
+            
269
+             try {
270
+                 const response = await fetch('/predict_disease', {
271
+                     method: 'POST',
272
+                     body: formData,
273
+                 });
274
 
275
+                 removeTypingIndicator();
276
 
277
+                 if (response.ok) {
278
+                     const data = await response.json();
279
+                     const predictionMessage = `**Prediction:** ${data.prediction}<br><br>${data.description}`;
280
+                     appendMessage('assistant', predictionMessage);
281
+                 } else {
282
+                     const errorData = await response.json();
283
+                     console.error('Server error:', errorData.error);
284
+                     appendMessage('assistant', `Error: ${errorData.error}`);
285
+                 }
286
+             } catch (error) {
287
+                 console.error('Network error:', error);
288
+                 removeTypingIndicator();
289
+                 appendMessage('assistant', 'Sorry, I\'m having trouble connecting to the image classification service. Please check the server logs for details.');
290
+             } finally {
291
+                 textarea.disabled = false;
292
+                 sendButton.disabled = false;
293
+                 imageUploadButton.disabled = false;
294
+                 textarea.focus();
295
+             }
296
+             chatContainer.scrollTop = chatContainer.scrollHeight;
297
+         };
298
+        
299
+         // --- Event Listeners ---
300
+         const toggleMenu = () => {
301
+             sidebar.classList.toggle('open');
302
+         };
303
 
304
+         menuButton.addEventListener('click', toggleMenu);
305
+        
306
+         newChatButton.addEventListener('click', () => {
307
+             messagesDiv.innerHTML = '';
308
+             welcomeMessageDiv.style.display = 'block';
309
+             textarea.value = '';
310
+             textarea.style.height = 'auto';
311
+             if (window.innerWidth < 769) {
312
+                 sidebar.classList.remove('open');
313
+             }
314
+             currentInputMode = 'Symptom Analysis';
315
+             currentModeText.textContent = 'Symptom Analysis';
316
+         });
317
 
318
+         textarea.addEventListener('input', () => {
319
+             textarea.style.height = 'auto';
320
+             textarea.style.height = `${textarea.scrollHeight}px`;
321
+         });
322
+        
323
+         sendButton.addEventListener('click', sendMessage);
324
 
325
+         textarea.addEventListener('keydown', (e) => {
326
+             if (e.key === 'Enter' && !e.shiftKey) {
327
+                 e.preventDefault();
328
+                 sendMessage();
329
+             }
330
+         });
331
+        
332
+         imageUploadButton.addEventListener('click', () => {
333
+             imageUploadInput.click();
334
+         });
 
335
 
336
+         imageUploadInput.addEventListener('change', (e) => {
337
+             const file = e.target.files[0];
338
+             if (!file) return;
 
339
 
340
+             if (currentInputMode === 'Eye Scan Analysis') {
341
+                 sendImage(file);
342
+             } else {
343
+                 appendMessage('assistant', "Image upload is only available in **Eye Scan Analysis** mode. Please switch modes to upload an eye scan image.");
344
+             }
345
+            
346
+             e.target.value = '';
347
+         });
 
 
 
 
348
 
349
 
350
+         // --- Input Mode Dropdown Logic ---
351
+         inputModeButton.addEventListener('click', (e) => {
352
+             e.stopPropagation();
353
+             inputModePanel.classList.toggle('hidden');
354
+         });
355
 
356
+         const modeOptions = document.querySelectorAll('.input-mode-option');
357
+         modeOptions.forEach(option => {
358
+             option.addEventListener('click', (e) => {
359
+                 e.preventDefault();
360
+                 currentInputMode = e.target.dataset.mode;
361
+                 currentModeText.textContent = currentInputMode;
362
+                 inputModePanel.classList.add('hidden');
363
+             });
364
+         });
365
 
366
+         window.addEventListener('click', (e) => {
367
+             if (!inputModePanel.classList.contains('hidden') && !inputModePanel.contains(e.target) && !inputModeButton.contains(e.target)) {
368
+                 inputModePanel.classList.add('hidden');
369
+             }
370
+             if (window.innerWidth < 769 && sidebar.classList.contains('open') && !sidebar.contains(e.target) && !menuButton.contains(e.target)) {
371
+                 toggleMenu();
372
+             }
373
+         });
374
+     </script>
375
  </body>
376
  </html>