kuro223 commited on
Commit
7d3a75b
·
1 Parent(s): e99fdac
app.py CHANGED
@@ -1,5 +1,3 @@
1
- # --- START OF FILE GeminiChatbot/app.py ---
2
-
3
  import os
4
  import logging
5
  import base64
@@ -24,9 +22,9 @@ api_key = os.environ.get("GEMINI_API_KEY")
24
  if not api_key:
25
  logger.warning("GEMINI_API_KEY not found in environment variables")
26
  else:
27
- genai.configure(api_key=api_key)
28
  logger.info("GEMINI_API_KEY found. API configured successfully.")
29
 
 
30
 
31
  # Initialize Flask app
32
  app = Flask(__name__)
@@ -47,194 +45,167 @@ def session_required(f):
47
  # Ensure upload directory exists
48
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
49
 
50
- # Initialize models only if API key is present
51
- model = None
52
- vision_model = None
53
- if api_key:
54
- # Configure Gemini model with specific parameters for better responses
55
- model = genai.GenerativeModel(
56
- model_name='gemini-1.0-pro', # Changed model name as 'gemini-2.0-flash' is not standard
57
- generation_config={
58
- 'temperature': 0.7, # Slightly creative but still focused
59
- 'top_p': 0.9, # Diverse output but not too random
60
- 'top_k': 40, # Reasonable range of tokens to consider
61
- 'max_output_tokens': 2048 # Allow longer responses
62
- }
63
- )
64
-
65
- # Configure Gemini vision model for image processing
66
- # Using gemini-1.5-flash as it's generally available and supports vision
67
- vision_model = genai.GenerativeModel('gemini-1.5-flash')
68
- else:
69
- logger.error("Cannot initialize Gemini models: API Key is missing.")
70
 
 
 
71
 
72
  @app.route('/')
73
  @session_required
74
  def index():
75
  """Render the chat interface."""
76
- if not api_key:
77
- return "Erreur: Clé API Gemini manquante. Veuillez configurer GEMINI_API_KEY.", 500
78
  return render_template('index.html')
79
 
80
  @app.route('/api/chat', methods=['POST'])
81
  @session_required
82
  def chat():
83
  """Process chat messages and get responses from Gemini API."""
84
- if not api_key:
85
- logger.error("Chat request failed: API Key is missing.")
86
- return jsonify({'error': 'Configuration serveur incomplète (clé API manquante).'}), 500
87
- if not model or not vision_model:
88
- logger.error("Chat request failed: Models not initialized.")
89
- return jsonify({'error': 'Configuration serveur incomplète (modèles non initialisés).'}), 500
90
-
91
  try:
92
  data = request.json
93
  user_message = data.get('message', '')
94
  chat_history = data.get('history', [])
95
- image_data = data.get('image', None) # Expecting a single image base64 string for now
96
-
97
  if not user_message and not image_data:
98
  return jsonify({'error': 'Veuillez entrer un message ou joindre une image.'}), 400
99
-
100
  # Log the incoming request (but not full chat history for privacy)
101
  session_id = session.get('session_id')
102
- logger.info(f"Received chat request from session {session_id}. Message length: {len(user_message)}. Image attached: {'Yes' if image_data else 'No'}")
103
-
104
- # Handle image processing if an image is included
 
 
 
 
 
 
105
  if image_data:
106
- if not vision_model:
107
- logger.error("Vision model not available.")
108
- return jsonify({'error': 'Le modèle de vision n\'est pas configuré.'}), 500
109
  try:
110
- # Decode image data
111
- # Assuming image_data is "data:image/jpeg;base64,..."
112
- image_info, image_base64 = image_data.split(',', 1)
113
- mime_type = image_info.split(':')[1].split(';')[0] # Extract mime type like "image/jpeg"
114
- image_bytes = base64.b64decode(image_base64) # Get raw bytes
115
-
116
- # Create the image part as a dictionary (CORRECTED METHOD)
117
- image_part = {
118
- "mime_type": mime_type,
119
- "data": image_bytes
120
- }
121
-
122
- # --- Save image (optional but good practice) ---
123
- session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
124
- os.makedirs(session_dir, exist_ok=True)
125
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
126
- # Try to get extension from mime type
127
- extension = mime_type.split('/')[-1] if '/' in mime_type else 'jpg'
128
- filename = secure_filename(f"image_{timestamp}.{extension}")
129
- filepath = os.path.join(session_dir, filename)
130
- with open(filepath, "wb") as f:
131
- f.write(image_bytes)
132
- logger.info(f"Saved uploaded image to {filepath}")
133
- # --- End Save image ---
134
-
135
- # Create message parts list for generate_content (CORRECTED METHOD)
136
  parts = []
137
- if user_message: # Add text first if it exists
138
- parts.append(user_message)
139
- parts.append(image_part) # Add the image dictionary
140
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # Generate response using vision model
142
- logger.debug(f"Sending parts to vision model: {[type(p) if not isinstance(p, dict) else 'dict(image)' for p in parts]}")
143
  response = vision_model.generate_content(parts)
144
- logger.info(f"Generated vision response successfully. Response length: {len(response.text)}")
145
  return jsonify({'response': response.text})
146
-
147
- except (ValueError, IndexError) as decode_error:
148
- logger.error(f"Error decoding image data: {str(decode_error)}")
149
- return jsonify({'error': 'Format de données d\'image invalide.'}), 400
150
  except Exception as img_error:
151
- # Log the full traceback for better debugging
152
- logger.exception(f"Error processing image: {str(img_error)}")
153
  return jsonify({
154
  'error': 'Désolé, une erreur est survenue lors du traitement de l\'image. Veuillez réessayer.'
155
  }), 500
156
  else:
157
  # Text-only processing
158
- if not model:
159
- logger.error("Text model not available.")
160
- return jsonify({'error': 'Le modèle de texte n\'est pas configuré.'}), 500
161
-
162
- # Format conversation history for context
163
- formatted_history = []
164
- for msg in chat_history[-15:]: # Use the last 15 messages for more context
165
- role = "user" if msg['sender'] == 'user' else "model"
166
- # Ensure message text is not None or empty before adding
167
- if msg.get('text'):
168
- formatted_history.append({"role": role, "parts": [msg['text']]})
169
- # Note: History currently doesn't include images sent previously.
170
- # Handling multimodal history requires storing image references/data
171
- # and formatting them correctly for the API on subsequent turns.
172
-
173
- try:
174
- # Create a chat session with history
175
- chat_session = model.start_chat(history=formatted_history)
176
-
177
- # Generate response
178
- response = chat_session.send_message(user_message)
179
-
180
- # Log successful response
181
- logger.info(f"Generated text response successfully. Response length: {len(response.text)}")
182
-
183
- # Return the response
184
- return jsonify({'response': response.text})
185
-
186
- except genai.types.generation_types.BlockedPromptException as be:
187
- logger.warning(f"Content blocked for session {session_id}: {str(be)}")
188
- return jsonify({
189
- 'error': 'Votre message ou la conversation contient du contenu potentiellement inapproprié et ne peut pas être traité.'
190
- }), 400
191
- except Exception as e:
192
- logger.exception(f"Error during text generation for session {session_id}: {str(e)}")
193
- return jsonify({
194
- 'error': 'Désolé, une erreur est survenue lors de la génération de la réponse texte.'
195
- }), 500
196
-
197
  except Exception as e:
