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 @@
3167 Advanced Formatting3167 Advanced Formatting
3168 </h3>3168 </h3>
3169 <div class="flex-container">3169 <div class="flex-container">
3170 <input id="af_master_import_file" type="file" hidden accept=".json" class="displayNone">
3170 <div id="af_master_import" class="menu_button menu_button_icon" title="Import Advanced Formatting settings" data-i18n="[title]Import Advanced Formatting settings">3171 <div id="af_master_import" class="menu_button menu_button_icon" title="Import Advanced Formatting settings" data-i18n="[title]Import Advanced Formatting settings">
3171 <i class="fa-solid fa-file-import"></i>3172 <i class="fa-solid fa-file-import"></i>
3172 <span data-i18n="Master Import">Master Import</span>3173 <span data-i18n="Master Import">Master Import</span>
public/scripts/preset-manager.js+230 -1
@@ -18,7 +18,7 @@ import {
18import { groups, selected_group } from './group-chats.js';18import { groups, selected_group } from './group-chats.js';
19import { instruct_presets } from './instruct-mode.js';19import { instruct_presets } from './instruct-mode.js';
20import { kai_settings } from './kai-settings.js';20import { kai_settings } from './kai-settings.js';
21import { Popup } from './popup.js';21import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
22import { context_presets, getContextSettings, power_user } from './power-user.js';22import { context_presets, getContextSettings, power_user } from './power-user.js';
23import { SlashCommand } from './slash-commands/SlashCommand.js';23import { SlashCommand } from './slash-commands/SlashCommand.js';
24import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';24import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
@@ -26,6 +26,7 @@ import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
26import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';26import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
27import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';27import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
28import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js';28import { checkForSystemPromptInInstructTemplate, system_prompts } from './sysprompt.js';
29import { renderTemplateAsync } from './templates.js';
29import {30import {
30 textgenerationwebui_preset_names,31 textgenerationwebui_preset_names,
31 textgenerationwebui_presets,32 textgenerationwebui_presets,
@@ -103,6 +104,205 @@ class PresetManager {
103 this.apiId = apiId;104 this.apiId = apiId;
104 }105 }
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
106 /**306 /**
107 * Gets all preset names.307 * Gets all preset names.
108 * @returns {string[]} List of preset names308 * @returns {string[]} List of preset names
@@ -691,4 +891,33 @@ export async function initPresetManager() {
691 toastr.success(successToast);891 toastr.success(successToast);
692 }892 }
693 });893 });
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 });
694}923}
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>