@@ -33622,6 +33622,7 @@ class MarkdownWYSIWYG {
33622
33622
}
33623
33623
}, 1000);
33624
33624
}
33625
+
33625
33626
</script>
33626
33627
<script>
33627
33628
/**
@@ -37579,6 +37580,24 @@ flowchart TD\n${treeToViewOutput.outputText.trim()}`
37579
37580
width: unset;
37580
37581
}
37581
37582
37583
+ /* Ensure form fields are readable inside popups */
37584
+ #popupContainer .popupContent input[type="text"],
37585
+ #popupContainer .popupContent input[type="file"],
37586
+ #popupContainer .popupContent textarea,
37587
+ #popupContainer .popupContent select {
37588
+ color: #000;
37589
+ background-color: #fff;
37590
+ border: 1px solid var(--colgrey, #ccc);
37591
+ border-radius: 6px;
37592
+ padding: 6px 8px;
37593
+ box-sizing: border-box;
37594
+ }
37595
+
37596
+ #popupContainer .popupContent input::placeholder,
37597
+ #popupContainer .popupContent textarea::placeholder {
37598
+ color: #666;
37599
+ }
37600
+
37582
37601
.containAndScaleImage.tile
37583
37602
{
37584
37603
height: 128px;
@@ -37615,6 +37634,21 @@ flowchart TD\n${treeToViewOutput.outputText.trim()}`
37615
37634
{
37616
37635
display: block;
37617
37636
}
37637
+
37638
+ .characterCreatorGrid {
37639
+ color: white;
37640
+ padding: 10px;
37641
+ display: grid;
37642
+ grid-template-columns: 1fr 1fr;
37643
+ gap: 8px;
37644
+ min-width: 600px;
37645
+ }
37646
+
37647
+ @media (max-width: 800px) {
37648
+ .characterCreatorGrid {
37649
+ grid-template-columns: 1fr;
37650
+ }
37651
+ }
37618
37652
</style>
37619
37653
<script>
37620
37654
class PopupUtils
@@ -37893,7 +37927,9 @@ flowchart TD\n${treeToViewOutput.outputText.trim()}`
37893
37927
}
37894
37928
popupUtils.reset().title(`Character List (${allCharacterNames.length})`)
37895
37929
containers.forEach(container => popupUtils.content(container))
37896
- popupUtils.button("Upload characters, lorebooks and world info", () => {
37930
+ popupUtils
37931
+ .button("New character", () => { try { showCharacterCreator(); } catch(e){ console.error(e); } })
37932
+ .button("Upload characters, lorebooks and world info", () => {
37897
37933
popupUtils.reset()
37898
37934
promptUserForLocalFile(async (result) => {
37899
37935
let { file, fileName, ext, content, plaintext, dataArr } = result;
@@ -37964,5 +38000,256 @@ flowchart TD\n${treeToViewOutput.outputText.trim()}`
37964
38000
waitingToast.hide()
37965
38001
}).button("Close", () => popupUtils.reset()).show();
37966
38002
}
38003
+
38004
+ // Native character creator popup for esobold
38005
+ function showCharacterCreator() {
38006
+ const form = document.createElement('div');
38007
+ form.classList.add("characterCreatorGrid")
38008
+
38009
+ const makeLabel = (text) => {
38010
+ const l = document.createElement('label');
38011
+ l.innerText = text;
38012
+ return l;
38013
+ };
38014
+ const makeInput = (type='text') => {
38015
+ const i = document.createElement('input');
38016
+ i.type = type;
38017
+ i.classList.add('textbox');
38018
+ return i;
38019
+ };
38020
+ const makeArea = () => {
38021
+ const a = document.createElement('textarea');
38022
+ a.classList.add('textbox');
38023
+ a.rows = 4;
38024
+ return a;
38025
+ };
38026
+ const addOption = (select, value, text) => {
38027
+ const o = document.createElement('option');
38028
+ o.value = value;
38029
+ o.text = text;
38030
+ select.appendChild(o);
38031
+ };
38032
+
38033
+ const nameInp = makeInput();
38034
+ const creatorInp = makeInput();
38035
+ const versionInp = makeInput();
38036
+ versionInp.placeholder = '1.0';
38037
+ const tagsInp = makeInput();
38038
+ tagsInp.placeholder = 'comma,separated,tags';
38039
+ const personalityArea = makeArea();
38040
+ const descriptionArea = makeArea();
38041
+ const mesExampleArea = makeArea();
38042
+ const firstMesArea = makeArea();
38043
+ firstMesArea.rows = 3;
38044
+ const altGreetingsArea = makeArea();
38045
+ altGreetingsArea.placeholder = 'One greeting per line';
38046
+ const creatorNotesArea = makeArea();
38047
+ const systemPromptArea = makeArea();
38048
+ const postHistoryArea = makeArea();
38049
+
38050
+ // World Info group selector
38051
+ const wiGroupSelect = document.createElement('select');
38052
+ wiGroupSelect.classList.add('textbox');
38053
+ const wiCustomGroup = makeInput();
38054
+ wiCustomGroup.placeholder = 'Or enter custom WI group (optional)';
38055
+ addOption(wiGroupSelect, '', '— No WI group —');
38056
+ try {
38057
+ const potentialWIGroups = (current_wi || []).map(w => {
38058
+ return (w?.wigroup || '').trim()
38059
+ }).filter(x => x.length > 0);
38060
+ const distinctWIGroups = [...new Set(potentialWIGroups)].sort();
38061
+ distinctWIGroups.forEach(g => addOption(wiGroupSelect, g, g));
38062
+ }
38063
+ catch(e) {
38064
+ handleError(e)
38065
+ }
38066
+
38067
+ // Image upload and preview
38068
+ let imageFile = null, imagePreview = document.createElement('img');
38069
+ imagePreview.style.maxHeight = '120px';
38070
+ imagePreview.style.borderRadius = '8px';
38071
+ imagePreview.style.display = 'none';
38072
+ const imageInp = makeInput('file');
38073
+ imageInp.accept = 'image/*';
38074
+ imageInp.onchange = () => {
38075
+ const f = imageInp.files?.[0];
38076
+ if (!f) {
38077
+ return;
38078
+ }
38079
+ imageFile = f;
38080
+ try {
38081
+ const url = URL.createObjectURL(f);
38082
+ imagePreview.src = url;
38083
+ imagePreview.style.display = '';
38084
+ // best-effort revoke after load
38085
+ imagePreview.onload = () => {
38086
+ try {
38087
+ URL.revokeObjectURL(url);
38088
+ }
38089
+ catch(e) {
38090
+ handleError(e)
38091
+ }
38092
+ };
38093
+ }
38094
+ catch(e) {
38095
+ handleError(e)
38096
+ }
38097
+ };
38098
+
38099
+ // Layout: two columns
38100
+ const col = () => {
38101
+ const d=document.createElement('div');
38102
+ d.style.display='flex';
38103
+ d.style.flexDirection='column';
38104
+ d.style.gap='8px';
38105
+ return d;
38106
+ };
38107
+ const leftCol = col();
38108
+ const rightCol = col();
38109
+ const add = (container, label, control, extra=null) => {
38110
+ container.append(label, control);
38111
+ if(extra) {
38112
+ container.append(extra);
38113
+ }
38114
+ };
38115
+
38116
+ // Left (primary)
38117
+ add(leftCol, makeLabel('Name'), nameInp);
38118
+ add(leftCol, makeLabel('Creator'), creatorInp);
38119
+ add(leftCol, makeLabel('Version'), versionInp);
38120
+ add(leftCol, makeLabel('Tags'), tagsInp);
38121
+ add(leftCol, makeLabel('Personality'), personalityArea);
38122
+ add(leftCol, makeLabel('Memory / Description'), descriptionArea);
38123
+ add(leftCol, makeLabel('First message'), firstMesArea);
38124
+
38125
+ // Right (secondary)
38126
+ add(rightCol, makeLabel('Example dialogue'), mesExampleArea);
38127
+ add(rightCol, makeLabel('Alternate greetings (one per line)'), altGreetingsArea);
38128
+ add(rightCol, makeLabel('Creator notes'), creatorNotesArea);
38129
+ add(rightCol, makeLabel('System prompt'), systemPromptArea);
38130
+ add(rightCol, makeLabel('Post history instructions'), postHistoryArea);
38131
+ add(rightCol, makeLabel('World Info group'), wiGroupSelect, wiCustomGroup);
38132
+ add(rightCol, makeLabel('Avatar image'), imageInp, imagePreview);
38133
+
38134
+ form.append(leftCol, rightCol);
38135
+
38136
+ const doSave = async () => {
38137
+ const name = (nameInp.value||'').trim();
38138
+ if (!name) {
38139
+ alert('Character must have a name.');
38140
+ return;
38141
+ }
38142
+ if (!imageFile) {
38143
+ alert('Please choose an avatar image.');
38144
+ return;
38145
+ }
38146
+
38147
+ // Confirm overwrite if needed
38148
+ const exists = (allCharacterNames || []).some(c => c?.name === name);
38149
+ if (exists && !confirm(`Character "${name}" exists. Overwrite?`)) {
38150
+ return;
38151
+ }
38152
+
38153
+ // Build Tavern v2-compatible inner data object (esobold stores inner fields)
38154
+ const tags = (tagsInp.value||'')
38155
+ .split(',')
38156
+ .map(s => s.trim())
38157
+ .filter(s => s.length>0);
38158
+ const altGreetings = (altGreetingsArea.value||'')
38159
+ .split(/\r?\n/)
38160
+ .map(s => s.trim())
38161
+ .filter(s => s.length>0);
38162
+
38163
+ const selectedGroup = (wiCustomGroup.value||'').trim() || wiGroupSelect.value;
38164
+ let character_book = null;
38165
+ if (selectedGroup) {
38166
+ try {
38167
+ const entries = (current_wi||[]).filter(e => (e?.wigroup||'') === selectedGroup);
38168
+ let id = 0, convertedEntries = entries.map(entry => {
38169
+ let convertedEntry = Object.assign(entry, {
38170
+ keys: entry?.key?.split(",").filter(k => k !== "") || [],
38171
+ secondary_keys: entry?.keysecondary?.split(",").filter(k => k !== "") || [],
38172
+ uid: id++
38173
+ });
38174
+ delete convertedEntry?.key;
38175
+ delete convertedEntry?.keysecondary;
38176
+ delete convertedEntry?.wigroup;
38177
+ return convertedEntry;
38178
+ })
38179
+ character_book = { name: selectedGroup, entries: convertedEntries };
38180
+ }
38181
+ catch(e) {
38182
+ handleError(e)
38183
+ }
38184
+ }
38185
+
38186
+ const charInner = {
38187
+ name,
38188
+ description: descriptionArea.value||'',
38189
+ personality: personalityArea.value||'',
38190
+ mes_example: mesExampleArea.value||'',
38191
+ first_mes: firstMesArea.value||'',
38192
+ creator: creatorInp.value||'',
38193
+ creator_notes: creatorNotesArea.value||'',
38194
+ system_prompt: systemPromptArea.value||'',
38195
+ post_history_instructions: postHistoryArea.value||'',
38196
+ alternate_greetings: altGreetings,
38197
+ character_book,
38198
+ tags,
38199
+ character_version: (versionInp.value||'1.0')
38200
+ };
38201
+
38202
+ try {
38203
+ waitingToast.setText(`Saving character ${name}`);
38204
+ waitingToast.show();
38205
+
38206
+ // Thumbnail and full image
38207
+ const thumbUrl = await generateThumbnail(imageFile, [256,256]);
38208
+ const dataUrl = await new Promise((resolve, reject) => {
38209
+ const fr = new FileReader();
38210
+ fr.onerror = reject;
38211
+ fr.onload = () => resolve(fr.result);
38212
+ fr.readAsDataURL(imageFile);
38213
+ });
38214
+
38215
+ const toSave = { name, data: charInner, image: String(dataUrl) };
38216
+ await indexeddb_save(`character_${name}`, JSON.stringify(toSave));
38217
+
38218
+ // Update list
38219
+ allCharacterNames = (allCharacterNames||[]).filter(c => c?.name !== name);
38220
+ allCharacterNames.push({ name, thumbnail: thumbUrl, type: 'Character' });
38221
+ await updateCharacterListFromAll();
38222
+
38223
+ waitingToast.hide();
38224
+ popupUtils.reset();
38225
+ showCharacterList();
38226
+ }
38227
+ catch (e) {
38228
+ handleError(e);
38229
+ waitingToast.hide();
38230
+ }
38231
+ };
38232
+
38233
+ popupUtils.reset()
38234
+ .title('New Character')
38235
+ .content(form)
38236
+ .button('Back', showCharacterList)
38237
+ .button('Save', doSave)
38238
+ .button('Close', () => popupUtils.reset())
38239
+ .show();
38240
+ }
38241
+
38242
+ // Backwards-compat for existing button in scenarios
38243
+ function character_creator() {
38244
+ try
38245
+ {
38246
+ hide_popups();
38247
+ showCharacterCreator();
38248
+ }
38249
+ catch(e)
38250
+ {
38251
+ handleError(e);
38252
+ }
38253
+ }
37967
38254
</script>
37968
38255
</html>
0 commit comments