| | import { |
| | animation_duration, |
| | chat, |
| | cleanUpMessage, |
| | event_types, |
| | eventSource, |
| | Generate, |
| | getGeneratingApi, |
| | is_send_press, |
| | isStreamingEnabled, |
| | substituteParamsExtended, |
| | } from '../script.js'; |
| | import { debounce, delay, getStringHash } from './utils.js'; |
| | import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js'; |
| | import { power_user } from './power-user.js'; |
| | import { callGenericPopup, POPUP_TYPE } from './popup.js'; |
| | import { t } from './i18n.js'; |
| |
|
| | const TINTS = 4; |
| | const MAX_MESSAGE_LOGPROBS = 100; |
| | const REROLL_BUTTON = $('#logprobsReroll'); |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | const state = { |
| | selectedTokenLogprobs: null, |
| | messageLogprobs: new Map(), |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | function renderAlternativeTokensView() { |
| | const view = $('#logprobs_generation_output'); |
| | if (!view.is(':visible')) { |
| | return; |
| | } |
| | view.empty(); |
| | state.selectedTokenLogprobs = null; |
| | renderTopLogprobs(); |
| |
|
| | const { messageLogprobs, continueFrom } = getActiveMessageLogprobData() || {}; |
| | const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming; |
| | if (!messageLogprobs?.length || usingSmoothStreaming) { |
| | const emptyState = $('<div></div>'); |
| | const noTokensMsg = !power_user.request_token_probabilities |
| | ? '<span>Enable <b>Request token probabilities</b> in the User Settings menu to use this feature.</span>' |
| | : usingSmoothStreaming |
| | ? t`Token probabilities are not available when using Smooth Streaming.` |
| | : is_send_press |
| | ? t`Generation in progress...` |
| | : t`No token probabilities available for the current message.`; |
| | emptyState.html(noTokensMsg); |
| | emptyState.addClass('logprobs_empty_state'); |
| | view.append(emptyState); |
| | return; |
| | } |
| |
|
| | const prefix = continueFrom || ''; |
| | const tokenSpans = []; |
| | REROLL_BUTTON.toggle(!!prefix); |
| |
|
| | if (prefix) { |
| | REROLL_BUTTON.off('click').on('click', () => onPrefixClicked(prefix.length)); |
| |
|
| | let cumulativeOffset = 0; |
| | const words = prefix.split(/\s+/); |
| | const delimiters = prefix.match(/\s+/g) || []; |
| |
|
| | words.forEach((word, i) => { |
| | const span = $('<span></span>'); |
| | span.text(`${word} `); |
| |
|
| | span.addClass('logprobs_output_prefix'); |
| | span.attr('title', t`Reroll from this point`); |
| |
|
| | let offset = cumulativeOffset; |
| | span.on('click', () => onPrefixClicked(offset)); |
| | addKeyboardProps(span); |
| |
|
| | tokenSpans.push(span); |
| | tokenSpans.push(delimiters[i]?.includes('\n') |
| | ? document.createElement('br') |
| | : document.createTextNode(delimiters[i] || ' '), |
| | ); |
| |
|
| | cumulativeOffset += word.length + (delimiters[i]?.length || 0); |
| | }); |
| | } |
| |
|
| | messageLogprobs.forEach((tokenData, i) => { |
| | const { token } = tokenData; |
| | const span = $('<span></span>'); |
| | const text = toVisibleWhitespace(token); |
| | span.text(text); |
| | span.addClass('logprobs_output_token'); |
| | span.addClass('logprobs_tint_' + (i % TINTS)); |
| | span.on('click', () => onSelectedTokenChanged(tokenData, span)); |
| | addKeyboardProps(span); |
| | tokenSpans.push(...withVirtualWhitespace(token, span)); |
| | }); |
| |
|
| | view.append(tokenSpans); |
| |
|
| | |
| | if (prefix) { |
| | const element = view.find('.logprobs_output_token').first(); |
| | const scrollOffset = element.offset().top - element.parent().offset().top; |
| | element.parent().scrollTop(scrollOffset); |
| | } |
| | } |
| |
|
| | function addKeyboardProps(element) { |
| | element.attr('role', 'button'); |
| | element.attr('tabindex', '0'); |
| | element.keydown(function (e) { |
| | if (e.key === 'Enter' || e.key === ' ') { |
| | element.click(); |
| | } |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function renderTopLogprobs() { |
| | $('#logprobs_top_logprobs_hint').hide(); |
| | const view = $('.logprobs_candidate_list'); |
| | view.empty(); |
| |
|
| | if (!state.selectedTokenLogprobs) { |
| | return; |
| | } |
| |
|
| | const { token: selectedToken, topLogprobs } = state.selectedTokenLogprobs; |
| |
|
| | let sum = 0; |
| | const nodes = []; |
| | const candidates = topLogprobs |
| | .sort(([, logA], [, logB]) => logB - logA) |
| | .map(([text, log]) => { |
| | if (log <= 0) { |
| | const probability = Math.exp(log); |
| | sum += probability; |
| | return [text, probability, log]; |
| | } else { |
| | return [text, log, null]; |
| | } |
| | }); |
| | candidates.push(['<others>', 1 - sum, 0]); |
| |
|
| | let matched = false; |
| | for (const [token, probability, log] of candidates) { |
| | const container = $('<button class="flex-container flexFlowColumn logprobs_top_candidate"></button>'); |
| | const tokenNormalized = String(token).replace(/^[▁Ġ]/g, ' '); |
| |
|
| | if (token === selectedToken || tokenNormalized === selectedToken) { |
| | matched = true; |
| | container.addClass('selected'); |
| | } |
| |
|
| | const tokenText = $('<span></span>').text(`${toVisibleWhitespace(token.toString())}`); |
| | const percentText = $('<span></span>').text(`${(+probability * 100).toFixed(2)}%`); |
| | container.append(tokenText, percentText); |
| | if (log) { |
| | container.attr('title', `logarithm: ${log}`); |
| | } |
| | addKeyboardProps(container); |
| | if (token !== '<others>') { |
| | container.on('click', () => onAlternativeClicked(state.selectedTokenLogprobs, token.toString())); |
| | } else { |
| | container.prop('disabled', true); |
| | } |
| | nodes.push(container); |
| | } |
| |
|
| | |
| | |
| | if (!matched) { |
| | nodes[nodes.length - 1].css('background-color', 'rgba(255, 0, 0, 0.1)'); |
| | } |
| |
|
| | view.append(nodes); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | function onSelectedTokenChanged(logprobs, span) { |
| | $('.logprobs_output_token.selected').removeClass('selected'); |
| | if (state.selectedTokenLogprobs === logprobs) { |
| | state.selectedTokenLogprobs = null; |
| | } else { |
| | state.selectedTokenLogprobs = logprobs; |
| | $(span).addClass('selected'); |
| | } |
| | renderTopLogprobs(); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function onAlternativeClicked(tokenLogprobs, alternative) { |
| | if (!checkGenerateReady()) { |
| | return; |
| | } |
| |
|
| | if (getGeneratingApi() === 'openai') { |
| | const title = t`Feature unavailable`; |
| | const message = t`Due to API limitations, rerolling a token is not supported with OpenAI. Try switching to a different API.`; |
| | const content = `<h3>${title}</h3><p>${message}</p>`; |
| | return callGenericPopup(content, POPUP_TYPE.TEXT); |
| | } |
| |
|
| | const { messageLogprobs, continueFrom } = getActiveMessageLogprobData(); |
| | const replaceIndex = messageLogprobs.findIndex(x => x === tokenLogprobs); |
| |
|
| | const tokens = messageLogprobs.slice(0, replaceIndex + 1).map(({ token }) => token); |
| | tokens[replaceIndex] = String(alternative).replace(/^[▁Ġ]/g, ' ').replace(/Ċ/g, '\n'); |
| |
|
| | const prefix = continueFrom || ''; |
| | const prompt = prefix + tokens.join(''); |
| | addGeneration(prompt); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function onPrefixClicked(offset = undefined) { |
| | if (!checkGenerateReady()) { |
| | return; |
| | } |
| |
|
| | const { continueFrom } = getActiveMessageLogprobData() || {}; |
| | const prefix = continueFrom ? continueFrom.substring(0, offset) : ''; |
| | addGeneration(prefix); |
| | } |
| |
|
| | function checkGenerateReady() { |
| | if (is_send_press) { |
| | toastr.warning('Please wait for the current generation to complete.'); |
| | return false; |
| | } |
| | return true; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | function addGeneration(prompt) { |
| | const messageId = chat.length - 1; |
| | if (prompt && prompt.length > 0) { |
| | createSwipe(messageId, prompt); |
| | $('.swipe_right:last').trigger('click'); |
| | void Generate('continue'); |
| | } else { |
| | $('.swipe_right:last').trigger('click'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function onToggleLogprobsPanel() { |
| | const logprobsViewer = $('#logprobsViewer'); |
| |
|
| | |
| | if (logprobsViewer.css('display') === 'none') { |
| | logprobsViewer.addClass('resizing'); |
| | logprobsViewer.css('display', 'flex'); |
| | logprobsViewer.css('opacity', 0.0); |
| | renderAlternativeTokensView(); |
| | logprobsViewer.transition({ |
| | opacity: 1.0, |
| | duration: animation_duration, |
| | }, async function () { |
| | await delay(50); |
| | logprobsViewer.removeClass('resizing'); |
| | }); |
| | } else { |
| | logprobsViewer.addClass('resizing'); |
| | logprobsViewer.transition({ |
| | opacity: 0.0, |
| | duration: animation_duration, |
| | }, |
| | async function () { |
| | await delay(50); |
| | logprobsViewer.removeClass('resizing'); |
| | }); |
| | setTimeout(function () { |
| | logprobsViewer.hide(); |
| | }, animation_duration); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function createSwipe(messageId, prompt) { |
| | |
| | |
| | let cleanedPrompt = cleanUpMessage({ |
| | getMessage: prompt, |
| | isImpersonate: false, |
| | isContinue: false, |
| | displayIncompleteSentences: true, |
| | }); |
| |
|
| | const msg = chat[messageId]; |
| |
|
| | const reasoningPrefix = substituteParamsExtended(power_user.reasoning.prefix); |
| | const reasoningSuffix = substituteParamsExtended(power_user.reasoning.suffix); |
| | const isReasoningAutoParsed = power_user.reasoning.auto_parse; |
| | const msgHasParsedReasoning = msg.extra?.reasoning?.length > 0; |
| | let shouldRerollReasoning = false; |
| |
|
| | |
| | if (isReasoningAutoParsed && msgHasParsedReasoning) { |
| | console.debug('saw autoparse on with reasoning in message'); |
| | |
| | if (cleanedPrompt.includes(reasoningPrefix) && !cleanedPrompt.includes(reasoningSuffix)) { |
| | |
| | |
| | console.debug('..with start tag but no end tag... reroll reasoning'); |
| | shouldRerollReasoning = true; |
| | } |
| |
|
| | let hasReasoningPrefix = cleanedPrompt.includes(reasoningPrefix); |
| | let hasReasoningSuffix = cleanedPrompt.includes(reasoningSuffix); |
| |
|
| | |
| | |
| | |
| | if (hasReasoningPrefix && hasReasoningSuffix) { |
| | |
| | console.debug('...incl. end tag...rerolling response'); |
| | const endOfThink = cleanedPrompt.indexOf(reasoningSuffix) + reasoningSuffix.length; |
| | cleanedPrompt = cleanedPrompt.substring(endOfThink); |
| | } |
| |
|
| | |
| | if (hasReasoningPrefix && !hasReasoningSuffix) { |
| | console.debug('..no end tag...rerolling reasoning, so removing prefix'); |
| | cleanedPrompt = cleanedPrompt.replace(reasoningPrefix, ''); |
| | } |
| | } |
| |
|
| | console.debug('cleanedPrompt: ', cleanedPrompt); |
| |
|
| | const newSwipeInfo = { |
| | send_date: msg.send_date, |
| | gen_started: msg.gen_started, |
| | gen_finished: msg.gen_finished, |
| | extra: { ...structuredClone(msg.extra), from_logprobs: new Date().getTime() }, |
| | }; |
| |
|
| | msg.swipes = msg.swipes || []; |
| | msg.swipe_info = msg.swipe_info || []; |
| |
|
| | |
| | |
| |
|
| | |
| | if (shouldRerollReasoning) { |
| | |
| | newSwipeInfo.extra.reasoning = cleanedPrompt; |
| | |
| | msg.swipes.push(''); |
| | } else { |
| | |
| | msg.swipes.push(cleanedPrompt); |
| | } |
| |
|
| | msg.swipe_info.push(newSwipeInfo); |
| | msg.swipe_id = Math.max(0, msg.swipes.length - 2); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | function toVisibleWhitespace(input) { |
| | return input.replace(/ /g, '·').replace(/[▁Ġ]/g, '·').replace(/[Ċ\n]/g, '↵'); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | function withVirtualWhitespace(text, span) { |
| | |
| | const result = [span]; |
| | if (text.match(/^\s/)) { |
| | result.unshift(document.createTextNode('\u200b')); |
| | } |
| | if (text.match(/\s$/)) { |
| | result.push($(document.createTextNode('\u200b'))); |
| | } |
| | if (text.match(/^[▁Ġ]/)) { |
| | result.unshift(document.createTextNode('\u200b')); |
| | } |
| | |
| | |
| | |
| |
|
| | |
| | if (text.match(/^\n(?:.|\n)+\n$/)) { |
| | result.unshift($('<br>')); |
| | result.push($('<br>')); |
| | } else if (text.match(/^\n/)) { |
| | result.unshift($('<br>')); |
| | } else if (text.match(/\n$/)) { |
| | result.push($('<br>')); |
| | } |
| | return result; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function saveLogprobsForActiveMessage(logprobs, continueFrom) { |
| | if (!logprobs) { |
| | |
| | return; |
| | } |
| |
|
| | |
| | if (getGeneratingApi() === 'novel') { |
| | convertTokenIdLogprobsToText(logprobs); |
| | } |
| |
|
| | const msgId = chat.length - 1; |
| | |
| | const data = { |
| | created: new Date().getTime(), |
| | api: getGeneratingApi(), |
| | messageId: msgId, |
| | swipeId: chat[msgId].swipe_id, |
| | messageLogprobs: logprobs, |
| | continueFrom, |
| | hash: getMessageHash(chat[msgId]), |
| | }; |
| |
|
| | state.messageLogprobs.set(data.hash, data); |
| |
|
| | |
| | const oldLogprobs = Array.from(state.messageLogprobs.values()) |
| | .sort((a, b) => b.created - a.created) |
| | .slice(MAX_MESSAGE_LOGPROBS); |
| | for (const oldData of oldLogprobs) { |
| | state.messageLogprobs.delete(oldData.hash); |
| | } |
| | } |
| |
|
| | function getMessageHash(message) { |
| | |
| | |
| | const hashParams = { |
| | name: message.name, |
| | mid: chat.indexOf(message), |
| | text: message.mes, |
| | }; |
| | return getStringHash(JSON.stringify(hashParams)); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | function getActiveMessageLogprobData() { |
| | if (chat.length === 0) { |
| | return null; |
| | } |
| |
|
| | const hash = getMessageHash(chat[chat.length - 1]); |
| | return state.messageLogprobs.get(hash) || null; |
| | } |
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | function convertTokenIdLogprobsToText(input) { |
| | const api = getGeneratingApi(); |
| | if (api !== 'novel') { |
| | |
| | throw new Error('convertTokenIdLogprobsToText should only be called for NovelAI'); |
| | } |
| |
|
| | const tokenizerId = getTokenizerBestMatch(api); |
| |
|
| | |
| | const tokenIds = Array.from(new Set(input.flatMap(logprobs => |
| | logprobs.topLogprobs.map(([token]) => token).concat(logprobs.token), |
| | ))); |
| |
|
| | |
| | |
| | const { chunks } = decodeTextTokens(tokenizerId, tokenIds); |
| | const tokenIdText = new Map(tokenIds.map((id, i) => [id, chunks[i]])); |
| |
|
| | |
| | input.forEach(logprobs => { |
| | logprobs.token = tokenIdText.get(logprobs.token); |
| | logprobs.topLogprobs = logprobs.topLogprobs.map(([token, logprob]) => |
| | [tokenIdText.get(token), logprob], |
| | ); |
| | }); |
| | } |
| |
|
| | export function initLogprobs() { |
| | REROLL_BUTTON.hide(); |
| | const debouncedRender = debounce(renderAlternativeTokensView); |
| | $('#logprobsViewerClose').on('click', onToggleLogprobsPanel); |
| | $('#option_toggle_logprobs').on('click', onToggleLogprobsPanel); |
| | eventSource.on(event_types.CHAT_CHANGED, debouncedRender); |
| | eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, debouncedRender); |
| | eventSource.on(event_types.IMPERSONATE_READY, debouncedRender); |
| | eventSource.on(event_types.MESSAGE_DELETED, debouncedRender); |
| | eventSource.on(event_types.MESSAGE_EDITED, debouncedRender); |
| | eventSource.on(event_types.MESSAGE_SWIPED, debouncedRender); |
| | } |
| |
|