From 504d3d9de1fbf180e94791294771ecda99c8d0f3 Mon Sep 17 00:00:00 2001 From: oobabooga <112222186+oobabooga@users.noreply.github.com> Date: Tue, 27 May 2025 18:42:07 -0700 Subject: [PATCH] Simplify the implementation --- css/main.css | 90 +++----------- js/global_scope_js.js | 40 +++++- js/main.js | 214 +++++++++----------------------- modules/chat.py | 73 ++++++++--- modules/html_generator.py | 46 ++++--- modules/message_versioning.py | 225 ---------------------------------- modules/ui.py | 2 + modules/ui_chat.py | 29 ++--- 8 files changed, 214 insertions(+), 505 deletions(-) delete mode 100644 modules/message_versioning.py diff --git a/css/main.css b/css/main.css index 888d50c0..46c0fec4 100644 --- a/css/main.css +++ b/css/main.css @@ -1260,7 +1260,7 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* { position: absolute; bottom: -23px; left: 0; - display: flex; + display: flex; gap: 5px; opacity: 0; transition: opacity 0.2s; @@ -1458,95 +1458,41 @@ strong { color: #ccc; } - -/* --- Message Versioning Styles --- */ - -.message-versioning-container { +/* --- Simple Version Navigation --- */ +.version-navigation { position: absolute; bottom: -23px; right: 0; display: flex; align-items: center; + gap: 5px; opacity: 0; transition: opacity 0.2s; - pointer-events: none; } -.message:hover .message-versioning-container, -.user-message:hover .message-versioning-container, -.assistant-message:hover .message-versioning-container { +.message:hover .version-navigation, +.user-message:hover .version-navigation, +.assistant-message:hover .version-navigation { opacity: 1; - pointer-events: auto; } -.selected-message .message-versioning-container { - opacity: 1; - pointer-events: auto; +.version-nav-button { + padding: 2px 6px; + font-size: 12px; + min-width: auto; } -.message-versioning-nav-container { - display: flex; - align-items: center; - gap: 5px; -} - -.message-versioning-nav-arrow { - position: static; - margin: 0; - padding: 2px; - width: 20px; - height: 20px; - line-height: 0; - transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; -} - -.message .message-versioning-nav-arrow:not([activated]), -.user-message .message-versioning-nav-arrow:not([activated]), -.assistant-message .message-versioning-nav-arrow:not([activated]) { +.version-nav-button[disabled] { + opacity: 0.3; cursor: not-allowed; - opacity: 0.3; - transform: scale(0.95); } -.message .message-versioning-nav-arrow:not([activated]):hover, -.user-message .message-versioning-nav-arrow:not([activated]):hover, -.assistant-message .message-versioning-nav-arrow:not([activated]):hover { - opacity: 0.3; -} - -.message-versioning-nav-pos { +.version-position { font-size: 11px; + color: currentColor; font-family: monospace; - color: rgb(156 163 175); /* Match SVG stroke color */ - user-select: none; - min-width: 25px; + min-width: 35px; text-align: center; - line-height: 20px; - padding: 0 2px; + opacity: 0.8; + user-select: none; } - -.dark .message-versioning-nav-pos { - color: rgb(156 163 175); /* Same color for dark mode */ -} - - /* If you want the other buttons to be visible when a message is selected, uncomment the lines below */ -.selected-message:not(:hover) .message-versioning-nav-arrow[activated]/*, -.selected-message:not(:hover) .footer-button*/ { - opacity: 0.4; -} - -.selected-message:not(:hover) .message-versioning-nav-arrow:not([activated]) { - opacity: 0.1; -} - -.selected-message:hover .message-versioning-nav-arrow[activated]/*, -.selected-message:hover .footer-button*/ { - opacity: 1; -} - -.selected-message:hover .message-versioning-nav-arrow:not([activated]) { - opacity: 0.3; -} - -.message-versioning-container[hidden] { - display: none; diff --git a/js/global_scope_js.js b/js/global_scope_js.js index b81736d3..9174622e 100644 --- a/js/global_scope_js.js +++ b/js/global_scope_js.js @@ -49,6 +49,44 @@ function branchHere(element) { } +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; + + const indexInput = document.getElementById("Navigate-message-index").querySelector("input"); + if (!indexInput) { + console.error("Element with ID 'Navigate-message-index' not found."); + return; + } + + const directionInput = document.getElementById("Navigate-direction").querySelector("textarea"); + if (!directionInput) { + console.error("Element with ID 'Navigate-direction' not found."); + return; + } + + const navigateButton = document.getElementById("Navigate-version"); + if (!navigateButton) { + console.error("Required element 'Navigate-version' not found."); + return; + } + + indexInput.value = index; + directionInput.value = direction; + + // Trigger any 'change' or 'input' events Gradio might be listening for + const event = new Event("input", { bubbles: true }); + indexInput.dispatchEvent(event); + directionInput.dispatchEvent(event); + + navigateButton.click(); +} + function regenerateClick() { document.getElementById("Regenerate").click(); } @@ -61,8 +99,6 @@ function removeLastClick() { document.getElementById("Remove-last").click(); } -// === Version navigation handled in main.js === // - function handleMorphdomUpdate(text) { // Track open blocks const openBlocks = new Set(); diff --git a/js/main.js b/js/main.js index 9f616fef..d90e8ade 100644 --- a/js/main.js +++ b/js/main.js @@ -39,23 +39,24 @@ document.querySelector(".header_bar").addEventListener("click", function(event) //------------------------------------------------ // Keyboard shortcuts //------------------------------------------------ + +// --- Helper functions --- // +function isModifiedKeyboardEvent() { + return (event instanceof KeyboardEvent && + event.shiftKey || + event.ctrlKey || + event.altKey || + event.metaKey); +} + +function isFocusedOnEditableTextbox() { + if (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA") { + return !!event.target.value; + } +} + let previousTabId = "chat-tab-button"; document.addEventListener("keydown", function(event) { - // --- Helper functions --- // - function isModifiedKeyboardEvent() { - return (event instanceof KeyboardEvent && - event.shiftKey || - event.ctrlKey || - event.altKey || - event.metaKey); - } - - function isFocusedOnEditableTextbox() { - if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { - return !!event.target.value; - } - } - // Stop generation on Esc pressed if (event.key === "Escape") { // Find the element with id 'stop' and click it @@ -119,51 +120,20 @@ document.addEventListener("keydown", function(event) { document.getElementById("Impersonate").click(); } - // --- Non-textbox controls --- // + // --- Simple version navigation --- // if (!isFocusedOnEditableTextbox()) { - // Version navigation on Ctrl + Arrow (horizontal) - if (!isModifiedKeyboardEvent() && event.key === 'ArrowLeft') { + // Version navigation on Arrow keys (horizontal) + if (!isModifiedKeyboardEvent() && event.key === "ArrowLeft") { event.preventDefault(); - triggerVersionNavigateBackend(selectedMessageHistoryIndex, selectedMessageType, 'left'); + navigateLastAssistantMessage("left"); } - else if (!isModifiedKeyboardEvent() && event.key === 'ArrowRight') { + else if (!isModifiedKeyboardEvent() && event.key === "ArrowRight") { event.preventDefault(); - let regenerateConditionMet = false; - - const chatContainer = gradioApp().querySelector('#chat'); - const selectedMessageElement = chat.querySelector('.selected-message'); - - if (selectedMessageElement) { - const rightNavArrow = selectedMessageElement.querySelector('.message-versioning-nav-right'); - const isRightArrowDisabled = !rightNavArrow?.hasAttribute('activated'); - - // Selected message is the last element in #chat - if (chatContainer) { - const allMessages = Array.from(chatContainer.querySelectorAll('.message, .user-message, .assistant-message')); - const isLastMessage = allMessages.length > 0 && selectedMessageElement === allMessages[allMessages.length - 1]; - - if (isRightArrowDisabled && isLastMessage) { - document.getElementById("Regenerate").click(); - regenerateConditionMet = true; - } - } + if (!navigateLastAssistantMessage("right")) { + // If can't navigate right (last version), regenerate + document.getElementById("Regenerate").click(); } - - if (!regenerateConditionMet) { - triggerVersionNavigateBackend(selectedMessageHistoryIndex, selectedMessageType, 'right'); - } - } - - // Select relative message on Ctrl + Arrow (vertical) - else if (!isModifiedKeyboardEvent() && event.key === 'ArrowUp') { - event.preventDefault(); - selectRelativeMessage(-1) - } - - else if (!isModifiedKeyboardEvent() && event.key === 'ArrowDown') { - event.preventDefault(); - selectRelativeMessage(1) } } @@ -857,118 +827,54 @@ function createMobileTopBar() { createMobileTopBar(); - //------------------------------------------------ -// Message Versioning Integration +// Simple Navigation Functions //------------------------------------------------ -// --- Message Versioning Variables --- -let selectedMessageHistoryIndex = null; -let selectedMessageType = null; +function navigateLastAssistantMessage(direction) { + const chat = document.querySelector("#chat"); + if (!chat) return false; -// --- Message Versioning Helper Functions --- + const messages = chat.querySelectorAll("[data-index]"); + if (messages.length === 0) return false; -// Helper function to get Gradio app root (if needed, otherwise use document) -function gradioApp() { - const elems = document.querySelectorAll('gradio-app'); - const gradioShadowRoot = elems.length > 0 ? elems[0].shadowRoot : null; - return gradioShadowRoot || document; -} - -// Helper to update Gradio text/number inputs (if needed for backend communication) -function updateGradioInput(element, value) { - if (element) { - element.value = value; - element.dispatchEvent(new Event('input', { bubbles: true })); - } else { - console.warn("Attempted to update a null Gradio input element."); + // Find the last assistant message (starting from the end) + let lastAssistantMessage = null; + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + if ( + msg.classList.contains("assistant-message") || + msg.querySelector(".circle-bot") || + msg.querySelector(".text-bot") + ) { + lastAssistantMessage = msg; + break; } -} - -// --- Message Versioning Core Functions --- - -function triggerVersionNavigateBackend(historyIndex, messageType, direction) { - const gradio = gradioApp(); - const historyIndexInput = gradio.querySelector('#message-versioning-history-index-hidden input[type="number"]'); - const messageTypeInput = gradio.querySelector('#message-versioning-message-type-hidden input[type="number"]'); - const directionInput = gradio.querySelector('#message-versioning-direction-hidden textarea'); - const navigateButton = gradio.querySelector('#message-versioning-navigate-hidden'); - - if (historyIndexInput && messageTypeInput && directionInput && navigateButton) { - console.debug("Found hidden Gradio elements for navigation.", navigateButton); - updateGradioInput(historyIndexInput, historyIndex); - updateGradioInput(messageTypeInput, messageType); - updateGradioInput(directionInput, direction); - navigateButton.click(); - } else { - console.error("Message Versioning: Could not find hidden Gradio elements for navigation. Backend communication needs setup."); - } -} - -// Function called by the nav arrow's onclick attribute -function versioningNavigateClick(arrowButton, historyIndex, messageType, direction) { - // Keep the message selected - const messageElement = arrowButton.closest('.message, .user-message, .assistant-message'); - if (messageElement) { - selectMessage(messageElement, historyIndex, messageType); } - triggerVersionNavigateBackend(historyIndex, messageType, direction); -} + if (!lastAssistantMessage) return false; + const buttons = lastAssistantMessage.querySelectorAll(".version-nav-button"); -function selectMessage(element, historyIndex, messageType) { - // Remove previous selection - deselectMessages(); + for (let i = 0; i < buttons.length; i++) { + const button = buttons[i]; + const onclick = button.getAttribute("onclick"); + const disabled = button.hasAttribute("disabled"); - if (element) { - selectedMessageHistoryIndex = historyIndex; - selectedMessageType = messageType; - element.classList.add('selected-message'); - } -} + const isLeft = onclick && onclick.includes("'left'"); + const isRight = onclick && onclick.includes("'right'"); -function deselectMessages() { - const selectedMessageElement = gradioApp().querySelector('#chat .selected-message'); - if (selectedMessageElement) { - selectedMessageElement.classList.remove('selected-message'); - } - selectedMessageHistoryIndex = null; - selectedMessageType = null; -} - -function selectRelativeMessage(offset) { - const chat = gradioApp().querySelector('#chat'); - if (!chat) return; - const messages = Array.from(chat.querySelectorAll('.message, .user-message, .assistant-message')); - if (messages.length === 0) return; - const selectedMessageChatIndex = messages.findIndex(msg => msg.classList.contains('selected-message')); // Could be saved in a variable rather than run each time - const index = selectedMessageChatIndex + offset; - if (index >= 0 && index < messages.length) messages[index]?.click(); -} - -// --- Message Versioning Global Listeners --- - -// Global click listener for buttons and (de)selecting messages -document.addEventListener('click', function(e) { - const target = e.target; - - const msg = target.closest('.message, .user-message, .assistant-message') - if (msg) { - const historyIndex = msg.getAttribute('data-index'); - const msgType = (msg.classList.contains('assistant-message') || msg.querySelector('.circle-bot, .text-bot')) ? 1 : 0; - if (target.closest('button')) { - const button = target.closest('.message-versioning-nav-arrow'); - if (button && button.hasAttribute('activated')) { - const direction = button.closest('.message-versioning-nav-left') ? 'left' : 'right'; - versioningNavigateClick(button, parseFloat(historyIndex), parseFloat(msgType), direction); + if (!disabled) { + if (direction === "left" && isLeft) { + navigateVersion(button, direction); + return true; + } + if (direction === "right" && isRight) { + navigateVersion(button, direction); + return true; } - } else if (msg.classList.contains('selected-message') && !e.ctrlKey) { - deselectMessages(); - } else { - selectMessage(msg, parseInt(historyIndex), parseInt(msgType)); } - } else if (target.closest('#chat') && !target.closest('#message-versioning-navigate-hidden')) { // Deselect if the click is in-chat, outside a message - deselectMessages(); } -}); + + return false; +} diff --git a/modules/chat.py b/modules/chat.py index 3f3c6f41..6eed47ee 100644 --- a/modules/chat.py +++ b/modules/chat.py @@ -414,22 +414,26 @@ def add_message_version(history, row_idx, is_current=True): if "versions" not in history['metadata'][key]: history['metadata'][key]["versions"] = [] - index = None - for index, version in enumerate(history['metadata'][key]["versions"]): - if version['content'] == history['internal'][row_idx][1] and version['visible_content'] == history['visible'][row_idx][1]: - break + # Check if this version already exists + current_content = history['internal'][row_idx][1] + current_visible = history['visible'][row_idx][1] - if index is None: - # Add current message as a version - history['metadata'][key]["versions"].append({ - "content": history['internal'][row_idx][1], - "visible_content": history['visible'][row_idx][1], - "timestamp": get_current_timestamp() - }) + for i, version in enumerate(history['metadata'][key]["versions"]): + if version['content'] == current_content and version['visible_content'] == current_visible: + if is_current: + history['metadata'][key]["current_version_index"] = i + return + + # Add current message as a version + history['metadata'][key]["versions"].append({ + "content": current_content, + "visible_content": current_visible, + "timestamp": get_current_timestamp() + }) # Update index if this is the current version if is_current: - history['metadata'][key]["current_version_index"] = index if index is not None else len(history['metadata'][key]["versions"]) - 1 + history['metadata'][key]["current_version_index"] = len(history['metadata'][key]["versions"]) - 1 def add_message_attachment(history, row_idx, file_path, is_user=True): @@ -687,8 +691,6 @@ def generate_chat_reply_wrapper(text, state, regenerate=False, _continue=False): save_history(history, state['unique_id'], state['character_menu'], state['mode']) - yield chat_html_wrapper(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu']), history - def remove_last_message(history): if 'metadata' not in history: @@ -729,12 +731,9 @@ def replace_last_reply(textbox, state): return history elif len(history['visible']) > 0: row_idx = len(history['internal']) - 1 - if not history['metadata'].get(f"assistant_{row_idx}", {}).get('versions'): - add_message_version(history, row_idx, is_current=False) history['visible'][-1][1] = html.escape(text) history['internal'][-1][1] = apply_extensions('input', text, state, is_chat=True) update_message_metadata(history['metadata'], "assistant", row_idx, timestamp=get_current_timestamp()) - add_message_version(history, row_idx, is_current=True) return history @@ -1426,6 +1425,46 @@ def handle_branch_chat_click(state): return [history, html, past_chats_update, -1] +def handle_navigate_version_click(state): + history = state['history'] + message_index = int(state['navigate_message_index']) + direction = state['navigate_direction'] + + # Get assistant message metadata + key = f"assistant_{message_index}" + if key not in history['metadata'] or 'versions' not in history['metadata'][key]: + # No versions to navigate + html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu']) + return [history, html] + + metadata = history['metadata'][key] + current_idx = metadata.get('current_version_index', 0) + versions = metadata['versions'] + + # Calculate new index + if direction == 'left': + new_idx = max(0, current_idx - 1) + else: # right + new_idx = min(len(versions) - 1, current_idx + 1) + + if new_idx == current_idx: + # No change needed + html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu']) + return [history, html] + + # Update history with new version + version = versions[new_idx] + history['internal'][message_index][1] = version['content'] + history['visible'][message_index][1] = version['visible_content'] + metadata['current_version_index'] = new_idx + + # Redraw and save + html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu']) + save_history(history, state['unique_id'], state['character_menu'], state['mode']) + + return [history, html] + + def handle_rename_chat_click(): return [ gr.update(value="My New Chat"), diff --git a/modules/html_generator.py b/modules/html_generator.py index 665cb4de..1dfeb445 100644 --- a/modules/html_generator.py +++ b/modules/html_generator.py @@ -9,7 +9,7 @@ from pathlib import Path import markdown from PIL import Image, ImageOps -from modules import shared, message_versioning +from modules import shared from modules.sane_markdown_lists import SaneListExtension from modules.utils import get_available_chat_styles @@ -380,6 +380,30 @@ def format_message_attachments(history, role, index): return "" +def get_version_navigation_html(history, i): + """Generate simple navigation arrows for message versions""" + key = f"assistant_{i}" + metadata = history.get('metadata', {}) + + if key not in metadata or 'versions' not in metadata[key]: + return "" + + versions = metadata[key]['versions'] + current_idx = metadata[key].get('current_version_index', 0) + + if len(versions) <= 1: + return "" + + left_disabled = ' disabled' if current_idx == 0 else '' + right_disabled = ' disabled' if current_idx >= len(versions) - 1 else '' + + left_arrow = f'' + right_arrow = f'' + position = f'{current_idx + 1}/{len(versions)}' + + return f'
' + + def actions_html(history, i, info_message=""): return (f' ' - f'{message_versioning.get_message_version_nav_elements(history, i, 1)}') + f'{get_version_navigation_html(history, i)}') def generate_instruct_html(history): @@ -422,9 +446,8 @@ def generate_instruct_html(history): info_message_assistant = info_button.replace("message", assistant_timestamp_value) if converted_visible[0]: # Don't display empty user messages - selected_class = " selected-message" if message_versioning.is_message_selected(i, 0) else "" output += ( - f'