jojdglo
Browse files- app.py +20 -16
- templates/index.html +71 -55
app.py
CHANGED
|
@@ -192,25 +192,24 @@ def upload_file():
|
|
| 192 |
try:
|
| 193 |
if 'file' not in request.files:
|
| 194 |
return jsonify({'error': 'No file uploaded'}), 400
|
| 195 |
-
|
| 196 |
file = request.files['file']
|
| 197 |
if file.filename == '':
|
| 198 |
return jsonify({'error': 'No file selected'}), 400
|
| 199 |
-
|
| 200 |
# Lire le fichier
|
| 201 |
file_bytes = file.read()
|
| 202 |
mime_type = file.content_type or mimetypes.guess_type(file.filename)[0]
|
| 203 |
-
|
| 204 |
# Encoder en base64 pour le stockage temporaire
|
| 205 |
file_b64 = base64.b64encode(file_bytes).decode()
|
| 206 |
-
|
| 207 |
return jsonify({
|
| 208 |
'success': True,
|
| 209 |
-
'filename': file.filename,
|
| 210 |
'mime_type': mime_type,
|
| 211 |
'data': file_b64
|
| 212 |
})
|
| 213 |
-
|
| 214 |
except Exception as e:
|
| 215 |
return jsonify({'error': str(e)}), 500
|
| 216 |
|
|
@@ -219,15 +218,20 @@ def chat_with_file():
|
|
| 219 |
try:
|
| 220 |
data = request.get_json()
|
| 221 |
message = data.get('message', '')
|
| 222 |
-
|
| 223 |
thinking_enabled = data.get('thinking_enabled', True)
|
| 224 |
conversation_id = data.get('conversation_id', 'default')
|
| 225 |
-
|
| 226 |
-
#
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
# Configuration du thinking
|
| 233 |
config_dict = DEFAULT_CONFIG.copy()
|
|
@@ -251,10 +255,10 @@ def chat_with_file():
|
|
| 251 |
|
| 252 |
chat = conversations[conversation_id]
|
| 253 |
|
| 254 |
-
# Préparation du contenu avec
|
| 255 |
contents = [message]
|
| 256 |
-
|
| 257 |
-
|
| 258 |
file_bytes = base64.b64decode(file_data['data'])
|
| 259 |
file_part = types.Part.from_bytes(
|
| 260 |
data=file_bytes,
|
|
|
|
| 192 |
try:
|
| 193 |
if 'file' not in request.files:
|
| 194 |
return jsonify({'error': 'No file uploaded'}), 400
|
| 195 |
+
|
| 196 |
file = request.files['file']
|
| 197 |
if file.filename == '':
|
| 198 |
return jsonify({'error': 'No file selected'}), 400
|
| 199 |
+
|
| 200 |
# Lire le fichier
|
| 201 |
file_bytes = file.read()
|
| 202 |
mime_type = file.content_type or mimetypes.guess_type(file.filename)[0]
|
| 203 |
+
|
| 204 |
# Encoder en base64 pour le stockage temporaire
|
| 205 |
file_b64 = base64.b64encode(file_bytes).decode()
|
| 206 |
+
|
| 207 |
return jsonify({
|
| 208 |
'success': True,
|
|
|
|
| 209 |
'mime_type': mime_type,
|
| 210 |
'data': file_b64
|
| 211 |
})
|
| 212 |
+
|
| 213 |
except Exception as e:
|
| 214 |
return jsonify({'error': str(e)}), 500
|
| 215 |
|
|
|
|
| 218 |
try:
|
| 219 |
data = request.get_json()
|
| 220 |
message = data.get('message', '')
|
| 221 |
+
file_data_list = data.get('file_data', [])
|
| 222 |
thinking_enabled = data.get('thinking_enabled', True)
|
| 223 |
conversation_id = data.get('conversation_id', 'default')
|
| 224 |
+
|
| 225 |
+
# Ensure file_data_list is a list
|
| 226 |
+
if not isinstance(file_data_list, list):
|
| 227 |
+
file_data_list = [file_data_list]
|
| 228 |
+
|
| 229 |
+
# Ajouter le message de l'utilisateur à l'historique (avec indication de fichiers)
|
| 230 |
+
display_message = message if message else 'Analyse ces fichiers'
|
| 231 |
+
if file_data_list:
|
| 232 |
+
file_count = len(file_data_list)
|
| 233 |
+
display_message += f" [{file_count} fichier{'s' if file_count > 1 else ''}]"
|
| 234 |
+
add_message_to_history(conversation_id, 'user', display_message, has_file=len(file_data_list) > 0, file_data=file_data_list)
|
| 235 |
|
| 236 |
# Configuration du thinking
|
| 237 |
config_dict = DEFAULT_CONFIG.copy()
|
|
|
|
| 255 |
|
| 256 |
chat = conversations[conversation_id]
|
| 257 |
|
| 258 |
+
# Préparation du contenu avec fichiers
|
| 259 |
contents = [message]
|
| 260 |
+
|
| 261 |
+
for file_data in file_data_list:
|
| 262 |
file_bytes = base64.b64decode(file_data['data'])
|
| 263 |
file_part = types.Part.from_bytes(
|
| 264 |
data=file_bytes,
|
templates/index.html
CHANGED
|
@@ -130,7 +130,7 @@
|
|
| 130 |
<div class="p-4 md:p-6 bg-dark-card border-t border-dark-border flex-shrink-0">
|
| 131 |
<div class="mb-3 p-3 bg-dark-elevated rounded-lg text-sm hidden" id="filePreview"></div>
|
| 132 |
<div class="flex gap-3 items-end">
|
| 133 |
-
<input type="file" id="fileInput" class="hidden" accept="image/*,video/*,.pdf,.txt,.csv,.json" onchange="handleFileSelect(event)">
|
| 134 |
<button class="bg-gray-700 text-white h-12 w-12 rounded-full hover:bg-gray-600 transition-colors flex-shrink-0 flex items-center justify-center text-xl" onclick="document.getElementById('fileInput').click()" title="Joindre un fichier">
|
| 135 |
📎
|
| 136 |
</button>
|
|
@@ -152,7 +152,7 @@
|
|
| 152 |
</div>
|
| 153 |
|
| 154 |
<script>
|
| 155 |
-
let
|
| 156 |
let conversationId = 'session_' + Date.now();
|
| 157 |
|
| 158 |
// Configuration de marked pour le markdown
|
|
@@ -174,61 +174,72 @@
|
|
| 174 |
}
|
| 175 |
|
| 176 |
function handleFileSelect(event) {
|
| 177 |
-
const
|
| 178 |
-
if (
|
| 179 |
|
| 180 |
const filePreview = document.getElementById('filePreview');
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
| 193 |
<div class="text-gray-500 text-xs">${(file.size / 1024).toFixed(1)} KB</div>
|
| 194 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
<button onclick="removeFile()" class="bg-gray-600 hover:bg-gray-500 text-white px-3 py-1 rounded transition-colors">✕</button>
|
| 196 |
</div>
|
| 197 |
`;
|
| 198 |
filePreview.classList.remove('hidden');
|
| 199 |
-
};
|
| 200 |
-
reader.readAsDataURL(file);
|
| 201 |
-
} else {
|
| 202 |
-
filePreview.innerHTML = `
|
| 203 |
-
<div class="flex justify-between items-center">
|
| 204 |
-
<span class="text-gray-300">📎 ${file.name}</span>
|
| 205 |
-
<button onclick="removeFile()" class="bg-gray-600 hover:bg-gray-500 text-white px-3 py-1 rounded transition-colors">✕</button>
|
| 206 |
-
</div>
|
| 207 |
-
`;
|
| 208 |
-
filePreview.classList.remove('hidden');
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
fetch('/upload', {
|
| 212 |
-
method: 'POST',
|
| 213 |
-
body: formData
|
| 214 |
-
})
|
| 215 |
-
.then(response => response.json())
|
| 216 |
-
.then(data => {
|
| 217 |
-
if (data.success) {
|
| 218 |
-
currentFile = data;
|
| 219 |
-
} else {
|
| 220 |
-
showError('Erreur lors du téléchargement: ' + data.error);
|
| 221 |
-
filePreview.classList.add('hidden');
|
| 222 |
}
|
| 223 |
-
}
|
| 224 |
-
.catch(error => {
|
| 225 |
-
showError('Erreur lors du téléchargement: ' + error.message);
|
| 226 |
-
filePreview.classList.add('hidden');
|
| 227 |
-
});
|
| 228 |
}
|
| 229 |
|
| 230 |
function removeFile() {
|
| 231 |
-
|
| 232 |
document.getElementById('filePreview').classList.add('hidden');
|
| 233 |
document.getElementById('fileInput').value = '';
|
| 234 |
}
|
|
@@ -237,7 +248,7 @@
|
|
| 237 |
const messageInput = document.getElementById('messageInput');
|
| 238 |
const message = messageInput.value.trim();
|
| 239 |
|
| 240 |
-
if (!message &&
|
| 241 |
|
| 242 |
const sendButton = document.getElementById('sendButton');
|
| 243 |
const typingIndicator = document.getElementById('typingIndicator');
|
|
@@ -245,8 +256,8 @@
|
|
| 245 |
messageInput.disabled = true;
|
| 246 |
sendButton.disabled = true;
|
| 247 |
|
| 248 |
-
if (message ||
|
| 249 |
-
addMessage('user', message,
|
| 250 |
}
|
| 251 |
|
| 252 |
typingIndicator.classList.remove('hidden');
|
|
@@ -256,15 +267,15 @@
|
|
| 256 |
autoResize(messageInput);
|
| 257 |
|
| 258 |
const thinkingEnabled = document.getElementById('thinkingToggle').checked;
|
| 259 |
-
const endpoint =
|
| 260 |
const payload = {
|
| 261 |
-
message: message || 'Analyse
|
| 262 |
thinking_enabled: thinkingEnabled,
|
| 263 |
conversation_id: conversationId
|
| 264 |
};
|
| 265 |
|
| 266 |
-
if (
|
| 267 |
-
payload.file_data =
|
| 268 |
}
|
| 269 |
|
| 270 |
fetch(endpoint, {
|
|
@@ -358,9 +369,14 @@
|
|
| 358 |
|
| 359 |
let messageContent = '';
|
| 360 |
|
| 361 |
-
if (fileData
|
| 362 |
-
const
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
}
|
| 365 |
|
| 366 |
if (content) {
|
|
|
|
| 130 |
<div class="p-4 md:p-6 bg-dark-card border-t border-dark-border flex-shrink-0">
|
| 131 |
<div class="mb-3 p-3 bg-dark-elevated rounded-lg text-sm hidden" id="filePreview"></div>
|
| 132 |
<div class="flex gap-3 items-end">
|
| 133 |
+
<input type="file" id="fileInput" class="hidden" accept="image/*,video/*,.pdf,.txt,.csv,.json" multiple onchange="handleFileSelect(event)">
|
| 134 |
<button class="bg-gray-700 text-white h-12 w-12 rounded-full hover:bg-gray-600 transition-colors flex-shrink-0 flex items-center justify-center text-xl" onclick="document.getElementById('fileInput').click()" title="Joindre un fichier">
|
| 135 |
📎
|
| 136 |
</button>
|
|
|
|
| 152 |
</div>
|
| 153 |
|
| 154 |
<script>
|
| 155 |
+
let currentFiles = [];
|
| 156 |
let conversationId = 'session_' + Date.now();
|
| 157 |
|
| 158 |
// Configuration de marked pour le markdown
|
|
|
|
| 174 |
}
|
| 175 |
|
| 176 |
function handleFileSelect(event) {
|
| 177 |
+
const files = Array.from(event.target.files);
|
| 178 |
+
if (files.length === 0) return;
|
| 179 |
|
| 180 |
const filePreview = document.getElementById('filePreview');
|
| 181 |
+
currentFiles = [];
|
| 182 |
+
let previews = [];
|
| 183 |
+
|
| 184 |
+
files.forEach((file, index) => {
|
| 185 |
+
const formData = new FormData();
|
| 186 |
+
formData.append('file', file);
|
| 187 |
+
|
| 188 |
+
// Show immediate preview for images
|
| 189 |
+
if (file.type.startsWith('image/')) {
|
| 190 |
+
const reader = new FileReader();
|
| 191 |
+
reader.onload = function(e) {
|
| 192 |
+
previews[index] = `
|
| 193 |
+
<div class="flex items-center gap-3">
|
| 194 |
+
<img src="${e.target.result}" class="w-16 h-16 object-cover rounded-lg border border-gray-600">
|
| 195 |
<div class="text-gray-500 text-xs">${(file.size / 1024).toFixed(1)} KB</div>
|
| 196 |
</div>
|
| 197 |
+
`;
|
| 198 |
+
updatePreview();
|
| 199 |
+
};
|
| 200 |
+
reader.readAsDataURL(file);
|
| 201 |
+
} else {
|
| 202 |
+
previews[index] = `
|
| 203 |
+
<div class="flex items-center gap-3">
|
| 204 |
+
<span class="text-gray-300">📎</span>
|
| 205 |
+
<div class="text-gray-500 text-xs">${(file.size / 1024).toFixed(1)} KB</div>
|
| 206 |
+
</div>
|
| 207 |
+
`;
|
| 208 |
+
updatePreview();
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
fetch('/upload', {
|
| 212 |
+
method: 'POST',
|
| 213 |
+
body: formData
|
| 214 |
+
})
|
| 215 |
+
.then(response => response.json())
|
| 216 |
+
.then(data => {
|
| 217 |
+
if (data.success) {
|
| 218 |
+
currentFiles.push(data);
|
| 219 |
+
} else {
|
| 220 |
+
showError('Erreur lors du téléchargement: ' + data.error);
|
| 221 |
+
}
|
| 222 |
+
})
|
| 223 |
+
.catch(error => {
|
| 224 |
+
showError('Erreur lors du téléchargement: ' + error.message);
|
| 225 |
+
});
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
function updatePreview() {
|
| 229 |
+
if (previews.filter(p => p).length === files.length) {
|
| 230 |
+
filePreview.innerHTML = `
|
| 231 |
+
<div class="flex flex-wrap gap-3 items-center">
|
| 232 |
+
${previews.join('')}
|
| 233 |
<button onclick="removeFile()" class="bg-gray-600 hover:bg-gray-500 text-white px-3 py-1 rounded transition-colors">✕</button>
|
| 234 |
</div>
|
| 235 |
`;
|
| 236 |
filePreview.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
}
|
| 238 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
}
|
| 240 |
|
| 241 |
function removeFile() {
|
| 242 |
+
currentFiles = [];
|
| 243 |
document.getElementById('filePreview').classList.add('hidden');
|
| 244 |
document.getElementById('fileInput').value = '';
|
| 245 |
}
|
|
|
|
| 248 |
const messageInput = document.getElementById('messageInput');
|
| 249 |
const message = messageInput.value.trim();
|
| 250 |
|
| 251 |
+
if (!message && currentFiles.length === 0) return;
|
| 252 |
|
| 253 |
const sendButton = document.getElementById('sendButton');
|
| 254 |
const typingIndicator = document.getElementById('typingIndicator');
|
|
|
|
| 256 |
messageInput.disabled = true;
|
| 257 |
sendButton.disabled = true;
|
| 258 |
|
| 259 |
+
if (message || currentFiles.length > 0) {
|
| 260 |
+
addMessage('user', message, currentFiles);
|
| 261 |
}
|
| 262 |
|
| 263 |
typingIndicator.classList.remove('hidden');
|
|
|
|
| 267 |
autoResize(messageInput);
|
| 268 |
|
| 269 |
const thinkingEnabled = document.getElementById('thinkingToggle').checked;
|
| 270 |
+
const endpoint = currentFiles.length > 0 ? '/chat_with_file' : '/chat';
|
| 271 |
const payload = {
|
| 272 |
+
message: message || 'Analyse ces fichiers',
|
| 273 |
thinking_enabled: thinkingEnabled,
|
| 274 |
conversation_id: conversationId
|
| 275 |
};
|
| 276 |
|
| 277 |
+
if (currentFiles.length > 0) {
|
| 278 |
+
payload.file_data = currentFiles;
|
| 279 |
}
|
| 280 |
|
| 281 |
fetch(endpoint, {
|
|
|
|
| 369 |
|
| 370 |
let messageContent = '';
|
| 371 |
|
| 372 |
+
if (fileData) {
|
| 373 |
+
const files = Array.isArray(fileData) ? fileData : [fileData];
|
| 374 |
+
files.forEach(file => {
|
| 375 |
+
if (file.mime_type && file.mime_type.startsWith('image/')) {
|
| 376 |
+
const imageUrl = `data:${file.mime_type};base64,${file.data}`;
|
| 377 |
+
messageContent += `<img src="${imageUrl}" class="max-w-full h-auto rounded-lg mb-2 border border-gray-600">`;
|
| 378 |
+
}
|
| 379 |
+
});
|
| 380 |
}
|
| 381 |
|
| 382 |
if (content) {
|