Implement master AF import / export

c2f945ef882313554348ca9261a749829e65ecc7

Cohee <18619528+Cohee1207@users.noreply.github.com>

4 files changed, +253 -1Showing whitespace changes
public/index.html+1 -0
@@ -3167,6 +3167,7 @@
31673167 Advanced Formatting
31683168 </h3>
31693169 <div class="flex-container">
3170+ <input id="af_master_import_file" type="file" hidden accept=".json" class="displayNone">
31703171 <div id="af_master_import" class="menu_button menu_button_icon" title="Import Advanced Formatting settings" data-i18n="[title]Import Advanced Formatting settings">
31713172 <i class="fa-solid fa-file-import"></i>
31723173 <span data-i18n="Master Import">Master Import</span>
public/scripts/preset-manager.js+230 -1
@@ -18,7 +18,7 @@ import {
1818import { groups, selected_group } from './group-chats.js';
1919import { instruct_presets } from './instruct-mode.js';
2020import { kai_settings } from './kai-settings.js';
2121import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
2222import { context_presets, getContextSettings, power_user } from './power-user.js';
2323import { SlashCommand } from './slash-commands/SlashCommand.js';
2424import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
@@ -26,6 +26,7 @@ import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
2626import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
2727import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
2828import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js';
29+import { renderTemplateAsync } from './templates.js';
2930import {
3031 textgenerationwebui_preset_names,
3132 textgenerationwebui_presets,
@@ -103,6 +104,205 @@ class PresetManager {
103104 this.apiId = apiId;
104105 }
105106
107+ static masterSections = {
108+ 'instruct': {
109+ name: 'Instruct Template',
110+ getData: () => {
111+ const manager = getPresetManager('instruct');
112+ const name = manager.getSelectedPresetName();
113+ return manager.getPresetSettings(name);
114+ },
115+ setData: (data) => {
116+ const manager = getPresetManager('instruct');
117+ const name = data.name;
118+ return manager.savePreset(name, data);
119+ },
120+ isValid: (data) => PresetManager.isPossiblyInstructData(data),
121+ },
122+ 'context': {
123+ name: 'Context Template',
124+ getData: () => {
125+ const manager = getPresetManager('context');
126+ const name = manager.getSelectedPresetName();
127+ return manager.getPresetSettings(name);
128+ },
129+ setData: (data) => {
130+ const manager = getPresetManager('context');
131+ const name = data.name;
132+ return manager.savePreset(name, data);
133+ },
134+ isValid: (data) => PresetManager.isPossiblyContextData(data),
135+ },
136+ 'sysprompt': {
137+ name: 'System Prompt',
138+ getData: () => {
139+ const manager = getPresetManager('sysprompt');
140+ const name = manager.getSelectedPresetName();
141+ return manager.getPresetSettings(name);
142+ },
143+ setData: (data) => {
144+ const manager = getPresetManager('sysprompt');
145+ const name = data.name;
146+ return manager.savePreset(name, data);
147+ },
148+ isValid: (data) => PresetManager.isPossiblySystemPromptData(data),
149+ },
150+ 'preset': {
151+ name: 'Text Completion Settings',
152+ getData: () => {
153+ const manager = getPresetManager('textgenerationwebui');
154+ const name = manager.getSelectedPresetName();
155+ const data = manager.getPresetSettings(name);
156+ data['name'] = name;
157+ return data;
158+ },
159+ setData: (data) => {
160+ const manager = getPresetManager('textgenerationwebui');
161+ const name = data.name;
162+ return manager.savePreset(name, data);
163+ },
164+ isValid: (data) => PresetManager.isPossiblyTextCompletionData(data),
165+ },
166+ };
167+
168+ static isPossiblyInstructData(data) {
169+ const instructProps = ['name', 'input_sequence', 'output_sequence'];
170+ return data && instructProps.every(prop => Object.keys(data).includes(prop));
171+ }
172+
173+ static isPossiblyContextData(data) {
174+ const contextProps = ['name', 'story_string'];
175+ return data && contextProps.every(prop => Object.keys(data).includes(prop));
176+ }
177+
178+ static isPossiblySystemPromptData(data) {
179+ const sysPromptProps = ['name', 'content'];
180+ return data && sysPromptProps.every(prop => Object.keys(data).includes(prop));
181+ }
182+
183+ static isPossiblyTextCompletionData(data) {
184+ const textCompletionProps = ['temp', 'top_k', 'top_p', 'rep_pen'];
185+ return data && textCompletionProps.every(prop => Object.keys(data).includes(prop));
186+ }
187+
188+ static async performMasterImport(data) {
189+ if (!data || typeof data !== 'object') {
190+ toastr.error('Invalid data provided for master import');
191+ return;
192+ }
193+
194+ // Check for legacy file imports
195+ // 1. Instruct Template
196+ if (this.isPossiblyInstructData(data)) {
197+ toastr.info('Importing instruct template...', 'Instruct template detected');
198+ return await getPresetManager('instruct').savePreset(data.name, data);
199+ }
200+
201+ // 2. Context Template
202+ if (this.isPossiblyContextData(data)) {
203+ toastr.info('Importing as context template...', 'Context template detected');
204+ return await getPresetManager('context').savePreset(data.name, data);
205+ }
206+
207+ // 3. System Prompt
208+ if (this.isPossiblySystemPromptData(data)) {
209+ toastr.info('Importing as system prompt...', 'System prompt detected');
210+ return await getPresetManager('sysprompt').savePreset(data.name, data);
211+ }
212+
213+ // 4. Text Completion settings
214+ if (this.isPossiblyTextCompletionData(data)) {
215+ toastr.info('Importing as settings preset...', 'Text Completion settings detected');
216+ return await getPresetManager('textgenerationwebui').savePreset(data.name, data);
217+ }
218+
219+ const validSections = [];
220+ for (const [key, section] of Object.entries(this.masterSections)) {
221+ if (key in data && section.isValid(data[key])) {
222+ validSections.push(key);
223+ }
224+ }
225+
226+ if (validSections.length === 0) {
227+ toastr.error('No valid sections found in imported data');
228+ return;
229+ }
230+
231+ const sectionNames = validSections.reduce((acc, key) => {
232+ acc[key] = this.masterSections[key].name;
233+ return acc;
234+ }, {});
235+
236+ const html = $(await renderTemplateAsync('masterImport', { sections: sectionNames }));
237+ const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
238+ okButton: 'Confirm',
239+ cancelButton: 'Cancel',
240+ });
241+
242+ const result = await popup.show();
243+
244+ // Import cancelled
245+ if (result !== POPUP_RESULT.AFFIRMATIVE) {
246+ return;
247+ }
248+
249+ const importedSections = [];
250+ const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
251+
252+ if (confirmedSections.length === 0) {
253+ toastr.info('No sections selected for import');
254+ return;
255+ }
256+
257+ for (const section of confirmedSections) {
258+ const sectionData = data[section];
259+ const masterSection = this.masterSections[section];
260+ if (sectionData && masterSection) {
261+ await masterSection.setData(sectionData);
262+ importedSections.push(masterSection.name);
263+ }
264+ }
265+
266+ toastr.success(`Imported ${importedSections.length} settings: ${importedSections.join(', ')}`);
267+ }
268+
269+ static async performMasterExport() {
270+ const sectionNames = Object.entries(this.masterSections).reduce((acc, [key, section]) => {
271+ acc[key] = section.name;
272+ return acc;
273+ }, {});
274+ const html = $(await renderTemplateAsync('masterExport', { sections: sectionNames }));
275+
276+ const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
277+ okButton: 'Export',
278+ cancelButton: 'Cancel',
279+ });
280+
281+ const result = await popup.show();
282+
283+ // Export cancelled
284+ if (result !== POPUP_RESULT.AFFIRMATIVE) {
285+ return;
286+ }
287+
288+ const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
289+ const data = {};
290+
291+ if (confirmedSections.length === 0) {
292+ toastr.info('No sections selected for export');
293+ return;
294+ }
295+
296+ for (const section of confirmedSections) {
297+ const masterSection = this.masterSections[section];
298+ if (masterSection) {
299+ data[section] = masterSection.getData();
300+ }
301+ }
302+
303+ return JSON.stringify(data, null, 4);
304+ }
305+
106306 /**
107307 * Gets all preset names.
108308 * @returns {string[]} List of preset names
@@ -691,4 +891,33 @@ export async function initPresetManager() {
691891 toastr.success(successToast);
692892 }
693893 });
894+
895+ $('#af_master_import').on('click', () => {
896+ $('#af_master_import_file').trigger('click');
897+ });
898+
899+ $('#af_master_import_file').on('change', async function (e) {
900+ if (!(e.target instanceof HTMLInputElement)) {
901+ return;
902+ }
903+ const file = e.target.files[0];
904+
905+ if (!file) {
906+ return;
907+ }
908+
909+ const data = await parseJsonFile(file);
910+ await PresetManager.performMasterImport(data);
911+ });
912+
913+ $('#af_master_export').on('click', async () => {
914+ const data = await PresetManager.performMasterExport();
915+
916+ if (!data) {
917+ return;
918+ }
919+
920+ const shortDate = new Date().toISOString().split('T')[0];
921+ download(data, `ST-formatting-${shortDate}.json`, 'application/json');
922+ });
694923}
public/scripts/templates/masterExport.html+11 -0
@@ -0,0 +1,11 @@
1+<h3>
2+ Choose what to export
3+</h3>
4+<div class="flex-container flexFlowColumn justifyLeft">
5+ {{#each sections}}
6+ <label class="checkbox_label">
7+ <input type="checkbox" value="{{@key}}" checked>
8+ <span data-i18n="{{this}}">{{this}}</span>
9+ </label>
10+ {{/each}}
11+</div>
public/scripts/templates/masterImport.html+11 -0
@@ -0,0 +1,11 @@
1+<h3>
2+ Choose what to import
3+</h3>
4+<div class="flex-container flexFlowColumn justifyLeft">
5+ {{#each sections}}
6+ <label class="checkbox_label">
7+ <input type="checkbox" value="{{@key}}" checked>
8+ <span data-i18n="{{this}}">{{this}}</span>
9+ </label>
10+ {{/each}}
11+</div>