diff --git a/css/main.js b/css/main.js index f3b3c05f..9663d464 100644 --- a/css/main.js +++ b/css/main.js @@ -17,6 +17,7 @@ main_parent.addEventListener('click', function(e) { } }); +// Add some scrollbars const textareaElements = document.querySelectorAll('.add_scrollbar textarea'); for(i = 0; i < textareaElements.length; i++) { textareaElements[i].classList.remove('scroll-hide'); diff --git a/css/save_files.js b/css/save_files.js new file mode 100644 index 00000000..7dfbcfda --- /dev/null +++ b/css/save_files.js @@ -0,0 +1,40 @@ +// Functions for downloading JSON files +function getCurrentTimestamp() { + const now = new Date(); + const timezoneOffset = now.getTimezoneOffset() * 60000; // Convert to milliseconds + const localTime = new Date(now.getTime() - timezoneOffset); + const formattedTimestamp = localTime.toISOString().replace(/[-:]/g, '').slice(0, 15); + return formattedTimestamp; +} + +function saveFile(contents, filename) { + const element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(contents)); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); +} + +function saveHistory(history, character, mode) { + let path = null; + + if (['chat', 'chat-instruct'].includes(mode) && character && character.trim() !== '') { + path = `history_${character}_${getCurrentTimestamp()}.json`; + } else { + try { + path = `history_${mode}_${getCurrentTimestamp()}.json`; + } catch (error) { + path = `history_${getCurrentTimestamp()}.json`; + } + } + saveFile(history, path); +} + +function saveSession(session, mode) { + let path = null; + + path = `session_${mode}_${getCurrentTimestamp()}.json`; + saveFile(session, path); +} diff --git a/modules/chat.py b/modules/chat.py index 070f45a4..57a04606 100644 --- a/modules/chat.py +++ b/modules/chat.py @@ -412,23 +412,6 @@ def load_history(file, history): return history -def save_history_at_user_request(history, character, mode): - def make_timestamp_path(character=None): - return f"logs/{character or ''}{'_' if character else ''}{datetime.now().strftime('%Y%m%d-%H%M%S')}.json" - - path = None - if mode in ['chat', 'chat-instruct'] and character not in ['', 'None', None]: - path = make_timestamp_path(character) - else: - # Try to use mode as the file name, otherwise just use the timestamp - try: - path = make_timestamp_path(mode.capitalize()) - except: - path = make_timestamp_path() - - return save_history(history, path) - - def save_persistent_history(history, character, mode): if mode in ['chat', 'chat-instruct'] and character not in ['', 'None', None] and not shared.args.multi_user: save_history(history, path=Path(f'logs/{character}_persistent.json')) diff --git a/modules/ui.py b/modules/ui.py index fe3482d2..df36a331 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -15,6 +15,8 @@ with open(Path(__file__).resolve().parent / '../css/main.js', 'r') as f: main_js = f.read() with open(Path(__file__).resolve().parent / '../css/chat.js', 'r') as f: chat_js = f.read() +with open(Path(__file__).resolve().parent / '../css/save_files.js', 'r') as f: + save_files_js = f.read() refresh_symbol = '🔄' delete_symbol = '🗑️' @@ -145,9 +147,6 @@ def gather_interface_values(*args): if not shared.args.multi_user: shared.persistent_interface_state = output - Path('logs').mkdir(exist_ok=True) - with open(Path(f'logs/session_{shared.get_mode()}_autosave.json'), 'w') as f: - f.write(json.dumps(output, indent=4)) return output diff --git a/server.py b/server.py index d622cdbe..b53d6a12 100644 --- a/server.py +++ b/server.py @@ -508,44 +508,24 @@ def create_file_saving_event_handlers(): if not shared.args.multi_user: - def load_session(session, state): - with open(Path(f'logs/{session}.json'), 'r') as f: - state.update(json.loads(f.read())) + def load_session(file, state): + decoded_file = file if type(file) == str else file.decode('utf-8') + data = json.loads(decoded_file) + state.update(data) if shared.is_chat(): chat.save_persistent_history(state['history'], state['character_menu'], state['mode']) return state - if shared.is_chat(): - shared.gradio['save_session'].click( - ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then( - lambda x: json.dumps(x, indent=4), gradio('interface_state'), gradio('save_contents')).then( - lambda: 'logs/', None, gradio('save_root')).then( - lambda x: f'session_{shared.get_mode()}_{x + "_" if x not in ["None", None, ""] else ""}{utils.current_time()}.json', gradio('character_menu'), gradio('save_filename')).then( - lambda: gr.update(visible=True), None, gradio('file_saver')) + shared.gradio['save_session'].click( + ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then( + lambda x: json.dumps(x, indent=4), gradio('interface_state'), gradio('temporary_text')).then( + None, gradio('temporary_text'), None, _js=f"(contents) => {{{ui.save_files_js}; saveSession(contents, \"{shared.get_mode()}\")}}") - shared.gradio['session_menu'].change( - load_session, gradio('session_menu', 'interface_state'), gradio('interface_state')).then( - ui.apply_interface_values, gradio('interface_state'), gradio(ui.list_interface_input_elements()), show_progress=False).then( - chat.redraw_html, shared.reload_inputs, gradio('display')) - - else: - shared.gradio['save_session'].click( - ui.gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then( - lambda x: json.dumps(x, indent=4), gradio('interface_state'), gradio('save_contents')).then( - lambda: 'logs/', None, gradio('save_root')).then( - lambda: f'session_{shared.get_mode()}_{utils.current_time()}.json', None, gradio('save_filename')).then( - lambda: gr.update(visible=True), None, gradio('file_saver')) - - shared.gradio['session_menu'].change( - load_session, gradio('session_menu', 'interface_state'), gradio('interface_state')).then( - ui.apply_interface_values, gradio('interface_state'), gradio(ui.list_interface_input_elements()), show_progress=False) - - shared.gradio['delete_session'].click( - lambda x: f'{x}.json', gradio('session_menu'), gradio('delete_filename')).then( - lambda: 'logs/', None, gradio('delete_root')).then( - lambda: gr.update(visible=True), None, gradio('file_deleter')) + shared.gradio['load_session'].upload( + load_session, gradio('load_session', 'interface_state'), gradio('interface_state')).then( + ui.apply_interface_values, gradio('interface_state'), gradio(ui.list_interface_input_elements()), show_progress=False) def set_interface_arguments(interface_mode, extensions, bool_active): @@ -558,7 +538,6 @@ def set_interface_arguments(interface_mode, extensions, bool_active): setattr(shared.args, k, False) if interface_mode != "default": setattr(shared.args, interface_mode, True) - for k in bool_list: setattr(shared.args, k, False) for k in bool_active: @@ -622,6 +601,9 @@ def create_interface(): # Floating menus for saving/deleting files create_file_saving_menus() + # Used for saving files using javascript + shared.gradio['temporary_text'] = gr.Textbox(visible=False) + # Create chat mode interface if shared.is_chat(): shared.input_elements = ui.list_interface_input_elements() @@ -702,11 +684,10 @@ def create_interface(): with gr.Tab('Chat history'): with gr.Row(): with gr.Column(): - shared.gradio['download'] = gr.File(label="Download") - shared.gradio['download_button'] = gr.Button(value='Refresh') + shared.gradio['save_chat_history'] = gr.Button(value='Save history') with gr.Column(): - shared.gradio['upload_chat_history'] = gr.File(type='binary', file_types=['.json', '.txt'], label="Upload") + shared.gradio['load_chat_history'] = gr.File(type='binary', file_types=['.json', '.txt'], label="Upload History JSON") with gr.Tab('Upload character'): with gr.Tab('YAML or JSON'): @@ -845,11 +826,8 @@ def create_interface(): with gr.Column(): if not shared.args.multi_user: - with gr.Row(): - shared.gradio['session_menu'] = gr.Dropdown(choices=utils.get_available_sessions(), value='None', label='Session', elem_classes='slim-dropdown', info='When saving a session, make sure to keep the initial part of the filename (session_chat, session_notebook, or session_default), otherwise it will not appear on this list afterwards.') - ui.create_refresh_button(shared.gradio['session_menu'], lambda: None, lambda: {'choices': utils.get_available_sessions()}, ['refresh-button']) - shared.gradio['save_session'] = gr.Button('💾', elem_classes=['refresh-button']) - shared.gradio['delete_session'] = gr.Button('🗑️', elem_classes=['refresh-button']) + shared.gradio['save_session'] = gr.Button('Save session') + shared.gradio['load_session'] = gr.File(type='binary', file_types=['.json'], label="Upload Session JSON") extension_name = gr.Textbox(lines=1, label='Install or update an extension', info='Enter the GitHub URL below and press Enter. For a list of extensions, see: https://github.com/oobabooga/text-generation-webui-extensions ⚠️ WARNING ⚠️ : extensions can execute arbitrary code. Make sure to inspect their source code before activating them.') extension_status = gr.Markdown() @@ -967,8 +945,8 @@ def create_interface(): shared.gradio['instruction_template'].change( partial(chat.load_character, instruct=True), gradio('instruction_template', 'name1_instruct', 'name2_instruct'), gradio('name1_instruct', 'name2_instruct', 'dummy', 'dummy', 'context_instruct', 'turn_template')) - shared.gradio['upload_chat_history'].upload( - chat.load_history, gradio('upload_chat_history', 'history'), gradio('history')).then( + shared.gradio['load_chat_history'].upload( + chat.load_history, gradio('load_chat_history', 'history'), gradio('history')).then( chat.redraw_html, shared.reload_inputs, gradio('display')) shared.gradio['Copy last reply'].click(chat.send_last_reply_to_input, gradio('history'), gradio('textbox'), show_progress=False) @@ -991,7 +969,9 @@ def create_interface(): lambda: 'characters/instruction-following/', None, gradio('delete_root')).then( lambda: gr.update(visible=True), None, gradio('file_deleter')) - shared.gradio['download_button'].click(chat.save_history_at_user_request, gradio('history', 'character_menu', 'mode'), gradio('download')) + shared.gradio['save_chat_history'].click(lambda x: json.dumps(x, indent=4), gradio('history'), gradio('temporary_text')).then( + None, gradio('temporary_text', 'character_menu', 'mode'), None, _js=f"(hist, char, mode) => {{{ui.save_files_js}; saveHistory(hist, char, mode)}}") + shared.gradio['Submit character'].click(chat.upload_character, gradio('upload_json', 'upload_img_bot'), gradio('character_menu')) shared.gradio['upload_json'].upload(lambda: gr.update(interactive=True), None, gradio('Submit character')) shared.gradio['upload_json'].clear(lambda: gr.update(interactive=False), None, gradio('Submit character'))