UI: Add footer buttons for editing messages (#7019)

---------

Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
This commit is contained in:
Underscore 2025-05-27 23:55:27 -04:00 committed by GitHub
parent 355b5f6c8b
commit 5028480eba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 282 additions and 74 deletions

View file

@ -712,32 +712,6 @@ def remove_last_message(history):
return html.unescape(last[0]), history
def send_last_reply_to_input(history):
if len(history['visible']) > 0:
return html.unescape(history['visible'][-1][1])
else:
return ''
def replace_last_reply(textbox, state):
history = state['history']
text = textbox['text']
# Initialize metadata if not present
if 'metadata' not in history:
history['metadata'] = {}
if len(text.strip()) == 0:
return history
elif len(history['visible']) > 0:
row_idx = len(history['internal']) - 1
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())
return history
def send_dummy_message(textbox, state):
history = state['history']
text = textbox['text']
@ -1330,14 +1304,6 @@ def my_yaml_output(data):
return result
def handle_replace_last_reply_click(text, state):
history = replace_last_reply(text, state)
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
html = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
return [history, html, {"text": "", "files": []}]
def handle_send_dummy_message_click(text, state):
history = send_dummy_message(text, state)
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
@ -1425,6 +1391,52 @@ def handle_branch_chat_click(state):
return [history, html, past_chats_update, -1]
def handle_edit_message_click(state):
history = state['history']
message_index = int(state['edit_message_index'])
new_text = state['edit_message_text']
role = state['edit_message_role'] # "user" or "assistant"
if message_index >= len(history['internal']):
html_output = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
return [history, html_output, gr.update()]
# Use the role passed from frontend
is_user_msg = (role == "user")
role_idx = 0 if is_user_msg else 1
# For assistant messages, save the original version BEFORE updating content
if not is_user_msg:
if not history['metadata'].get(f"assistant_{message_index}", {}).get('versions'):
add_message_version(history, message_index, is_current=False)
# NOW update the message content
history['internal'][message_index][role_idx] = apply_extensions('input', new_text, state, is_chat=True)
history['visible'][message_index][role_idx] = html.escape(new_text)
# Branch if editing user message, add version if editing assistant message
if is_user_msg:
# Branch like branch-here
history['visible'] = history['visible'][:message_index + 1]
history['internal'] = history['internal'][:message_index + 1]
new_unique_id = datetime.now().strftime('%Y%m%d-%H-%M-%S')
save_history(history, new_unique_id, state['character_menu'], state['mode'])
histories = find_all_histories_with_first_prompts(state)
past_chats_update = gr.update(choices=histories, value=new_unique_id)
state['unique_id'] = new_unique_id
elif not is_user_msg:
# Add the new version as current
add_message_version(history, message_index, is_current=True)
past_chats_update = gr.update()
else:
past_chats_update = gr.update()
save_history(history, state['unique_id'], state['character_menu'], state['mode'])
html_output = redraw_html(history, state['name1'], state['name2'], state['mode'], state['chat_style'], state['character_menu'])
return [history, html_output, past_chats_update]
def handle_navigate_version_click(state):
history = state['history']
message_index = int(state['navigate_message_index'])

View file