198
- # Catch-all for unexpected errors (like issues reading request JSON)
199
- logger.exception(f"Unhandled error in chat endpoint: {str(e)}")
200
  return jsonify({
201
- 'error': 'Désolé, j\'ai rencontré une erreur inattendue. Veuillez réessayer.'
202
  }), 500
203
 
204
-
205
  @app.route('/api/save-chat', methods=['POST'])
206
  @session_required
207
  def save_chat():
208
  """Save the current chat history."""
209
  try:
210
  session_id = session.get('session_id')
211
- if not session_id:
212
- return jsonify({'error': 'Session introuvable.'}), 400
213
-
214
  # Create session-specific directory
215
  session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
216
  os.makedirs(session_dir, exist_ok=True)
217
-
218
  data = request.json
219
  chat_history = data.get('history', [])
220
-
221
  if not chat_history:
222
  return jsonify({'error': 'Aucune conversation à sauvegarder.'}), 400
223
-
224
  # Generate filename with timestamp
225
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
226
  filename = f"chat_{timestamp}.json"
227
  filepath = os.path.join(session_dir, filename)
228
-
229
  # Save chat history to file
230
  with open(filepath, 'w', encoding='utf-8') as f:
231
  json.dump(chat_history, f, ensure_ascii=False, indent=2)
232
-
233
- logger.info(f"Chat history saved for session {session_id} to {filename}")
234
- return jsonify({'success': True, 'filename': filename, 'timestamp': timestamp}) # Return timestamp too
235
-
236
  except Exception as e:
237
- logger.exception(f"Error saving chat for session {session_id}: {str(e)}")
238
  return jsonify({
239
  'error': 'Désolé, une erreur est survenue lors de la sauvegarde de la conversation.'
240
  }), 500
@@ -245,42 +216,33 @@ def load_chats():
245
  """Get a list of saved chat files for current session."""
246
  try:
247
  session_id = session.get('session_id')
248
- if not session_id:
249
- return jsonify({'error': 'Session introuvable.'}), 400
250
-
251
  # Get session-specific directory
252
  session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
253
-
254
  # If the directory doesn't exist yet, return empty list
255
  if not os.path.exists(session_dir):
256
- logger.info(f"No chat directory found for session {session_id}")
257
  return jsonify({'chats': []})
258
-
259
  chat_files = []
260
  for filename in os.listdir(session_dir):
261
- # Ensure we only list chat files, not uploaded images etc.
262
  if filename.startswith('chat_') and filename.endswith('.json'):
263
- try:
264
- # Extract timestamp from filename 'chat_YYYYMMDD_HHMMSS.json'
265
- timestamp_str = filename[5:-5] # Remove 'chat_' and '.json'
266
- # Validate timestamp format (optional but good)
267
- datetime.strptime(timestamp_str, "%Y%m%d_%H%M%S")
268
- chat_files.append({
269
- 'filename': filename,
270
- 'timestamp': timestamp_str # Keep original string for sorting/display
271
- })
272
- except ValueError:
273
- logger.warning(f"Skipping file with unexpected format: {filename} in {session_dir}")
274
-
275
-
276
- # Sort by timestamp string (lexicographical sort works for YYYYMMDD_HHMMSS)
277
  chat_files.sort(key=lambda x: x['timestamp'], reverse=True)
278
-
279
  logger.info(f"Loaded {len(chat_files)} chats for session {session_id}")
280
  return jsonify({'chats': chat_files})
281
-
282
  except Exception as e:
283
- logger.exception(f"Error loading chat list for session {session_id}: {str(e)}")
284
  return jsonify({
285
  'error': 'Désolé, une erreur est survenue lors du chargement des conversations.'
286
  }), 500
@@ -291,58 +253,25 @@ def load_chat(filename):
291
  """Load a specific chat history file."""
292
  try:
293
  session_id = session.get('session_id')
294
- if not session_id:
295
- return jsonify({'error': 'Session introuvable.'}), 400
296
-
297
- # Secure the filename before using it
298
- safe_filename = secure_filename(filename)
299
- if not safe_filename.startswith('chat_') or not safe_filename.endswith('.json'):
300
- logger.warning(f"Attempt to load invalid chat filename: {filename} (secured: {safe_filename}) for session {session_id}")
301
- return jsonify({'error': 'Nom de fichier de conversation invalide.'}), 400
302
-
303
  # Load from session-specific directory
304
  session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
305
- filepath = os.path.join(session_dir, safe_filename)
306
-
307
  if not os.path.exists(filepath):
308
- logger.warning(f"Chat file not found: {filepath} for session {session_id}")
309
  return jsonify({'error': 'Conversation introuvable.'}), 404
310
-
311
- # Check if path is still within the intended directory (security measure)
312
- if not os.path.abspath(filepath).startswith(os.path.abspath(session_dir)):
313
- logger.error(f"Attempt to access file outside session directory: {filepath}")
314
- return jsonify({'error': 'Accès non autorisé.'}), 403
315
-
316
  with open(filepath, 'r', encoding='utf-8') as f:
317
  chat_history = json.load(f)
318
-
319
- # Basic validation of loaded history format (optional)
320
- if not isinstance(chat_history, list):
321
- raise ValueError("Invalid chat history format in file.")
322
- for item in chat_history:
323
- if not isinstance(item, dict) or 'sender' not in item or 'text' not in item:
324
- # Allow for messages that might only have text or image data later
325
- if 'sender' not in item:
326
- raise ValueError("Invalid message format in chat history.")
327
-
328
-
329
- logger.info(f"Loaded chat {safe_filename} for session {session_id}")
330
  return jsonify({'history': chat_history})
331
-
332
- except json.JSONDecodeError:
333
- logger.error(f"Error decoding JSON from chat file: {safe_filename} for session {session_id}")
334
- return jsonify({'error': 'Le fichier de conversation est corrompu.'}), 500
335
- except ValueError as ve:
336
- logger.error(f"Invalid content in chat file {safe_filename}: {str(ve)}")
337
- return jsonify({'error': f'Format invalide dans le fichier de conversation: {str(ve)}'}), 500
338
  except Exception as e:
339
- logger.exception(f"Error loading chat file {safe_filename} for session {session_id}: {str(e)}")
340
  return jsonify({
341
  'error': 'Désolé, une erreur est survenue lors du chargement de la conversation.'
342
  }), 500
343
 
344
  if __name__ == '__main__':
345
- # Use 0.0.0.0 to be accessible on the network, debug=False for production
346
- # Port 8080 is often used as an alternative to 5000
347
- app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)), debug=os.environ.get('FLASK_DEBUG', 'False').lower() == 'true')
348
- # --- END OF FILE GeminiChatbot/app.py ---
 
 
 
1
  import os
2
  import logging
3
  import base64
 
22
  if not api_key:
23
  logger.warning("GEMINI_API_KEY not found in environment variables")
24
  else:
 
25
  logger.info("GEMINI_API_KEY found. API configured successfully.")
26
 
27
+ genai.configure(api_key=api_key)
28
 
29
  # Initialize Flask app
30
  app = Flask(__name__)
 
45
  # Ensure upload directory exists
