kuro223 commited on
Commit
7bc796e
·
1 Parent(s): 83a69af
app.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import base64
4
+ import json
5
+ import uuid
6
+ import google.generativeai as genai
7
+ from datetime import datetime
8
+ from functools import wraps
9
+ from flask import Flask, render_template, request, jsonify, session, redirect
10
+ from dotenv import load_dotenv
11
+ from werkzeug.utils import secure_filename
12
+
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.DEBUG)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Configure Google Gemini API
21
+ api_key = os.environ.get("GEMINI_API_KEY")
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__)
31
+ app.secret_key = os.environ.get("SESSION_SECRET", "default-dev-secret-key")
32
+ app.config['UPLOAD_FOLDER'] = 'static/uploads'
33
+ app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 10 MB max
34
+
35
+ # Middleware to ensure user has a session_id
36
+ def session_required(f):
37
+ @wraps(f)
38
+ def decorated_function(*args, **kwargs):
39
+ if 'session_id' not in session:
40
+ session['session_id'] = str(uuid.uuid4())
41
+ logger.info(f"Created new session: {session['session_id']}")
42
+ return f(*args, **kwargs)
43
+ return decorated_function
44
+
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 an image is included
92
+ if image_data:
93
+ try:
94
+ # Process image with vision model
95
+ image_base64 = image_data.split(',')[1]
96
+ image = genai.types.Part.from_data(
97
+ data=base64.b64decode(image_base64),
98
+ mime_type="image/jpeg"
99
+ )
100
+
101
+ # Save image with timestamp in the session directory
102
+ session_id = session.get('session_id')
103
+ session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
104
+ os.makedirs(session_dir, exist_ok=True)
105
+
106
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
107
+ filename = secure_filename(f"image_{timestamp}.jpg")
108
+ filepath = os.path.join(session_dir, filename)
109
+ with open(filepath, "wb") as f:
110
+ f.write(base64.b64decode(image_base64))
111
+
112
+ # Create message parts with both text and image
113
+ parts = [image]
114
+ if user_message:
115
+ parts.append(user_message)
116
+
117
+ # Generate response using vision model
118
+ response = vision_model.generate_content(parts)
119
+ return jsonify({'response': response.text})
120
+
121
+ except Exception as img_error:
122
+ logger.error(f"Error processing image: {str(img_error)}")
123
+ return jsonify({
124
+ 'error': 'Désolé, une erreur est survenue lors du traitement de l\'image. Veuillez réessayer.'
125
+ }), 500
126
+ else:
127
+ # Text-only processing
128
+ # Create a chat session with history
129
+ chat = model.start_chat(history=formatted_history)
130
+
131
+ # Generate response
132
+ response = chat.send_message(user_message)
133
+
134
+ # Log successful response
135
+ logger.info(f"Generated response successfully. Response length: {len(response.text)}")
136
+
137
+ # Return the response
138
+ return jsonify({'response': response.text})
139
+
140
+ except genai.types.generation_types.BlockedPromptException as be:
141
+ logger.warning(f"Content blocked: {str(be)}")
142
+ return jsonify({
143
+ 'error': 'Votre message ou la conversation ne peut pas être traitée car elle contient du contenu potentiellement inapproprié.'
144
+ }), 400
145
+
146
+ except Exception as e:
147
+ logger.error(f"Error in chat endpoint: {str(e)}")
148
+ return jsonify({
149
+ 'error': 'Désolé, j\'ai rencontré une erreur lors du traitement de votre demande. Veuillez réessayer.'
150
+ }), 500
151
+
152
+ @app.route('/api/save-chat', methods=['POST'])
153
+ @session_required
154
+ def save_chat():
155
+ """Save the current chat history."""
156
+ try:
157
+ session_id = session.get('session_id')
158
+
159
+ # Create session-specific directory
160
+ session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
161
+ os.makedirs(session_dir, exist_ok=True)
162
+
163
+ data = request.json
164
+ chat_history = data.get('history', [])
165
+
166
+ if not chat_history:
167
+ return jsonify({'error': 'Aucune conversation à sauvegarder.'}), 400
168
+
169
+ # Generate filename with timestamp
170
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
171
+ filename = f"chat_{timestamp}.json"
172
+ filepath = os.path.join(session_dir, filename)
173
+
174
+ # Save chat history to file
175
+ with open(filepath, 'w', encoding='utf-8') as f:
176
+ json.dump(chat_history, f, ensure_ascii=False, indent=2)
177
+
178
+ return jsonify({'success': True, 'filename': filename})
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error saving chat: {str(e)}")
182
+ return jsonify({
183
+ 'error': 'Désolé, une erreur est survenue lors de la sauvegarde de la conversation.'
184
+ }), 500
185
+
186
+ @app.route('/api/load-chats', methods=['GET'])
187
+ @session_required
188
+ def load_chats():
189
+ """Get a list of saved chat files for current session."""
190
+ try:
191
+ session_id = session.get('session_id')
192
+
193
+ # Get session-specific directory
194
+ session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
195
+
196
+ # If the directory doesn't exist yet, return empty list
197
+ if not os.path.exists(session_dir):
198
+ return jsonify({'chats': []})
199
+
200
+ chat_files = []
201
+ for filename in os.listdir(session_dir):
202
+ if filename.startswith('chat_') and filename.endswith('.json'):
203
+ # Extract timestamp from filename
204
+ timestamp = filename[5:-5] # Remove 'chat_' and '.json'
205
+ # Add to list
206
+ chat_files.append({
207
+ 'filename': filename,
208
+ 'timestamp': timestamp
209
+ })
210
+
211
+ # Sort by timestamp (newest first)
212
+ chat_files.sort(key=lambda x: x['timestamp'], reverse=True)
213
+
214
+ logger.info(f"Loaded {len(chat_files)} chats for session {session_id}")
215
+ return jsonify({'chats': chat_files})
216
+
217
+ except Exception as e:
218
+ logger.error(f"Error loading chat list: {str(e)}")
219
+ return jsonify({
220
+ 'error': 'Désolé, une erreur est survenue lors du chargement des conversations.'
221
+ }), 500
222
+
223
+ @app.route('/api/load-chat/<filename>', methods=['GET'])
224
+ @session_required
225
+ def load_chat(filename):
226
+ """Load a specific chat history file."""
227
+ try:
228
+ session_id = session.get('session_id')
229
+
230
+ # Load from session-specific directory
231
+ session_dir = os.path.join(app.config['UPLOAD_FOLDER'], session_id)
232
+ filepath = os.path.join(session_dir, secure_filename(filename))
233
+
234
+ if not os.path.exists(filepath):
235
+ return jsonify({'error': 'Conversation introuvable.'}), 404
236
+
237
+ with open(filepath, 'r', encoding='utf-8') as f:
238
+ chat_history = json.load(f)
239
+
240
+ logger.info(f"Loaded chat {filename} for session {session_id}")
241
+ return jsonify({'history': chat_history})
242
+
243
+ except Exception as e:
244
+ logger.error(f"Error loading chat file: {str(e)}")
245
+ return jsonify({
246
+ 'error': 'Désolé, une erreur est survenue lors du chargement de la conversation.'
247
+ }), 500
248
+
249
+ if __name__ == '__main__':
250
+ app.run(host='0.0.0.0', port=5000, debug=True)
main.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from app import app # noqa: F401
2
+
3
+ if __name__ == "__main__":
4
+ app.run(host="0.0.0.0", port=5000, debug=True)
static/css/style.css ADDED
@@ -0,0 +1,899 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Base Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: 'Inter', sans-serif;
10
+ background-color: #FFFFFF;
11
+ color: #212121;
12
+ height: 100%;
13
+ overflow-x: hidden;
14
+ overflow-y: auto;
15
+ position: fixed;
16
+ width: 100%;
17
+ top: 0;
18
+ left: 0;
19
+ right: 0;
20
+ bottom: 0;
21
+ }
22
+
23
+ /* App Container */
24
+ .app-container {
25
+ display: flex;
26
+ height: 100%;
27
+ width: 100%;
28
+ position: relative;
29
+ overflow: hidden;
30
+ }
31
+
32
+ /* Side Navigation Bar */
33
+ .side-nav {
34
+ width: 280px;
35
+ height: 100%;
36
+ background-color: #FFFFFF;
37
+ border-right: 1px solid #EEEEEE;
38
+ display: flex;
39
+ flex-direction: column;
40
+ position: absolute;
41
+ top: 0;
42
+ left: -280px; /* Hidden by default */
43
+ transition: left 0.3s ease;
44
+ z-index: 1000;
45
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
46
+ }
47
+
48
+ .side-nav.active {
49
+ left: 0;
50
+ }
51
+
52
+ .side-nav-header {
53
+ display: flex;
54
+ align-items: center;
55
+ padding: 16px;
56
+ border-bottom: 1px solid #EEEEEE;
57
+ position: relative;
58
+ }
59
+
60
+ .side-nav-header .logo {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ width: 36px;
65
+ height: 36px;
66
+ background-color: #E3F2FD;
67
+ border-radius: 50%;
68
+ margin-right: 12px;
69
+ color: #2196F3;
70
+ }
71
+
72
+ .side-nav-header h1 {
73
+ font-size: 20px;
74
+ font-weight: 600;
75
+ color: #424242;
76
+ margin: 0;
77
+ }
78
+
79
+ .close-button {
80
+ position: absolute;
81
+ right: 12px;
82
+ top: 50%;
83
+ transform: translateY(-50%);
84
+ background-color: transparent;
85
+ color: #9E9E9E;
86
+ border: none;
87
+ width: 32px;
88
+ height: 32px;
89
+ border-radius: 50%;
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ cursor: pointer;
94
+ transition: all 0.2s ease;
95
+ }
96
+
97
+ .close-button:hover {
98
+ background-color: #F5F5F5;
99
+ color: #757575;
100
+ }
101
+
102
+ .side-nav-section {
103
+ flex: 1;
104
+ overflow-y: auto;
105
+ padding: 16px 0;
106
+ border-bottom: 1px solid #EEEEEE;
107
+ }
108
+
109
+ .section-header {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ padding: 0 16px 12px;
114
+ }
115
+
116
+ .section-header h3 {
117
+ font-size: 16px;
118
+ font-weight: 600;
119
+ color: #424242;
120
+ margin: 0;
121
+ }
122
+
123
+ .action-button-small {
124
+ background-color: transparent;
125
+ color: #2196F3;
126
+ border: 1px solid #2196F3;
127
+ border-radius: 4px;
128
+ width: 32px;
129
+ height: 32px;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ cursor: pointer;
134
+ transition: all 0.2s ease;
135
+ }
136
+
137
+ .action-button-small:hover {
138
+ background-color: #E3F2FD;
139
+ }
140
+
141
+ .history-list {
142
+ max-height: 100%;
143
+ overflow-y: auto;
144
+ }
145
+
146
+ .chat-history-item {
147
+ padding: 12px 16px;
148
+ cursor: pointer;
149
+ display: flex;
150
+ align-items: center;
151
+ transition: background-color 0.2s ease;
152
+ border-radius: 8px;
153
+ margin: 0 8px 8px;
154
+ }
155
+
156
+ .chat-history-item:hover {
157
+ background-color: #F5F5F5;
158
+ }
159
+
160
+ .chat-history-item .icon {
161
+ margin-right: 12px;
162
+ color: #757575;
163
+ }
164
+
165
+ .chat-history-item .details {
166
+ flex: 1;
167
+ }
168
+
169
+ .chat-history-item .timestamp {
170
+ font-size: 12px;
171
+ color: #9E9E9E;
172
+ margin-top: 4px;
173
+ }
174
+
175
+ .empty-state {
176
+ padding: 24px 16px;
177
+ text-align: center;
178
+ color: #9E9E9E;
179
+ font-size: 14px;
180
+ }
181
+
182
+ .side-nav-footer {
183
+ padding: 16px;
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 8px;
187
+ }
188
+
189
+ .nav-button {
190
+ display: flex;
191
+ align-items: center;
192
+ padding: 10px 16px;
193
+ background-color: transparent;
194
+ color: #616161;
195
+ border: none;
196
+ border-radius: 8px;
197
+ cursor: pointer;
198
+ transition: all 0.2s ease;
199
+ font-size: 14px;
200
+ text-align: left;
201
+ }
202
+
203
+ .nav-button i {
204
+ margin-right: 12px;
205
+ font-size: 16px;
206
+ }
207
+
208
+ .nav-button:hover {
209
+ background-color: #F5F5F5;
210
+ }
211
+
212
+ .nav-button#clearButtonNav:hover {
213
+ color: #F44336;
214
+ }
215
+
216
+ /* Main Content */
217
+ .main-content {
218
+ flex: 1;
219
+ display: flex;
220
+ flex-direction: column;
221
+ width: 100%;
222
+ position: relative;
223
+ left: 0;
224
+ transition: left 0.3s ease;
225
+ }
226
+
227
+ .main-content.nav-open {
228
+ left: 280px;
229
+ }
230
+
231
+ /* Top Bar */
232
+ .top-bar {
233
+ display: flex;
234
+ justify-content: space-between;
235
+ align-items: center;
236
+ padding: 12px 16px;
237
+ background-color: #2196F3;
238
+ color: white;
239
+ height: 60px;
240
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
241
+ z-index: 10;
242
+ }
243
+
244
+ .top-bar-left {
245
+ display: flex;
246
+ align-items: center;
247
+ }
248
+
249
+ .menu-button {
250
+ background-color: transparent;
251
+ color: white;
252
+ border: none;
253
+ width: 42px;
254
+ height: 42px;
255
+ border-radius: 50%;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ cursor: pointer;
260
+ margin-right: 16px;
261
+ font-size: 24px;
262
+ transition: background-color 0.2s ease;
263
+ }
264
+
265
+ .menu-button:hover {
266
+ background-color: rgba(255, 255, 255, 0.1);
267
+ }
268
+
269
+ .top-bar h1 {
270
+ font-size: 20px;
271
+ font-weight: 500;
272
+ margin: 0;
273
+ color: white;
274
+ }
275
+
276
+ .top-bar-right {
277
+ display: flex;
278
+ align-items: center;
279
+ }
280
+
281
+ .icon-button {
282
+ background-color: transparent;
283
+ color: white;
284
+ border: none;
285
+ width: 40px;
286
+ height: 40px;
287
+ border-radius: 50%;
288
+ display: flex;
289
+ align-items: center;
290
+ justify-content: center;
291
+ cursor: pointer;
292
+ transition: background-color 0.2s ease;
293
+ }
294
+
295
+ .icon-button:hover {
296
+ background-color: rgba(255, 255, 255, 0.1);
297
+ }
298
+
299
+ /* Welcome Container with Suggestion Bubbles */
300
+ .welcome-container {
301
+ display: flex;
302
+ flex-direction: column;
303
+ align-items: center;
304
+ justify-content: center;
305
+ height: 100%;
306
+ padding: 24px;
307
+ text-align: center;
308
+ }
309
+
310
+ .welcome-logo {
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ width: 64px;
315
+ height: 64px;
316
+ background-color: #E3F2FD;
317
+ border-radius: 50%;
318
+ margin-bottom: 24px;
319
+ color: #2196F3;
320
+ }
321
+
322
+ .welcome-logo i {
323
+ font-size: 32px;
324
+ }
325
+
326
+ .welcome-header h2 {
327
+ font-size: 28px;
328
+ font-weight: 600;
329
+ color: #212121;
330
+ margin-bottom: 12px;
331
+ }
332
+
333
+ .welcome-subtitle {
334
+ font-size: 16px;
335
+ color: #757575;
336
+ margin-bottom: 32px;
337
+ }
338
+
339
+ .suggestion-bubbles {
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 16px;
343
+ max-width: 500px;
344
+ width: 100%;
345
+ }
346
+
347
+ .suggestion-bubble {
348
+ display: flex;
349
+ align-items: center;
350
+ padding: 16px;
351
+ background-color: #F5F5F5;
352
+ border-radius: 12px;
353
+ cursor: pointer;
354
+ transition: all 0.2s ease;
355
+ text-align: left;
356
+ }
357
+
358
+ .suggestion-bubble i {
359
+ font-size: 20px;
360
+ color: #2196F3;
361
+ margin-right: 16px;
362
+ }
363
+
364
+ .suggestion-bubble span {
365
+ font-size: 15px;
366
+ color: #424242;
367
+ }
368
+
369
+ .suggestion-bubble:hover {
370
+ background-color: #E3F2FD;
371
+ transform: translateY(-2px);
372
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
373
+ }
374
+
375
+ /* Chat Container */
376
+ .chat-container {
377
+ display: flex;
378
+ flex-direction: column;
379
+ flex: 1;
380
+ overflow: hidden;
381
+ background-color: #FAFAFA;
382
+ height: calc(100% - 60px); /* 60px is the height of top-bar */
383
+ }
384
+
385
+ /* Chat Window */
386
+ .chat-window {
387
+ flex: 1;
388
+ overflow-y: auto;
389
+ padding: 16px;
390
+ scrollbar-width: thin;
391
+ scrollbar-color: #E0E0E0 #FAFAFA;
392
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS devices */
393
+ }
394
+
395
+ .chat-window::-webkit-scrollbar {
396
+ width: 6px;
397
+ }
398
+
399
+ .chat-window::-webkit-scrollbar-track {
400
+ background: #FAFAFA;
401
+ }
402
+
403
+ .chat-window::-webkit-scrollbar-thumb {
404
+ background-color: #E0E0E0;
405
+ border-radius: 10px;
406
+ }
407
+
408
+ /* Input Area */
409
+ .input-area {
410
+ border-top: 1px solid #EEEEEE;
411
+ background-color: #FFFFFF;
412
+ }
413
+
414
+ /* Image Preview Area */
415
+ .image-preview-area {
416
+ padding: 8px 16px;
417
+ background-color: #F8F9FA;
418
+ border-bottom: 1px solid #EEEEEE;
419
+ max-height: 130px;
420
+ overflow-y: auto;
421
+ }
422
+
423
+ .image-preview-area.hidden {
424
+ display: none;
425
+ }
426
+
427
+ .image-preview-list {
428
+ display: flex;
429
+ flex-wrap: wrap;
430
+ gap: 10px;
431
+ padding: 5px 0;
432
+ }
433
+
434
+ .image-preview-container {
435
+ position: relative;
436
+ display: inline-block;
437
+ width: 100px;
438
+ height: 100px;
439
+ border-radius: 8px;
440
+ overflow: hidden;
441
+ border: 1px solid #EEEEEE;
442
+ }
443
+
444
+ .image-preview-container img {
445
+ width: 100%;
446
+ height: 100%;
447
+ display: block;
448
+ object-fit: cover;
449
+ }
450
+
451
+ .remove-image-button {
452
+ position: absolute;
453
+ top: 4px;
454
+ right: 4px;
455
+ background-color: rgba(0, 0, 0, 0.5);
456
+ color: white;
457
+ border: none;
458
+ border-radius: 50%;
459
+ width: 24px;
460
+ height: 24px;
461
+ display: flex;
462
+ align-items: center;
463
+ justify-content: center;
464
+ cursor: pointer;
465
+ transition: background-color 0.2s ease;
466
+ }
467
+
468
+ .remove-image-button:hover {
469
+ background-color: rgba(0, 0, 0, 0.7);
470
+ }
471
+
472
+ /* Message Containers */
473
+ .message-container {
474
+ display: flex;
475
+ margin: 12px 0;
476
+ }
477
+
478
+ .message-container.user {
479
+ justify-content: flex-end;
480
+ }
481
+
482
+ .message-container.bot {
483
+ justify-content: flex-start;
484
+ }
485
+
486
+ /* Message Bubbles */
487
+ .message-bubble {
488
+ max-width: 80%;
489
+ padding: 14px 18px;
490
+ border-radius: 18px;
491
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
492
+ word-wrap: break-word;
493
+ }
494
+
495
+ .message-container.user .message-bubble {
496
+ background-color: #E3F2FD;
497
+ border-top-right-radius: 4px;
498
+ }
499
+
500
+ .message-container.bot .message-bubble {
501
+ background-color: #FFFFFF;
502
+ border-top-left-radius: 4px;
503
+ border: 1px solid #EEEEEE;
504
+ }
505
+
506
+ /* Message Text and Images */
507
+ .message-bubble p {
508
+ margin: 0;
509
+ line-height: 1.5;
510
+ font-size: 15px;
511
+ }
512
+
513
+ .message-bubble .image-container {
514
+ margin-bottom: 8px;
515
+ }
516
+
517
+ .message-bubble .chat-image {
518
+ max-width: 100%;
519
+ border-radius: 8px;
520
+ margin-bottom: 8px;
521
+ max-height: 300px;
522
+ object-fit: contain;
523
+ }
524
+
525
+ /* Markdown Styling */
526
+ .message-bubble ul,
527
+ .message-bubble ol {
528
+ margin-left: 20px;
529
+ margin-top: 8px;
530
+ margin-bottom: 8px;
531
+ }
532
+
533
+ .message-bubble li {
534
+ margin-bottom: 4px;
535
+ }
536
+
537
+ .message-bubble h1,
538
+ .message-bubble h2,
539
+ .message-bubble h3,
540
+ .message-bubble h4 {
541
+ margin-top: 16px;
542
+ margin-bottom: 8px;
543
+ font-weight: 600;
544
+ }
545
+
546
+ .message-bubble code {
547
+ background-color: #F5F5F5;
548
+ padding: 2px 4px;
549
+ border-radius: 4px;
550
+ font-family: 'Courier New', monospace;
551
+ font-size: 14px;
552
+ color: #E91E63;
553
+ }
554
+
555
+ .message-bubble pre {
556
+ background-color: #F5F5F5;
557
+ padding: 12px;
558
+ border-radius: 8px;
559
+ overflow-x: auto;
560
+ margin: 8px 0;
561
+ border: 1px solid #EEEEEE;
562
+ }
563
+
564
+ .message-bubble pre code {
565
+ background-color: transparent;
566
+ padding: 0;
567
+ color: inherit;
568
+ display: block;
569
+ }
570
+
571
+ .message-bubble a {
572
+ color: #2196F3;
573
+ text-decoration: none;
574
+ }
575
+
576
+ .message-bubble a:hover {
577
+ text-decoration: underline;
578
+ }
579
+
580
+ .message-bubble table {
581
+ border-collapse: collapse;
582
+ width: 100%;
583
+ margin: 8px 0;
584
+ }
585
+
586
+ .message-bubble th,
587
+ .message-bubble td {
588
+ border: 1px solid #EEEEEE;
589
+ padding: 8px;
590
+ text-align: left;
591
+ }
592
+
593
+ .message-bubble th {
594
+ background-color: #F5F5F5;
595
+ }
596
+
597
+ /* Error Message */
598
+ .message-container.error .message-bubble {
599
+ background-color: #FFEBEE;
600
+ }
601
+
602
+ .retry-button {
603
+ display: inline-block;
604
+ margin-top: 8px;
605
+ padding: 6px 12px;
606
+ background-color: #F44336;
607
+ color: white;
608
+ border: none;
609
+ border-radius: 4px;
610
+ cursor: pointer;
611
+ font-size: 14px;
612
+ }
613
+
614
+ .retry-button:hover {
615
+ background-color: #D32F2F;
616
+ }
617
+
618
+ /* Loading Animation */
619
+ .loading {
620
+ display: flex;
621
+ align-items: center;
622
+ justify-content: center;
623
+ min-width: 60px;
624
+ }
625
+
626
+ .dot-typing {
627
+ position: relative;
628
+ width: 6px;
629
+ height: 6px;
630
+ border-radius: 50%;
631
+ background-color: #9E9E9E;
632
+ color: #9E9E9E;
633
+ animation: dotTyping 1.5s infinite linear;
634
+ }
635
+
636
+ .dot-typing::before,
637
+ .dot-typing::after {
638
+ content: '';
639
+ display: inline-block;
640
+ position: absolute;
641
+ top: 0;
642
+ width: 6px;
643
+ height: 6px;
644
+ border-radius: 50%;
645
+ background-color: #9E9E9E;
646
+ color: #9E9E9E;
647
+ }
648
+
649
+ .dot-typing::before {
650
+ left: -12px;
651
+ animation: dotTypingBefore 1.5s infinite linear;
652
+ }
653
+
654
+ .dot-typing::after {
655
+ left: 12px;
656
+ animation: dotTypingAfter 1.5s infinite linear;
657
+ }
658
+
659
+ @keyframes dotTyping {
660
+ 0% { transform: scale(1); opacity: 0.6; }
661
+ 25% { transform: scale(1.3); opacity: 1; }
662
+ 50% { transform: scale(1); opacity: 0.6; }
663
+ 100% { transform: scale(1); opacity: 0.6; }
664
+ }
665
+
666
+ @keyframes dotTypingBefore {
667
+ 0% { transform: scale(1); opacity: 0.6; }
668
+ 25% { transform: scale(1); opacity: 0.6; }
669
+ 50% { transform: scale(1.3); opacity: 1; }
670
+ 100% { transform: scale(1); opacity: 0.6; }
671
+ }
672
+
673
+ @keyframes dotTypingAfter {
674
+ 0% { transform: scale(1); opacity: 0.6; }
675
+ 25% { transform: scale(1); opacity: 0.6; }
676
+ 50% { transform: scale(1); opacity: 0.6; }
677
+ 75% { transform: scale(1.3); opacity: 1; }
678
+ 100% { transform: scale(1); opacity: 0.6; }
679
+ }
680
+
681
+ /* Input Bar */
682
+ .input-bar {
683
+ padding: 16px;
684
+ display: flex;
685
+ align-items: flex-end;
686
+ background-color: #FFFFFF;
687
+ }
688
+
689
+ .upload-button {
690
+ background-color: transparent;
691
+ color: #616161;
692
+ border: none;
693
+ border-radius: 50%;
694
+ width: 42px;
695
+ height: 42px;
696
+ display: flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ cursor: pointer;
700
+ transition: all 0.2s ease;
701
+ margin-right: 8px;
702
+ }
703
+
704
+ .upload-button:hover {
705
+ background-color: #F5F5F5;
706
+ color: #2196F3;
707
+ }
708
+
709
+ .input-container {
710
+ flex: 1;
711
+ display: flex;
712
+ align-items: flex-end;
713
+ background-color: #F5F5F5;
714
+ border-radius: 24px;
715
+ padding: 10px 16px;
716
+ transition: box-shadow 0.3s ease;
717
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
718
+ }
719
+
720
+ .input-container:focus-within {
721
+ box-shadow: 0 0 0 2px #42A5F5;
722
+ }
723
+
724
+ /* Text Area */
725
+ #userInput {
726
+ flex: 1;
727
+ border: none;
728
+ background: transparent;
729
+ padding: 8px 0;
730
+ max-height: 120px;
731
+ resize: none;
732
+ font-family: inherit;
733
+ font-size: 15px;
734
+ outline: none;
735
+ }
736
+
737
+ /* Send Button */
738
+ .send-button {
739
+ background-color: #2196F3;
740
+ color: white;
741
+ border: none;
742
+ border-radius: 50%;
743
+ width: 40px;
744
+ height: 40px;
745
+ display: flex;
746
+ align-items: center;
747
+ justify-content: center;
748
+ cursor: pointer;
749
+ transition: all 0.2s ease;
750
+ margin-left: 8px;
751
+ }
752
+
753
+ .send-button:hover {
754
+ background-color: #1976D2;
755
+ transform: scale(1.05);
756
+ }
757
+
758
+ .send-button:disabled {
759
+ background-color: #BDBDBD;
760
+ cursor: not-allowed;
761
+ transform: none;
762
+ }
763
+
764
+ /* Overlay when side nav is open on mobile */
765
+ .overlay {
766
+ display: none;
767
+ position: fixed;
768
+ top: 0;
769
+ left: 0;
770
+ right: 0;
771
+ bottom: 0;
772
+ background-color: rgba(0, 0, 0, 0.5);
773
+ z-index: 999;
774
+ }
775
+
776
+ .overlay.active {
777
+ display: block;
778
+ }
779
+
780
+ /* Responsive adjustments */
781
+ @media (min-width: 992px) {
782
+ /* For larger screens */
783
+ .side-nav {
784
+ position: relative;
785
+ left: 0;
786
+ width: 280px;
787
+ flex-shrink: 0;
788
+ }
789
+
790
+ .main-content {
791
+ width: calc(100% - 280px);
792
+ }
793
+
794
+ .menu-button {
795
+ display: none;
796
+ }
797
+ }
798
+
799
+ @media (max-width: 991px) {
800
+ /* For tablets and below */
801
+ .app-container {
802
+ flex-direction: column;
803
+ }
804
+
805
+ .side-nav {
806
+ width: 280px;
807
+ }
808
+ }
809
+
810
+ @media (max-width: 768px) {
811
+ /* For small tablets and phones */
812
+ .message-bubble {
813
+ max-width: 85%;
814
+ }
815
+
816
+ .suggestion-bubbles {
817
+ max-width: 100%;
818
+ }
819
+
820
+ .welcome-logo {
821
+ width: 56px;
822
+ height: 56px;
823
+ }
824
+
825
+ .welcome-logo i {
826
+ font-size: 28px;
827
+ }
828
+
829
+ .welcome-header h2 {
830
+ font-size: 24px;
831
+ }
832
+ }
833
+
834
+ @media (max-width: 480px) {
835
+ /* For mobile phones */
836
+ .side-nav {
837
+ width: 85%;
838
+ }
839
+
840
+ .message-bubble {
841
+ max-width: 90%;
842
+ }
843
+
844
+ .chat-window {
845
+ padding: 12px;
846
+ }
847
+
848
+ .input-bar {
849
+ padding: 12px;
850
+ }
851
+
852
+ .top-bar {
853
+ padding: 10px 12px;
854
+ height: 56px;
855
+ position: sticky;
856
+ top: 0;
857
+ z-index: 100;
858
+ }
859
+
860
+ .top-bar h1 {
861
+ font-size: 18px;
862
+ }
863
+
864
+ .welcome-header h2 {
865
+ font-size: 22px;
866
+ }
867
+
868
+ .welcome-subtitle {
869
+ font-size: 14px;
870
+ }
871
+
872
+ .suggestion-bubble {
873
+ padding: 12px;
874
+ }
875
+
876
+ .suggestion-bubble i {
877
+ font-size: 18px;
878
+ margin-right: 12px;
879
+ }
880
+
881
+ .suggestion-bubble span {
882
+ font-size: 14px;
883
+ }
884
+
885
+ /* Fix input bar to bottom on mobile */
886
+ .input-area {
887
+ position: sticky;
888
+ bottom: 0;
889
+ z-index: 100;
890
+ box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
891
+ }
892
+
893
+ /* Ensure welcome container is scrollable on small screens */
894
+ .welcome-container {
895
+ height: auto;
896
+ min-height: calc(100% - 120px); /* Leave space for input area */
897
+ overflow-y: auto;
898
+ }
899
+ }
static/js/chat.js ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // DOM Elements
3
+ const chatWindow = document.getElementById('chatWindow');
4
+ const userInput = document.getElementById('userInput');
5
+ const sendButton = document.getElementById('sendButton');
6
+ const clearButton = document.getElementById('clearButton');
7
+ const clearButtonNav = document.getElementById('clearButtonNav');
8
+ const imageInput = document.getElementById('imageInput');
9
+ const uploadImageButton = document.getElementById('uploadImageButton');
10
+ const imagePreviewArea = document.getElementById('imagePreviewArea');
11
+ const imagePreviewContainer = document.getElementById('imagePreviewContainer');
12
+ const chatHistoryList = document.getElementById('chatHistoryList');
13
+ const saveCurrentChatButton = document.getElementById('saveCurrentChatButton');
14
+ const settingsButton = document.getElementById('settingsButtonNav');
15
+ const toggleNavButton = document.getElementById('toggleNavButton');
16
+ const closeSideNavButton = document.getElementById('closeSideNavButton');
17
+ const sideNav = document.getElementById('sideNav');
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');
24
+ const userMessageTemplate = document.getElementById('userMessageTemplate');
25
+ const userImageMessageTemplate = document.getElementById('userImageMessageTemplate');
26
+ const botMessageTemplate = document.getElementById('botMessageTemplate');
27
+ const errorMessageTemplate = document.getElementById('errorMessageTemplate');
28
+
29
+ // State variables
30
+ let chatHistory = [];
31
+ let selectedImagesData = []; // Pour stocker plusieurs images
32
+ let conversationStarted = false;
33
+
34
+ // --- IMAGE HANDLING ---
35
+
36
+ // Upload image button click
37
+ uploadImageButton.addEventListener('click', function() {
38
+ imageInput.click();
39
+ });
40
+
41
+ // Image input change - support for multiple images
42
+ imageInput.addEventListener('change', function(e) {
43
+ const files = Array.from(e.target.files);
44
+ if (!files.length) return;
45
+
46
+ // Clear previous image container
47
+ imagePreviewContainer.innerHTML = '';
48
+ selectedImagesData = [];
49
+
50
+ // Process each file
51
+ files.forEach(file => {
52
+ // Check file type
53
+ if (!file.type.startsWith('image/')) {
54
+ alert('Veuillez sélectionner uniquement des fichiers image valides.');
55
+ return;
56
+ }
57
+
58
+ // Check file size (max 5MB)
59
+ if (file.size > 5 * 1024 * 1024) {
60
+ alert('La taille de chaque image ne doit pas dépasser 5 Mo.');
61
+ return;
62
+ }
63
+
64
+ const reader = new FileReader();
65
+ reader.onload = function(event) {
66
+ // Create preview container
67
+ const previewContainer = document.createElement('div');
68
+ previewContainer.className = 'image-preview-container';
69
+
70
+ // Create image preview
71
+ const imgPreview = document.createElement('img');
72
+ imgPreview.src = event.target.result;
73
+ imgPreview.alt = 'Image preview';
74
+ previewContainer.appendChild(imgPreview);
75
+
76
+ // Create remove button
77
+ const removeBtn = document.createElement('button');
78
+ removeBtn.className = 'remove-image-button';
79
+ removeBtn.innerHTML = '<i class="bi bi-x-circle-fill"></i>';
80
+ previewContainer.appendChild(removeBtn);
81
+
82
+ // Add preview to container
83
+ imagePreviewContainer.appendChild(previewContainer);
84
+
85
+ // Store image data
86
+ const imageData = event.target.result;
87
+ selectedImagesData.push(imageData);
88
+
89
+ // Add event listener to remove button
90
+ removeBtn.addEventListener('click', () => {
91
+ // Remove this specific image
92
+ const index = selectedImagesData.indexOf(imageData);
93
+ if (index > -1) {
94
+ selectedImagesData.splice(index, 1);
95
+ }
96
+ previewContainer.remove();
97
+
98
+ // Hide preview area if no images left
99
+ if (selectedImagesData.length === 0) {
100
+ imagePreviewArea.classList.add('hidden');
101
+ imageInput.value = '';
102
+ }
103
+ });
104
+ };
105
+ reader.readAsDataURL(file);
106
+ });
107
+
108
+ // Show preview area
109
+ imagePreviewArea.classList.remove('hidden');
110
+
111
+ // Focus on input for caption
112
+ userInput.focus();
113
+ });
114
+
115
+ // --- SIDE NAVIGATION HANDLING ---
116
+
117
+ // Toggle side navigation
118
+ toggleNavButton.addEventListener('click', function() {
119
+ sideNav.classList.add('active');
120
+ document.body.insertAdjacentHTML('beforeend', '<div class="overlay" id="navOverlay"></div>');
121
+ const overlay = document.getElementById('navOverlay');
122
+ overlay.classList.add('active');
123
+ loadChatHistoryList();
124
+
125
+ // Close side nav when clicking overlay
126
+ overlay.addEventListener('click', closeSideNav);
127
+ });
128
+
129
+ // Close side navigation
130
+ closeSideNavButton.addEventListener('click', closeSideNav);
131
+
132
+ function closeSideNav() {
133
+ sideNav.classList.remove('active');
134
+ const overlay = document.getElementById('navOverlay');
135
+ if (overlay) {
136
+ overlay.remove();
137
+ }
138
+ }
139
+
140
+ // Clear chat from nav button
141
+ if (clearButtonNav) {
142
+ clearButtonNav.addEventListener('click', function() {
143
+ clearChat(true); // With confirmation
144
+ closeSideNav();
145
+ });
146
+ }
147
+
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
+ }
155
+
156
+ // Load chat history list
157
+ function loadChatHistoryList() {
158
+ fetch('/api/load-chats')
159
+ .then(response => response.json())
160
+ .then(data => {
161
+ if (data.error) {
162
+ chatHistoryList.innerHTML = `<div class="empty-state">${data.error}</div>`;
163
+ return;
164
+ }
165
+
166
+ if (!data.chats || data.chats.length === 0) {
167
+ chatHistoryList.innerHTML = '<div class="empty-state">Aucune conversation sauvegardée</div>';
168
+ return;
169
+ }
170
+
171
+ // Create history items
172
+ let historyHTML = '';
173
+ data.chats.forEach(chat => {
174
+ // Format timestamp (YYYYMMDD_HHMMSS to readable format)
175
+ const timestamp = chat.timestamp;
176
+ const year = timestamp.substring(0, 4);
177
+ const month = timestamp.substring(4, 6);
178
+ const day = timestamp.substring(6, 8);
179
+ const hour = timestamp.substring(9, 11);
180
+ const minute = timestamp.substring(11, 13);
181
+ const formattedDate = `${day}/${month}/${year} ${hour}:${minute}`;
182
+
183
+ historyHTML += `
184
+ <div class="chat-history-item" data-filename="${chat.filename}">
185
+ <div class="icon"><i class="bi bi-chat-dots"></i></div>
186
+ <div class="details">
187
+ <div>Conversation</div>
188
+ <div class="timestamp">${formattedDate}</div>
189
+ </div>
190
+ </div>
191
+ `;
192
+ });
193
+
194
+ chatHistoryList.innerHTML = historyHTML;
195
+
196
+ // Add click event to history items
197
+ const historyItems = chatHistoryList.querySelectorAll('.chat-history-item');
198
+ historyItems.forEach(item => {
199
+ item.addEventListener('click', function() {
200
+ const filename = this.getAttribute('data-filename');
201
+ loadChatHistory(filename);
202
+ toggleHistoryDropdown();
203
+ });
204
+ });
205
+ })
206
+ .catch(error => {
207
+ console.error('Error loading chat history:', error);
208
+ chatHistoryList.innerHTML = '<div class="empty-state">Erreur lors du chargement de l\'historique</div>';
209
+ });
210
+ }
211
+
212
+ // Load specific chat history
213
+ function loadChatHistory(filename) {
214
+ fetch(`/api/load-chat/${filename}`)
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;
228
+
229
+ // Display messages
230
+ data.history.forEach(msg => {
231
+ if (msg.sender === 'user') {
232
+ const messageElement = userMessageTemplate.content.cloneNode(true);
233
+ messageElement.querySelector('p').textContent = msg.text;
234
+ chatWindow.appendChild(messageElement);
235
+ } else {
236
+ const messageElement = botMessageTemplate.content.cloneNode(true);
237
+ const messageParagraph = messageElement.querySelector('p');
238
+
239
+ if (window.marked) {
240
+ messageParagraph.innerHTML = marked.parse(msg.text);
241
+ } else {
242
+ messageParagraph.textContent = msg.text;
243
+ }
244
+
245
+ chatWindow.appendChild(messageElement);
246
+
247
+ if (window.Prism) {
248
+ const codeBlocks = messageParagraph.querySelectorAll('pre code');
249
+ codeBlocks.forEach(block => {
250
+ Prism.highlightElement(block);
251
+ });
252
+ }
253
+ }
254
+ });
255
+
256
+ // Scroll to bottom
257
+ scrollToBottom();
258
+ }
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
+
273
+ fetch('/api/save-chat', {
274
+ method: 'POST',
275
+ headers: {
276
+ 'Content-Type': 'application/json'
277
+ },
278
+ body: JSON.stringify({
279
+ history: chatHistory
280
+ })
281
+ })
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
+
297
+ // --- TEXT INPUT HANDLING ---
298
+
299
+ // Auto-resize textarea based on content
300
+ userInput.addEventListener('input', function() {
301
+ this.style.height = 'auto';
302
+ this.style.height = (this.scrollHeight) + 'px';
303
+ // Reset height if empty
304
+ if (this.value === '') {
305
+ this.style.height = '';
306
+ }
307
+ });
308
+
309
+ // Send message when Enter key is pressed (without shift)
310
+ userInput.addEventListener('keydown', function(e) {
311
+ if (e.key === 'Enter' && !e.shiftKey) {
312
+ e.preventDefault();
313
+ sendMessage();
314
+ }
315
+ });
316
+
317
+ // Send message when send button is clicked
318
+ sendButton.addEventListener('click', sendMessage);
319
+
320
+ // Clear chat history when clear button is clicked
321
+ if (clearButton) {
322
+ clearButton.addEventListener('click', function() {
323
+ clearChat(true); // With confirmation
324
+ });
325
+ }
326
+
327
+ // --- CHAT FUNCTIONS ---
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();
349
+
350
+ // Require either text or images
351
+ if (message === '' && selectedImagesData.length === 0) return;
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);
359
+ }
360
+
361
+ // Clear input field and reset height
362
+ userInput.value = '';
363
+ userInput.style.height = '';
364
+
365
+ // Show loading indicator
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) {
373
+ imagePreviewContainer.innerHTML = '';
374
+ imagePreviewArea.classList.add('hidden');
375
+ selectedImagesData = [];
376
+ imageInput.value = '';
377
+ }
378
+ }
379
+
380
+ // Add user message to chat window
381
+ function addUserMessage(message) {
382
+ // Hide welcome container if visible (first message)
383
+ if (!conversationStarted && welcomeContainer) {
384
+ welcomeContainer.style.display = 'none';
385
+ conversationStarted = true;
386
+ }
387
+
388
+ const messageElement = userMessageTemplate.content.cloneNode(true);
389
+ messageElement.querySelector('p').textContent = message;
390
+ chatWindow.appendChild(messageElement);
391
+
392
+ // Add to chat history
393
+ chatHistory.push({
394
+ sender: 'user',
395
+ text: message
396
+ });
397
+
398
+ // Scroll to bottom
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';
407
+ conversationStarted = true;
408
+ }
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');
418
+ if (message) {
419
+ textElement.textContent = message;
420
+ } else {
421
+ textElement.style.display = 'none'; // Hide text element if no message
422
+ }
423
+
424
+ chatWindow.appendChild(messageElement);
425
+
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
433
+ scrollToBottom();
434
+ }
435
+
436
+ // Add bot message to chat window with markdown support
437
+ function addBotMessage(message) {
438
+ // Hide welcome container if visible (first message)
439
+ if (!conversationStarted && welcomeContainer) {
440
+ welcomeContainer.style.display = 'none';
441
+ conversationStarted = true;
442
+ }
443
+
444
+ const messageElement = botMessageTemplate.content.cloneNode(true);
445
+ const messageParagraph = messageElement.querySelector('p');
446
+
447
+ // Use the marked library to parse Markdown if available
448
+ if (window.marked) {
449
+ messageParagraph.innerHTML = marked.parse(message);
450
+ } else {
451
+ messageParagraph.textContent = message;
452
+ }
453
+
454
+ chatWindow.appendChild(messageElement);
455
+
456
+ // Add syntax highlighting to code blocks if Prism is available
457
+ if (window.Prism) {
458
+ const codeBlocks = messageParagraph.querySelectorAll('pre code');
459
+ codeBlocks.forEach(block => {
460
+ Prism.highlightElement(block);
461
+ });
462
+ }
463
+
464
+ // Add to chat history
465
+ chatHistory.push({
466
+ sender: 'bot',
467
+ text: message
468
+ });
469
+
470
+ // Scroll to bottom
471
+ scrollToBottom();
472
+ }
473
+
474
+ // Add loading indicator to chat window
475
+ function addLoadingIndicator() {
476
+ const loadingElement = loadingTemplate.content.cloneNode(true);
477
+ chatWindow.appendChild(loadingElement);
478
+
479
+ // Scroll to bottom
480
+ scrollToBottom();
481
+
482
+ // Return the loading element so it can be removed later
483
+ return chatWindow.lastElementChild;
484
+ }
485
+
486
+ // Add error message to chat window
487
+ function addErrorMessage(error, retryMessage, retryImage) {
488
+ const errorElement = errorMessageTemplate.content.cloneNode(true);
489
+ errorElement.querySelector('p').textContent = error;
490
+
491
+ // Add retry functionality if a message to retry is provided
492
+ if (retryMessage || retryImage) {
493
+ const retryButton = errorElement.querySelector('.retry-button');
494
+ retryButton.addEventListener('click', function() {
495
+ // Remove the error message
496
+ this.closest('.message-container').remove();
497
+
498
+ // Show loading indicator
499
+ const loadingElement = addLoadingIndicator();
500
+
501
+ // Retry sending the message
502
+ sendToAPI(retryMessage, loadingElement, retryImage);
503
+ });
504
+ } else {
505
+ // Hide retry button if no retry message
506
+ errorElement.querySelector('.retry-button').style.display = 'none';
507
+ }
508
+
509
+ chatWindow.appendChild(errorElement);
510
+
511
+ // Scroll to bottom
512
+ scrollToBottom();
513
+ }
514
+
515
+ // Send message to API
516
+ function sendToAPI(message, loadingElement, imageData = null) {
517
+ // Disable input while processing
518
+ userInput.disabled = true;
519
+ sendButton.disabled = true;
520
+ uploadImageButton.disabled = true;
521
+ if (clearButton) clearButton.disabled = true;
522
+
523
+ // Prepare request data
524
+ const requestData = {
525
+ message: message,
526
+ history: chatHistory
527
+ };
528
+
529
+ // Add image data if provided
530
+ if (imageData) {
531
+ requestData.image = imageData;
532
+ }
533
+
534
+ fetch('/api/chat', {
535
+ method: 'POST',
536
+ headers: {
537
+ 'Content-Type': 'application/json'
538
+ },
539
+ body: JSON.stringify(requestData)
540
+ })
541
+ .then(response => {
542
+ if (!response.ok) {
543
+ throw new Error(`Server responded with status: ${response.status}`);
544
+ }
545
+ return response.json();
546
+ })
547
+ .then(data => {
548
+ // Remove loading indicator
549
+ if (loadingElement) {
550
+ loadingElement.remove();
551
+ }
552
+
553
+ // Check for error
554
+ if (data.error) {
555
+ addErrorMessage(data.error, message, imageData);
556
+ } else {
557
+ // Add bot response
558
+ addBotMessage(data.response);
559
+ }
560
+ })
561
+ .catch(error => {
562
+ console.error('Error:', error);
563
+
564
+ // Remove loading indicator
565
+ if (loadingElement) {
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
574
+ userInput.disabled = false;
575
+ sendButton.disabled = false;
576
+ uploadImageButton.disabled = false;
577
+ if (clearButton) clearButton.disabled = false;
578
+ userInput.focus();
579
+ });
580
+ }
581
+
582
+ // Scroll chat window to bottom
583
+ function scrollToBottom() {
584
+ chatWindow.scrollTop = chatWindow.scrollHeight;
585
+ }
586
+
587
+ // Setup suggestion bubbles click handlers
588
+ if (suggestionBubbles) {
589
+ suggestionBubbles.forEach(bubble => {
590
+ bubble.addEventListener('click', function() {
591
+ const prompt = this.getAttribute('data-prompt');
592
+ if (prompt) {
593
+ userInput.value = prompt;
594
+ sendMessage();
595
+ }
596
+ });
597
+ });
598
+ }
599
+
600
+ // Initial focus on input field
601
+ userInput.focus();
602
+ });
static/uploads/chat_20250502_065334.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "sender": "user",
4
+ "text": "Salut"
5
+ },
6
+ {
7
+ "sender": "bot",
8
+ "text": "Salut ! Comment puis-je vous aider aujourd'hui ?\n"
9
+ }
10
+ ]
templates/index.html ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
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 -->
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/themes/prism.min.css">
14
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/prism.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/components/prism-python.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/components/prism-javascript.min.js"></script>
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">
23
+ <div class="side-nav-header">
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>
31
+ </div>
32
+
33
+ <div class="side-nav-section">
34
+ <div class="section-header">
35
+ <h3>Historique</h3>
36
+ <button id="saveCurrentChatButton" class="action-button-small" title="Sauvegarder cette conversation">
37
+ <i class="bi bi-save"></i>
38
+ </button>
39
+ </div>
40
+ <div id="chatHistoryList" class="history-list">
41
+ <!-- History items will be added here -->
42
+ <div class="empty-state">Aucune conversation sauvegardée</div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="side-nav-footer">
47
+ <button id="clearButtonNav" class="nav-button" title="Effacer la conversation">
48
+ <i class="bi bi-trash"></i> Effacer la conversation
49
+ </button>
50
+ <button id="settingsButtonNav" class="nav-button" title="Paramètres">
51
+ <i class="bi bi-gear"></i> Paramètres
52
+ </button>
53
+ </div>
54
+ </div>
55
+
56
+ <!-- Main content -->
57
+ <div class="main-content">
58
+ <!-- Top Bar -->
59
+ <div class="top-bar">
60
+ <div class="top-bar-left">
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">
68
+ <i class="bi bi-trash"></i>
69
+ </button>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="chat-container">
74
+ <div class="chat-window" id="chatWindow">
75
+ <!-- Welcome content with suggestion bubbles -->
76
+ <div class="welcome-container">
77
+ <div class="welcome-header">
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
+
85
+ <div class="suggestion-bubbles">
86
+ <div class="suggestion-bubble" data-prompt="Explique-moi comment fonctionne l'intelligence artificielle en termes simples">
87
+ <i class="bi bi-lightbulb"></i>
88
+ <span>Explique-moi comment fonctionne l'intelligence artificielle</span>
89
+ </div>
90
+ <div class="suggestion-bubble" data-prompt="Quelles sont les dernières avancées en matière d'énergie renouvelable ?">
91
+ <i class="bi bi-wind"></i>
92
+ <span>Avancées en énergie renouvelable</span>
93
+ </div>
94
+ <div class="suggestion-bubble" data-prompt="Donne-moi des idées de recettes rapides pour un dîner équilibré">
95
+ <i class="bi bi-egg-fried"></i>
96
+ <span>Idées de recettes rapides et équilibrées</span>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <!-- Messages will be dynamically inserted here -->
101
+ </div>
102
+ <div class="input-area">
103
+ <!-- Image preview area -->
104
+ <div id="imagePreviewArea" class="image-preview-area hidden">
105
+ <div id="imagePreviewContainer" class="image-preview-list">
106
+ <!-- Les miniatures d'images seront ajoutées ici dynamiquement -->
107
+ </div>
108
+ </div>
109
+
110
+ <div class="input-bar">
111
+ <button id="uploadImageButton" class="upload-button" title="Joindre une image">
112
+ <i class="bi bi-image"></i>
113
+ </button>
114
+ <input type="file" id="imageInput" accept="image/*" multiple hidden>
115
+ <div class="input-container">
116
+ <textarea id="userInput" placeholder="Écrivez votre message..." rows="1"></textarea>
117
+ <button id="sendButton" class="send-button" title="Envoyer">
118
+ <i class="bi bi-send"></i>
119
+ </button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Loading indicator template -->
128
+ <template id="loadingTemplate">
129
+ <div class="message-container bot">
130
+ <div class="message-bubble loading">
131
+ <div class="dot-typing"></div>
132
+ </div>
133
+ </div>
134
+ </template>
135
+
136
+ <!-- User message template -->
137
+ <template id="userMessageTemplate">
138
+ <div class="message-container user">
139
+ <div class="message-bubble">
140
+ <p></p>
141
+ </div>
142
+ </div>
143
+ </template>
144
+
145
+ <!-- User message with image template -->
146
+ <template id="userImageMessageTemplate">
147
+ <div class="message-container user">
148
+ <div class="message-bubble">
149
+ <div class="image-container">
150
+ <img src="" alt="Uploaded image" class="chat-image">
151
+ </div>
152
+ <p></p>
153
+ </div>
154
+ </div>
155
+ </template>
156
+
157
+ <!-- Bot message template -->
158
+ <template id="botMessageTemplate">
159
+ <div class="message-container bot">
160
+ <div class="message-bubble">
161
+ <p></p>
162
+ </div>
163
+ </div>
164
+ </template>
165
+
166
+ <!-- Error message template -->
167
+ <template id="errorMessageTemplate">
168
+ <div class="message-container bot error">
169
+ <div class="message-bubble">
170
+ <p></p>
171
+ <button class="retry-button">Réessayer</button>
172
+ </div>
173
+ </div>
174
+ </template>
175
+
176
+ <script src="{{ url_for('static', filename='js/chat.js') }}"></script>
177
+ </body>
178
+ </html>