DJAKOUA KWANKAM BRAYAN STEVE commited on
Commit
3da5847
·
1 Parent(s): 09e1ef6

feat: add message actions toolbar

Browse files
cypher-chat-app/public/app.js CHANGED
@@ -262,18 +262,8 @@ function loadConversation(id) {
262
  attachedFiles = [];
263
  localStorage.setItem("cypher_current_id", currentConversationId);
264
 
265
- messagesContainer.innerHTML = "";
266
- if (currentMessages.length === 0) {
267
- messagesContainer.appendChild(welcomeView);
268
- welcomeView.style.display = "flex";
269
- } else {
270
- welcomeView.style.display = "none";
271
- currentMessages.forEach(msg => {
272
- renderMessage(msg.role, msg.content);
273
- });
274
- }
275
  renderAttachmentChips();
276
- scrollToBottom();
277
  renderConversationsList();
278
  }
279
 
@@ -302,22 +292,178 @@ function renderMessage(role, content) {
302
  bubble.classList.add("message-bubble");
303
 
304
  // Check if message content has file markers to display them nicely
305
- // Format: [Fichier attaché : name]\n```\ncontent\n```
306
  let displayContent = content;
307
- const fileHeaderRegex = /\[Fichier attaché : ([^\]]+)\]\n```[\s\S]*?```\n\n/g;
308
-
309
- // We clean the display text slightly if we want, or just let marked parse it.
310
- // Standard markdown code blocks parsed by marked look excellent, so we just let it run.
311
  bubble.innerHTML = parseMarkdown(displayContent);
312
 
313
  wrapper.appendChild(label);
314
  wrapper.appendChild(bubble);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  messagesContainer.appendChild(wrapper);
316
 
 
317
  addCopyCodeButtons(bubble);
318
  return bubble;
