Spaces:
Runtime error
Runtime error
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 +169 -18
- cypher-chat-app/public/style.css +110 -0
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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
const botBubble = renderMessage("assistant", "...");
|
| 404 |
let botResponseText = "";
|
| 405 |
|
| 406 |
-
//
|
| 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 |
+
|