Remove baked Diceroll after extension extraction

1be11f8dd2d443b8eb5db77b5a9fdb8a7359eb06

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

4 files changed, +0 -731Ignore whitespace
public/scripts/extensions/diceroll/index.js+0 -591
@@ -1,591 +0,0 @@
1-import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from '../../extensions.js';
2-import {
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-
16-export { init };
17-
18-const 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).
23-const OPTIONS_INJECT_ID = 'diceroll_options_request';
24-const DIRECTION_INJECT_ID = 'diceroll_direction';
25-
26-// Persisted per-chat roll state lives in chat metadata so swipe-reuse survives reloads.
27-const 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.
31-const STEERED_TYPES = new Set(['normal', 'swipe', 'regenerate']);
32-
33-const 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-
35-const 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-
37-const 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-
64-const 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.
84-let isRolling = false;
85-
86-// Roll data waiting to be stamped onto the message the steered generation produces.
87-let 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.
92-let 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.
97-let mainGenHasCustomPrompt = false;
98-
99-globalThis.dicerollGenerateInterceptor = (...args) => onGenerationIntercept(...args);
100-
101-function 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- */
121-function 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-
133-function getRole(name) {
134- return name === 'user' ? extension_prompt_roles.USER : extension_prompt_roles.SYSTEM;
135-}
136-
137-function setDirectionInjection(text, roleName) {
138- setExtensionPrompt(DIRECTION_INJECT_ID, text, extension_prompt_types.IN_CHAT, 0, false, getRole(roleName));
139-}
140-
141-function clearDirectionInjection() {
142- setDirectionInjection('', 'system');
143-}
144-
145-function 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- */
156-async 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-
173-function 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- */
201-function 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- */
248-function 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-
263-function 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- */
277-function 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- */
296-async 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- */
401-function 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-
418-function 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-
460-function renderAllDebug() {
461- $('#chat .mes').each((_, element) => {
462- renderDebugForMessage(Number(element.getAttribute('mesid')));
463- });
464-}
465-
466-function 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-
482-function 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-
541-async 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-}