46
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
47
 
48
+ # Configure Gemini model with specific parameters for better responses
49
+ model = genai.GenerativeModel(
50
+ model_name='gemini-2.0-flash',
51
+ generation_config={
52
+ 'temperature': 0.7, # Slightly creative but still focused
53
+ 'top_p': 0.9, # Diverse output but not too random
54
+ 'top_k': 40, # Reasonable range of tokens to consider
55
+ 'max_output_tokens': 2048 # Allow longer responses
56
+ }
57
+ )
 
 
 
 
 
 
 
 
 
 
58
 
59
+ # Configure Gemini vision model for image processing
60
+ vision_model = genai.GenerativeModel('gemini-2.0-vision-flash')
61
 
62
  @app.route('/')
63
  @session_required
64
  def index():
65
  """Render the chat interface."""
 
 
66
  return render_template('index.html')
67
 
68
  @app.route('/api/chat', methods=['POST'])
69
  @session_required
70
  def chat():
71
  """Process chat messages and get responses from Gemini API."""
 
 
 
 
 
 
 
72
  try:
73
  data = request.json
74
  user_message = data.get('message', '')
75
  chat_history = data.get('history', [])
76
+ image_data = data.get('image', None)
77
+
78
  if not user_message and not image_data:
79
  return jsonify({'error': 'Veuillez entrer un message ou joindre une image.'}), 400
80
+
81
  # Log the incoming request (but not full chat history for privacy)
82
  session_id = session.get('session_id')
83
+ logger.info(f"Received chat request from session {session_id}. Message length: {len(user_message)}")
84
+
85
+ # Format conversation history for context
86
+ formatted_history = []
87
+ for msg in chat_history[-15:]: # Use the last 15 messages for more context
88
+ role = "user" if msg['sender'] == 'user' else "model"
89
+ formatted_history.append({"role": role, "parts": [msg['text']]})
90
+
91
+ # Handle image processing if images are included
92
  if image_data:
 
 
 
93
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  parts = []
95
+
96
+ # Process single image or multiple images
97
+ if isinstance(image_data, list):
98
+ # Handle multiple images
99
+ for img in image_data:
100
+ if isinstance(img, str) and ',' in img:
101
+ # Extract the base64 part after the comma
102
+ image_base64 = img.split(',')[1]
103
+ # Create image part
104
+ image = genai.types.Part.from_data(
105
+ data=base64.b64decode(image_base64),
106
+ mime_type="image/jpeg"
107
+ )
108
+ parts.append(image)
109
+
110
+ # Save each image
111
+ session_id = session.get('session_id')
112
+ session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
113
+ os.makedirs(session_dir, exist_ok=True)
114
+
115
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
116
+ filename = secure_filename(f"image_{timestamp}_{len(parts)}.jpg")
117
+ filepath = os.path.join(session_dir, filename)
118
+ with open(filepath, "wb") as f:
119
+ f.write(base64.b64decode(image_base64))
120
+ else:
121
+ # Handle single image
122
+ image_base64 = image_data.split(',')[1]
123
+ image = genai.types.Part.from_data(
124
+ data=base64.b64decode(image_base64),
125
+ mime_type="image/jpeg"
126
+ )
127
+ parts.append(image)
128
+
129
+ # Save image with timestamp in the session directory
130
+ session_id = session.get('session_id')
131
+ session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
132
+ os.makedirs(session_dir, exist_ok=True)
133
+
134
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
135
+ filename = secure_filename(f"image_{timestamp}.jpg")
136
+ filepath = os.path.join(session_dir, filename)
137
+ with open(filepath, "wb") as f:
138
+ f.write(base64.b64decode(image_base64))
139
+
140
+ # Add text message if provided
141
+ if user_message:
142
+ parts.append(user_message)
143
+
144
  # Generate response using vision model
 
145
  response = vision_model.generate_content(parts)
 
146
  return jsonify({'response': response.text})
147
+
 
 
 
148
  except Exception as img_error:
149
+ logger.error(f"Error processing image: {str(img_error)}")
 
150
  return jsonify({
151
  'error': 'Désolé, une erreur est survenue lors du traitement de l\'image. Veuillez réessayer.'
152
  }), 500
153
  else:
154
  # Text-only processing
155
+ # Create a chat session with history
156
+ chat = model.start_chat(history=formatted_history)
157
+
158
+ # Generate response
159
+ response = chat.send_message(user_message)
160
+
161
+ # Log successful response
162
+ logger.info(f"Generated response successfully. Response length: {len(response.text)}")
163
+
164
+ # Return the response
165
+ return jsonify({'response': response.text})
166
+
167
+ except genai.types.generation_types.BlockedPromptException as be:
168
+ logger.warning(f"Content blocked: {str(be)}")
169
+ return jsonify({
170
+ 'error': 'Votre message ou la conversation ne peut pas être traitée car elle contient du contenu potentiellement inapproprié.'
171
+ }), 400
172
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  except Exception as e:
174
+ logger.error(f"Error in chat endpoint: {str(e)}")
 
175
  return jsonify({
176
+ 'error': 'Désolé, j\'ai rencontré une erreur lors du traitement de votre demande. Veuillez réessayer.'
177
  }), 500
178
 
 
179
  @app.route('/api/save-chat', methods=['POST'])
180
  @session_required
181
  def save_chat():
182
  """Save the current chat history."""
183
  try:
184
  session_id = session.get('session_id')
185
+
 
 
186
  # Create session-specific directory
187
  session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
188
  os.makedirs(session_dir, exist_ok=True)
189
+
190
  data = request.json
191
  chat_history = data.get('history', [])
192
+
193
  if not chat_history:
194
  return jsonify({'error': 'Aucune conversation à sauvegarder.'}), 400
195
+
196
  # Generate filename with timestamp
197
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
198
  filename = f"chat_{timestamp}.json"
199
  filepath = os.path.join(session_dir, filename)
200
+
201
  # Save chat history to file
202
  with open(filepath, 'w', encoding='utf-8') as f:
203
  json.dump(chat_history, f, ensure_ascii=False, indent=2)
204
+
205
+ return jsonify({'success': True, 'filename': filename})
206
+
 
207
  except Exception as e:
208
+ logger.error(f"Error saving chat: {str(e)}")
209
  return jsonify({
210
  'error': 'Désolé, une erreur est survenue lors de la sauvegarde de la conversation.'
211
  }), 500
 
216
  """Get a list of saved chat files for current session."""
217
  try:
218
  session_id = session.get('session_id')
219
+
 
 
220
  # Get session-specific directory
221
  session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
222
+
223
  # If the directory doesn't exist yet, return empty list
224
  if not os.path.exists(session_dir):
 
225
  return jsonify({'chats': []})
226
+
227
  chat_files = []
228
  for filename in os.listdir(session_dir):
 
229
  if filename.startswith('chat_') and filename.endswith('.json'):
230
+ # Extract timestamp from filename
231
+ timestamp = filename[5:-5] # Remove 'chat_' and '.json'
232
+ # Add to list
233
+ chat_files.append({
234
+ 'filename': filename,
235
+ 'timestamp': timestamp
236
+ })
237
+
238
+ # Sort by timestamp (newest first)
 
 
 
 
 
239
  chat_files.sort(key=lambda x: x['timestamp'], reverse=True)
