| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>RISU File Tools</title> |
| <script src="./pako.min.js"></script> |
| <script src="./msgpackr.min.js"></script> |
| <style> |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| line-height: 1.6; |
| padding: 1rem; |
| background-color: #f5f5f5; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 1rem; |
| } |
| |
| .tool-section { |
| background: #fff; |
| border-radius: 8px; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| padding: 1.5rem; |
| margin-bottom: 2rem; |
| } |
| |
| h1 { |
| font-size: 1.5rem; |
| color: #333; |
| margin-bottom: 1rem; |
| } |
| |
| h2 { |
| font-size: 1.2rem; |
| color: #444; |
| margin: 1rem 0; |
| } |
| |
| .input-group { |
| margin-bottom: 1rem; |
| } |
| |
| label { |
| display: block; |
| margin-bottom: 0.5rem; |
| color: #555; |
| } |
| |
| textarea { |
| width: 100%; |
| min-height: 150px; |
| padding: 0.8rem; |
| border: 1px solid #ddd; |
| border-radius: 4px; |
| resize: vertical; |
| font-family: monospace; |
| margin-bottom: 1rem; |
| } |
| |
| .button-group { |
| display: flex; |
| gap: 0.5rem; |
| flex-wrap: wrap; |
| margin: 1rem 0; |
| } |
| |
| button { |
| padding: 0.6rem 1.2rem; |
| border: none; |
| border-radius: 4px; |
| background-color: #007bff; |
| color: white; |
| cursor: pointer; |
| transition: background-color 0.2s; |
| } |
| |
| button:hover { |
| background-color: #0056b3; |
| } |
| |
| .file-input-wrapper { |
| display: flex; |
| align-items: center; |
| gap: 1rem; |
| margin-bottom: 1rem; |
| flex-wrap: wrap; |
| } |
| |
| input[type="file"] { |
| max-width: 100%; |
| } |
| |
| .output { |
| margin-top: 1.5rem; |
| padding: 1rem; |
| background: #f8f9fa; |
| border-radius: 4px; |
| } |
| |
| pre { |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| padding: 1rem; |
| background: #fff; |
| border: 1px solid #ddd; |
| border-radius: 4px; |
| font-size: 0.9rem; |
| overflow-x: auto; |
| } |
| |
| @media screen and (max-width: 768px) { |
| .container { |
| padding: 0.5rem; |
| } |
| |
| .tool-section { |
| padding: 1rem; |
| } |
| |
| h1 { |
| font-size: 1.3rem; |
| } |
| |
| h2 { |
| font-size: 1.1rem; |
| } |
| |
| button { |
| width: 100%; |
| margin-bottom: 0.5rem; |
| } |
| |
| .file-input-wrapper { |
| flex-direction: column; |
| align-items: stretch; |
| } |
| |
| input[type="file"] { |
| width: 100%; |
| } |
| |
| textarea { |
| min-height: 120px; |
| } |
| } |
| |
| .loading { |
| display: none; |
| text-align: center; |
| margin: 1rem 0; |
| } |
| |
| .loading.active { |
| display: block; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="tool-section"> |
| <h1>ST to Risu Converter</h1> |
| <div class="input-group"> |
| <label for="stInput">Paste ST JSON here:</label> |
| <div class="file-input-wrapper"> |
| <button id="stfileSelectButton">Choose File</button> |
| <input type="file" id="stfileInput" accept=".txt,.json" style="display:none;" /> |
| </div> |
| <textarea id="stInput" placeholder="Paste ST JSON content..."></textarea> |
| </div> |
|
|
| <div class="input-group"> |
| <label for="risuDefault">Paste Risu Default JSON here:</label> |
| <textarea id="risuDefault" placeholder="Paste Risu Default JSON content...">{ |
| "name": "Default", |
| "apiType": "instructgpt35", |
| "openAIKey": "", |
| "mainPrompt": "", |
| "jailbreak": "", |
| "globalNote": "", |
| "temperature": 100, |
| "maxContext": 16000, |
| "maxResponse": 1000, |
| "frequencyPenalty": 0, |
| "PresensePenalty": 0, |
| "formatingOrder": [ |
| "main", |
| "description", |
| "personaPrompt", |
| "chats", |
| "lastChat", |
| "jailbreak", |
| "lorebook", |
| "globalNote", |
| "authorNote" |
| ], |
| "aiModel": "reverse_proxy", |
| "subModel": "reverse_proxy", |
| "currentPluginProvider": "", |
| "textgenWebUIStreamURL": "", |
| "textgenWebUIBlockingURL": "", |
| "forceReplaceUrl": "", |
| "forceReplaceUrl2": "", |
| "promptPreprocess": false, |
| "bias": [], |
| "koboldURL": "http://localho.st:5001/api/v1", |
| "proxyKey": "", |
| "ooba": { |
| "max_new_tokens": 180, |
| "do_sample": true, |
| "temperature": 0.7, |
| "top_p": 0.9, |
| "typical_p": 1, |
| "repetition_penalty": 1.15, |
| "encoder_repetition_penalty": 1, |
| "top_k": 20, |
| "min_length": 0, |
| "no_repeat_ngram_size": 0, |
| "num_beams": 1, |
| "penalty_alpha": 0, |
| "length_penalty": 1, |
| "early_stopping": false, |
| "seed": -1, |
| "add_bos_token": true, |
| "truncation_length": 4096, |
| "ban_eos_token": false, |
| "skip_special_tokens": true, |
| "top_a": 0, |
| "tfs": 1, |
| "epsilon_cutoff": 0, |
| "eta_cutoff": 0, |
| "formating": { |
| "header": "Below is an instruction that describes a task. Write a response that appropriately completes the request.", |
| "systemPrefix": "### Instruction:", |
| "userPrefix": "### Input:", |
| "assistantPrefix": "### Response:", |
| "seperator": "", |
| "useName": false |
| } |
| }, |
| "ainconfig": { |
| "top_p": 0.7, |
| "rep_pen": 1.0625, |
| "top_a": 0.08, |
| "rep_pen_slope": 1.7, |
| "rep_pen_range": 1024, |
| "typical_p": 1, |
| "badwords": "", |
| "stoptokens": "", |
| "top_k": 140 |
| }, |
| "proxyRequestModel": "claude-3-5-sonnet-20240620", |
| "openrouterRequestModel": "anthropic/claude-2", |
| "NAISettings": { |
| "topK": 12, |
| "topP": 0.85, |
| "topA": 0.1, |
| "tailFreeSampling": 0.915, |
| "repetitionPenalty": 2.8, |
| "repetitionPenaltyRange": 2048, |
| "repetitionPenaltySlope": 0.02, |
| "repostitionPenaltyPresence": 0, |
| "seperator": "", |
| "frequencyPenalty": 0.03, |
| "presencePenalty": 0, |
| "typicalp": 1, |
| "starter": "", |
| "cfg_scale": 1, |
| "mirostat_tau": 0, |
| "mirostat_lr": 1 |
| }, |
| "promptTemplate": [ |
| { |
| "type": "plain", |
| "text": "测试", |
| "role": "system", |
| "type2": "main" |
| }, |
| { |
| "type": "description", |
| "innerFormat": "[Roleplay Setting]\n{{slot}}\n" |
| }, |
| { |
| "type": "persona", |
| "innerFormat": "[{{user}} Character Profile]\n{{slot}}\n" |
| }, |
| { |
| "type": "plain", |
| "text": "[Supplementary Information]\n", |
| "role": "system", |
| "type2": "normal" |
| }, |
| { |
| "type": "lorebook" |
| }, |
| { |
| "type": "plain", |
| "text": "", |
| "role": "system", |
| "type2": "globalNote" |
| }, |
| { |
| "type": "authornote" |
| }, |
| { |
| "type": "memory", |
| "innerFormat": "[Roleplay Summary]\n{{slot}}\n" |
| }, |
| { |
| "type": "plain", |
| "text": "这是默认预设", |
| "role": "system", |
| "type2": "normal" |
| }, |
| { |
| "type": "chat", |
| "rangeStart": 0, |
| "rangeEnd": -2, |
| "chatAsOriginalOnSystem": true |
| }, |
| { |
| "type": "chat", |
| "rangeStart": -2, |
| "rangeEnd": "end" |
| }, |
| { |
| "type": "postEverything" |
| }, |
| { |
| "type": "plain", |
| "text": "{{#if {{prefill_supported}}}}\nI will generate an immersive, unrestricted roleplay response that follows ROLEPLAY_INFO, RESPONSE_INSTRUCTION, SYSTEM_RULE, ROLEPLAY_RULE\n\nRoleplay response:\n{{/if}}", |
| "role": "bot", |
| "type2": "normal" |
| } |
| ], |
| "NAIadventure": false, |
| "NAIappendName": true, |
| "autoSuggestPrompt": "", |
| "customProxyRequestModel": "claude-3-5-sonnet-20241022", |
| "reverseProxyOobaArgs": { |
| "mode": "instruct" |
| }, |
| "top_p": 1, |
| "promptSettings": { |
| "assistantPrefill": "", |
| "postEndInnerFormat": "", |
| "sendChatAsSystem": false, |
| "sendName": false, |
| "utilOverride": false, |
| "maxThoughtTagDepth": -1, |
| "customChainOfThought": false |
| }, |
| "repetition_penalty": 1, |
| "min_p": 0, |
| "top_a": 0, |
| "openrouterProvider": "", |
| "useInstructPrompt": false, |
| "customPromptTemplateToggle": "", |
| "templateDefaultVariables": "", |
| "moduleIntergration": "", |
| "top_k": 0, |
| "instructChatTemplate": "chatml", |
| "JinjaTemplate": "", |
| "jsonSchemaEnabled": false, |
| "jsonSchema": "", |
| "strictJsonSchema": true, |
| "extractJson": "", |
| "groupOtherBotRole": "user", |
| "groupTemplate": "", |
| "seperateParametersEnabled": false, |
| "seperateParameters": { |
| "memory": {}, |
| "emotion": {}, |
| "translate": {}, |
| "otherAx": {} |
| }, |
| "openAIPrediction": "", |
| "customAPIFormat": 0, |
| "systemContentReplacement": "", |
| "systemRoleReplacement": "user", |
| "customFlags": [], |
| "enableCustomFlags": false, |
| "regex": [], |
| "image": "" |
| }</textarea> |
| </div> |
|
|
| <div class="button-group"> |
| <button id="convertButton">Convert</button> |
| </div> |
|
|
| <div class="output"> |
| <div class="button-group"> |
| <button id="copyButton">Copy</button> |
| <button id="saveButton">Save as JSON</button> |
| </div> |
| <pre id="stOutput"></pre> |
| </div> |
| </div> |
|
|
| <div class="tool-section"> |
| <h1>RISU Preset File Tools</h1> |
| <div class="input-group"> |
| <h2>Decrypt RISU Preset</h2> |
| <div class="file-input-wrapper"> |
| <input type="file" id="fileInput" accept=".risupreset,.risup"/> |
| <button id="recoverButton">Recover Preset</button> |
| </div> |
| </div> |
|
|
| <div class="input-group"> |
| <h2>Encrypt to RISU Preset</h2> |
| <div class="file-input-wrapper"> |
| <input type="file" id="jsonInput" accept=".json"/> |
| <button id="encryptButton">Create .risup</button> |
| </div> |
| </div> |
|
|
| <div class="loading" id="loadingIndicator">Processing...</div> |
| <pre id="output"></pre> |
| </div> |
| </div> |
|
|
| <script> |
| |
| document.getElementById("stfileSelectButton").addEventListener("click", function() { |
| |
| document.getElementById("stfileInput").click(); |
| }); |
| |
| |
| document.getElementById("stfileInput").addEventListener("change", function(event) { |
| const file = event.target.files[0]; |
| |
| if (file) { |
| const reader = new FileReader(); |
| |
| |
| reader.onload = function(e) { |
| const fileContent = e.target.result; |
| document.getElementById("stInput").value = fileContent; |
| }; |
| |
| |
| reader.readAsText(file); |
| } |
| }); |
| |
| |
| document.getElementById("copyButton").addEventListener("click", function() { |
| const outputText = document.getElementById("stOutput").textContent; |
| navigator.clipboard.writeText(outputText).then(function() { |
| alert("Copied to clipboard!"); |
| }).catch(function(err) { |
| alert("Failed to copy text: " + err); |
| }); |
| }); |
| |
| |
| |
| document.getElementById("saveButton").addEventListener("click", function() { |
| const outputText = document.getElementById("stOutput").textContent; |
| |
| |
| try { |
| const jsonObject = JSON.parse(outputText); |
| const blob = new Blob([JSON.stringify(jsonObject, null, 2)], { type: 'application/json' }); |
| const link = document.createElement('a'); |
| link.href = URL.createObjectURL(blob); |
| link.download = 'converted_risu.json'; |
| link.click(); |
| } catch (error) { |
| alert("The output is not valid JSON."); |
| } |
| }); |
| |
| |
| document.getElementById('convertButton').addEventListener('click', function convert() { |
| try { |
| const stInput = JSON.parse(document.getElementById('stInput').value); |
| const risuDefault = JSON.parse(document.getElementById('risuDefault').value); |
| |
| if (!stInput || !risuDefault) { |
| throw new Error('Invalid JSON input.'); |
| } |
| |
| const promptOrder = stInput.prompt_order.find(order => order.character_id === 100001); |
| if (!promptOrder) { |
| throw new Error('Character ID 100001 not found in ST prompt_order.'); |
| } |
| |
| const stPrompts = stInput.prompts; |
| const risuPrompts = risuDefault.promptTemplate; |
| |
| const specialTypeOrder = ["description", "persona", "lorebook", "globalNote", "authornote", "memory", "chat"]; |
| const specialTemplates = []; |
| |
| specialTypeOrder.forEach(type => { |
| const templates = risuPrompts.filter(p => p.type === type || p.type2 === type); |
| if (templates.length > 0) { |
| specialTemplates.push(...templates); |
| } else { |
| if (type === "chat") { |
| const chatTemplates = risuPrompts.filter(p => p.type === "chat"); |
| if (chatTemplates.length < 2) { |
| specialTemplates.push(...chatTemplates); |
| } |
| } else { |
| throw new Error(`Missing required special type: ${type}`); |
| } |
| } |
| }); |
| |
| const convertedPrompts = []; |
| const stPromptMap = new Map(stPrompts.map(p => [p.identifier, p])); |
| |
| let mainPrompt; |
| |
| promptOrder.order.forEach(({ identifier, enabled }) => { |
| if (!enabled) return; |
| |
| if (identifier === "main") { |
| const mainPromptData = stPromptMap.get(identifier); |
| if (mainPromptData) { |
| const template = risuPrompts.find(p => p.type2 === "main"); |
| if (template) { |
| mainPrompt = { |
| ...template, |
| text: mainPromptData.content || "未命名" |
| }; |
| } |
| } |
| return; |
| } |
| |
| if (["charDescription", "dialogueExamples", "charPersonality", "scenario", "worldInfoBefore", "worldInfoAfter"].includes(identifier)) { |
| return; |
| } |
| |
| const stPrompt = stPromptMap.get(identifier); |
| if (!stPrompt) { |
| console.warn(`Prompt with identifier ${identifier} not found, skipping.`); |
| return; |
| } |
| |
| const name = stPrompt.name || "未命名"; |
| convertedPrompts.push({ |
| type: "plain", |
| text: stPrompt.content || "\n", |
| role: stPrompt.role, |
| type2: "normal", |
| name |
| }); |
| }); |
| |
| const charDescriptionIndex = promptOrder.order.findIndex(o => o.identifier === "charDescription"); |
| if (charDescriptionIndex === -1) { |
| throw new Error('charDescription not found in prompt_order.'); |
| } |
| |
| const filteredSpecialTemplates = [ |
| risuPrompts.find(p => p.type === "description"), |
| mainPrompt, |
| ...specialTemplates.filter(p => p.type !== "description" && p.type2 !== "main") |
| ]; |
| |
| const result = [ |
| ...convertedPrompts.slice(0, charDescriptionIndex), |
| ...filteredSpecialTemplates, |
| ...convertedPrompts.slice(charDescriptionIndex) |
| ].filter(Boolean); |
| |
| risuDefault.promptTemplate = result; |
| |
| document.getElementById('stOutput').textContent = JSON.stringify(risuDefault, null, 2); |
| } catch (error) { |
| document.getElementById('stOutput').textContent = `Error: ${error.message}`; |
| } |
| }); |
| |
| |
| async function initWasm() { |
| try { |
| const response = await fetch('./rpack_bg.wasm'); |
| if (!response.ok) throw new Error('Failed to fetch Wasm module'); |
| const wasmModule = await response.arrayBuffer(); |
| if (!WebAssembly.validate(wasmModule)) throw new Error('Invalid WebAssembly module'); |
| const { instance } = await WebAssembly.instantiate(wasmModule); |
| return instance.exports; |
| } catch (error) { |
| console.error('Error initializing Wasm module:', error); |
| throw error; |
| } |
| } |
| |
| function getUint8ArrayMemory0(wasmInstance) { |
| return new Uint8Array(wasmInstance.memory.buffer); |
| } |
| |
| function passArray8ToWasm0(arg, malloc, wasmInstance) { |
| const ptr = malloc(arg.length * 1, 1) >>> 0; |
| getUint8ArrayMemory0(wasmInstance).set(arg, ptr / 1); |
| WASM_VECTOR_LEN = arg.length; |
| return ptr; |
| } |
| |
| let WASM_VECTOR_LEN = 0; |
| |
| function getDataViewMemory0(wasmInstance) { |
| return new DataView(wasmInstance.memory.buffer); |
| } |
| |
| function getArrayU8FromWasm0(ptr, len, wasmInstance) { |
| return getUint8ArrayMemory0(wasmInstance).subarray(ptr / 1, ptr / 1 + len); |
| } |
| |
| async function decodeRPack(datas, wasmInstance) { |
| try { |
| const retptr = wasmInstance.__wbindgen_add_to_stack_pointer(-16); |
| const ptr0 = passArray8ToWasm0(datas, wasmInstance.__wbindgen_malloc, wasmInstance); |
| const len0 = WASM_VECTOR_LEN; |
| wasmInstance.decode(retptr, ptr0, len0); |
| var r0 = getDataViewMemory0(wasmInstance).getInt32(retptr + 4 * 0, true); |
| var r1 = getDataViewMemory0(wasmInstance).getInt32(retptr + 4 * 1, true); |
| var v2 = getArrayU8FromWasm0(r0, r1, wasmInstance).slice(); |
| wasmInstance.__wbindgen_free(r0, r1 * 1, 1); |
| return v2; |
| } finally { |
| wasmInstance.__wbindgen_add_to_stack_pointer(16); |
| } |
| } |
| |
| async function encodeRPack(datas, wasmInstance) { |
| try { |
| const retptr = wasmInstance.__wbindgen_add_to_stack_pointer(-16); |
| const ptr0 = passArray8ToWasm0(datas, wasmInstance.__wbindgen_malloc, wasmInstance); |
| const len0 = WASM_VECTOR_LEN; |
| wasmInstance.encode(retptr, ptr0, len0); |
| var r0 = getDataViewMemory0(wasmInstance).getInt32(retptr + 4 * 0, true); |
| var r1 = getDataViewMemory0(wasmInstance).getInt32(retptr + 4 * 1, true); |
| var v2 = getArrayU8FromWasm0(r0, r1, wasmInstance).slice(); |
| wasmInstance.__wbindgen_free(r0, r1 * 1, 1); |
| return v2; |
| } finally { |
| wasmInstance.__wbindgen_add_to_stack_pointer(16); |
| } |
| } |
| |
| async function decryptBuffer(data, key) { |
| const encoder = new TextEncoder(); |
| const keyData = encoder.encode(key); |
| const hash = await window.crypto.subtle.digest("SHA-256", keyData); |
| const cryptoKey = await window.crypto.subtle.importKey( |
| "raw", |
| hash, |
| "AES-GCM", |
| false, |
| ["decrypt"] |
| ); |
| return await window.crypto.subtle.decrypt( |
| { |
| name: "AES-GCM", |
| iv: new Uint8Array(12) |
| }, |
| cryptoKey, |
| data |
| ); |
| } |
| |
| async function encryptBuffer(data, key) { |
| const encoder = new TextEncoder(); |
| const keyData = encoder.encode(key); |
| const hash = await window.crypto.subtle.digest("SHA-256", keyData); |
| const cryptoKey = await window.crypto.subtle.importKey( |
| "raw", |
| hash, |
| "AES-GCM", |
| false, |
| ["encrypt"] |
| ); |
| return await window.crypto.subtle.encrypt( |
| { |
| name: "AES-GCM", |
| iv: new Uint8Array(12) |
| }, |
| cryptoKey, |
| data |
| ); |
| } |
| |
| async function recoverPresetFromFile(file) { |
| try { |
| showLoading(); |
| const arrayBuffer = await file.arrayBuffer(); |
| const uint8Array = new Uint8Array(arrayBuffer); |
| |
| let finalData; |
| |
| if (file.name.endsWith('.risup')) { |
| const wasmInstance = await initWasm(); |
| const decodedData = await decodeRPack(uint8Array, wasmInstance); |
| console.log("1. RPack decoded:", decodedData); |
| |
| const decompressedData = pako.inflate(decodedData); |
| console.log("2. Decompressed:", decompressedData); |
| |
| const firstDecode = msgpackr.decode(decompressedData); |
| console.log("3. First msgpack decode:", firstDecode); |
| |
| const decryptedData = await decryptBuffer(firstDecode.preset, 'risupreset'); |
| console.log("4. Decrypted data:", new Uint8Array(decryptedData)); |
| |
| finalData = msgpackr.decode(new Uint8Array(decryptedData)); |
| console.log("5. Final decoded data:", finalData); |
| |
| } else if (file.name.endsWith('.risupreset')) { |
| const decompressedData = pako.inflate(uint8Array); |
| console.log("1. Decompressed:", decompressedData); |
| |
| const firstDecode = msgpackr.decode(decompressedData); |
| console.log("2. First msgpack decode:", firstDecode); |
| |
| const encryptedPreset = firstDecode.preset ?? firstDecode.pres; |
| if (!encryptedPreset) { |
| throw new Error("Missing `preset` or `pres` field in .risupreset file."); |
| } |
| |
| const decryptedData = await decryptBuffer(encryptedPreset, 'risupreset'); |
| console.log("3. Decrypted data:", new Uint8Array(decryptedData)); |
| |
| finalData = msgpackr.decode(new Uint8Array(decryptedData)); |
| console.log("4. Final decoded data:", finalData); |
| } else { |
| throw new Error('Unsupported file format'); |
| } |
| |
| document.getElementById('output').textContent = JSON.stringify(finalData, null, 2); |
| } catch (error) { |
| console.error('Error recovering preset:', error); |
| document.getElementById('output').textContent = `Error: ${error.message}\n\nStack: ${error.stack}`; |
| } finally { |
| hideLoading(); |
| } |
| } |
| |
| async function createPresetFile(file) { |
| try { |
| showLoading(); |
| const text = await file.text(); |
| const jsonData = JSON.parse(text); |
| |
| const firstEncode = msgpackr.encode(jsonData); |
| console.log("1. First msgpack encode:", firstEncode); |
| |
| const encryptedData = await encryptBuffer(firstEncode, 'risupreset'); |
| console.log("2. Encrypted data:", new Uint8Array(encryptedData)); |
| |
| const wrapper = { |
| presetVersion: 2, |
| type: "preset", |
| preset: new Uint8Array(encryptedData) |
| }; |
| const secondEncode = msgpackr.encode(wrapper); |
| console.log("3. Second msgpack encode:", secondEncode); |
| |
| const compressedData = pako.deflate(secondEncode); |
| console.log("4. Compressed:", compressedData); |
| |
| const wasmInstance = await initWasm(); |
| const rpackData = await encodeRPack(compressedData, wasmInstance); |
| console.log("5. RPack encoded:", rpackData); |
| |
| const blob = new Blob([rpackData], { type: 'application/octet-stream' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = file.name.replace('.json', '.risup'); |
| a.click(); |
| URL.revokeObjectURL(url); |
| |
| document.getElementById('output').textContent = "Preset file created successfully!"; |
| } catch (error) { |
| console.error('Error creating preset:', error); |
| document.getElementById('output').textContent = `Error: ${error.message}\n\nStack: ${error.stack}`; |
| } finally { |
| hideLoading(); |
| } |
| } |
| |
| |
| function showLoading() { |
| document.getElementById('loadingIndicator').classList.add('active'); |
| } |
| |
| function hideLoading() { |
| document.getElementById('loadingIndicator').classList.remove('active'); |
| } |
| |
| |
| document.getElementById('recoverButton').addEventListener('click', () => { |
| const fileInput = document.getElementById('fileInput'); |
| const file = fileInput.files[0]; |
| if (file) { |
| recoverPresetFromFile(file); |
| } else { |
| document.getElementById('output').textContent = 'Please select a file first.'; |
| } |
| }); |
| |
| document.getElementById('encryptButton').addEventListener('click', () => { |
| const fileInput = document.getElementById('jsonInput'); |
| const file = fileInput.files[0]; |
| if (file) { |
| createPresetFile(file); |
| } else { |
| document.getElementById('output').textContent = 'Please select a JSON file first.'; |
| } |
| }); |
| </script> |
| </body> |
| </html> |