Remove baked Diceroll after extension extraction

1be11f8dd2d443b8eb5db77b5a9fdb8a7359eb06

permissionBRICK <40219477+permissionBRICK@users.noreply.github.com>

4 files changed, +0 -731Showing whitespace changes
public/scripts/extensions/diceroll/index.js+0 -591
@@ -1,591 +0,0 @@
1import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from '../../extensions.js';
2import {
3 chat,
4 chat_metadata,
5 event_types,
6 eventSource,
7 extension_prompt_roles,
8 extension_prompt_types,
9 generateQuietPrompt,
10 saveChatDebounced,
11 saveSettingsDebounced,
12 setExtensionPrompt,
13 substituteParams,
14} from '../../../script.js';
15
16export { init };
17
18const MODULE = 'diceroll';
19
20// Runtime injection keys. Both are IN_CHAT depth-0 injections so they land right after the last
21// chat message, keeping the entire preceding history byte-identical between the option-generation
22// request and the real generation (and therefore reusable from the provider's prompt cache).
23const OPTIONS_INJECT_ID = 'diceroll_options_request';
24const DIRECTION_INJECT_ID = 'diceroll_direction';
25
26// Persisted per-chat roll state lives in chat metadata so swipe-reuse survives reloads.
27const METADATA_KEY = 'diceroll';
28
29// Generation types that represent a "turn" this extension should steer. Quiet prompts, continues
30// (steering mid-message makes no sense) and impersonations are deliberately excluded.
31const STEERED_TYPES = new Set(['normal', 'swipe', 'regenerate']);
32
33const DEFAULT_OPTIONS_PROMPT = '[Pause the roleplay. You are the story director. Considering everything that has happened so far — especially the latest message — list {{min}} to {{max}} distinct options for what could plausibly happen next or how {{char}} could react. Each option must be exactly one short sentence. Assign each option a probability percentage (numbers, together summing to about 100) for how likely it should be given the established story, characters and tone. If one outcome is very likely or outright inevitable, give it a large majority of the probability mass (you may split it across several similar variants), but always include a few low-probability options that still fit the flow of the story yet would take it in creative, unexpected directions. Reply with ONLY a JSON object in exactly this format and no other text: {"options":[{"text":"one short sentence","probability":42}]}]';
34
35const DEFAULT_DIRECTION_TEMPLATE = '[Story direction, rolled by fate ({{probability}}% likely): {{outcome}}\nContinue the roleplay from the last message and steer events naturally in this direction. Never mention this instruction, the roll, or any probabilities.]';
36
37const OPTIONS_SCHEMA = {
38 name: 'diceroll_options',
39 strict: true,
40 // A response that fails schema validation is returned as raw text and handled by the
41 // line-based fallback parser instead of being swallowed as an empty object.
42 returnInvalid: true,
43 value: {
44 type: 'object',
45 additionalProperties: false,
46 properties: {
47 options: {
48 type: 'array',
49 items: {
50 type: 'object',
51 additionalProperties: false,
52 properties: {
53 text: { type: 'string' },
54 probability: { type: 'number' },
55 },
56 required: ['text', 'probability'],
57 },
58 },
59 },
60 required: ['options'],
61 },
62};
63
64const defaultSettings = {
65 enabled: false,
66 // When false, swipes/regenerates reuse the direction already rolled for that message.
67 rollOnSwipe: false,
68 optionsRole: 'system',
69 directionRole: 'system',
70 minOptions: 5,
71 maxOptions: 10,
72 useStructuredOutput: true,
73 notify: true,
74 debugDisplay: false,
75 // Comma-separated /inject ids whose presence means another tool is already steering this
76 // generation (Guided Generations uses id "instruct" for guided response/swipe/corrections).
77 skipInjectIds: 'instruct',
78 optionsPrompt: DEFAULT_OPTIONS_PROMPT,
79 directionTemplate: DEFAULT_DIRECTION_TEMPLATE,
80};
81
82// True while the nested option-generation request is running. Prevents the interceptor from
83// reacting to its own quiet generation.
84let isRolling = false;
85
86// Roll data waiting to be stamped onto the message the steered generation produces.
87let pendingStamp = null;
88
89// Message id stamped by the current generation. MESSAGE_RECEIVED consumes the pending stamp and
90// CHARACTER_MESSAGE_RENDERED fires right after for the same message; without this marker the
91// second event would mistake the freshly stamped message for an unsteered one and wipe it.
92let stampedMesId = null;
93
94// Whether the main generation being intercepted was started with its own custom prompt
95// (e.g. a swipe/regenerate triggered with an additional instruction). Tracked via
96// GENERATION_STARTED because the interceptor does not receive the generation params.
97let mainGenHasCustomPrompt = false;
98
99globalThis.dicerollGenerateInterceptor = (...args) => onGenerationIntercept(...args);
100
101function getSettings() {
102 if (extension_settings[MODULE] === undefined) {
103 extension_settings[MODULE] = {};
104 }
105 for (const key of Object.keys(defaultSettings)) {
106 if (extension_settings[MODULE][key] === undefined) {
107 extension_settings[MODULE][key] = structuredClone(defaultSettings[key]);
108 }
109 }
110 return extension_settings[MODULE];
111}
112
113/**
114 * True when another tool is already steering this generation with a manual instruction:
115 * either a script inject with a configured id (Guided Generations' guided swipe/response
116 * inject `id=instruct` before triggering the generation), or a custom prompt passed
117 * directly to the generation call.
118 * @param {object} s Settings
119 * @returns {boolean}
120 */
121function hasManualSteering(s) {
122 if (mainGenHasCustomPrompt) {
123 return true;
124 }
125 const injects = chat_metadata.script_injects ?? {};
126 return String(s.skipInjectIds ?? '')
127 .split(',')
128 .map(id => id.trim())
129 .filter(Boolean)
130 .some(id => String(injects[id]?.value ?? '').trim());
131}
132
133function getRole(name) {
134 return name === 'user' ? extension_prompt_roles.USER : extension_prompt_roles.SYSTEM;
135}
136
137function setDirectionInjection(text, roleName) {
138 setExtensionPrompt(DIRECTION_INJECT_ID, text, extension_prompt_types.IN_CHAT, 0, false, getRole(roleName));
139}
140
141function clearDirectionInjection() {
142 setDirectionInjection('', 'system');
143}
144
145function formatProbability(value) {
146 const number = Number(value);
147 return Number.isInteger(number) ? String(number) : number.toFixed(1);
148}
149
150/**
151 * Runs the option-generation request: the full current chat history plus the instruction injected
152 * at depth 0, through the currently selected model/API.
153 * @param {object} s Settings
154 * @returns {Promise<string>} Raw model response
155 */
156async function generateOptions(s) {
157 const instruction = substituteParams(
158 String(s.optionsPrompt)
159 .replaceAll('{{min}}', String(s.minOptions))
160 .replaceAll('{{max}}', String(s.maxOptions)),
161 );
162 setExtensionPrompt(OPTIONS_INJECT_ID, instruction, extension_prompt_types.IN_CHAT, 0, false, getRole(s.optionsRole));
163 try {
164 return await generateQuietPrompt({
165 quietPrompt: '',
166 jsonSchema: s.useStructuredOutput ? OPTIONS_SCHEMA : null,
167 });
168 } finally {
169 setExtensionPrompt(OPTIONS_INJECT_ID, '', extension_prompt_types.IN_CHAT, 0);
170 }
171}
172
173function tryParseJson(text) {
174 const candidates = [text];
175 const objStart = text.indexOf('{');
176 const objEnd = text.lastIndexOf('}');
177 if (objStart >= 0 && objEnd > objStart) {
178 candidates.push(text.slice(objStart, objEnd + 1));
179 }
180 const arrStart = text.indexOf('[');
181 const arrEnd = text.lastIndexOf(']');
182 if (arrStart >= 0 && arrEnd > arrStart) {
183 candidates.push(text.slice(arrStart, arrEnd + 1));
184 }
185 for (const candidate of candidates) {
186 try {
187 return JSON.parse(candidate);
188 } catch {
189 // try the next candidate
190 }
191 }
192 return null;
193}
194
195/**
196 * Parses model output into a weighted option list. Prefers JSON (structured output or inline),
197 * falls back to "text (25%)" / "25% - text" style lines.
198 * @param {string} raw Raw model response
199 * @returns {{text: string, probability: number}[]|null} At least two options, or null
200 */
201function parseOptions(raw) {
202 const text = String(raw ?? '').trim();
203 if (!text) {
204 return null;
205 }
206
207 let options = [];
208 const parsed = tryParseJson(text);
209 const list = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.options) ? parsed.options : null);
210 if (list) {
211 options = list.map(item => ({
212 text: String(item?.text ?? item?.option ?? '').trim(),
213 probability: Number(item?.probability ?? item?.percent ?? item?.chance),
214 }));
215 } else {
216 const trailing = /^(.+?)\s*[-–—:([]?\s*(\d{1,3}(?:\.\d+)?)\s*%\s*[)\]]?\s*$/;
217 const leading = /^(\d{1,3}(?:\.\d+)?)\s*%\s*[-–—:]?\s*(.+)$/;
218 for (let line of text.split('\n')) {
219 line = line.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, '').trim();
220 let match = line.match(trailing);
221 if (match) {
222 options.push({ text: match[1].trim(), probability: Number(match[2]) });
223 continue;
224 }
225 match = line.match(leading);
226 if (match) {
227 options.push({ text: match[2].trim(), probability: Number(match[1]) });
228 }
229 }
230 }
231
232 options = options.filter(option => option.text && Number.isFinite(option.probability) && option.probability > 0);
233
234 // Models occasionally return fractions instead of percentages.
235 const total = options.reduce((sum, option) => sum + option.probability, 0);
236 if (total > 0 && total <= 1.5) {
237 options = options.map(option => ({ ...option, probability: option.probability * 100 }));
238 }
239
240 return options.length >= 2 ? options : null;
241}
242
243/**
244 * Weighted random roll over the options.
245 * @param {{text: string, probability: number}[]} options Parsed options
246 * @returns {{options: object[], chosenIndex: number, roll: number, total: number}}
247 */
248function rollOptions(options) {
249 const total = options.reduce((sum, option) => sum + option.probability, 0);
250 const roll = Math.random() * total;
251 let cumulative = 0;
252 let chosenIndex = options.length - 1;
253 for (let i = 0; i < options.length; i++) {
254 cumulative += options[i].probability;
255 if (roll < cumulative) {
256 chosenIndex = i;
257 break;
258 }
259 }
260 return { options, chosenIndex, roll, total };
261}
262
263function buildDirectionText(rollData, s) {
264 const chosen = rollData.options[rollData.chosenIndex];
265 return substituteParams(
266 String(s.directionTemplate)
267 .replaceAll('{{outcome}}', chosen.text)
268 .replaceAll('{{probability}}', formatProbability(chosen.probability)),
269 );
270}
271
272/**
273 * Temporarily hides the message being swiped away so the option request sees the same history as
274 * the swipe generation itself (which pops the last message from its prompt).
275 * @returns {(() => void)|null} Restore function
276 */
277function hideLastMessageForSwipe() {
278 const target = chat[chat.length - 1];
279 if (!target || target.is_user || target.is_system) {
280 return null;
281 }
282 target.is_system = true;
283 return () => {
284 target.is_system = false;
285 };
286}
287
288/**
289 * Generation interceptor. Runs the option roll before the actual generation and injects the
290 * rolled direction for the upcoming prompt build.
291 * @param {object[]} coreChat Filtered chat that will be used for the prompt
292 * @param {number} _contextSize Max context size
293 * @param {(immediately: boolean) => void} _abort Abort function
294 * @param {string} type Generation type
295 */
296async function onGenerationIntercept(coreChat, _contextSize, _abort, type) {
297 const s = getSettings();
298
299 // The nested option request triggers interceptors itself (as type 'quiet').
300 if (isRolling || type === 'quiet') {
301 return;
302 }
303
304 if (!s.enabled) {
305 clearDirectionInjection();
306 return;
307 }
308
309 // Tool-call recursion re-enters Generate as 'normal'; keep the current steering untouched
310 // instead of rolling again mid-turn.
311 const lastMessage = chat[chat.length - 1];
312 if (lastMessage && !lastMessage.is_user && Array.isArray(lastMessage.extra?.tool_invocations)) {
313 return;
314 }
315
316 // Never leak a stale direction into an unrelated generation type.
317 clearDirectionInjection();
318 pendingStamp = null;
319
320 if (!STEERED_TYPES.has(type) || !chat.length) {
321 return;
322 }
323
324 stampedMesId = null;
325
326 // The message being replaced still shows the previous swipe's debug block; hide it as soon as
327 // the new generation starts instead of leaving it stuck until the result arrives.
328 if (type === 'swipe' || type === 'regenerate') {
329 $(`#chat .mes[mesid="${chat.length - 1}"] .diceroll_debug`).remove();
330 }
331
332 // A guided swipe/response (or any generation carrying its own instruction) takes precedence:
333 // no roll, no direction injection — the manual instruction alone steers this generation.
334 if (hasManualSteering(s)) {
335 console.debug('[Diceroll] Manual instruction detected, skipping the roll for this generation.');
336 return;
337 }
338
339 // The roll belongs to the current user turn; swipes/regenerates of the same turn can reuse it.
340 const anchor = chat.findLastIndex(x => x.is_user);
341 const stored = chat_metadata[METADATA_KEY];
342 const isRedo = type === 'swipe' || type === 'regenerate';
343
344 if (isRedo && !s.rollOnSwipe && stored?.direction && stored.anchor === anchor) {
345 pendingStamp = stored;
346 setDirectionInjection(stored.direction, s.directionRole);
347 return;
348 }
349
350 let rollData = null;
351 isRolling = true;
352 const restoreHidden = type === 'swipe' ? hideLastMessageForSwipe() : null;
353 try {
354 const raw = await generateOptions(s);
355 const options = parseOptions(raw);
356 if (!options) {
357 throw new Error('Could not parse any options from the model response.');
358 }
359 rollData = rollOptions(options);
360 } catch (error) {
361 console.error('[Diceroll] Option generation failed:', error);
362 toastr.warning('Continuing without steering. ' + (error?.message ?? ''), 'Diceroll: option roll failed', { escapeHtml: true });
363 } finally {
364 restoreHidden?.();
365 isRolling = false;
366 }
367
368 if (!rollData) {
369 return;
370 }
371
372 const data = {
373 anchor,
374 options: rollData.options,
375 chosenIndex: rollData.chosenIndex,
376 roll: rollData.roll,
377 total: rollData.total,
378 direction: '',
379 };
380 data.direction = buildDirectionText(rollData, s);
381
382 chat_metadata[METADATA_KEY] = data;
383 saveMetadataDebounced();
384 pendingStamp = data;
385 setDirectionInjection(data.direction, s.directionRole);
386
387 if (s.notify) {
388 const chosen = rollData.options[rollData.chosenIndex];
389 toastr.info(`${chosen.text} (${formatProbability(chosen.probability)}%)`, '🎲 Diceroll', { escapeHtml: true });
390 }
391}
392
393/**
394 * Copies the roll that steered a finished generation onto the produced message, so the debug view
395 * stays correct per message and survives reloads. When a main-type generation finishes WITHOUT a
396 * roll (extension disabled, roll failed, or a manual instruction steered it), the roll record
397 * inherited in place from the previous swipe's extra is dropped instead.
398 * @param {number} chatId Message index
399 * @param {string} type Generation type the message event was emitted with
400 */
401function stampMessage(chatId, type) {
402 const message = chat[chatId];
403 if (!message || message.is_user || message.is_system) {
404 return;
405 }
406 if (pendingStamp) {
407 message.extra = message.extra || {};
408 message.extra.diceroll = structuredClone(pendingStamp);
409 pendingStamp = null;
410 stampedMesId = chatId;
411 saveChatDebounced();
412 } else if (STEERED_TYPES.has(type) && stampedMesId !== chatId && message.extra?.diceroll) {
413 delete message.extra.diceroll;
414 saveChatDebounced();
415 }
416}
417
418function renderDebugForMessage(chatId) {
419 const mesElement = $(`#chat .mes[mesid="${chatId}"]`);
420 if (!mesElement.length) {
421 return;
422 }
423 mesElement.find('.diceroll_debug').remove();
424
425 const message = chat[chatId];
426 const data = message?.extra?.diceroll;
427 if (!getSettings().debugDisplay || !data || !Array.isArray(data.options)) {
428 return;
429 }
430
431 // An overswipe points swipe_id one past the existing swipes while its generation is running;
432 // the roll record on the message still belongs to the previous swipe then, so show nothing.
433 if (typeof message.swipe_id === 'number' && Array.isArray(message.swipes) && message.swipe_id >= message.swipes.length) {
434 return;
435 }
436
437 const chosen = data.options[data.chosenIndex];
438 const details = $('<details class="diceroll_debug"></details>');
439 const rollInfo = Number.isFinite(data.roll) ? `, roll ${data.roll.toFixed(1)}/${formatProbability(data.total)}` : '';
440 details.append($('<summary></summary>').text(`🎲 ${chosen?.text ?? '?'} (${formatProbability(chosen?.probability ?? 0)}%${rollInfo})`));
441
442 const table = $('<table class="diceroll_debug_table"></table>');
443 data.options.forEach((option, index) => {
444 const row = $('<tr></tr>').toggleClass('diceroll_chosen', index === data.chosenIndex);
445 row.append($('<td></td>').text(`${formatProbability(option.probability)}%`));
446 row.append($('<td></td>').text(option.text));
447 table.append(row);
448 });
449 details.append(table);
450 details.append($('<div class="diceroll_debug_direction"></div>').text(data.direction ?? ''));
451
452 const anchorElement = mesElement.find('.mes_block .mes_text').first();
453 if (anchorElement.length) {
454 anchorElement.after(details);
455 } else {
456 mesElement.append(details);
457 }
458}
459
460function renderAllDebug() {
461 $('#chat .mes').each((_, element) => {
462 renderDebugForMessage(Number(element.getAttribute('mesid')));
463 });
464}
465
466function loadSettingsUi() {
467 const s = getSettings();
468 $('#diceroll_enabled').prop('checked', s.enabled);
469 $('#diceroll_roll_on_swipe').prop('checked', s.rollOnSwipe);
470 $('#diceroll_structured').prop('checked', s.useStructuredOutput);
471 $('#diceroll_notify').prop('checked', s.notify);
472 $('#diceroll_debug').prop('checked', s.debugDisplay);
473 $('#diceroll_min_options').val(s.minOptions);
474 $('#diceroll_max_options').val(s.maxOptions);
475 $('#diceroll_options_role').val(s.optionsRole);
476 $('#diceroll_direction_role').val(s.directionRole);
477 $('#diceroll_skip_inject_ids').val(s.skipInjectIds);
478 $('#diceroll_options_prompt').val(s.optionsPrompt);
479 $('#diceroll_direction_template').val(s.directionTemplate);
480}
481
482function setupListeners() {
483 const bindCheckbox = (id, key, onChange = null) => {
484 $(id).on('change', function () {
485 getSettings()[key] = !!$(this).prop('checked');
486 saveSettingsDebounced();
487 onChange?.();
488 });
489 };
490
491 bindCheckbox('#diceroll_enabled', 'enabled', () => {
492 if (!getSettings().enabled) {
493 clearDirectionInjection();
494 }
495 });
496 bindCheckbox('#diceroll_roll_on_swipe', 'rollOnSwipe');
497 bindCheckbox('#diceroll_structured', 'useStructuredOutput');
498 bindCheckbox('#diceroll_notify', 'notify');
499 bindCheckbox('#diceroll_debug', 'debugDisplay', renderAllDebug);
500
501 $('#diceroll_min_options').on('input', function () {
502 getSettings().minOptions = Math.max(2, Number($(this).val()) || defaultSettings.minOptions);
503 saveSettingsDebounced();
504 });
505 $('#diceroll_max_options').on('input', function () {
506 getSettings().maxOptions = Math.max(2, Number($(this).val()) || defaultSettings.maxOptions);
507 saveSettingsDebounced();
508 });
509 $('#diceroll_options_role').on('change', function () {
510 getSettings().optionsRole = String($(this).val());
511 saveSettingsDebounced();
512 });
513 $('#diceroll_direction_role').on('change', function () {
514 getSettings().directionRole = String($(this).val());
515 saveSettingsDebounced();
516 });
517 $('#diceroll_skip_inject_ids').on('input', function () {
518 getSettings().skipInjectIds = String($(this).val());
519 saveSettingsDebounced();
520 });
521 $('#diceroll_options_prompt').on('input', function () {
522 getSettings().optionsPrompt = String($(this).val());
523 saveSettingsDebounced();
524 });
525 $('#diceroll_direction_template').on('input', function () {
526 getSettings().directionTemplate = String($(this).val());
527 saveSettingsDebounced();
528 });
529 $('#diceroll_options_prompt_restore').on('click', () => {
530 getSettings().optionsPrompt = DEFAULT_OPTIONS_PROMPT;
531 $('#diceroll_options_prompt').val(DEFAULT_OPTIONS_PROMPT);
532 saveSettingsDebounced();
533 });
534 $('#diceroll_direction_template_restore').on('click', () => {
535 getSettings().directionTemplate = DEFAULT_DIRECTION_TEMPLATE;
536 $('#diceroll_direction_template').val(DEFAULT_DIRECTION_TEMPLATE);
537 saveSettingsDebounced();
538 });
539}
540
541async function init() {
542 const settingsHtml = await renderExtensionTemplateAsync(MODULE, 'settings');
543 $('#extensions_settings2').append(settingsHtml);
544 loadSettingsUi();
545 setupListeners();
546
547 // A new user message starts a new turn; any previous roll no longer applies.
548 eventSource.on(event_types.MESSAGE_SENT, () => {
549 delete chat_metadata[METADATA_KEY];
550 });
551
552 // Runs before the interceptor within the same Generate() call, so the flag is always fresh.
553 // The nested option request (type 'quiet') must not overwrite the outer generation's flag.
554 eventSource.on(event_types.GENERATION_STARTED, (type, params, dryRun) => {
555 if (type !== 'quiet' && !dryRun) {
556 mainGenHasCustomPrompt = !!params?.quiet_prompt;
557 }
558 });
559
560 eventSource.on(event_types.MESSAGE_RECEIVED, (chatId, type) => stampMessage(chatId, type));
561 eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (chatId, type) => {
562 stampMessage(chatId, type);
563 renderDebugForMessage(chatId);
564 });
565 eventSource.on(event_types.MESSAGE_SWIPED, (chatId) => renderDebugForMessage(chatId));
566 eventSource.on(event_types.CHAT_CHANGED, () => {
567 pendingStamp = null;
568 stampedMesId = null;
569 clearDirectionInjection();
570 renderAllDebug();
571 });
572
573 // The direction is consumed by exactly one generation. GENERATION_ENDED also fires when the
574 // nested option request finishes, which is why clearing is skipped while a roll is in flight.
575 // With streaming, the UI unlock that emits GENERATION_ENDED happens BEFORE MESSAGE_RECEIVED,
576 // so only the injection may be cleared here — the pending stamp must survive until the
577 // message events consume it (the interceptor resets it at the start of every next turn).
578 eventSource.on(event_types.GENERATION_ENDED, () => {
579 if (isRolling) {
580 return;
581 }
582 clearDirectionInjection();
583 });
584 eventSource.on(event_types.GENERATION_STOPPED, () => {
585 if (isRolling) {
586 return;
587 }
588 pendingStamp = null;
589 clearDirectionInjection();
590 });
591}
public/scripts/extensions/diceroll/manifest.json+0 -15
@@ -1,15 +0,0 @@
1{
2 "display_name": "Diceroll",
3 "loading_order": 100,
4 "requires": [],
5 "optional": [],
6 "js": "index.js",
7 "css": "style.css",
8 "author": "SillyTavern",
9 "version": "1.0.0",
10 "homePage": "https://github.com/SillyTavern/SillyTavern",
11 "generate_interceptor": "dicerollGenerateInterceptor",
12 "hooks": {
13 "activate": "init"
14 }
15}
public/scripts/extensions/diceroll/settings.html+0 -83
@@ -1,83 +0,0 @@
1<div id="diceroll_settings">
2 <div class="inline-drawer">
3 <div class="inline-drawer-toggle inline-drawer-header">
4 <b data-i18n="ext_diceroll_title">Diceroll</b>
5 <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
6 </div>
7 <div class="inline-drawer-content">
8 <label class="checkbox_label" for="diceroll_enabled">
9 <input id="diceroll_enabled" type="checkbox" />
10 <span data-i18n="ext_diceroll_enable">Enable Diceroll</span>
11 </label>
12 <small data-i18n="ext_diceroll_help">Before each response, the current model is quietly asked (with the exact same chat history, so the prompt cache is reused) to list short options for how the story could continue, each with a probability. A weighted dice roll picks one, and the chosen direction is injected after the latest user message for that one generation only — it is never saved to the chat, and the next turn's history stays clean.</small>
13
14 <label class="checkbox_label" for="diceroll_roll_on_swipe">
15 <input id="diceroll_roll_on_swipe" type="checkbox" />
16 <span data-i18n="ext_diceroll_roll_on_swipe">Roll again on swipes and regenerates</span>
17 </label>
18 <small data-i18n="ext_diceroll_roll_on_swipe_help">When off, swiping or regenerating reuses the direction already rolled for that turn.</small>
19
20 <label class="checkbox_label" for="diceroll_structured">
21 <input id="diceroll_structured" type="checkbox" />
22 <span data-i18n="ext_diceroll_structured">Request structured output (JSON schema)</span>
23 </label>
24 <small data-i18n="ext_diceroll_structured_help">Turn off if your backend rejects requests containing a JSON schema; the options are then parsed from plain text.</small>
25
26 <label class="checkbox_label" for="diceroll_notify">
27 <input id="diceroll_notify" type="checkbox" />
28 <span data-i18n="ext_diceroll_notify">Show a notification with the rolled outcome</span>
29 </label>
30
31 <label class="checkbox_label" for="diceroll_debug">
32 <input id="diceroll_debug" type="checkbox" />
33 <span data-i18n="ext_diceroll_debug">Debug: show the roll table and injected direction under each message</span>
34 </label>
35
36 <div class="flex-container">
37 <div class="flex1">
38 <label for="diceroll_min_options" data-i18n="ext_diceroll_min">Min options</label>
39 <input id="diceroll_min_options" class="text_pole" type="number" min="2" max="20" step="1" />
40 </div>
41 <div class="flex1">
42 <label for="diceroll_max_options" data-i18n="ext_diceroll_max">Max options</label>
43 <input id="diceroll_max_options" class="text_pole" type="number" min="2" max="20" step="1" />
44 </div>
45 </div>
46
47 <div class="flex-container">
48 <div class="flex1">
49 <label for="diceroll_options_role" data-i18n="ext_diceroll_options_role">Option request sent as</label>
50 <select id="diceroll_options_role" class="text_pole">
51 <option value="system" data-i18n="ext_diceroll_role_system">System message</option>
52 <option value="user" data-i18n="ext_diceroll_role_user">User message</option>
53 </select>
54 </div>
55 <div class="flex1">
56 <label for="diceroll_direction_role" data-i18n="ext_diceroll_direction_role">Direction sent as</label>
57 <select id="diceroll_direction_role" class="text_pole">
58 <option value="system" data-i18n="ext_diceroll_role_system">System message</option>
59 <option value="user" data-i18n="ext_diceroll_role_user">User message</option>
60 </select>
61 </div>
62 </div>
63
64 <label for="diceroll_skip_inject_ids" data-i18n="ext_diceroll_skip_inject_ids">Skip when these /inject ids are active (comma-separated)</label>
65 <input id="diceroll_skip_inject_ids" class="text_pole" type="text" placeholder="instruct" />
66 <small data-i18n="ext_diceroll_skip_inject_ids_help">When another tool steers the reply with its own instruction — e.g. Guided Generations' guided response/swipe use /inject id=instruct — Diceroll stays out of the way and neither rolls nor injects a direction for that generation. Generations triggered with a custom prompt are skipped as well.</small>
67
68 <label for="diceroll_options_prompt" data-i18n="ext_diceroll_options_prompt">Option-generation prompt (\{{min}}, \{{max}} available)</label>
69 <textarea id="diceroll_options_prompt" class="text_pole textarea_compact" rows="7"></textarea>
70 <div id="diceroll_options_prompt_restore" class="menu_button menu_button_icon">
71 <i class="fa-solid fa-clock-rotate-left"></i>
72 <span data-i18n="ext_diceroll_restore">Restore default</span>
73 </div>
74
75 <label for="diceroll_direction_template" data-i18n="ext_diceroll_direction_template">Direction message template (\{{outcome}}, \{{probability}} available)</label>
76 <textarea id="diceroll_direction_template" class="text_pole textarea_compact" rows="4"></textarea>
77 <div id="diceroll_direction_template_restore" class="menu_button menu_button_icon">
78 <i class="fa-solid fa-clock-rotate-left"></i>
79 <span data-i18n="ext_diceroll_restore">Restore default</span>
80 </div>
81 </div>
82 </div>
83</div>
public/scripts/extensions/diceroll/style.css+0 -42
@@ -1,42 +0,0 @@
1.diceroll_debug {
2 margin-top: 5px;
3 padding: 5px 10px;
4 border: 1px dashed var(--SmartThemeBorderColor, #888);
5 border-radius: 5px;
6 font-size: calc(var(--mainFontSize) * 0.85);
7 opacity: 0.85;
8}
9
10.diceroll_debug summary {
11 cursor: pointer;
12}
13
14.diceroll_debug_table {
15 border-collapse: collapse;
16 margin: 5px 0;
17 width: 100%;
18}
19
20.diceroll_debug_table td {
21 padding: 1px 8px 1px 0;
22 vertical-align: top;
23 border: none;
24}
25
26.diceroll_debug_table td:first-child {
27 text-align: right;
28 white-space: nowrap;
29 font-variant-numeric: tabular-nums;
30 width: 1%;
31}
32
33.diceroll_debug_table tr.diceroll_chosen {
34 font-weight: bold;
35 color: var(--SmartThemeQuoteColor, inherit);
36}
37
38.diceroll_debug_direction {
39 white-space: pre-wrap;
40 font-style: italic;
41 margin-top: 4px;
42}