240
+
241
  logger.info(f"Loaded {len(chat_files)} chats for session {session_id}")
242
  return jsonify({'chats': chat_files})
243
+
244
  except Exception as e:
245
+ logger.error(f"Error loading chat list: {str(e)}")
246
  return jsonify({
247
  'error': 'Désolé, une erreur est survenue lors du chargement des conversations.'
248
  }), 500
 
253
  """Load a specific chat history file."""
254
  try:
255
  session_id = session.get('session_id')
256
+
 
 
 
 
 
 
 
 
257
  # Load from session-specific directory
258
  session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
259
+ filepath = os.path.join(session_dir, secure_filename(filename))
260
+
261
  if not os.path.exists(filepath):
 
262
  return jsonify({'error': 'Conversation introuvable.'}), 404
263
+
 
 
 
 
 
264
  with open(filepath, 'r', encoding='utf-8') as f:
265
  chat_history = json.load(f)
266
+
267
+ logger.info(f"Loaded chat {filename} for session {session_id}")
 
 
 
 
 
 
 
 
 
 
268
  return jsonify({'history': chat_history})
269
+
 
 
 
 
 
 
270
  except Exception as e:
271
+ logger.error(f"Error loading chat file: {str(e)}")
272
  return jsonify({
273
  'error': 'Désolé, une erreur est survenue lors du chargement de la conversation.'
274
  }), 500
275
 
276
  if __name__ == '__main__':
277
+ app.run(host='0.0.0.0', port=5000, debug=True)
 
 
 