319
  }
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  // Parse markdown securely
322
  function parseMarkdown(text) {
323
  try {
@@ -399,11 +545,16 @@ async function sendMessage() {
399
  sendBtn.disabled = true;
400
  scrollToBottom();
401
 
402
- // 2. Add empty bot bubble for streaming response
 
 
 
 
 
403
  const botBubble = renderMessage("assistant", "...");
404
  let botResponseText = "";
405
 
406
- // 3. Search logs UI variables
407
  let searchLogsBox = null;
408
  let searchLogsList = null;
409
  let searchSourcesList = null;
 
262
  attachedFiles = [];
263
  localStorage.setItem("cypher_current_id", currentConversationId);
264
 
265
+ redrawCurrentConversation();
 
 
 
 
 
 
 
 
 
266
  renderAttachmentChips();
 
267
  renderConversationsList();
268
  }
269
 
 
292
  bubble.classList.add("message-bubble");
293
 
294
  // Check if message content has file markers to display them nicely
 
295
  let displayContent = content;
 
 
 
 
296
  bubble.innerHTML = parseMarkdown(displayContent);
297
 
298
  wrapper.appendChild(label);
299
  wrapper.appendChild(bubble);
300
+
301
+ // Create Toolbar Actions Row
302
+ const actionsRow = document.createElement("div");
303
+ actionsRow.className = "message-actions";
304
+
305
+ if (role === "assistant") {
306
+ actionsRow.innerHTML = `
307
+ <button class="action-btn copy-btn" title="Copier la réponse">
308
+ <i data-lucide="copy" size="14"></i>
309
+ </button>
310
+ <button class="action-btn like-btn" title="Aimer la réponse">
311
+ <i data-lucide="thumbs-up" size="14"></i>
312
+ </button>
313
+ <button class="action-btn dislike-btn" title="Détester la réponse">
314
+ <i data-lucide="thumbs-down" size="14"></i>
315
+ </button>
316
+ <button class="action-btn share-btn" title="Partager la discussion">
317
+ <i data-lucide="share-2" size="14"></i>
318
+ </button>
319
+ `;
320
+
321
+ // Copy action
322
+ const copyBtn = actionsRow.querySelector(".copy-btn");
323
+ copyBtn.addEventListener("click", () => {
324
+ navigator.clipboard.writeText(content).then(() => {
325
+ const icon = copyBtn.querySelector("i");
326
+ icon.setAttribute("data-lucide", "check");
327
+ lucide.createIcons({ scope: copyBtn });
328
+ setTimeout(() => {
329
+ icon.setAttribute("data-lucide", "copy");
330
+ lucide.createIcons({ scope: copyBtn });
331
+ }, 2000);
332
+ });
333
+ });
334
+
335
+ // Like action
336
+ const likeBtn = actionsRow.querySelector(".like-btn");
337
+ const dislikeBtn = actionsRow.querySelector(".dislike-btn");
338
+ likeBtn.addEventListener("click", () => {
339
+ likeBtn.classList.toggle("active");
340
+ dislikeBtn.classList.remove("active");
341
+ });
342
+
343
+ // Dislike action
344
+ dislikeBtn.addEventListener("click", () => {
345
+ dislikeBtn.classList.toggle("active");
346
+ likeBtn.classList.remove("active");
347
+ });
348
+
349
+ // Share action
350
+ const shareBtn = actionsRow.querySelector(".share-btn");
351
+ shareBtn.addEventListener("click", () => {
352
+ let shareText = `--- Discussion Cypher AI ---\n\n`;
353
+ currentMessages.forEach(m => {
354
+ shareText += `${m.role === 'user' ? 'Utilisateur' : 'Cypher AI'}:\n${m.content}\n\n`;
355
+ });
356
+ navigator.clipboard.writeText(shareText).then(() => {
357
+ alert("Discussion copiée dans le presse-papiers pour partage !");
358
+ });
359
+ });
360
+
361
+ } else { // user
362
+ actionsRow.innerHTML = `
363
+ <button class="action-btn copy-btn" title="Copier le message">
364
+ <i data-lucide="copy" size="14"></i>
365
+ </button>
366
+ <button class="action-btn edit-btn" title="Modifier le message">
367
+ <i data-lucide="edit-3" size="14"></i>
368
+ </button>
369
+ <button class="action-btn retry-btn" title="Réessayer l'envoi">
370
+ <i data-lucide="refresh-cw" size="14"></i>
371
+ </button>
372
+ `;
373
+
374
+ // Copy action
375
+ const copyBtn = actionsRow.querySelector(".copy-btn");
376
+ copyBtn.addEventListener("click", () => {
377
+ navigator.clipboard.writeText(content).then(() => {
378
+ const icon = copyBtn.querySelector("i");
379
+ icon.setAttribute("data-lucide", "check");
380
+ lucide.createIcons({ scope: copyBtn });
381
+ setTimeout(() => {
382
+ icon.setAttribute("data-lucide", "copy");
383
+ lucide.createIcons({ scope: copyBtn });
384
+ }, 2000);
385
+ });
386
+ });
387
+
388
+ // Edit Action
389
+ const editBtn = actionsRow.querySelector(".edit-btn");
390
+ editBtn.addEventListener("click", () => {
391
+ if (bubble.querySelector(".edit-textarea")) return;
392
+
393
+ const originalText = content;
394
+ bubble.innerHTML = `
395
+ <textarea class="edit-textarea">${originalText}</textarea>
396
+ <div class="edit-actions">
397
+ <button class="edit-save-btn">Enregistrer & Renvoyer</button>
398
+ <button class="edit-cancel-btn">Annuler</button>
399
+ </div>
400
+ `;
401
+
402
+ const textarea = bubble.querySelector(".edit-textarea");
403
+ textarea.focus();
404
+ textarea.style.height = "auto";
405
+ textarea.style.height = textarea.scrollHeight + "px";
406
+ textarea.addEventListener("input", () => {
407
+ textarea.style.height = "auto";
408
+ textarea.style.height = textarea.scrollHeight + "px";
409
+ });
410
+
411
+ bubble.querySelector(".edit-cancel-btn").addEventListener("click", (e) => {
412
+ e.stopPropagation();
413
+ bubble.innerHTML = parseMarkdown(originalText);
414
+ addCopyCodeButtons(bubble);
415
+ });
416
+
417
+ bubble.querySelector(".edit-save-btn").addEventListener("click", async (e) => {
418
+ e.stopPropagation();
419
+ const newText = textarea.value.trim();
420
+ if (!newText) return;
421
+
422
+ const msgIndex = currentMessages.findIndex(m => m.role === "user" && m.content === originalText);
423
+ if (msgIndex > -1) {
424
+ currentMessages[msgIndex].content = newText;
425
+ currentMessages = currentMessages.slice(0, msgIndex + 1);
426
+ redrawCurrentConversation();
427
+ await streamAIResponse();
428
+ }
429
+ });
430
+ });
431
+
432
+ // Retry Action
433
+ const retryBtn = actionsRow.querySelector(".retry-btn");
434
+ retryBtn.addEventListener("click", async () => {
435
+ const msgIndex = currentMessages.findIndex(m => m.role === "user" && m.content === content);
436
+ if (msgIndex > -1) {
437
+ currentMessages = currentMessages.slice(0, msgIndex + 1);
438
+ redrawCurrentConversation();
439
+ await streamAIResponse();
440
+ }
441
+ });
442
+ }
443
+
444
+ wrapper.appendChild(actionsRow);
445
  messagesContainer.appendChild(wrapper);
446
 
447
+ lucide.createIcons({ scope: actionsRow });
448
  addCopyCodeButtons(bubble);
449
  return bubble;
450
  }
451
 
452
+ // Redraw entire chat history in view
453
+ function redrawCurrentConversation() {
454
+ messagesContainer.innerHTML = "";
455
+ if (currentMessages.length === 0) {
456
+ messagesContainer.appendChild(welcomeView);
457
+ welcomeView.style.display = "flex";
458
+ } else {
459
+ welcomeView.style.display = "none";
460
+ currentMessages.forEach(msg => {
461
+ renderMessage(msg.role, msg.content);
462
+ });
463
+ }
464
+ scrollToBottom();
465
+ }
466
+
467
  // Parse markdown securely
468
  function parseMarkdown(text) {
469
  try {
 
545
  sendBtn.disabled = true;
546
  scrollToBottom();
547
 
548
+ await streamAIResponse();
549
+ }
550
+
551
+ // Stream AI Response with logs and sources
552
+ async function streamAIResponse() {
553
+ // Add empty bot bubble for streaming response
554
  const botBubble = renderMessage("assistant", "...");
555
  let botResponseText = "";
556
 
557
+ // Search logs UI variables
558
  let searchLogsBox = null;
559
  let searchLogsList = null;
560
  let searchSourcesList = null;
cypher-chat-app/public/style.css CHANGED
@@ -1100,3 +1100,113 @@ input:checked + .slider:before {
1100
  to { opacity: 1; transform: translateY(0); }
1101
  }
1102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1100
  to { opacity: 1; transform: translateY(0); }
1101
  }
1102
 
1103
+ /* -----------------------------------------------------
1104
+ MESSAGE TOOLBAR ACTIONS & EDIT MODES
1105
+ ----------------------------------------------------- */
1106
+ .message-actions {
1107
+ display: flex;
1108
+ gap: 8px;
1109
+ margin-top: 6px;
1110
+ opacity: 0.5;
1111
+ transition: opacity 0.2s ease;
1112
+ padding: 0 4px;
1113
+ }
1114
+
1115
+ .message-wrapper:hover .message-actions {
1116
+ opacity: 1;
1117
+ }
1118
+
1119
+ .message-wrapper.user .message-actions {
1120
+ justify-content: flex-end;
1121
+ }
1122
+
1123
+ .message-wrapper.bot .message-actions {
1124
+ justify-content: flex-start;
1125
+ }
1126
+
1127
+ .action-btn {
1128
+ background: none;
1129
+ border: none;
1130
+ color: var(--text-muted);
1131
+ cursor: pointer;
1132
+ padding: 6px;
1133
+ border-radius: 6px;
1134
+ display: flex;
1135
+ align-items: center;
1136
+ justify-content: center;
1137
+ transition: all 0.2s ease;
1138
+ }
1139
+
1140
+ .action-btn:hover {
1141
+ color: var(--accent-cyan);
1142
+ background: rgba(0, 229, 255, 0.08);
1143
+ }
1144
+
1145
+ .action-btn.like-btn.active {
1146
+ color: var(--accent-green);
1147
+ background: rgba(0, 255, 170, 0.08);
1148
+ }
1149
+
1150
+ .action-btn.dislike-btn.active {
1151
+ color: #EF4444;
1152
+ background: rgba(239, 68, 68, 0.08);
1153
+ }
1154
+
1155
+ /* Edit Form inside bubble */
1156
+ .edit-textarea {
1157
+ width: 100%;
1158
+ min-height: 80px;
1159
+ background: rgba(8, 10, 18, 0.5);
1160
+ border: 1px solid var(--border-color);
1161
+ border-radius: 8px;
1162
+ color: var(--text-primary);
1163
+ font-family: inherit;
1164
+ font-size: 14px;
1165
+ padding: 10px;
1166
+ outline: none;
1167
+ resize: none;
1168
+ margin-bottom: 8px;
1169
+ }
1170
+
1171
+ .edit-textarea:focus {
1172
+ border-color: var(--accent-cyan);
1173
+ }
1174
+
1175
+ .edit-actions {
1176
+ display: flex;
1177
+ gap: 8px;
1178
+ justify-content: flex-end;
1179
+ }
1180
+
1181
+ .edit-save-btn {
1182
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-green));
1183
+ border: none;
1184
+ border-radius: 6px;
1185
+ padding: 6px 12px;
1186
+ color: #090A0F;
1187
+ font-size: 12px;
1188
+ font-weight: 700;
1189
+ cursor: pointer;
1190
+ transition: all 0.2s;
1191
+ }
1192
+
1193
+ .edit-save-btn:hover {
1194
+ transform: scale(1.02);
1195
+ }
1196
+
1197
+ .edit-cancel-btn {
1198
+ background: rgba(30, 34, 53, 0.6);
1199
+ border: 1px solid var(--border-color);
1200
+ border-radius: 6px;
1201
+ padding: 6px 12px;
1202
+ color: var(--text-secondary);
1203
+ font-size: 12px;
1204
+ font-weight: 600;
1205
+ cursor: pointer;
1206
+ transition: all 0.2s;
1207
+ }
1208
+
1209
+ .edit-cancel-btn:hover {
1210
+ background: rgba(30, 34, 53, 0.8);
1211
+ }
1212
+