@ -336,12 +336,14 @@ refresh_svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
continue_svg = '''<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="icon icon-tabler icons-tabler-outline icon-tabler-player-play"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 4v16l13 -8z" /></svg>'''
remove_svg = '''<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="icon icon-tabler icons-tabler-outline icon-tabler-trash"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" /></svg>'''
branch_svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-git-branch"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M7 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M17 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M7 8l0 8" /><path d="M9 18h6a2 2 0 0 0 2 -2v-5" /><path d="M14 14l3 -3l3 3" /></svg>'''
edit_svg = '''<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="tabler-icon tabler-icon-pencil"><path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4"></path><path d="M13.5 6.5l4 4"></path></svg>'''
info_svg = '''<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="thinking-icon tabler-icon tabler-icon-info-circle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 2a10 10 0 0 1 0 20a10 10 0 0 1 0 -20z" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>'''
info_svg_small = '''<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="thinking-icon tabler-icon tabler-icon-info-circle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 2a10 10 0 0 1 0 20a10 10 0 0 1 0 -20z" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>'''
attachment_svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.48-8.48l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>'''
copy_button = f'<button class="footer-button footer-copy-button" title="Copy" onclick="copyToClipboard(this)">{copy_svg}</button>'
branch_button = f'<button class="footer-button footer-branch-button" title="Branch here" onclick="branchHere(this)">{branch_svg}</button>'
edit_button = f'<button class="footer-button footer-edit-button" title="Edit" onclick="editHere(this)">{edit_svg}</button>'
refresh_button = f'<button class="footer-button footer-refresh-button" title="Regenerate" onclick="regenerateClick()">{refresh_svg}</button>'
continue_button = f'<button class="footer-button footer-continue-button" title="Continue" onclick="continueClick()">{continue_svg}</button>'
remove_button = f'<button class="footer-button footer-remove-button" title="Remove last reply" onclick="removeLastClick()">{remove_svg}</button>'
@ -404,16 +406,23 @@ def get_version_navigation_html(history, i):
return f'<div class="version-navigation">{left_arrow}{position}{right_arrow}</div>'
def actions_html(history, i, info_message=""):
def actions_html(history, i, role, info_message=""):
if role == "assistant":
return (f'<div class="message-actions">'
f'{copy_button}'
f'{edit_button}'
f'{refresh_button if i == len(history["visible"]) - 1 else ""}'
f'{continue_button if i == len(history["visible"]) - 1 else ""}'
f'{remove_button if i == len(history["visible"]) - 1 else ""}'
f'{branch_button}'
f'{info_message}'
f'</div>'
f'{get_version_navigation_html(history, i)}')
return (f'<div class="message-actions">'
f'{copy_button}'
f'{refresh_button if i == len(history["visible"]) - 1 else ""}'
f'{continue_button if i == len(history["visible"]) - 1 else ""}'
f'{remove_button if i == len(history["visible"]) - 1 else ""}'
f'{branch_button}'
f'{edit_button}'
f'{info_message}'
f'</div>'
f'{get_version_navigation_html(history, i)}')
f'</div>')
def generate_instruct_html(history):
@ -448,11 +457,12 @@ def generate_instruct_html(history):
if converted_visible[0]: # Don't display empty user messages
output += (
f'<div class="user-message" '
f'data-raw="{html.escape(row_internal[0], quote=True)}">'
f'data-raw="{html.escape(row_internal[0], quote=True)}"'
f'data-index={i}>'
f'<div class="text">'
f'<div class="message-body">{converted_visible[0]}</div>'
f'{user_attachments}'
f'<div class="message-actions">{copy_button}{info_message_user}</div>'
f'{actions_html(history, i, "user", info_message_user)}'
f'</div>'
f'</div>'
)
@ -464,7 +474,7 @@ def generate_instruct_html(history):
f'<div class="text">'
f'<div class="message-body">{converted_visible[1]}</div>'
f'{assistant_attachments}'
f'{actions_html(history, i, info_message_assistant)}'
f'{actions_html(history, i, "assistant", info_message_assistant)}'
f'</div>'
f'</div>'
)
@ -503,13 +513,14 @@ def generate_cai_chat_html(history, name1, name2, style, character, reset_cache=
if converted_visible[0]: # Don't display empty user messages
output += (
f'<div class="message" '
f'data-raw="{html.escape(row_internal[0], quote=True)}">'
f'data-raw="{html.escape(row_internal[0], quote=True)}"'
f'data-index={i}>'
f'<div class="circle-you">{img_me}</div>'
f'<div class="text">'
f'<div class="username">{name1}{user_timestamp}</div>'
f'<div class="message-body">{converted_visible[0]}</div>'
f'{user_attachments}'
f'<div class="message-actions">{copy_button}</div>'
f'{actions_html(history, i, "user")}'
f'</div>'
f'</div>'
)
@ -523,7 +534,7 @@ def generate_cai_chat_html(history, name1, name2, style, character, reset_cache=
f'<div class="username">{name2}{assistant_timestamp}</div>'
f'<div class="message-body">{converted_visible[1]}</div>'
f'{assistant_attachments}'
f'{actions_html(history, i)}'
f'{actions_html(history, i, "assistant")}'
f'</div>'
f'</div>'
)
@ -564,11 +575,12 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
if converted_visible[0]: # Don't display empty user messages
output += (
f'<div class="message" '
f'data-raw="{html.escape(row_internal[0], quote=True)}">'
f'data-raw="{html.escape(row_internal[0], quote=True)}"'
f'data-index={i}>'
f'<div class="text-you">'
f'<div class="message-body">{converted_visible[0]}</div>'
f'{user_attachments}'
f'<div class="message-actions">{copy_button}{info_message_user}</div>'
f'{actions_html(history, i, "user", info_message_user)}'
f'</div>'
f'</div>'
)
@ -580,7 +592,7 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
f'<div class="text-bot">'
f'<div class="message-body">{converted_visible[1]}</div>'
f'{assistant_attachments}'
f'{actions_html(history, i, info_message_assistant)}'
f'{actions_html(history, i, "assistant", info_message_assistant)}'
f'</div>'
f'</div>'
)

View file

@ -212,7 +212,12 @@ def list_interface_input_elements():
'negative_prompt',
'dry_sequence_breakers',
'grammar_string',
'branch_index'
'navigate_message_index',
'navigate_direction',
'edit_message_index',
'edit_message_text',
'edit_message_role',
'branch_index',
]
# Chat elements

View file

@ -71,8 +71,6 @@ def create_ui():
shared.gradio['Remove last'] = gr.Button('Remove last reply (Ctrl + Shift + Backspace)', elem_id='Remove-last')
with gr.Row():
shared.gradio['Replace last reply'] = gr.Button('Replace last reply (Ctrl + Shift + L)', elem_id='Replace-last')
shared.gradio['Copy last reply'] = gr.Button('Copy last reply (Ctrl + Shift + K)', elem_id='Copy-last')
shared.gradio['Impersonate'] = gr.Button('Impersonate (Ctrl + Shift + M)', elem_id='Impersonate')
with gr.Row():
@ -97,11 +95,15 @@ def create_ui():
with gr.Row():
shared.gradio['chat-instruct_command'] = gr.Textbox(value=shared.settings['chat-instruct_command'], lines=12, label='Command for chat-instruct mode', info='<|character|> and <|prompt|> get replaced with the bot name and the regular chat prompt respectively.', visible=shared.settings['mode'] == 'chat-instruct', elem_classes=['add_scrollbar'])
# Hidden elements for version navigation (similar to branch)
# Hidden elements for version navigation and editing
with gr.Row(visible=False):
shared.gradio['navigate_message_index'] = gr.Number(value=-1, precision=0, elem_id="Navigate-message-index")
shared.gradio['navigate_direction'] = gr.Textbox(value="", elem_id="Navigate-direction")
shared.gradio['navigate_version'] = gr.Button(elem_id="Navigate-version")
shared.gradio['edit_message_index'] = gr.Number(value=-1, precision=0, elem_id="Edit-message-index")
shared.gradio['edit_message_text'] = gr.Textbox(value="", elem_id="Edit-message-text")
shared.gradio['edit_message_role'] = gr.Textbox(value="", elem_id="Edit-message-role")
shared.gradio['edit_message'] = gr.Button(elem_id="Edit-message")
def create_chat_settings_ui():
@ -228,10 +230,6 @@ def create_event_handlers():
None, None, None, js='() => document.getElementById("chat").parentNode.parentNode.parentNode.classList.remove("_generating")').then(
None, None, None, js=f'() => {{{ui.audio_notification_js}}}')
shared.gradio['Replace last reply'].click(
ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
chat.handle_replace_last_reply_click, gradio('textbox', 'interface_state'), gradio('history', 'display', 'textbox'), show_progress=False)
shared.gradio['Send dummy message'].click(
ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
chat.handle_send_dummy_message_click, gradio('textbox', 'interface_state'), gradio('history', 'display', 'textbox'), show_progress=False)
@ -297,12 +295,16 @@ def create_event_handlers():
None, gradio('mode'), None, js="(mode) => {mode === 'instruct' ? document.getElementById('character-menu').parentNode.parentNode.style.display = 'none' : document.getElementById('character-menu').parentNode.parentNode.style.display = ''}")
shared.gradio['chat_style'].change(chat.redraw_html, gradio(reload_arr), gradio('display'), show_progress=False)
shared.gradio['Copy last reply'].click(chat.send_last_reply_to_input, gradio('history'), gradio('textbox'), show_progress=False)
shared.gradio['navigate_version'].click(
ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
chat.handle_navigate_version_click, gradio('interface_state'), gradio('history', 'display'), show_progress=False)
shared.gradio['edit_message'].click(
ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
chat.handle_edit_message_click, gradio('interface_state'), gradio('history', 'display', 'unique_id'), show_progress=False).then(
lambda: None, None, None, js='() => { const role = document.getElementById("Edit-message-role").querySelector("textarea").value; if (role === "user") document.getElementById("Regenerate").click(); }')
# Save/delete a character
shared.gradio['save_character'].click(chat.handle_save_character_click, gradio('name2'), gradio('save_character_filename', 'character_saver'), show_progress=False)
shared.gradio['delete_character'].click(lambda: gr.update(visible=True), None, gradio('character_deleter'), show_progress=False)