static/css/style.css CHANGED
@@ -1,9 +1,49 @@
1
  /**
2
  * File: static/css/style.css
3
- * Description: Stylesheet for the Gemini Chatbot Web Application.
4
- * Version: Corrected for scrolling issues.
5
  */
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  /* Base Styles */
8
  * {
9
  margin: 0;
@@ -543,6 +583,7 @@ body {
543
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
544
  word-wrap: break-word; /* Allow long words to break */
545
  overflow-wrap: break-word; /* Better word breaking */
 
546
  }
547
 
548
  .message-container.user .message-bubble {
@@ -697,6 +738,40 @@ body {
697
  background-color: #D32F2F;
698
  }
699
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  /* Loading Animation */
701
  .message-bubble.loading {
702
  display: flex; /* Use flex for alignment */
 
1
  /**
2
  * File: static/css/style.css
3
+ * Description: Stylesheet for the Mariam AI Chatbot Web Application.
4
+ * Version: Enhanced with PWA support and improved responsiveness.
5
  */
6
 
7
+ /* PWA Specific Styles */
8
+ @media (display-mode: standalone) {
9
+ /* Adjustments for standalone mode (when installed as PWA) */
10
+ body {
11
+ /* Add safe area insets for mobile devices */
12
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
13
+ /* Better touch handling */
14
+ touch-action: manipulation;
15
+ }
16
+
17
+ /* Hide browser UI when installed as PWA */
18
+ .top-bar {
19
+ padding-top: max(12px, env(safe-area-inset-top));
20
+ }
21
+
22
+ /* Ensure content doesn't get hidden beneath the bottom nav bar on iOS */
23
+ .input-area {
24
+ padding-bottom: env(safe-area-inset-bottom);
25
+ }
26
+ }
27
+
28
+ /* Offline indicator for PWA */
29
+ .offline-indicator {
30
+ position: fixed;
31
+ top: 0;
32
+ left: 0;
33
+ right: 0;
34
+ background-color: #FFC107;
35
+ color: #212121;
36
+ text-align: center;
37
+ padding: 8px;
38
+ font-size: 14px;
39
+ z-index: 9999;
40
+ display: none; /* Hidden by default, shown via JS when offline */
41
+ }
42
+
43
+ .offline-indicator.visible {
44
+ display: block;
45
+ }
46
+
47
  /* Base Styles */
48
  * {
49
  margin: 0;
 
583
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
584
  word-wrap: break-word; /* Allow long words to break */
585
  overflow-wrap: break-word; /* Better word breaking */
586
+ position: relative; /* For absolute positioning of action buttons */
587
  }
588
 
589
  .message-container.user .message-bubble {
 
738
  background-color: #D32F2F;
739
  }
740
 
741
+ /* Message actions (copy button, etc.) */
742
+ .message-actions {
743
+ position: absolute;
744
+ top: 8px;
745
+ right: 8px;
746
+ display: flex;
747
+ gap: 5px;
748
+ opacity: 0;
749
+ transition: opacity 0.2s ease;
750
+ }
751
+
752
+ .message-bubble:hover .message-actions {
753
+ opacity: 1;
754
+ }
755
+
756
+ .copy-button {
757
+ background-color: rgba(255, 255, 255, 0.8);
758
+ color: #616161;
759
+ border: none;
760
+ border-radius: 4px;
761
+ width: 28px;
762
+ height: 28px;
763
+ display: flex;
764
+ align-items: center;
765
+ justify-content: center;
766
+ cursor: pointer;
767
+ transition: all 0.2s ease;
768
+ }
769
+
770
+ .copy-button:hover {
771
+ background-color: #E3F2FD;
772
+ color: #2196F3;
773
+ }
774
+
775
  /* Loading Animation */
776
  .message-bubble.loading {
777
  display: flex; /* Use flex for alignment */
static/icons/icon-192x192.png ADDED
static/icons/icon-512x512.png ADDED
static/icons/icon.svg ADDED
static/js/chat.js CHANGED
@@ -18,6 +18,7 @@ document.addEventListener('DOMContentLoaded', function() {
18
  const mainContent = document.querySelector('.main-content');
19
  const welcomeContainer = document.querySelector('.welcome-container');
20
  const suggestionBubbles = document.querySelectorAll('.suggestion-bubble');
 
21
 
22
  // Templates
23
  const loadingTemplate = document.getElementById('loadingTemplate');
@@ -28,6 +29,33 @@ document.addEventListener('DOMContentLoaded', function() {
28
 
29
  // State variables
30
  let chatHistory = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  let selectedImagesData = []; // Pour stocker plusieurs images
32
  let conversationStarted = false;
33
 
@@ -148,7 +176,11 @@ document.addEventListener('DOMContentLoaded', function() {
148
  // Settings button in side nav
149
  if (settingsButtonNav) {
150
  settingsButtonNav.addEventListener('click', function() {
151
- alert('Fonctionnalité en cours de développement');
 
 
 
 
152
  closeSideNav();
153
  });
154
  }
@@ -215,13 +247,17 @@ document.addEventListener('DOMContentLoaded', function() {
215
  .then(response => response.json())
216
  .then(data => {
217
  if (data.error) {
218
- alert(data.error);
 
 
 
 
219
  return;
220
  }
221
 
222
  if (data.history) {
223
  // Clear current chat
224
- clearChat(false); // No confirmation needed
225
 
226
  // Load history
227
  chatHistory = data.history;
@@ -259,14 +295,22 @@ document.addEventListener('DOMContentLoaded', function() {
259
  })
260
  .catch(error => {
261
  console.error('Error loading chat:', error);
262
- alert('Erreur lors du chargement de la conversation.');
 
 
 
 
263
  });
264
  }
265
 
266
  // Save current chat
267
  saveCurrentChatButton.addEventListener('click', function() {
268
  if (chatHistory.length <= 1) {
269
- alert('Aucune conversation à sauvegarder. Veuillez d\'abord discuter avec le chatbot.');
 
 
 
 
270
  return;
271
  }
272
 
@@ -282,15 +326,29 @@ document.addEventListener('DOMContentLoaded', function() {
282
  .then(response => response.json())
283
  .then(data => {
284
  if (data.error) {
285
- alert(data.error);
 
 
 
 
286
  } else {
287
- alert('Conversation sauvegardée avec succès !');
 
 
 
 
 
 
288
  loadChatHistoryList();
289
  }
290
  })
291
  .catch(error => {
292
  console.error('Error saving chat:', error);
293
- alert('Erreur lors de la sauvegarde de la conversation.');
 
 
 
 
294
  });
295
  });
296
 
@@ -328,21 +386,51 @@ document.addEventListener('DOMContentLoaded', function() {
328
 
329
  // Function to clear the chat history
330
  function clearChat(withConfirmation = true) {
331
- if (!withConfirmation || (chatHistory.length > 0 && confirm('Êtes-vous sûr de vouloir effacer toute la conversation ?'))) {
332
- // Clear the chat window except for the welcome message
333
- while (chatWindow.childElementCount > 1) {
334
- chatWindow.removeChild(chatWindow.lastChild);
335
- }
336
-
337
- // Reset chat history but keep the welcome message
338
- const welcomeMsg = chatHistory[0];
339
- chatHistory = welcomeMsg ? [welcomeMsg] : [];
340
-
341
- // Focus on input field
342
- userInput.focus();
 
 
 
 
 
 
 
 
 
343
  }
344
  }
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  // Function to send message
347
  function sendMessage() {
348
  const message = userInput.value.trim();
@@ -352,7 +440,7 @@ document.addEventListener('DOMContentLoaded', function() {
352
 
353
  if (selectedImagesData.length > 0) {
354
  // Add user message with images to UI
355
- addUserImageMessage(message, selectedImagesData[0]); // Pour l'instant afficher seulement la première image dans l'UI
356
  } else {
357
  // Add user text message to UI
358
  addUserMessage(message);
@@ -366,7 +454,8 @@ document.addEventListener('DOMContentLoaded', function() {
366
  const loadingElement = addLoadingIndicator();
367
 
368
  // Send message to API
369
- sendToAPI(message, loadingElement, selectedImagesData.length > 0 ? selectedImagesData[0] : null);
 
370
 
371
  // Clear image previews if any
372
  if (selectedImagesData.length > 0) {
@@ -399,8 +488,8 @@ document.addEventListener('DOMContentLoaded', function() {
399
  scrollToBottom();
400
  }
401
 
402
- // Add user message with image to chat window
403
- function addUserImageMessage(message, imageData) {
404
  // Hide welcome container if visible (first message)
405
  if (!conversationStarted && welcomeContainer) {
406
  welcomeContainer.style.display = 'none';
@@ -409,9 +498,27 @@ document.addEventListener('DOMContentLoaded', function() {
409
 
410
  const messageElement = userImageMessageTemplate.content.cloneNode(true);
411
 
412
- // Set image source
413
- const imageElement = messageElement.querySelector('.chat-image');
414
- imageElement.src = imageData;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
  // Set message text if any
417
  const textElement = messageElement.querySelector('p');
@@ -426,7 +533,7 @@ document.addEventListener('DOMContentLoaded', function() {
426
  // Add to chat history (we don't add the image to history, just the text)
427
  chatHistory.push({
428
  sender: 'user',
429
- text: message || 'Image envoyée' // Default text if no message provided
430
  });
431
 
432
  // Scroll to bottom
@@ -451,6 +558,35 @@ document.addEventListener('DOMContentLoaded', function() {
451
  messageParagraph.textContent = message;
452
  }
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  chatWindow.appendChild(messageElement);
455
 
456
  // Add syntax highlighting to code blocks if Prism is available
@@ -528,6 +664,7 @@ document.addEventListener('DOMContentLoaded', function() {
528
 
529
  // Add image data if provided
530
  if (imageData) {
 
531
  requestData.image = imageData;
532
  }
533
 
@@ -566,8 +703,16 @@ document.addEventListener('DOMContentLoaded', function() {
566
  loadingElement.remove();
567
  }
568
 
569
- // Add error message
570
- addErrorMessage('Désolé, il y a eu un problème de connexion avec le serveur. Veuillez réessayer.', message, imageData);
 
 
 
 
 
 
 
 
571
  })
572
  .finally(() => {
573
  // Re-enable input
 
18
  const mainContent = document.querySelector('.main-content');
19
  const welcomeContainer = document.querySelector('.welcome-container');
20
  const suggestionBubbles = document.querySelectorAll('.suggestion-bubble');
21
+ const offlineIndicator = document.getElementById('offlineIndicator');
22
 
23
  // Templates
24
  const loadingTemplate = document.getElementById('loadingTemplate');
 
29
 
30
  // State variables
31
  let chatHistory = [];
32
+ let isOffline = !navigator.onLine; // Vérifier l'état de la connexion au chargement
33
+
34
+ // Gestion de l'état de la connexion pour PWA
35
+ function updateOnlineStatus() {
36
+ isOffline = !navigator.onLine;
37
+
38
+ if (isOffline) {
39
+ // L'utilisateur est hors ligne
40
+ offlineIndicator.classList.add('visible');
41
+ // Désactiver temporairement le bouton d'envoi
42
+ sendButton.disabled = true;
43
+ sendButton.title = "Hors ligne - Reconnectez-vous pour envoyer des messages";
44
+ } else {
45
+ // L'utilisateur est en ligne
46
+ offlineIndicator.classList.remove('visible');
47
+ // Réactiver le bouton d'envoi
48
+ sendButton.disabled = false;
49
+ sendButton.title = "Envoyer";
50
+ }
51
+ }
52
+
53
+ // Installer les événements pour détecter les changements de connectivité
54
+ window.addEventListener('online', updateOnlineStatus);
55
+ window.addEventListener('offline', updateOnlineStatus);
56
+
57
+ // Appliquer l'état initial
58
+ updateOnlineStatus();
59
  let selectedImagesData = []; // Pour stocker plusieurs images
60
  let conversationStarted = false;
61
 
 
176
  // Settings button in side nav
177
  if (settingsButtonNav) {
178
  settingsButtonNav.addEventListener('click', function() {
179
+ Swal.fire({
180
+ icon: 'info',
181
+ title: 'En cours de développement',
182
+ text: 'Cette fonctionnalité sera disponible prochainement.'
183
+ });
184
  closeSideNav();
185
  });
186
  }
 
247
  .then(response => response.json())
248
  .then(data => {
249
  if (data.error) {
250
+ Swal.fire({
251
+ icon: 'error',
252
+ title: 'Erreur',
253
+ text: data.error
254
+ });
255
  return;
256
  }
257
 
258
  if (data.history) {
259
  // Clear current chat
260
+ clearChatContent(); // No confirmation needed
261
 
262
  // Load history
263
  chatHistory = data.history;
 
295
  })
296
  .catch(error => {
297
  console.error('Error loading chat:', error);
298
+ Swal.fire({
299
+ icon: 'error',
300
+ title: 'Erreur',
301
+ text: 'Erreur lors du chargement de la conversation.'
302
+ });
303
  });
304
  }
305
 
306
  // Save current chat
307
  saveCurrentChatButton.addEventListener('click', function() {
308
  if (chatHistory.length <= 1) {
309
+ Swal.fire({
310
+ icon: 'info',
311
+ title: 'Information',
312
+ text: 'Aucune conversation à sauvegarder. Veuillez d\'abord discuter avec le chatbot.'
313
+ });
314
  return;
315
  }
316
 
 
326
  .then(response => response.json())
327
  .then(data => {
328
  if (data.error) {
329
+ Swal.fire({
330
+ icon: 'error',
331
+ title: 'Erreur',
332
+ text: data.error
333
+ });
334
  } else {
335
+ Swal.fire({
336
+ icon: 'success',
337
+ title: 'Sauvegardé',
338
+ text: 'Conversation sauvegardée avec succès !',
339
+ timer: 1500,
340
+ showConfirmButton: false
341
+ });
342
  loadChatHistoryList();
343
  }
344
  })
345
  .catch(error => {
346
  console.error('Error saving chat:', error);
347
+ Swal.fire({
348
+ icon: 'error',
349
+ title: 'Erreur',
350
+ text: 'Erreur lors de la sauvegarde de la conversation.'
351
+ });
352
  });
353
  });
354
 
 
386
 
387
  // Function to clear the chat history
388
  function clearChat(withConfirmation = true) {
389
+ if (!withConfirmation) {
390
+ // Clear chat immediately without confirmation
391
+ clearChatContent();
392
+ return;
393
+ }
394
+
395
+ if (chatHistory.length > 0) {
396
+ Swal.fire({
397
+ title: 'Êtes-vous sûr ?',
398
+ text: 'Voulez-vous effacer toute la conversation ?',
399
+ icon: 'warning',
400
+ showCancelButton: true,
401
+ confirmButtonColor: '#3085d6',
402
+ cancelButtonColor: '#d33',
403
+ confirmButtonText: 'Oui, effacer',
404
+ cancelButtonText: 'Annuler'
405
+ }).then((result) => {
406
+ if (result.isConfirmed) {
407
+ clearChatContent();
408
+ }
409
+ });
410
  }
411
  }
412
 
413
+ // Helper function to clear chat content
414
+ function clearChatContent() {
415
+ // Clear the chat window except for the welcome message
416
+ while (chatWindow.childElementCount > 1) {
417
+ chatWindow.removeChild(chatWindow.lastChild);
418
+ }
419
+
420
+ // Reset chat history but keep the welcome message
421
+ const welcomeMsg = chatHistory[0];
422
+ chatHistory = welcomeMsg ? [welcomeMsg] : [];
423
+
424
+ // Show welcome container again
425
+ if (welcomeContainer) {
426
+ welcomeContainer.style.display = 'flex';
427
+ conversationStarted = false;
428
+ }
429
+
430
+ // Focus on input field
431
+ userInput.focus();
432
+ }
433
+
434
  // Function to send message
435
  function sendMessage() {
436
  const message = userInput.value.trim();
 
440
 
441
  if (selectedImagesData.length > 0) {
442
  // Add user message with images to UI
443
+ addUserImageMessage(message, selectedImagesData); // Maintenant on passe toutes les images
444
  } else {
445
  // Add user text message to UI
446
  addUserMessage(message);
 
454
  const loadingElement = addLoadingIndicator();
455
 
456
  // Send message to API
457
+ const imagesToSend = selectedImagesData.length > 0 ? selectedImagesData : null;
458
+ sendToAPI(message, loadingElement, imagesToSend);
459
 
460
  // Clear image previews if any
461
  if (selectedImagesData.length > 0) {
 
488
  scrollToBottom();
489
  }
490
 
491
+ // Add user message with multiple images to chat window
492
+ function addUserImageMessage(message, imagesData) {
493
  // Hide welcome container if visible (first message)
494
  if (!conversationStarted && welcomeContainer) {
495
  welcomeContainer.style.display = 'none';
 
498
 
499
  const messageElement = userImageMessageTemplate.content.cloneNode(true);
500
 
501
+ // Handle multiple images
502
+ const imageContainer = messageElement.querySelector('.image-container');
503
+
504
+ // If imagesData is an array, add all images
505
+ if (Array.isArray(imagesData)) {
506
+ // Clear existing image elements
507
+ imageContainer.innerHTML = '';
508
+
509
+ // Add each image
510
+ imagesData.forEach(imgData => {
511
+ const img = document.createElement('img');
512
+ img.className = 'chat-image';
513
+ img.src = imgData;
514
+ img.alt = 'Chat image';
515
+ imageContainer.appendChild(img);
516
+ });
517
+ } else if (imagesData) {
518
+ // Single image case
519
+ const imageElement = messageElement.querySelector('.chat-image');
520
+ imageElement.src = imagesData;
521
+ }
522
 
523
  // Set message text if any
524
  const textElement = messageElement.querySelector('p');
 
533
  // Add to chat history (we don't add the image to history, just the text)
534
  chatHistory.push({
535
  sender: 'user',
536
+ text: message || 'Images envoyées' // Default text if no message provided
537
  });
538
 
539
  // Scroll to bottom
 
558
  messageParagraph.textContent = message;
559
  }
560
 
561
+ // Add copy button functionality
562
+ const copyButton = messageElement.querySelector('.copy-button');
563
+ if (copyButton) {
564
+ copyButton.addEventListener('click', function() {
565
+ // Get the text to copy (original message, not HTML)
566
+ navigator.clipboard.writeText(message).then(() => {
567
+ // Show success notification with SweetAlert2
568
+ Swal.fire({
569
+ toast: true,
570
+ position: 'top-end',
571
+ icon: 'success',
572
+ title: 'Texte copié !',
573
+ showConfirmButton: false,
574
+ timer: 1500
575
+ });
576
+ }).catch(err => {
577
+ console.error('Erreur lors de la copie :', err);
578
+ Swal.fire({
579
+ toast: true,
580
+ position: 'top-end',
581
+ icon: 'error',
582
+ title: 'Erreur lors de la copie',
583
+ showConfirmButton: false,
584
+ timer: 1500
585
+ });
586
+ });
587
+ });
588
+ }
589
+
590
  chatWindow.appendChild(messageElement);
591
 
592
  // Add syntax highlighting to code blocks if Prism is available
 
664
 
665
  // Add image data if provided
666
  if (imageData) {
667
+ // If imageData is an array of images, send them all
668
  requestData.image = imageData;
669
  }
670
 
 
703
  loadingElement.remove();
704
  }
705
 
706
+ // Add different error message based on online/offline status
707
+ if (!navigator.onLine) {
708
+ // Custom error message for offline mode
709
+ addErrorMessage('Vous êtes actuellement hors ligne. Connectez-vous à Internet pour discuter avec Mariam AI.', message, imageData);
710
+ // Update the status indicator
711
+ updateOnlineStatus();
712
+ } else {
713
+ // Generic error for other connection issues
714
+ addErrorMessage('Désolé, il y a eu un problème de connexion avec le serveur. Veuillez réessayer.', message, imageData);
715
+ }
716
  })
717
  .finally(() => {
718
  // Re-enable input
static/js/pwa.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Vérifier si le navigateur prend en charge les Service Workers
2
+ if ('serviceWorker' in navigator) {
3
+ window.addEventListener('load', function() {
4
+ navigator.serviceWorker.register('/static/js/service-worker.js')
5
+ .then(function(registration) {
6
+ console.log('Service Worker enregistré avec succès:', registration.scope);
7
+ })
8
+ .catch(function(error) {
9
+ console.log('Échec de l\'enregistrement du Service Worker:', error);
10
+ });
11
+ });
12
+ }
13
+
14
+ // Gestion des événements d'installation de PWA
15
+ let deferredPrompt;
16
+
17
+ window.addEventListener('beforeinstallprompt', (e) => {
18
+ // Empêcher Chrome 67 et versions ultérieures d'afficher automatiquement l'invite
19
+ e.preventDefault();
20
+ // Stocker l'événement pour l'utiliser plus tard
21
+ deferredPrompt = e;
22
+
23
+ // Vous pouvez ajouter ici un bouton ou une bannière pour suggérer l'installation
24
+ // Par exemple :
25
+ /*
26
+ const installBanner = document.createElement('div');
27
+ installBanner.id = 'install-banner';
28
+ installBanner.innerHTML = `
29
+ <p>Installez Mariam AI sur votre appareil!</p>
30
+ <button id="install-button">Installer</button>
31
+ <button id="close-banner">✕</button>
32
+ `;
33
+ document.body.appendChild(installBanner);
34
+
35
+ document.getElementById('install-button').addEventListener('click', (e) => {
36
+ // Afficher l'invite d'installation
37
+ deferredPrompt.prompt();
38
+ // Attendre que l'utilisateur réponde à l'invite
39
+ deferredPrompt.userChoice.then((choiceResult) => {
40
+ if (choiceResult.outcome === 'accepted') {
41
+ console.log('L\'utilisateur a accepté l\'installation');
42
+ } else {
43
+ console.log('L\'utilisateur a refusé l\'installation');
44
+ }
45
+ // Réinitialiser la variable car on ne peut utiliser l'événement qu'une fois
46
+ deferredPrompt = null;
47
+ // Cacher la bannière
48
+ installBanner.style.display = 'none';
49
+ });
50
+ });
51
+
52
+ document.getElementById('close-banner').addEventListener('click', (e) => {
53
+ installBanner.style.display = 'none';
54
+ });
55
+ */
56
+ });
static/js/service-worker.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Nom du cache
2
+ const CACHE_NAME = 'mariam-ai-cache-v1';
3
+
4
+ // Ressources à mettre en cache
5
+ const CACHE_ASSETS = [
6
+ '/',
7
+ '/static/css/style.css',
8
+ '/static/js/chat.js',
9
+ '/static/icons/icon-192x192.png',
10
+ '/static/icons/icon-512x512.png',
11
+ '/static/manifest.json'
12
+ ];
13
+
14
+ // Installation du Service Worker
15
+ self.addEventListener('install', (event) => {
16
+ event.waitUntil(
17
+ caches.open(CACHE_NAME)
18
+ .then((cache) => {
19
+ return cache.addAll(CACHE_ASSETS);
20
+ })
21
+ .then(() => self.skipWaiting())
22
+ );
23
+ });
24
+
25
+ // Activation du Service Worker
26
+ self.addEventListener('activate', (event) => {
27
+ const currentCaches = [CACHE_NAME];
28
+ event.waitUntil(
29
+ caches.keys().then((cacheNames) => {
30
+ return cacheNames.filter((cacheName) => !currentCaches.includes(cacheName));
31
+ }).then((cachesToDelete) => {
32
+ return Promise.all(cachesToDelete.map((cacheToDelete) => {
33
+ return caches.delete(cacheToDelete);
34
+ }));
35
+ }).then(() => self.clients.claim())
36
+ );
37
+ });
38
+
39
+ // Stratégie de cache : "Cache, falling back to network"
40
+ self.addEventListener('fetch', (event) => {
41
+ // Ne pas mettre en cache les requêtes API
42
+ if (event.request.url.includes('/api/')) {
43
+ return;
44
+ }
45
+
46
+ event.respondWith(
47
+ caches.match(event.request)
48
+ .then((response) => {
49
+ // Cache hit - return response
50
+ if (response) {
51
+ return response;
52
+ }
53
+
54
+ // Si pas dans le cache, on fait la requête au réseau
55
+ return fetch(event.request).then(
56
+ (response) => {
57
+ // Return the network response
58
+ if(!response || response.status !== 200 || response.type !== 'basic') {
59
+ return response;
60
+ }
61
+
62
+ // On clone la réponse pour l'utiliser deux fois
63
+ const responseToCache = response.clone();
64
+
65
+ caches.open(CACHE_NAME)
66
+ .then((cache) => {
67
+ cache.put(event.request, responseToCache);
68
+ });
69
+
70
+ return response;
71
+ }
72
+ );
73
+ })
74
+ );
75
+ });
static/manifest.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Mariam AI",
3
+ "short_name": "Mariam AI",
4
+ "description": "Assistant IA personnel alimenté par Mariam AI",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#2196F3",
9
+ "icons": [
10
+ {
11
+ "src": "/static/icons/icon-192x192.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png",
14
+ "purpose": "any maskable"
15
+ },
16
+ {
17
+ "src": "/static/icons/icon-512x512.png",
18
+ "sizes": "512x512",
19
+ "type": "image/png",
20
+ "purpose": "any maskable"
21
+ }
22
+ ]
23
+ }
static/uploads/0aac323b-143c-442a-82dd-56038be56bec/chat_20250502_073406.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "sender": "user",
4
+ "text": "Donne-moi des idées de recettes rapides pour un dîner équilibré"
5
+ },
6
+ {
7
+ "sender": "bot",
8
+ "text": "Absolument ! Voici quelques idées de recettes rapides et équilibrées pour un dîner, classées par type d'ingrédient principal, pour varier les plaisirs :\n\n**Avec du Poulet :**\n\n* **Poulet sauté aux légumes et sauce soja:**\n * Coupez le poulet en dés et faites-le revenir dans une poêle avec un peu d'huile.\n * Ajoutez des légumes de saison coupés (brocoli, carottes, poivrons, courgettes).\n * Assaisonnez avec de la sauce soja, du gingembre râpé et de l'ail.\n * Servez avec du riz brun ou des nouilles de riz.\n* **Salade de poulet grillé à l'avocat:**\n * Grillez des filets de poulet.\n * Préparez une salade avec de la laitue, des tomates cerises, du concombre, de l'avocat et des oignons rouges.\n * Coupez le poulet en tranches et ajoutez-le à la salade.\n * Assaisonnez avec une vinaigrette légère à base d'huile d'olive, de jus de citron et de moutarde.\n\n**Avec du Poisson :**\n\n* **Saumon poêlé à la sauce citronnée et légumes verts:**\n * Faites cuire des filets de saumon à la poêle avec un peu d'huile.\n * Préparez une sauce rapide avec du jus de citron, du beurre (ou huile d'olive), de l'ail et des herbes fraîches.\n * Servez avec des légumes verts vapeur (haricots verts, brocolis, épinards).\n* **Thon à la niçoise express:**\n * Utilisez du thon en conserve au naturel.\n * Mélangez le thon avec des tomates, des olives noires, des œufs durs, des haricots verts et des anchois (facultatif).\n * Assaisonnez avec de l'huile d'olive, du vinaigre de vin rouge, du sel et du poivre.\n\n**Végétarien :**\n\n* **Curry de lentilles corail:**\n * Faites revenir des oignons, de l'ail et du gingembre dans une casserole.\n * Ajoutez des lentilles corail, du lait de coco, des tomates concassées et des épices à curry.\n * Laissez mijoter jusqu'à ce que les lentilles soient tendres.\n * Servez avec du riz basmati et de la coriandre fraîche.\n* **Omelette aux légumes:**\n * Battez des œufs avec du sel, du poivre et des herbes fraîches.\n * Faites revenir des légumes coupés (champignons, poivrons, épinards) dans une poêle.\n * Versez les œufs battus sur les légumes et faites cuire l'omelette.\n * Servez avec une salade verte.\n* **Pâtes aux légumes d'été et pesto:**\n * Faites cuire vos pâtes préférées.\n * Pendant ce temps, faites revenir des légumes de saison coupés (courgettes, aubergines, tomates cerises) dans une poêle avec de l'huile d'olive.\n * Mélangez les pâtes avec les légumes et du pesto frais.\n * Parsemez de parmesan râpé.\n\n**Astuces pour gagner du temps :**\n\n* **Préparez à l'avance:** Coupez les légumes à l'avance, faites cuire du riz en grande quantité pour plusieurs repas.\n* **Utilisez des ingrédients pré-coupés:** Achetez des légumes pré-coupés ou des salades en sachet.\n* **Cuisinez en grande quantité:** Préparez une double portion et congelez la moitié pour un autre repas.\n* **Simplifiez les recettes:** N'hésitez pas à adapter les recettes en fonction de ce que vous avez sous la main.\n\n**Conseils pour un dîner équilibré :**\n\n* **Variez les sources de protéines:** Alternez entre viande, poisson, œufs, légumineuses et tofu.\n* **Mangez des légumes à chaque repas:** Les légumes sont riches en vitamines, minéraux et fibres.\n* **Choisissez des glucides complexes:** Privilégiez le riz brun, les pâtes complètes, le quinoa et les patates douces.\n* **Utilisez des matières grasses saines:** Huile d'olive, avocat, noix et graines.\n* **Limitez les aliments transformés et les sucres ajoutés.**\n\nJ'espère que ces idées vous inspireront pour préparer des dîners rapides, délicieux et équilibrés ! N'hésitez pas à me dire si vous souhaitez d'autres suggestions.\n"
9
+ }
10
+ ]
templates/index.html CHANGED
@@ -3,10 +3,18 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Gemini Chat</title>
 
 
 
 
 
 
7
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
 
 
10
  <!-- Marked.js pour le parsing Markdown -->
11
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js"></script>
12
  <!-- Prism.js pour la coloration syntaxique -->
@@ -17,6 +25,11 @@
17
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
18
  </head>
19
  <body>
 
 
 
 
 
20
  <div class="app-container">
21
  <!-- Side navigation bar -->
22
  <div class="side-nav" id="sideNav">
@@ -24,7 +37,7 @@
24
  <div class="logo">
25
  <i class="bi bi-stars"></i>
26
  </div>
27
- <h1>Gemini</h1>
28
  <button id="closeSideNavButton" class="close-button">
29
  <i class="bi bi-x-lg"></i>
30
  </button>
@@ -61,7 +74,7 @@
61
  <button id="toggleNavButton" class="menu-button" title="Menu">
62
  <i class="bi bi-list"></i>
63
  </button>
64
- <h1>Assistant Gemini</h1>
65
  </div>
66
  <div class="top-bar-right">
67
  <button class="icon-button" id="clearButton" title="Effacer la conversation">
@@ -78,7 +91,7 @@
78
  <div class="welcome-logo">
79
  <i class="bi bi-stars"></i>
80
  </div>
81
- <h2>Bienvenue sur Gemini</h2>
82
  <p class="welcome-subtitle">Comment puis-je vous aider aujourd'hui ?</p>
83
  </div>
84
 
@@ -158,6 +171,11 @@
158
  <template id="botMessageTemplate">
159
  <div class="message-container bot">
160
  <div class="message-bubble">
 
 
 
 
 
161
  <p></p>
162
  </div>
163
  </div>
@@ -174,5 +192,7 @@
174
  </template>
175
 
176
  <script src="{{ url_for('static', filename='js/chat.js') }}"></script>
 
 
177
  </body>
178
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Assistant IA personnel alimenté par Mariam AI">
7
+ <meta name="theme-color" content="#2196F3">
8
+ <title>Mariam AI Chat</title>
9
+ <!-- PWA support -->
10
+ <link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
11
+ <link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/icon-192x192.png') }}">
12
+ <!-- Fonts et styles -->
13
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
14
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
15
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
16
+ <!-- SweetAlert2 pour les alertes -->
17
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
18
  <!-- Marked.js pour le parsing Markdown -->
19
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js"></script>
20
  <!-- Prism.js pour la coloration syntaxique -->
 
25
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
26
  </head>
27
  <body>
28
+ <!-- Indicateur hors ligne pour PWA -->
29
+ <div class="offline-indicator" id="offlineIndicator">
30
+ <i class="bi bi-wifi-off"></i> Vous êtes hors ligne. Certaines fonctionnalités peuvent être limitées.
31
+ </div>
32
+
33
  <div class="app-container">
34
  <!-- Side navigation bar -->
35
  <div class="side-nav" id="sideNav">
 
37
  <div class="logo">
38
  <i class="bi bi-stars"></i>
39
  </div>
40
+ <h1>Mariam AI</h1>
41
  <button id="closeSideNavButton" class="close-button">
42
  <i class="bi bi-x-lg"></i>
43
  </button>
 
74
  <button id="toggleNavButton" class="menu-button" title="Menu">
75
  <i class="bi bi-list"></i>
76
  </button>
77
+ <h1>Assistant Mariam AI</h1>
78
  </div>
79
  <div class="top-bar-right">
80
  <button class="icon-button" id="clearButton" title="Effacer la conversation">
 
91
  <div class="welcome-logo">
92
  <i class="bi bi-stars"></i>
93
  </div>
94
+ <h2>Bienvenue sur Mariam AI</h2>
95
  <p class="welcome-subtitle">Comment puis-je vous aider aujourd'hui ?</p>
96
  </div>
97
 
 
171
  <template id="botMessageTemplate">
172
  <div class="message-container bot">
173
  <div class="message-bubble">
174
+ <div class="message-actions">
175
+ <button class="copy-button" title="Copier le message">
176
+ <i class="bi bi-clipboard"></i>
177
+ </button>
178
+ </div>
179
  <p></p>
180
  </div>
181
  </div>
 
192
  </template>
193
 
194
  <script src="{{ url_for('static', filename='js/chat.js') }}"></script>
195
+ <!-- PWA Service Worker Registration -->
196
+ <script src="{{ url_for('static', filename='js/pwa.js') }}"></script>
197
  </body>
198
  </html>