diff --git a/css/main.css b/css/main.css index be73a7cf..498b3c6c 100644 --- a/css/main.css +++ b/css/main.css @@ -687,6 +687,13 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* { margin-top: 6px !important; } +/* ---------------------------------------------- + Past chats menus +---------------------------------------------- */ +#rename-row label { + margin-top: var(--layout-gap); +} + /* ---------------------------------------------- Past chat histories in a side bar on desktop ---------------------------------------------- */ diff --git a/js/main.js b/js/main.js index f8610095..e9a980e2 100644 --- a/js/main.js +++ b/js/main.js @@ -488,6 +488,34 @@ function updateDocumentWidth() { updateDocumentWidth(); window.addEventListener("resize", updateDocumentWidth); +//------------------------------------------------ +// Focus on the rename text area when it becomes visible +//------------------------------------------------ +const renameTextArea = document.getElementById("rename-row").querySelector("textarea"); + +function respondToRenameVisibility(element, callback) { + var options = { + root: document.documentElement, + }; + + var observer = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + callback(entry.intersectionRatio > 0); + }); + }, options); + + observer.observe(element); +} + + +function handleVisibilityChange(isVisible) { + if (isVisible) { + renameTextArea.focus(); + } +} + +respondToRenameVisibility(renameTextArea, handleVisibilityChange); + //------------------------------------------------ // Adjust the chat tab margin if no extension UI // is present at the bottom diff --git a/modules/chat.py b/modules/chat.py index 31c1c012..5d2bdd63 100644 --- a/modules/chat.py +++ b/modules/chat.py @@ -495,6 +495,23 @@ def save_history(history, unique_id, character, mode): f.write(json.dumps(history, indent=4, ensure_ascii=False)) +def rename_history(old_id, new_id, character, mode): + if shared.args.multi_user: + return + + old_p = get_history_file_path(old_id, character, mode) + new_p = get_history_file_path(new_id, character, mode) + if new_p.parent != old_p.parent: + logger.error(f"The following path is not allowed: \"{new_p}\".") + elif new_p == old_p: + logger.info("The provided path is identical to the old one.") + elif new_p.exists(): + logger.error(f"The new path already exists and will not be overwritten: \"{new_p}\".") + else: + logger.info(f"Renaming \"{old_p}\" to \"{new_p}\"") + old_p.rename(new_p) + + def get_paths(state): if state['mode'] == 'instruct': return Path('logs/instruct').glob('*.json') @@ -537,28 +554,31 @@ def find_all_histories_with_first_prompts(state): result = [] for i, path in enumerate(histories): filename = path.stem - with open(path, 'r', encoding='utf-8') as f: - data = json.load(f) + if re.match(r'^[0-9]{8}-[0-9]{2}-[0-9]{2}-[0-9]{2}$', filename): + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) - first_prompt = "" - if 'visible' in data and len(data['visible']) > 0: - if data['internal'][0][0] == '<|BEGIN-VISIBLE-CHAT|>': - if len(data['visible']) > 1: - first_prompt = html.unescape(data['visible'][1][0]) - elif i == 0: - first_prompt = "New chat" - else: - first_prompt = html.unescape(data['visible'][0][0]) - elif i == 0: - first_prompt = "New chat" + first_prompt = "" + if 'visible' in data and len(data['visible']) > 0: + if data['internal'][0][0] == '<|BEGIN-VISIBLE-CHAT|>': + if len(data['visible']) > 1: + first_prompt = html.unescape(data['visible'][1][0]) + elif i == 0: + first_prompt = "New chat" + else: + first_prompt = html.unescape(data['visible'][0][0]) + elif i == 0: + first_prompt = "New chat" + else: + first_prompt = filename - first_prompt = first_prompt.strip() + first_prompt = first_prompt.strip() - # Truncate the first prompt if it's longer than 32 characters - if len(first_prompt) > 32: - first_prompt = first_prompt[:29] + '...' + # Truncate the first prompt if it's longer than 32 characters + if len(first_prompt) > 32: + first_prompt = first_prompt[:29] + '...' - result.append((first_prompt, filename)) + result.append((first_prompt, filename)) return result diff --git a/modules/ui_chat.py b/modules/ui_chat.py index ab8d720a..91951624 100644 --- a/modules/ui_chat.py +++ b/modules/ui_chat.py @@ -63,11 +63,18 @@ def create_ui(): with gr.Row(elem_id='past-chats-row', elem_classes=['pretty_scrollbar']): with gr.Column(): with gr.Row(): + shared.gradio['rename_chat'] = gr.Button('Rename', elem_classes='refresh-button', interactive=not mu) shared.gradio['delete_chat'] = gr.Button('🗑️', elem_classes='refresh-button', interactive=not mu) shared.gradio['delete_chat-confirm'] = gr.Button('Confirm', variant='stop', visible=False, elem_classes=['refresh-button', 'focus-on-chat-input']) shared.gradio['delete_chat-cancel'] = gr.Button('Cancel', visible=False, elem_classes=['refresh-button', 'focus-on-chat-input']) shared.gradio['Start new chat'] = gr.Button('New chat', elem_classes=['refresh-button', 'focus-on-chat-input']) + with gr.Row(elem_id='rename-row'): + shared.gradio['rename_to'] = gr.Textbox(label='Rename to:', placeholder='New name', visible=False, elem_classes=['no-background']) + with gr.Row(): + shared.gradio['rename_to-confirm'] = gr.Button('Confirm', visible=False, elem_classes=['refresh-button', 'focus-on-chat-input']) + shared.gradio['rename_to-cancel'] = gr.Button('Cancel', visible=False, elem_classes=['refresh-button', 'focus-on-chat-input']) + gr.Markdown("Past chats") with gr.Row(): shared.gradio['unique_id'] = gr.Radio(label="", elem_classes=['slim-dropdown', 'pretty_scrollbar'], interactive=not mu, elem_id='past-chats') @@ -259,6 +266,23 @@ def create_event_handlers(): chat.redraw_html, gradio(reload_arr), gradio('display')).then( lambda: [gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)], None, gradio(clear_arr)) + shared.gradio['rename_chat'].click( + lambda: "My New Chat", None, gradio('rename_to')).then( + lambda: [gr.update(visible=True)] * 3, None, gradio('rename_to', 'rename_to-confirm', 'rename_to-cancel'), show_progress=False) + + shared.gradio['rename_to-cancel'].click( + lambda: [gr.update(visible=False)] * 3, None, gradio('rename_to', 'rename_to-confirm', 'rename_to-cancel'), show_progress=False) + + shared.gradio['rename_to-confirm'].click( + chat.rename_history, gradio('unique_id', 'rename_to', 'character_menu', 'mode'), None).then( + lambda: [gr.update(visible=False)] * 3, None, gradio('rename_to', 'rename_to-confirm', 'rename_to-cancel'), show_progress=False).then( + lambda x, y: gr.update(choices=chat.find_all_histories_with_first_prompts(x), value=y), gradio('interface_state', 'rename_to'), gradio('unique_id')) + + shared.gradio['rename_to'].submit( + chat.rename_history, gradio('unique_id', 'rename_to', 'character_menu', 'mode'), None).then( + lambda: [gr.update(visible=False)] * 3, None, gradio('rename_to', 'rename_to-confirm', 'rename_to-cancel'), show_progress=False).then( + lambda x, y: gr.update(choices=chat.find_all_histories_with_first_prompts(x), value=y), gradio('interface_state', 'rename_to'), gradio('unique_id')) + shared.gradio['load_chat_history'].upload( ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then( chat.start_new_chat, gradio('interface_state'), gradio('history')).then(