mirror of
https://github.com/oobabooga/text-generation-webui.git
synced 2025-06-07 14:17:09 -04:00
338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
// -------------------------------------------------
|
|
// Event handlers
|
|
// -------------------------------------------------
|
|
|
|
function copyToClipboard(element) {
|
|
if (!element) return;
|
|
|
|
const messageElement = element.closest(".message, .user-message, .assistant-message");
|
|
if (!messageElement) return;
|
|
|
|
const rawText = messageElement.getAttribute("data-raw");
|
|
if (!rawText) return;
|
|
|
|
navigator.clipboard.writeText(rawText).then(function() {
|
|
const originalSvg = element.innerHTML;
|
|
element.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"text-green-500 dark:text-green-400\"><path d=\"M5 12l5 5l10 -10\"></path></svg>";
|
|
setTimeout(() => {
|
|
element.innerHTML = originalSvg;
|
|
}, 1000);
|
|
}).catch(function(err) {
|
|
console.error("Failed to copy text: ", err);
|
|
});
|
|
}
|
|
|
|
function branchHere(element) {
|
|
if (!element) return;
|
|
|
|
const messageElement = element.closest(".message, .user-message, .assistant-message");
|
|
if (!messageElement) return;
|
|
|
|
const index = messageElement.getAttribute("data-index");
|
|
if (!index) return;
|
|
|
|
const branchIndexInput = document.getElementById("Branch-index").querySelector("input");
|
|
if (!branchIndexInput) {
|
|
console.error("Element with ID 'Branch-index' not found.");
|
|
return;
|
|
}
|
|
const branchButton = document.getElementById("Branch");
|
|
|
|
if (!branchButton) {
|
|
console.error("Required element 'Branch' not found.");
|
|
return;
|
|
}
|
|
|
|
branchIndexInput.value = index;
|
|
|
|
// Trigger any 'change' or 'input' events Gradio might be listening for
|
|
const event = new Event("input", { bubbles: true });
|
|
branchIndexInput.dispatchEvent(event);
|
|
|
|
branchButton.click();
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Message Editing Functions
|
|
// -------------------------------------------------
|
|
|
|
function editHere(buttonElement) {
|
|
if (!buttonElement) return;
|
|
|
|
const messageElement = buttonElement.closest(".message, .user-message, .assistant-message");
|
|
if (!messageElement) return;
|
|
|
|
const messageBody = messageElement.querySelector(".message-body");
|
|
if (!messageBody) return;
|
|
|
|
// If already editing, focus the textarea
|
|
const existingTextarea = messageBody.querySelector(".editing-textarea");
|
|
if (existingTextarea) {
|
|
existingTextarea.focus();
|
|
return;
|
|
}
|
|
|
|
// Determine role based on message element - handle different chat modes
|
|
const isUserMessage = messageElement.classList.contains("user-message") ||
|
|
messageElement.querySelector(".text-you") !== null ||
|
|
messageElement.querySelector(".circle-you") !== null;
|
|
|
|
startEditing(messageElement, messageBody, isUserMessage);
|
|
}
|
|
|
|
function startEditing(messageElement, messageBody, isUserMessage) {
|
|
const rawText = messageElement.getAttribute("data-raw") || messageBody.textContent;
|
|
const originalHTML = messageBody.innerHTML;
|
|
|
|
// Create editing interface
|
|
const editingInterface = createEditingInterface(rawText);
|
|
|
|
// Replace message content
|
|
messageBody.innerHTML = "";
|
|
messageBody.appendChild(editingInterface.textarea);
|
|
messageBody.appendChild(editingInterface.controls);
|
|
|
|
editingInterface.textarea.focus();
|
|
editingInterface.textarea.setSelectionRange(rawText.length, rawText.length);
|
|
|
|
// Setup event handlers
|
|
setupEditingHandlers(editingInterface.textarea, messageElement, originalHTML, messageBody, isUserMessage);
|
|
}
|
|
|
|
function createEditingInterface(text) {
|
|
const textarea = document.createElement("textarea");
|
|
textarea.value = text;
|
|
textarea.className = "editing-textarea";
|
|
textarea.rows = Math.max(3, text.split("\n").length);
|
|
|
|
const controls = document.createElement("div");
|
|
controls.className = "edit-controls-container";
|
|
|
|
const saveButton = document.createElement("button");
|
|
saveButton.textContent = "Save";
|
|
saveButton.className = "edit-control-button";
|
|
saveButton.type = "button";
|
|
|
|
const cancelButton = document.createElement("button");
|
|
cancelButton.textContent = "Cancel";
|
|
cancelButton.className = "edit-control-button edit-cancel-button";
|
|
cancelButton.type = "button";
|
|
|
|
controls.appendChild(saveButton);
|
|
controls.appendChild(cancelButton);
|
|
|
|
return { textarea, controls, saveButton, cancelButton };
|
|
}
|
|
|
|
function setupEditingHandlers(textarea, messageElement, originalHTML, messageBody, isUserMessage) {
|
|
const saveButton = messageBody.querySelector(".edit-control-button:not(.edit-cancel-button)");
|
|
const cancelButton = messageBody.querySelector(".edit-cancel-button");
|
|
|
|
const submitEdit = () => {
|
|
const index = messageElement.getAttribute("data-index");
|
|
if (!index || !submitMessageEdit(index, textarea.value, isUserMessage)) {
|
|
cancelEdit();
|
|
}
|
|
};
|
|
|
|
const cancelEdit = () => {
|
|
messageBody.innerHTML = originalHTML;
|
|
};
|
|
|
|
// Event handlers
|
|
saveButton.onclick = submitEdit;
|
|
cancelButton.onclick = cancelEdit;
|
|
|
|
textarea.onkeydown = (e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
submitEdit();
|
|
} else if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
cancelEdit();
|
|
}
|
|
};
|
|
}
|
|
|
|
function submitMessageEdit(index, newText, isUserMessage) {
|
|
const editIndexInput = document.getElementById("Edit-message-index")?.querySelector("input");
|
|
const editTextInput = document.getElementById("Edit-message-text")?.querySelector("textarea");
|
|
const editRoleInput = document.getElementById("Edit-message-role")?.querySelector("textarea");
|
|
const editButton = document.getElementById("Edit-message");
|
|
|
|
if (!editIndexInput || !editTextInput || !editRoleInput || !editButton) {
|
|
console.error("Edit elements not found");
|
|
return false;
|
|
}
|
|
|
|
editIndexInput.value = index;
|
|
editTextInput.value = newText;
|
|
editRoleInput.value = isUserMessage ? "user" : "assistant";
|
|
|
|
editIndexInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
editTextInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
editRoleInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
|
|
editButton.click();
|
|
return true;
|
|
}
|
|
|
|
function navigateVersion(element, direction) {
|
|
if (!element) return;
|
|
|
|
const messageElement = element.closest(".message, .user-message, .assistant-message");
|
|
if (!messageElement) return;
|
|
|
|
const index = messageElement.getAttribute("data-index");
|
|
if (!index) return;
|
|
|
|
// Determine role based on message element classes
|
|
let role = "assistant"; // Default role
|
|
if (messageElement.classList.contains("user-message") ||
|
|
messageElement.querySelector(".text-you") ||
|
|
messageElement.querySelector(".circle-you")) {
|
|
role = "user";
|
|
}
|
|
|
|
const indexInput = document.getElementById("Navigate-message-index")?.querySelector("input");
|
|
const directionInput = document.getElementById("Navigate-direction")?.querySelector("textarea");
|
|
const roleInput = document.getElementById("Navigate-message-role")?.querySelector("textarea");
|
|
const navigateButton = document.getElementById("Navigate-version");
|
|
|
|
if (!indexInput || !directionInput || !roleInput || !navigateButton) {
|
|
console.error("Navigation control elements (index, direction, role, or button) not found.");
|
|
return;
|
|
}
|
|
|
|
indexInput.value = index;
|
|
directionInput.value = direction;
|
|
roleInput.value = role;
|
|
|
|
// Trigger 'input' events for Gradio to pick up changes
|
|
const event = new Event("input", { bubbles: true });
|
|
indexInput.dispatchEvent(event);
|
|
directionInput.dispatchEvent(event);
|
|
roleInput.dispatchEvent(event);
|
|
|
|
navigateButton.click();
|
|
}
|
|
|
|
function regenerateClick() {
|
|
document.getElementById("Regenerate").click();
|
|
}
|
|
|
|
function continueClick() {
|
|
document.getElementById("Continue").click();
|
|
}
|
|
|
|
function removeLastClick() {
|
|
document.getElementById("Remove-last").click();
|
|
}
|
|
|
|
function handleMorphdomUpdate(data) {
|
|
// Determine target element and use it as query scope
|
|
var target_element, target_html;
|
|
if (data.last_message_only) {
|
|
const childNodes = document.getElementsByClassName("messages")[0].childNodes;
|
|
target_element = childNodes[childNodes.length - 1];
|
|
target_html = data.html;
|
|
} else {
|
|
target_element = document.getElementById("chat").parentNode;
|
|
target_html = "<div class=\"prose svelte-1ybaih5\">" + data.html + "</div>";
|
|
}
|
|
|
|
const queryScope = target_element;
|
|
|
|
// Track open blocks
|
|
const openBlocks = new Set();
|
|
queryScope.querySelectorAll(".thinking-block").forEach(block => {
|
|
const blockId = block.getAttribute("data-block-id");
|
|
// If block exists and is open, add to open set
|
|
if (blockId && block.hasAttribute("open")) {
|
|
openBlocks.add(blockId);
|
|
}
|
|
});
|
|
|
|
// Store scroll positions for any open blocks
|
|
const scrollPositions = {};
|
|
queryScope.querySelectorAll(".thinking-block[open]").forEach(block => {
|
|
const content = block.querySelector(".thinking-content");
|
|
const blockId = block.getAttribute("data-block-id");
|
|
if (content && blockId) {
|
|
const isAtBottom = Math.abs((content.scrollHeight - content.scrollTop) - content.clientHeight) < 5;
|
|
scrollPositions[blockId] = {
|
|
position: content.scrollTop,
|
|
isAtBottom: isAtBottom
|
|
};
|
|
}
|
|
});
|
|
|
|
morphdom(
|
|
target_element,
|
|
target_html,
|
|
{
|
|
onBeforeElUpdated: function(fromEl, toEl) {
|
|
// Preserve code highlighting
|
|
if (fromEl.tagName === "PRE" && fromEl.querySelector("code[data-highlighted]")) {
|
|
const fromCode = fromEl.querySelector("code");
|
|
const toCode = toEl.querySelector("code");
|
|
|
|
if (fromCode && toCode && fromCode.textContent === toCode.textContent) {
|
|
toEl.className = fromEl.className;
|
|
toEl.innerHTML = fromEl.innerHTML;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// For thinking blocks, assume closed by default
|
|
if (fromEl.classList && fromEl.classList.contains("thinking-block") &&
|
|
toEl.classList && toEl.classList.contains("thinking-block")) {
|
|
const blockId = toEl.getAttribute("data-block-id");
|
|
// Remove open attribute by default
|
|
toEl.removeAttribute("open");
|
|
// If this block was explicitly opened by user, keep it open
|
|
if (blockId && openBlocks.has(blockId)) {
|
|
toEl.setAttribute("open", "");
|
|
}
|
|
}
|
|
|
|
return !fromEl.isEqualNode(toEl);
|
|
},
|
|
|
|
onElUpdated: function(el) {
|
|
// Restore scroll positions for open thinking blocks
|
|
if (el.classList && el.classList.contains("thinking-block") && el.hasAttribute("open")) {
|
|
const blockId = el.getAttribute("data-block-id");
|
|
const content = el.querySelector(".thinking-content");
|
|
|
|
if (content && blockId && scrollPositions[blockId]) {
|
|
setTimeout(() => {
|
|
if (scrollPositions[blockId].isAtBottom) {
|
|
content.scrollTop = content.scrollHeight;
|
|
} else {
|
|
content.scrollTop = scrollPositions[blockId].position;
|
|
}
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Add toggle listeners for new blocks
|
|
queryScope.querySelectorAll(".thinking-block").forEach(block => {
|
|
if (!block._hasToggleListener) {
|
|
block.addEventListener("toggle", function(e) {
|
|
if (this.open) {
|
|
const content = this.querySelector(".thinking-content");
|
|
if (content) {
|
|
setTimeout(() => {
|
|
content.scrollTop = content.scrollHeight;
|
|
}, 0);
|
|
}
|
|
}
|
|
});
|
|
block._hasToggleListener = true;
|
|
}
|
|
});
|
|
}
|