Merge branch 'staging' into claude-caching-at-depth

c3483bc4323c40cf3a936c5ae4fbd3c47fabe219

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

18 files changed, +411 -164Showing whitespace changes
default/content/Char_Avatar_Comfy_Workflow.json+137 -0
@@ -0,0 +1,137 @@
1{
2 "3": {
3 "inputs": {
4 "seed": "%seed%",
5 "steps": "%steps%",
6 "cfg": "%scale%",
7 "sampler_name": "%sampler%",
8 "scheduler": "%scheduler%",
9 "denoise": "%denoise%",
10 "model": [
11 "4",
12 0
13 ],
14 "positive": [
15 "6",
16 0
17 ],
18 "negative": [
19 "7",
20 0
21 ],
22 "latent_image": [
23 "12",
24 0
25 ]
26 },
27 "class_type": "KSampler",
28 "_meta": {
29 "title": "KSampler"
30 }
31 },
32 "4": {
33 "inputs": {
34 "ckpt_name": "%model%"
35 },
36 "class_type": "CheckpointLoaderSimple",
37 "_meta": {
38 "title": "Load Checkpoint"
39 }
40 },
41 "6": {
42 "inputs": {
43 "text": "%prompt%",
44 "clip": [
45 "4",
46 1
47 ]
48 },
49 "class_type": "CLIPTextEncode",
50 "_meta": {
51 "title": "CLIP Text Encode (Prompt)"
52 }
53 },
54 "7": {
55 "inputs": {
56 "text": "%negative_prompt%",
57 "clip": [
58 "4",
59 1
60 ]
61 },
62 "class_type": "CLIPTextEncode",
63 "_meta": {
64 "title": "CLIP Text Encode (Negative Prompt)"
65 }
66 },
67 "8": {
68 "inputs": {
69 "samples": [
70 "3",
71 0
72 ],
73 "vae": [
74 "4",
75 2
76 ]
77 },
78 "class_type": "VAEDecode",
79 "_meta": {
80 "title": "VAE Decode"
81 }
82 },
83 "9": {
84 "inputs": {
85 "filename_prefix": "SillyTavern",
86 "images": [
87 "8",
88 0
89 ]
90 },
91 "class_type": "SaveImage",
92 "_meta": {
93 "title": "Save Image"
94 }
95 },
96 "10": {
97 "inputs": {
98 "image": "%char_avatar%"
99 },
100 "class_type": "ETN_LoadImageBase64",
101 "_meta": {
102 "title": "Load Image (Base64) [https://github.com/Acly/comfyui-tooling-nodes]"
103 }
104 },
105 "12": {
106 "inputs": {
107 "pixels": [
108 "13",
109 0
110 ],
111 "vae": [
112 "4",
113 2
114 ]
115 },
116 "class_type": "VAEEncode",
117 "_meta": {
118 "title": "VAE Encode"
119 }
120 },
121 "13": {
122 "inputs": {
123 "upscale_method": "bicubic",
124 "width": "%width%",
125 "height": "%height%",
126 "crop": "center",
127 "image": [
128 "10",
129 0
130 ]
131 },
132 "class_type": "ImageScale",
133 "_meta": {
134 "title": "Upscale Image"
135 }
136 }
137}
default/content/index.json+4 -0
@@ -136,6 +136,10 @@
136 "type": "workflow"136 "type": "workflow"
137 },137 },
138 {138 {
139 "filename": "Char_Avatar_Comfy_Workflow.json",
140 "type": "workflow"
141 },
142 {
139 "filename": "presets/kobold/Ace of Spades.json",143 "filename": "presets/kobold/Ace of Spades.json",
140 "type": "kobold_preset"144 "type": "kobold_preset"
141 },145 },
package-lock.json+3 -3
@@ -3019,9 +3019,9 @@
3019 }3019 }
3020 },3020 },
3021 "node_modules/cross-spawn": {3021 "node_modules/cross-spawn": {
3022 "version": "7.0.3",3022 "version": "7.0.5",
3023 "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",3023 "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
3024 "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",3024 "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
3025 "license": "MIT",3025 "license": "MIT",
3026 "dependencies": {3026 "dependencies": {
3027 "path-key": "^3.1.0",3027 "path-key": "^3.1.0",
public/css/logprobs.css+8 -0
@@ -72,6 +72,14 @@
72 opacity: 0.5;72 opacity: 0.5;
73}73}
7474
75.logprobs_output_prefix:hover {
76 background-color: rgba(255, 0, 50, 0.4);
77}
78
79.logprobs_output_prefix:hover ~ .logprobs_output_prefix {
80 background-color: rgba(255, 0, 50, 0.4);
81}
82
75.logprobs_candidate_list {83.logprobs_candidate_list {
76 grid-row-start: 3;84 grid-row-start: 3;
77 grid-row-end: 4;85 grid-row-end: 4;
public/css/scrollable-button.css+19 -0
@@ -0,0 +1,19 @@
1.scrollable-buttons-container {
2 max-height: 50vh; /* Use viewport height instead of fixed pixels */
3 overflow-y: auto;
4 -webkit-overflow-scrolling: touch; /* Momentum scrolling on iOS */
5 margin-top: 1rem; /* m-t-1 is equivalent to margin-top: 1rem; */
6 flex-shrink: 1;
7 min-height: 0;
8 scrollbar-width: thin;
9 scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
10}
11
12.scrollable-buttons-container::-webkit-scrollbar {
13 width: 6px;
14}
15
16.scrollable-buttons-container::-webkit-scrollbar-thumb {
17 background-color: rgba(255, 255, 255, 0.3);
18 border-radius: 3px;
19}
public/index.html+8 -2
@@ -3026,6 +3026,7 @@
3026 <option value="codestral-latest">codestral-latest</option>3026 <option value="codestral-latest">codestral-latest</option>
3027 <option value="codestral-mamba-latest">codestral-mamba-latest</option>3027 <option value="codestral-mamba-latest">codestral-mamba-latest</option>
3028 <option value="pixtral-12b-latest">pixtral-12b-latest</option>3028 <option value="pixtral-12b-latest">pixtral-12b-latest</option>
3029 <option value="pixtral-large-latest">pixtral-large-latest</option>
3029 </optgroup>3030 </optgroup>
3030 <optgroup label="Sub-versions">3031 <optgroup label="Sub-versions">
3031 <option value="open-mistral-nemo-2407">open-mistral-nemo-2407</option>3032 <option value="open-mistral-nemo-2407">open-mistral-nemo-2407</option>
@@ -3040,10 +3041,12 @@
3040 <option value="mistral-medium-2312">mistral-medium-2312</option>3041 <option value="mistral-medium-2312">mistral-medium-2312</option>
3041 <option value="mistral-large-2402">mistral-large-2402</option>3042 <option value="mistral-large-2402">mistral-large-2402</option>
3042 <option value="mistral-large-2407">mistral-large-2407</option>3043 <option value="mistral-large-2407">mistral-large-2407</option>
3044 <option value="mistral-large-2411">mistral-large-2411</option>
3043 <option value="codestral-2405">codestral-2405</option>3045 <option value="codestral-2405">codestral-2405</option>
3044 <option value="codestral-2405-blue">codestral-2405-blue</option>3046 <option value="codestral-2405-blue">codestral-2405-blue</option>
3045 <option value="codestral-mamba-2407">codestral-mamba-2407</option>3047 <option value="codestral-mamba-2407">codestral-mamba-2407</option>
3046 <option value="pixtral-12b-2409">pixtral-12b-2409</option>3048 <option value="pixtral-12b-2409">pixtral-12b-2409</option>
3049 <option value="pixtral-large-2411">pixtral-large-2411</option>
3047 </optgroup>3050 </optgroup>
3048 <optgroup id="mistralai_other_models" label="Other"></optgroup>3051 <optgroup id="mistralai_other_models" label="Other"></optgroup>
3049 </select>3052 </select>
@@ -4503,7 +4506,7 @@
4503 </div>4506 </div>
4504 </div>4507 </div>
4505 <div name="AutoCompleteToggle" class="inline-drawer wide100p flexFlowColumn">4508 <div name="AutoCompleteToggle" class="inline-drawer wide100p flexFlowColumn">
4506 <div class="inline-drawer-toggle inline-drawer-header userSettingsInnerExpandable" title="Options for the various autocompelte input boxes.">4509 <div class="inline-drawer-toggle inline-drawer-header userSettingsInnerExpandable" title="Options for the various autocomplete input boxes.">
4507 <b><span data-i18n="AutoComplete Settings">AutoComplete Settings</span></b>4510 <b><span data-i18n="AutoComplete Settings">AutoComplete Settings</span></b>
4508 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>4511 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
4509 </div>4512 </div>
@@ -6631,8 +6634,11 @@
6631 </div>6634 </div>
6632 </div>6635 </div>
6633 <div class="logprobs_panel_content inline-drawer-content flex-container flexFlowColumn">6636 <div class="logprobs_panel_content inline-drawer-content flex-container flexFlowColumn">
6634 <small>6637 <small class="flex-container alignItemsCenter justifySpaceBetween flexNoWrap">
6635 <b data-i18n="Select a token to see alternatives considered by the AI.">Select a token to see alternatives considered by the AI.</b>6638 <b data-i18n="Select a token to see alternatives considered by the AI.">Select a token to see alternatives considered by the AI.</b>
6639 <button id="logprobsReroll" class="menu_button margin0" title="Reroll with the entire prefix" data-i18n="[title]Reroll with the entire prefix">
6640 <span class="fa-solid fa-redo logprobs_reroll"></span>
6641 </button>
6636 </small>6642 </small>
6637 <hr>6643 <hr>
6638 <div id="logprobs_generation_output"></div>6644 <div id="logprobs_generation_output"></div>
public/scripts/RossAscends-mods.js+4 -7
@@ -703,16 +703,13 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
703 */703 */
704function autoFitSendTextArea() {704function autoFitSendTextArea() {
705 const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);705 const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
706 if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {706
707 const sendTextAreaMinHeight = '0px';707 sendTextArea.style.height = '1px'; // Reset height to 1px to force recalculation of scrollHeight
708 sendTextArea.style.height = sendTextAreaMinHeight;708 const newHeight = sendTextArea.scrollHeight;
709 }
710 const newHeight = sendTextArea.scrollHeight + 3;
711 sendTextArea.style.height = `${newHeight}px`;709 sendTextArea.style.height = `${newHeight}px`;
712710
713 if (!isFirefox) {711 if (!isFirefox) {
714 const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));712 chatBlock.scrollTop = chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom);
715 chatBlock.scrollTop = newScrollTop;
716 }713 }
717}714}
718export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short);715export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short);
public/scripts/extensions/caption/settings.html+2 -0
@@ -37,6 +37,8 @@
37 <select id="caption_multimodal_model" class="flex1 text_pole">37 <select id="caption_multimodal_model" class="flex1 text_pole">
38 <option data-type="mistral" value="pixtral-12b-latest">pixtral-12b-latest</option>38 <option data-type="mistral" value="pixtral-12b-latest">pixtral-12b-latest</option>
39 <option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option>39 <option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option>
40 <option data-type="mistral" value="pixtral-large-latest">pixtral-large-latest</option>
41 <option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option>
40 <option data-type="zerooneai" value="yi-vision">yi-vision</option>42 <option data-type="zerooneai" value="yi-vision">yi-vision</option>
41 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>43 <option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
42 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>44 <option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html+1 -0
@@ -17,6 +17,7 @@
17 <li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li>17 <li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li>
18 <li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li>18 <li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li>
19 <li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li>19 <li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li>
20 <li data-placeholder="denoise" class="sd_comfy_workflow_editor_not_found">"%denoise%"</li>
20 <li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li>21 <li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li>
21 <li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>22 <li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>
22 <li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>23 <li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>
public/scripts/extensions/stable-diffusion/index.js+8 -2
@@ -3269,6 +3269,10 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
32693269
3270 const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);3270 const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
3271 workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));3271 workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
3272
3273 const denoising_strength = extension_settings.sd.denoising_strength === undefined ? 1.0 : extension_settings.sd.denoising_strength;
3274 workflow = workflow.replaceAll('"%denoise%"', JSON.stringify(denoising_strength));
3275
3272 placeholders.forEach(ph => {3276 placeholders.forEach(ph => {
3273 workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));3277 workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
3274 });3278 });
@@ -3279,7 +3283,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3279 const response = await fetch(getUserAvatarUrl());3283 const response = await fetch(getUserAvatarUrl());
3280 if (response.ok) {3284 if (response.ok) {
3281 const avatarBlob = await response.blob();3285 const avatarBlob = await response.blob();
3282 const avatarBase64 = await getBase64Async(avatarBlob);3286 const avatarBase64DataUrl = await getBase64Async(avatarBlob);
3287 const avatarBase64 = avatarBase64DataUrl.split(',')[1];
3283 workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64));3288 workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64));
3284 } else {3289 } else {
3285 workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));3290 workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
@@ -3289,7 +3294,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3289 const response = await fetch(getCharacterAvatarUrl());3294 const response = await fetch(getCharacterAvatarUrl());
3290 if (response.ok) {3295 if (response.ok) {
3291 const avatarBlob = await response.blob();3296 const avatarBlob = await response.blob();
3292 const avatarBase64 = await getBase64Async(avatarBlob);3297 const avatarBase64DataUrl = await getBase64Async(avatarBlob);
3298 const avatarBase64 = avatarBase64DataUrl.split(',')[1];
3293 workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64));3299 workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64));
3294 } else {3300 } else {
3295 workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));3301 workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
public/scripts/extensions/stable-diffusion/settings.html+1 -1
@@ -319,7 +319,7 @@
319 <input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" >319 <input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" >
320 </div>320 </div>
321321
322 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad">322 <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy">
323 <small>323 <small>
324 <span data-i18n="Denoising strength">Denoising strength</span>324 <span data-i18n="Denoising strength">Denoising strength</span>
325 </small>325 </small>
public/scripts/extensions/vectors/index.js+15 -5
@@ -23,7 +23,7 @@ import {
23import { collapseNewlines, registerDebugFunction } from '../../power-user.js';23import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
24import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';24import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
25import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';25import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
26import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence } from '../../utils.js';26import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence, escapeHtml } from '../../utils.js';
27import { debounce_timeout } from '../../constants.js';27import { debounce_timeout } from '../../constants.js';
28import { getSortedEntries } from '../../world-info.js';28import { getSortedEntries } from '../../world-info.js';
29import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';29import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
@@ -44,6 +44,9 @@ const MODULE_NAME = 'vectors';
44export const EXTENSION_PROMPT_TAG = '3_vectors';44export const EXTENSION_PROMPT_TAG = '3_vectors';
45export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank';45export const EXTENSION_PROMPT_TAG_DB = '4_vectors_data_bank';
4646
47// Force solo chunks for sources that don't support batching.
48const getBatchSize = () => ['transformers', 'palm', 'ollama'].includes(settings.source) ? 1 : 5;
49
47const settings = {50const settings = {
48 // For both51 // For both
49 source: 'transformers',52 source: 'transformers',
@@ -125,7 +128,7 @@ async function onVectorizeAllClick() {
125 // upon request of a full vectorise128 // upon request of a full vectorise
126 cachedSummaries.clear();129 cachedSummaries.clear();
127130
128 const batchSize = 5;131 const batchSize = getBatchSize();
129 const elapsedLog = [];132 const elapsedLog = [];
130 let finished = false;133 let finished = false;
131 $('#vectorize_progress').show();134 $('#vectorize_progress').show();
@@ -560,7 +563,9 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
560 fileText = translatedText;563 fileText = translatedText;
561 }564 }
562565
563 const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);566 const batchSize = getBatchSize();
567 const toastBody = $('<span>').text('This may take a while. Please wait...');
568 const toast = toastr.info(toastBody, `Ingesting file ${escapeHtml(fileName)}`, { closeButton: false, escapeHtml: false, timeOut: 0, extendedTimeOut: 0 });
564 const overlapSize = Math.round(chunkSize * overlapPercent / 100);569 const overlapSize = Math.round(chunkSize * overlapPercent / 100);
565 const delimiters = getChunkDelimiters();570 const delimiters = getChunkDelimiters();
566 // Overlap should not be included in chunk size. It will be later compensated by overlapChunks571 // Overlap should not be included in chunk size. It will be later compensated by overlapChunks
@@ -569,7 +574,12 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overla
569 console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks);574 console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks);
570575
571 const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));576 const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
572 await insertVectorItems(collectionId, items);577
578 for (let i = 0; i < items.length; i += batchSize) {
579 toastBody.text(`${i}/${items.length} (${Math.round((i / items.length) * 100)}%) chunks processed`);
580 const chunkedBatch = items.slice(i, i + batchSize);
581 await insertVectorItems(collectionId, chunkedBatch);
582 }
573583
574 toastr.clear(toast);584 toastr.clear(toast);
575 console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);585 console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);
@@ -1050,7 +1060,7 @@ async function onViewStatsClick() {
1050 toastr.info(`Total hashes: <b>${totalHashes}</b><br>1060 toastr.info(`Total hashes: <b>${totalHashes}</b><br>
1051 Unique hashes: <b>${uniqueHashes}</b><br><br>1061 Unique hashes: <b>${uniqueHashes}</b><br><br>
1052 I'll mark collected messages with a green circle.`,1062 I'll mark collected messages with a green circle.`,
1053 `Stats for chat ${chatId}`,1063 `Stats for chat ${escapeHtml(chatId)}`,
1054 { timeOut: 10000, escapeHtml: false },1064 { timeOut: 10000, escapeHtml: false },
1055 );1065 );
10561066
public/scripts/logprobs.js+129 -114
@@ -1,6 +1,5 @@
1import {1import {
2 animation_duration,2 animation_duration,
3 callPopup,
4 chat,3 chat,
5 cleanUpMessage,4 cleanUpMessage,
6 event_types,5 event_types,
@@ -13,9 +12,12 @@ import {
13import { debounce, delay, getStringHash } from './utils.js';12import { debounce, delay, getStringHash } from './utils.js';
14import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js';13import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js';
15import { power_user } from './power-user.js';14import { power_user } from './power-user.js';
15import { callGenericPopup, POPUP_TYPE } from './popup.js';
16import { t } from './i18n.js';
1617
17const TINTS = 4;18const TINTS = 4;
18const MAX_MESSAGE_LOGPROBS = 100;19const MAX_MESSAGE_LOGPROBS = 100;
20const REROLL_BUTTON = $('#logprobsReroll');
1921
20/**22/**
21 * Tuple of a candidate token and its logarithm of probability of being chosen23 * Tuple of a candidate token and its logarithm of probability of being chosen
@@ -23,6 +25,10 @@ const MAX_MESSAGE_LOGPROBS = 100;
23 */25 */
2426
25/**27/**
28 * @typedef {(Node|JQuery<Text>|JQuery<HTMLElement>)[]} NodeArray - Array of DOM nodes
29 */
30
31/**
26 * Logprob data for a single message32 * Logprob data for a single message
27 * @typedef {Object} MessageLogprobData33 * @typedef {Object} MessageLogprobData
28 * @property {number} created - timestamp of when the message was generated34 * @property {number} created - timestamp of when the message was generated
@@ -43,17 +49,26 @@ const MAX_MESSAGE_LOGPROBS = 100;
43 * @property {Candidate[]} topLogprobs - Array of top candidate tokens49 * @property {Candidate[]} topLogprobs - Array of top candidate tokens
44 */50 */
4551
46let state = {52/**
47 /** @type {TokenLogprobs | null} */53 * State object for Token Probabilities
54 * @typedef {Object} LogprobsState
55 * @property {?TokenLogprobs} selectedTokenLogprobs Log probabilities for
56 * currently-selected token.
57 * @property {Map<number, MessageLogprobData>} messageLogprobs Log probabilities for
58 * each message, keyed by message hash.
59 */
60
61/**
62 * @type {LogprobsState} state
63 */
64const state = {
48 selectedTokenLogprobs: null,65 selectedTokenLogprobs: null,
49 /** @type {Map<number, MessageLogprobData>} */
50 messageLogprobs: new Map(),66 messageLogprobs: new Map(),
51};67};
5268
53/**69/**
54 * renderAlternativeTokensView renders the Token Probabilities UI and all70 * Renders the Token Probabilities UI and all subviews with the active message's
55 * subviews with the active message's logprobs data. If the message has no token71 * logprobs data. If the message has no token logprobs, a message is displayed.
56 * logprobs, a zero-state is rendered.
57 */72 */
58function renderAlternativeTokensView() {73function renderAlternativeTokensView() {
59 const view = $('#logprobs_generation_output');74 const view = $('#logprobs_generation_output');
@@ -68,13 +83,14 @@ function renderAlternativeTokensView() {
68 const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming;83 const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming;
69 if (!messageLogprobs?.length || usingSmoothStreaming) {84 if (!messageLogprobs?.length || usingSmoothStreaming) {
70 const emptyState = $('<div></div>');85 const emptyState = $('<div></div>');
71 const noTokensMsg = usingSmoothStreaming86 const noTokensMsg = !power_user.request_token_probabilities
72 ? 'Token probabilities are not available when using Smooth Streaming.'87 ? '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>'
73 : 'No token probabilities available for the current message.';88 : usingSmoothStreaming
74 const msg = power_user.request_token_probabilities89 ? t`Token probabilities are not available when using Smooth Streaming.`
75 ? noTokensMsg90 : is_send_press
76 : '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>';91 ? t`Generation in progress...`
77 emptyState.html(msg);92 : t`No token probabilities available for the current message.`;
93 emptyState.html(noTokensMsg);
78 emptyState.addClass('logprobs_empty_state');94 emptyState.addClass('logprobs_empty_state');
79 view.append(emptyState);95 view.append(emptyState);
80 return;96 return;
@@ -82,16 +98,34 @@ function renderAlternativeTokensView() {
8298
83 const prefix = continueFrom || '';99 const prefix = continueFrom || '';
84 const tokenSpans = [];100 const tokenSpans = [];
101 REROLL_BUTTON.toggle(!!prefix);
85102
86 if (prefix) {103 if (prefix) {
87 const prefixSpan = $('<span></span>');104 REROLL_BUTTON.off('click').on('click', () => onPrefixClicked(prefix.length));
88 prefixSpan.text(prefix);105
89 prefixSpan.html(prefixSpan.html().replace(/\n/g, '<br>'));106 let cumulativeOffset = 0;
90 prefixSpan.addClass('logprobs_output_prefix');107 const words = prefix.split(/\s+/);
91 prefixSpan.attr('title', 'Select to reroll the last \'Continue\' generation.\nHold the CTRL key when clicking to reroll from before that word.');108 const delimiters = prefix.match(/\s+/g) || []; // Capture the actual delimiters
92 prefixSpan.click(onPrefixClicked);109
93 addKeyboardProps(prefixSpan);110 words.forEach((word, i) => {
94 tokenSpans.push(...withVirtualWhitespace(prefix, prefixSpan));111 const span = $('<span></span>');
112 span.text(`${word} `);
113
114 span.addClass('logprobs_output_prefix');
115 span.attr('title', t`Reroll from this point`);
116
117 let offset = cumulativeOffset;
118 span.on('click', () => onPrefixClicked(offset));
119 addKeyboardProps(span);
120
121 tokenSpans.push(span);
122 tokenSpans.push(delimiters[i]?.includes('\n')
123 ? document.createElement('br')
124 : document.createTextNode(delimiters[i] || ' '),
125 );
126
127 cumulativeOffset += word.length + (delimiters[i]?.length || 0);
128 });
95 }129 }
96130
97 messageLogprobs.forEach((tokenData, i) => {131 messageLogprobs.forEach((tokenData, i) => {
@@ -101,7 +135,7 @@ function renderAlternativeTokensView() {
101 span.text(text);135 span.text(text);
102 span.addClass('logprobs_output_token');136 span.addClass('logprobs_output_token');
103 span.addClass('logprobs_tint_' + (i % TINTS));137 span.addClass('logprobs_tint_' + (i % TINTS));
104 span.click(() => onSelectedTokenChanged(tokenData, span));138 span.on('click', () => onSelectedTokenChanged(tokenData, span));
105 addKeyboardProps(span);139 addKeyboardProps(span);
106 tokenSpans.push(...withVirtualWhitespace(token, span));140 tokenSpans.push(...withVirtualWhitespace(token, span));
107 });141 });
@@ -129,6 +163,10 @@ function addKeyboardProps(element) {
129/**163/**
130 * renderTopLogprobs renders the top logprobs subview with the currently164 * renderTopLogprobs renders the top logprobs subview with the currently
131 * selected token highlighted. If no token is selected, the subview is hidden.165 * selected token highlighted. If no token is selected, the subview is hidden.
166 *
167 * Callers:
168 * - renderAlternativeTokensView, to render the entire view
169 * - onSelectedTokenChanged, to update the view when a token is selected
132 */170 */
133function renderTopLogprobs() {171function renderTopLogprobs() {
134 $('#logprobs_top_logprobs_hint').hide();172 $('#logprobs_top_logprobs_hint').hide();
@@ -150,8 +188,7 @@ function renderTopLogprobs() {
150 const probability = Math.exp(log);188 const probability = Math.exp(log);
151 sum += probability;189 sum += probability;
152 return [text, probability, log];190 return [text, probability, log];
153 }191 } else {
154 else {
155 return [text, log, null];192 return [text, log, null];
156 }193 }
157 });194 });
@@ -167,15 +204,15 @@ function renderTopLogprobs() {
167 container.addClass('selected');204 container.addClass('selected');
168 }205 }
169206
170 const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token)}`);207 const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token.toString())}`);
171 const percentText = $('<span></span>').text(`${(probability * 100).toFixed(2)}%`);208 const percentText = $('<span></span>').text(`${(+probability * 100).toFixed(2)}%`);
172 container.append(tokenText, percentText);209 container.append(tokenText, percentText);
173 if (log) {210 if (log) {
174 container.attr('title', `logarithm: ${log}`);211 container.attr('title', `logarithm: ${log}`);
175 }212 }
176 addKeyboardProps(container);213 addKeyboardProps(container);
177 if (token !== '<others>') {214 if (token !== '<others>') {
178 container.click(() => onAlternativeClicked(state.selectedTokenLogprobs, token));215 container.on('click', () => onAlternativeClicked(state.selectedTokenLogprobs, token.toString()));
179 } else {216 } else {
180 container.prop('disabled', true);217 container.prop('disabled', true);
181 }218 }
@@ -192,11 +229,10 @@ function renderTopLogprobs() {
192}229}
193230
194/**231/**
195 * onSelectedTokenChanged is called when the user clicks on a token in the232 * User clicks on a token in the token output view. It updates the selected token state
196 * token output view. It updates the selected token state and re-renders the233 * and re-renders the top logprobs view, or deselects the token if it was already selected.
197 * top logprobs view, or deselects the token if it was already selected.
198 * @param {TokenLogprobs} logprobs - logprob data for the selected token234 * @param {TokenLogprobs} logprobs - logprob data for the selected token
199 * @param {Element} span - target span node that was clicked235 * @param {Node|JQuery} span - target span node that was clicked
200 */236 */
201function onSelectedTokenChanged(logprobs, span) {237function onSelectedTokenChanged(logprobs, span) {
202 $('.logprobs_output_token.selected').removeClass('selected');238 $('.logprobs_output_token.selected').removeClass('selected');
@@ -223,7 +259,10 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
223 }259 }
224260
225 if (getGeneratingApi() === 'openai') {261 if (getGeneratingApi() === 'openai') {
226 return callPopup('<h3>Feature unavailable</h3><p>Due to API limitations, rerolling a token is not supported with OpenAI. Try switching to a different API.</p>', 'text');262 const title = t`Feature unavailable`;
263 const message = t`Due to API limitations, rerolling a token is not supported with OpenAI. Try switching to a different API.`;
264 const content = `<h3>${title}</h3><p>${message}</p>`;
265 return callGenericPopup(content, POPUP_TYPE.TEXT);
227 }266 }
228267
229 const { messageLogprobs, continueFrom } = getActiveMessageLogprobData();268 const { messageLogprobs, continueFrom } = getActiveMessageLogprobData();
@@ -234,79 +273,29 @@ function onAlternativeClicked(tokenLogprobs, alternative) {
234273
235 const prefix = continueFrom || '';274 const prefix = continueFrom || '';
236 const prompt = prefix + tokens.join('');275 const prompt = prefix + tokens.join('');
237 const messageId = chat.length - 1;276 addGeneration(prompt);
238 createSwipe(messageId, prompt);
239
240 $('.swipe_right:last').click(); // :see_no_evil:
241
242 Generate('continue').then(_ => void _);
243}277}
244278
245/**279/**
246 * getTextBeforeClickedWord retrieves the portion of text within a span280 * User clicks on the reroll button in the token output view, or on a word in the
247 * that appears before the word clicked by the user. Using the x and y281 * prefix. Retrieve the prefix for the current message and truncate it at the
248 * coordinates from a PointerEvent, this function identifies the exact282 * offset for the selected word. Then request a `continue` completion from the
249 * word clicked and returns the text preceding it within the span.283 * model with the new prompt.
250 *
251 * If the clicked position does not resolve to a valid word or text node,
252 * the entire span text is returned as a fallback.
253 *284 *
254 * @param {PointerEvent} event - The click event containing the x and y coordinates.285 * If no offset is provided, the entire prefix will be rerolled.
255 * @param {string} spanText - The full text content of the span element.
256 * @returns {string} The text before the clicked word, or the entire span text as fallback.
257 */
258function getTextBeforeClickedWord(event, spanText) {
259 const x = event.clientX;
260 const y = event.clientY;
261 const range = document.caretRangeFromPoint(x, y);
262
263 if (range && range.startContainer.nodeType === Node.TEXT_NODE) {
264 const textNode = range.startContainer;
265 const offset = range.startOffset;
266
267 // Get the full text content of the text node
268 const text = textNode.nodeValue;
269
270 // Find the boundaries of the clicked word
271 const start = text.lastIndexOf(' ', offset - 1) + 1;
272
273 // Return the text before the clicked word
274 return text.slice(0, start);
275 }
276
277 // If we can't determine the exact word, return the full span text as a fallback
278 return spanText;
279}
280
281
282/**
283 * onPrefixClicked is called when the user clicks on the carried-over prefix
284 * in the token output view. It allows them to reroll the last 'continue'
285 * completion with none of the output generated from it, in case they don't
286 * like the results.
287 *286 *
288 * If the user holds the Ctrl key while clicking, only the portion of text287 * @param {number} offset - index of the token in the prefix to reroll from
289 * before the clicked word is retained as the prefix for rerolling288 * @returns {void}
289 * @param offset
290 */290 */
291function onPrefixClicked() {291function onPrefixClicked(offset = undefined) {
292 if (!checkGenerateReady()) {292 if (!checkGenerateReady()) {
293 return;293 return;
294 }294 }
295295
296 const { continueFrom } = getActiveMessageLogprobData();296 const { continueFrom } = getActiveMessageLogprobData() || {};
297 const messageId = chat.length - 1;297 const prefix = continueFrom ? continueFrom.substring(0, offset) : '';
298298 addGeneration(prefix);
299 // Check if Ctrl key is pressed during the click
300 let prefix = continueFrom || '';
301 if (event.ctrlKey) {
302 // Ctrl is pressed - use the text before the clicked word
303 prefix = getTextBeforeClickedWord(event, continueFrom);
304 }
305
306 // Use the determined `prefix`
307 createSwipe(messageId, prefix);
308 $('.swipe_right:last').click();
309 Generate('continue').then(_ => void _);
310}299}
311300
312function checkGenerateReady() {301function checkGenerateReady() {
@@ -317,6 +306,22 @@ function checkGenerateReady() {
317 return true;306 return true;
318}307}
319308
309/**
310 * Generates a new swipe as a continuation of the given prompt, when user selects
311 * an alternative token or rerolls from a prefix.
312 *
313 * @param prompt
314 */
315function addGeneration(prompt) {
316 const messageId = chat.length - 1;
317 if (prompt && prompt.length > 0) {
318 createSwipe(messageId, prompt);
319 $('.swipe_right:last').trigger('click');
320 void Generate('continue');
321 } else {
322 $('.swipe_right:last').trigger('click');
323 }
324}
320325
321/**326/**
322 * onToggleLogprobsPanel is called when the user performs an action that toggles327 * onToggleLogprobsPanel is called when the user performs an action that toggles
@@ -356,15 +361,14 @@ function onToggleLogprobsPanel() {
356}361}
357362
358/**363/**
359 * createSwipe appends a new swipe to the target chat message with the given364 * Appends a new swipe to the target chat message with the given text.
360 * text.
361 * @param {number} messageId - target chat message ID365 * @param {number} messageId - target chat message ID
362 * @param {string} prompt - initial prompt text which will be continued366 * @param {string} prompt - initial prompt text which will be continued
363 */367 */
364function createSwipe(messageId, prompt) {368function createSwipe(messageId, prompt) {
365 // need to call `cleanUpMessage` on our new prompt, because we were working369 // need to call `cleanUpMessage` on our new prompt, because we were working
366 // with raw model output and our new prompt is missing trimming/macro replacements370 // with raw model output and our new prompt is missing trimming/macro replacements
367 const cleanedPrompt = cleanUpMessage(prompt, false, false);371 const cleanedPrompt = cleanUpMessage(prompt, false, false, true);
368372
369 const msg = chat[messageId];373 const msg = chat[messageId];
370 const newSwipeInfo = {374 const newSwipeInfo = {
@@ -399,10 +403,11 @@ function toVisibleWhitespace(input) {
399 * after the span node if its token begins or ends with whitespace in order to403 * after the span node if its token begins or ends with whitespace in order to
400 * allow text to wrap despite whitespace characters being replaced with a dot.404 * allow text to wrap despite whitespace characters being replaced with a dot.
401 * @param {string} text - token text being evaluated for whitespace405 * @param {string} text - token text being evaluated for whitespace
402 * @param {Element} span - target span node to be wrapped406 * @param {Node|JQuery} span - target span node to be wrapped
403 * @returns {Element[]} array of nodes to be appended to the DOM407 * @returns {NodeArray} - array of nodes to be appended to the parent element
404 */408 */
405function withVirtualWhitespace(text, span) {409function withVirtualWhitespace(text, span) {
410 /** @type {NodeArray} */
406 const result = [span];411 const result = [span];
407 if (text.match(/^\s/)) {412 if (text.match(/^\s/)) {
408 result.unshift(document.createTextNode('\u200b'));413 result.unshift(document.createTextNode('\u200b'));
@@ -430,12 +435,16 @@ function withVirtualWhitespace(text, span) {
430}435}
431436
432/**437/**
433 * saveLogprobsForActiveMessage receives an array of TokenLogprobs objects438 * Receives the top logprobs for each token in a message and associates it with the active message.
434 * representing the top logprobs for each token in a message and associates it439 *
435 * with the active message.440 * Ensure the active message has been updated and rendered before calling this function
441 * or the logprobs data will be saved to the wrong message.
442 *
443 * Callers:
444 * - Generate:onSuccess via saveLogprobsForActiveMessage, for non-streaming text completion
445 * - StreamingProcessor:onFinishStreaming, for streaming text completion
446 * - sendOpenAIRequest, for non-streaming chat completion
436 *447 *
437 * **Ensure the active message has been updated and rendered before calling
438 * this function or the logprobs data will be saved to the wrong message.**
439 * @param {TokenLogprobs[]} logprobs - array of logprobs data for each token448 * @param {TokenLogprobs[]} logprobs - array of logprobs data for each token
440 * @param {string | null} continueFrom - for 'continue' generations, the prompt449 * @param {string | null} continueFrom - for 'continue' generations, the prompt
441 */450 */
@@ -445,7 +454,10 @@ export function saveLogprobsForActiveMessage(logprobs, continueFrom) {
445 return;454 return;
446 }455 }
447456
457 // NovelAI only returns token IDs in logprobs data; convert to text tokens in-place
458 if (getGeneratingApi() === 'novel') {
448 convertTokenIdLogprobsToText(logprobs);459 convertTokenIdLogprobsToText(logprobs);
460 }
449461
450 const msgId = chat.length - 1;462 const msgId = chat.length - 1;
451 /** @type {MessageLogprobData} */463 /** @type {MessageLogprobData} */
@@ -491,17 +503,18 @@ function getActiveMessageLogprobData() {
491 return state.messageLogprobs.get(hash) || null;503 return state.messageLogprobs.get(hash) || null;
492}504}
493505
506
494/**507/**
495 * convertLogprobTokenIdsToText mutates the given logprobs data's topLogprobs508 * convertLogprobTokenIdsToText replaces token IDs in logprobs data with text tokens,
496 * field keyed by token text instead of token ID. This is only necessary for509 * for APIs that return token IDs instead of text tokens, to wit: NovelAI.
497 * APIs which only return token IDs in their logprobs data; for others this510 *
498 * function is a no-op.
499 * @param {TokenLogprobs[]} input - logprobs data with numeric token IDs511 * @param {TokenLogprobs[]} input - logprobs data with numeric token IDs
500 */512 */
501function convertTokenIdLogprobsToText(input) {513function convertTokenIdLogprobsToText(input) {
502 const api = getGeneratingApi();514 const api = getGeneratingApi();
503 if (api !== 'novel') {515 if (api !== 'novel') {
504 return input;516 // should have been checked by the caller
517 throw new Error('convertTokenIdLogprobsToText should only be called for NovelAI');
505 }518 }
506519
507 const tokenizerId = getTokenizerBestMatch(api);520 const tokenizerId = getTokenizerBestMatch(api);
@@ -512,7 +525,8 @@ function convertTokenIdLogprobsToText(input) {
512 )));525 )));
513526
514 // Submit token IDs to tokenizer to get token text, then build ID->text map527 // Submit token IDs to tokenizer to get token text, then build ID->text map
515 const { chunks } = decodeTextTokens(tokenizerId, tokenIds);528 // noinspection JSCheckFunctionSignatures - mutates input in-place
529 const { chunks } = decodeTextTokens(tokenizerId, tokenIds.map(parseInt));
516 const tokenIdText = new Map(tokenIds.map((id, i) => [id, chunks[i]]));530 const tokenIdText = new Map(tokenIds.map((id, i) => [id, chunks[i]]));
517531
518 // Fixup logprobs data with token text532 // Fixup logprobs data with token text
@@ -525,9 +539,10 @@ function convertTokenIdLogprobsToText(input) {
525}539}
526540
527export function initLogprobs() {541export function initLogprobs() {
542 REROLL_BUTTON.hide();
528 const debouncedRender = debounce(renderAlternativeTokensView);543 const debouncedRender = debounce(renderAlternativeTokensView);
529 $('#logprobsViewerClose').click(onToggleLogprobsPanel);544 $('#logprobsViewerClose').on('click', onToggleLogprobsPanel);
530 $('#option_toggle_logprobs').click(onToggleLogprobsPanel);545 $('#option_toggle_logprobs').on('click', onToggleLogprobsPanel);
531 eventSource.on(event_types.CHAT_CHANGED, debouncedRender);546 eventSource.on(event_types.CHAT_CHANGED, debouncedRender);
532 eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, debouncedRender);547 eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, debouncedRender);
533 eventSource.on(event_types.IMPERSONATE_READY, debouncedRender);548 eventSource.on(event_types.IMPERSONATE_READY, debouncedRender);
public/scripts/openai.js+3 -1
@@ -4165,7 +4165,7 @@ async function onModelChange() {
4165 $('#openai_max_context').attr('max', unlocked_max);4165 $('#openai_max_context').attr('max', unlocked_max);
4166 } else if (oai_settings.mistralai_model.includes('codestral-mamba')) {4166 } else if (oai_settings.mistralai_model.includes('codestral-mamba')) {
4167 $('#openai_max_context').attr('max', max_256k);4167 $('#openai_max_context').attr('max', max_256k);
4168 } else if (['mistral-large-2407', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {4168 } else if (['mistral-large-2407', 'mistral-large-2411', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {
4169 $('#openai_max_context').attr('max', max_128k);4169 $('#openai_max_context').attr('max', max_128k);
4170 } else if (oai_settings.mistralai_model.includes('mistral-nemo')) {4170 } else if (oai_settings.mistralai_model.includes('mistral-nemo')) {
4171 $('#openai_max_context').attr('max', max_128k);4171 $('#openai_max_context').attr('max', max_128k);
@@ -4764,6 +4764,8 @@ export function isImageInliningSupported() {
4764 'pixtral-12b-latest',4764 'pixtral-12b-latest',
4765 'pixtral-12b',4765 'pixtral-12b',
4766 'pixtral-12b-2409',4766 'pixtral-12b-2409',
4767 'pixtral-large-latest',
4768 'pixtral-large-2411',
4767 ];4769 ];
47684770
4769 switch (oai_settings.chat_completion_source) {4771 switch (oai_settings.chat_completion_source) {
public/scripts/slash-commands.js+12 -2
@@ -2083,7 +2083,10 @@ async function buttonsCallback(args, text) {
2083 let popup;2083 let popup;
20842084
2085 const buttonContainer = document.createElement('div');2085 const buttonContainer = document.createElement('div');
2086 buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p', 'm-t-1');2086 buttonContainer.classList.add('flex-container', 'flexFlowColumn', 'wide100p');
2087
2088 const scrollableContainer = document.createElement('div');
2089 scrollableContainer.classList.add('scrollable-buttons-container');
20872090
2088 for (const [result, button] of resultToButtonMap) {2091 for (const [result, button] of resultToButtonMap) {
2089 const buttonElement = document.createElement('div');2092 const buttonElement = document.createElement('div');
@@ -2096,9 +2099,16 @@ async function buttonsCallback(args, text) {
2096 buttonContainer.appendChild(buttonElement);2099 buttonContainer.appendChild(buttonElement);
2097 }2100 }
20982101
2102 scrollableContainer.appendChild(buttonContainer);
2103
2099 const popupContainer = document.createElement('div');2104 const popupContainer = document.createElement('div');
2100 popupContainer.innerHTML = safeValue;2105 popupContainer.innerHTML = safeValue;
2101 popupContainer.appendChild(buttonContainer);2106 popupContainer.appendChild(scrollableContainer);
2107
2108 // Ensure the popup uses flex layout
2109 popupContainer.style.display = 'flex';
2110 popupContainer.style.flexDirection = 'column';
2111 popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup
21022112
2103 popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });2113 popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true });
2104 popup.show()2114 popup.show()
public/scripts/textgen-models.js+24 -11
@@ -23,29 +23,42 @@ export let openRouterModels = [];
23const OPENROUTER_PROVIDERS = [23const OPENROUTER_PROVIDERS = [
24 'OpenAI',24 'OpenAI',
25 'Anthropic',25 'Anthropic',
26 'HuggingFace',
27 'Google',26 'Google',
28 'Mancer',27 'Google AI Studio',
29 'Mancer 2',28 'Groq',
29 'SambaNova',
30 'Cohere',
31 'Mistral',
30 'Together',32 'Together',
33 'Together 2',
34 'Fireworks',
31 'DeepInfra',35 'DeepInfra',
36 'Lepton',
37 'Novita',
38 'Avian',
39 'Lambda',
32 'Azure',40 'Azure',
33 'Modal',41 'Modal',
34 'AnyScale',42 'AnyScale',
35 'Replicate',43 'Replicate',
36 'Perplexity',44 'Perplexity',
37 'Recursal',45 'Recursal',
38 'Fireworks',
39 'Mistral',
40 'Groq',
41 'Cohere',
42 'Lepton',
43 'OctoAI',46 'OctoAI',
44 'Novita',
45 'Lynn',
46 'Lynn 2',
47 'DeepSeek',47 'DeepSeek',
48 'Infermatic',48 'Infermatic',
49 'AI21',
50 'Featherless',
51 'Inflection',
52 'xAI',
53 '01.AI',
54 'HuggingFace',
55 'Mancer',
56 'Mancer 2',
57 'Hyperbolic',
58 'Hyperbolic 2',
59 'Lynn 2',
60 'Lynn',
61 'Reflection',
49];62];
5063
51export async function loadOllamaModels(data) {64export async function loadOllamaModels(data) {
public/style.css+1 -0
@@ -9,6 +9,7 @@
9@import url(css/logprobs.css);9@import url(css/logprobs.css);
10@import url(css/accounts.css);10@import url(css/accounts.css);
11@import url(css/tags.css);11@import url(css/tags.css);
12@import url(css/scrollable-button.css);
1213
13:root {14:root {
14 --doc-height: 100%;15 --doc-height: 100%;
src/endpoints/stable-diffusion.js+32 -16
@@ -7,7 +7,7 @@ import sanitize from 'sanitize-filename';
7import { sync as writeFileAtomicSync } from 'write-file-atomic';7import { sync as writeFileAtomicSync } from 'write-file-atomic';
8import FormData from 'form-data';8import FormData from 'form-data';
99
10import { getBasicAuthHeader, delay } from '../util.js';10import { delay, getBasicAuthHeader, tryParse } from '../util.js';
11import { jsonParser } from '../express-common.js';11import { jsonParser } from '../express-common.js';
12import { readSecret, SECRET_KEYS } from './secrets.js';12import { readSecret, SECRET_KEYS } from './secrets.js';
1313
@@ -19,7 +19,7 @@ import { readSecret, SECRET_KEYS } from './secrets.js';
19function getComfyWorkflows(directories) {19function getComfyWorkflows(directories) {
20 return fs20 return fs
21 .readdirSync(directories.comfyWorkflows)21 .readdirSync(directories.comfyWorkflows)
22 .filter(file => file[0] != '.' && file.toLowerCase().endsWith('.json'))22 .filter(file => file[0] !== '.' && file.toLowerCase().endsWith('.json'))
23 .sort(Intl.Collator().compare);23 .sort(Intl.Collator().compare);
24}24}
2525
@@ -67,8 +67,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
6767
68 /** @type {any} */68 /** @type {any} */
69 const data = await result.json();69 const data = await result.json();
70 const names = data.map(x => x.name);70 return data.map(x => x.name);
71 return names;
72 }71 }
7372
74 async function getLatentUpscalers() {73 async function getLatentUpscalers() {
@@ -88,8 +87,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
8887
89 /** @type {any} */88 /** @type {any} */
90 const data = await result.json();89 const data = await result.json();
91 const names = data.map(x => x.name);90 return data.map(x => x.name);
92 return names;
93 }91 }
9492
95 const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]);93 const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]);
@@ -241,8 +239,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
241 'Authorization': getBasicAuthHeader(request.body.auth),239 'Authorization': getBasicAuthHeader(request.body.auth),
242 },240 },
243 });241 });
244 const data = await result.json();242 return await result.json();
245 return data;
246 }243 }
247244
248 const url = new URL(request.body.url);245 const url = new URL(request.body.url);
@@ -274,7 +271,7 @@ router.post('/set-model', jsonParser, async (request, response) => {
274271
275 const progress = progressState['progress'];272 const progress = progressState['progress'];
276 const jobCount = progressState['state']['job_count'];273 const jobCount = progressState['state']['job_count'];
277 if (progress == 0.0 && jobCount === 0) {274 if (progress === 0.0 && jobCount === 0) {
278 break;275 break;
279 }276 }
280277
@@ -412,7 +409,18 @@ comfy.post('/models', jsonParser, async (request, response) => {
412 }409 }
413 /** @type {any} */410 /** @type {any} */
414 const data = await result.json();411 const data = await result.json();
415 return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })));412
413 const ckpts = data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })) || [];
414 const unets = data.UNETLoader.input.required.unet_name[0].map(it => ({ value: it, text: `UNet: ${it}` })) || [];
415
416 // load list of GGUF unets from diffusion_models if the loader node is available
417 const ggufs = data.UnetLoaderGGUF?.input.required.unet_name[0].map(it => ({ value: it, text: `GGUF: ${it}` })) || [];
418 const models = [...ckpts, ...unets, ...ggufs];
419
420 // make the display names of the models somewhat presentable
421 models.forEach(it => it.text = it.text.replace(/\.[^.]*$/, '').replace(/_/g, ' '));
422
423 return response.send(models);
416 } catch (error) {424 } catch (error) {
417 console.log(error);425 console.log(error);
418 return response.sendStatus(500);426 return response.sendStatus(500);
@@ -527,7 +535,8 @@ comfy.post('/generate', jsonParser, async (request, response) => {
527 body: request.body.prompt,535 body: request.body.prompt,
528 });536 });
529 if (!promptResult.ok) {537 if (!promptResult.ok) {
530 throw new Error('ComfyUI returned an error.');538 const text = await promptResult.text();
539 throw new Error('ComfyUI returned an error.', { cause: tryParse(text) });
531 }540 }
532541
533 /** @type {any} */542 /** @type {any} */
@@ -550,7 +559,13 @@ comfy.post('/generate', jsonParser, async (request, response) => {
550 await delay(100);559 await delay(100);
551 }560 }
552 if (item.status.status_str === 'error') {561 if (item.status.status_str === 'error') {
553 throw new Error('ComfyUI generation did not succeed.');562 // Report node tracebacks if available
563 const errorMessages = item.status?.messages
564 ?.filter(it => it[0] === 'execution_error')
565 .map(it => it[1])
566 .map(it => `${it.node_type} [${it.node_id}] ${it.exception_type}: ${it.exception_message}`)
567 .join('\n') || '';
568 throw new Error(`ComfyUI generation did not succeed.\n\n${errorMessages}`.trim());
554 }569 }
555 const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0];570 const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0];
556 const imgUrl = new URL(request.body.url);571 const imgUrl = new URL(request.body.url);
@@ -560,11 +575,12 @@ comfy.post('/generate', jsonParser, async (request, response) => {
560 if (!imgResponse.ok) {575 if (!imgResponse.ok) {
561 throw new Error('ComfyUI returned an error.');576 throw new Error('ComfyUI returned an error.');
562 }577 }
563 const imgBuffer = await imgResponse.buffer();578 const imgBuffer = await imgResponse.arrayBuffer();
564 return response.send(imgBuffer.toString('base64'));579 return response.send(Buffer.from(imgBuffer).toString('base64'));
565 } catch (error) {580 } catch (error) {
566 console.log(error);581 console.log('ComfyUI error:', error);
567 return response.sendStatus(500);582 response.status(500).send(error.message);
583 return response;
568 }584 }
569});585});
570586