mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
- Add thinking mode toggle in MessageInput component - Show toggle only for compatible Ollama thinking models (deepseek-r1, qwen3, magistral) - Send thinking option in API payload when enabled (Ollama only) - Add visual indicator when thinking mode is active - Include translations for thinking mode UI - Currently only available for Ollama thinking-capable models
169 lines
4.4 KiB
TypeScript
169 lines
4.4 KiB
TypeScript
import katex from 'katex';
|
||
|
||
const DELIMITER_LIST = [
|
||
{ left: '$$', right: '$$', display: true },
|
||
{ left: '$', right: '$', display: false },
|
||
{ left: '\\pu{', right: '}', display: false },
|
||
{ left: '\\ce{', right: '}', display: false },
|
||
{ left: '\\(', right: '\\)', display: false },
|
||
{ left: '\\[', right: '\\]', display: true },
|
||
{ left: '\\begin{equation}', right: '\\end{equation}', display: true }
|
||
];
|
||
|
||
// Defines characters that are allowed to immediately precede or follow a math delimiter.
|
||
const ALLOWED_SURROUNDING_CHARS =
|
||
'\\s。,、、;;„“‘’“”()「」『』[]《》【】‹›«»…⋯::?!~⇒?!-\\/:-@\\[-`{-~\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}';
|
||
// Modified to fit more formats in different languages. Originally: '\\s?。,、;!-\\/:-@\\[-`{-~\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}';
|
||
|
||
// const DELIMITER_LIST = [
|
||
// { left: '$$', right: '$$', display: false },
|
||
// { left: '$', right: '$', display: false },
|
||
// ];
|
||
|
||
// const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/;
|
||
// const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
|
||
|
||
const inlinePatterns = [];
|
||
const blockPatterns = [];
|
||
|
||
function escapeRegex(string) {
|
||
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||
}
|
||
|
||
function generateRegexRules(delimiters) {
|
||
delimiters.forEach((delimiter) => {
|
||
const { left, right, display } = delimiter;
|
||
// Ensure regex-safe delimiters
|
||
const escapedLeft = escapeRegex(left);
|
||
const escapedRight = escapeRegex(right);
|
||
|
||
if (!display) {
|
||
// For inline delimiters, we match everything
|
||
inlinePatterns.push(`${escapedLeft}((?:\\\\[^]|[^\\\\])+?)${escapedRight}`);
|
||
} else {
|
||
// Block delimiters doubles as inline delimiters when not followed by a newline
|
||
inlinePatterns.push(`${escapedLeft}(?!\\n)((?:\\\\[^]|[^\\\\])+?)(?!\\n)${escapedRight}`);
|
||
blockPatterns.push(`${escapedLeft}\\n((?:\\\\[^]|[^\\\\])+?)\\n${escapedRight}`);
|
||
}
|
||
});
|
||
|
||
// Math formulas can end in special characters
|
||
const inlineRule = new RegExp(
|
||
`^(${inlinePatterns.join('|')})(?=[${ALLOWED_SURROUNDING_CHARS}]|$)`,
|
||
'u'
|
||
);
|
||
const blockRule = new RegExp(
|
||
`^(${blockPatterns.join('|')})(?=[${ALLOWED_SURROUNDING_CHARS}]|$)`,
|
||
'u'
|
||
);
|
||
|
||
return { inlineRule, blockRule };
|
||
}
|
||
|
||
const { inlineRule, blockRule } = generateRegexRules(DELIMITER_LIST);
|
||
|
||
export default function (options = {}) {
|
||
return {
|
||
extensions: [inlineKatex(options), blockKatex(options)]
|
||
};
|
||
}
|
||
|
||
function katexStart(src, displayMode: boolean) {
|
||
const ruleReg = displayMode ? blockRule : inlineRule;
|
||
|
||
let indexSrc = src;
|
||
|
||
while (indexSrc) {
|
||
let index = -1;
|
||
let startIndex = -1;
|
||
let startDelimiter = '';
|
||
let endDelimiter = '';
|
||
for (const delimiter of DELIMITER_LIST) {
|
||
if (delimiter.display !== displayMode) {
|
||
continue;
|
||
}
|
||
|
||
startIndex = indexSrc.indexOf(delimiter.left);
|
||
if (startIndex === -1) {
|
||
continue;
|
||
}
|
||
|
||
index = startIndex;
|
||
startDelimiter = delimiter.left;
|
||
endDelimiter = delimiter.right;
|
||
}
|
||
|
||
if (index === -1) {
|
||
return;
|
||
}
|
||
|
||
// Check if the delimiter is preceded by a special character.
|
||
// If it does, then it's potentially a math formula.
|
||
const f =
|
||
index === 0 ||
|
||
indexSrc.charAt(index - 1).match(new RegExp(`[${ALLOWED_SURROUNDING_CHARS}]`, 'u'));
|
||
if (f) {
|
||
const possibleKatex = indexSrc.substring(index);
|
||
|
||
if (possibleKatex.match(ruleReg)) {
|
||
return index;
|
||
}
|
||
}
|
||
|
||
indexSrc = indexSrc.substring(index + startDelimiter.length).replace(endDelimiter, '');
|
||
}
|
||
}
|
||
|
||
function katexTokenizer(src, tokens, displayMode: boolean) {
|
||
const ruleReg = displayMode ? blockRule : inlineRule;
|
||
const type = displayMode ? 'blockKatex' : 'inlineKatex';
|
||
|
||
const match = src.match(ruleReg);
|
||
|
||
if (match) {
|
||
const text = match
|
||
.slice(2)
|
||
.filter((item) => item)
|
||
.find((item) => item.trim());
|
||
|
||
return {
|
||
type,
|
||
raw: match[0],
|
||
text: text,
|
||
displayMode
|
||
};
|
||
}
|
||
}
|
||
|
||
function inlineKatex(options) {
|
||
return {
|
||
name: 'inlineKatex',
|
||
level: 'inline',
|
||
start(src) {
|
||
return katexStart(src, false);
|
||
},
|
||
tokenizer(src, tokens) {
|
||
return katexTokenizer(src, tokens, false);
|
||
},
|
||
renderer(token) {
|
||
return `${token?.text ?? ''}`;
|
||
}
|
||
};
|
||
}
|
||
|
||
function blockKatex(options) {
|
||
return {
|
||
name: 'blockKatex',
|
||
level: 'block',
|
||
start(src) {
|
||
return katexStart(src, true);
|
||
},
|
||
tokenizer(src, tokens) {
|
||
return katexTokenizer(src, tokens, true);
|
||
},
|
||
renderer(token) {
|
||
return `${token?.text ?? ''}`;
|
||
}
|
||
};
|
||
}
|