mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
Add thinking mode toggle for Ollama thinking models
- 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
This commit is contained in:
parent
aef0ad2d10
commit
6bb677c9b3
@ -456,7 +456,7 @@ export const executeToolServer = async (
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
};
|
||||
|
||||
let requestOptions: RequestInit = {
|
||||
const requestOptions: RequestInit = {
|
||||
method: httpMethod.toUpperCase(),
|
||||
headers
|
||||
};
|
||||
@ -1005,7 +1005,7 @@ export const getPipelinesList = async (token: string = '') => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let pipelines = res?.data ?? [];
|
||||
const pipelines = res?.data ?? [];
|
||||
return pipelines;
|
||||
};
|
||||
|
||||
@ -1148,7 +1148,7 @@ export const getPipelines = async (token: string, urlIdx?: string) => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let pipelines = res?.data ?? [];
|
||||
const pipelines = res?.data ?? [];
|
||||
return pipelines;
|
||||
};
|
||||
|
||||
|
@ -331,7 +331,7 @@ export const generateTextCompletion = async (token: string = '', model: string,
|
||||
};
|
||||
|
||||
export const generateChatCompletion = async (token: string = '', body: object) => {
|
||||
let controller = new AbortController();
|
||||
const controller = new AbortController();
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, {
|
||||
|
@ -126,7 +126,7 @@ export const getUsers = async (
|
||||
let error = null;
|
||||
let res = null;
|
||||
|
||||
let searchParams = new URLSearchParams();
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.set('page', `${page}`);
|
||||
|
||||
|
@ -336,7 +336,7 @@
|
||||
<div class=" flex-1 self-center {(model?.is_active ?? true) ? '' : 'text-gray-500'}">
|
||||
<Tooltip
|
||||
content={marked.parse(
|
||||
!!model?.meta?.description
|
||||
model?.meta?.description
|
||||
? model?.meta?.description
|
||||
: model?.ollama?.digest
|
||||
? `${model?.ollama?.digest} **(${model?.ollama?.modified_at})**`
|
||||
@ -349,7 +349,7 @@
|
||||
</Tooltip>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
|
||||
<span class=" line-clamp-1">
|
||||
{!!model?.meta?.description
|
||||
{model?.meta?.description
|
||||
? model?.meta?.description
|
||||
: model?.ollama?.digest
|
||||
? `${model.id} (${model?.ollama?.digest})`
|
||||
|
@ -125,6 +125,7 @@
|
||||
let imageGenerationEnabled = false;
|
||||
let webSearchEnabled = false;
|
||||
let codeInterpreterEnabled = false;
|
||||
let thinkingEnabled = false;
|
||||
|
||||
let chat = null;
|
||||
let tags = [];
|
||||
@ -152,6 +153,8 @@
|
||||
selectedFilterIds = [];
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
thinkingEnabled = false;
|
||||
|
||||
if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
try {
|
||||
@ -167,6 +170,7 @@
|
||||
webSearchEnabled = input.webSearchEnabled;
|
||||
imageGenerationEnabled = input.imageGenerationEnabled;
|
||||
codeInterpreterEnabled = input.codeInterpreterEnabled;
|
||||
thinkingEnabled = input.thinkingEnabled;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@ -215,6 +219,7 @@
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
thinkingEnabled = false;
|
||||
};
|
||||
|
||||
const setToolIds = async () => {
|
||||
@ -454,6 +459,7 @@
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
thinkingEnabled = false;
|
||||
|
||||
try {
|
||||
const input = JSON.parse(
|
||||
@ -468,6 +474,7 @@
|
||||
webSearchEnabled = input.webSearchEnabled;
|
||||
imageGenerationEnabled = input.imageGenerationEnabled;
|
||||
codeInterpreterEnabled = input.codeInterpreterEnabled;
|
||||
thinkingEnabled = input.thinkingEnabled;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
@ -1659,6 +1666,19 @@
|
||||
: undefined
|
||||
},
|
||||
|
||||
// Add thinking mode option for compatible models
|
||||
...(thinkingEnabled && (model.owned_by === 'ollama' || model?.info?.meta?.owned_by === 'ollama') &&
|
||||
(() => {
|
||||
const modelName = model?.name?.toLowerCase() || model.id.toLowerCase();
|
||||
return modelName.includes('deepseek-r1') || modelName.includes('qwen3') || modelName.includes('magistral');
|
||||
})()
|
||||
? {
|
||||
options: {
|
||||
thinking: true
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
files: (files?.length ?? 0) > 0 ? files : undefined,
|
||||
|
||||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||
@ -2112,6 +2132,7 @@
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:thinkingEnabled
|
||||
bind:atSelectedModel
|
||||
toolServers={$toolServers}
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
@ -2171,6 +2192,7 @@
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:thinkingEnabled
|
||||
bind:atSelectedModel
|
||||
transparentBackground={$settings?.backgroundImageUrl ?? false}
|
||||
toolServers={$toolServers}
|
||||
|
@ -89,6 +89,9 @@
|
||||
export let webSearchEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
|
||||
// Add thinking mode state
|
||||
export let thinkingEnabled = false;
|
||||
|
||||
$: onChange({
|
||||
prompt,
|
||||
files: files
|
||||
@ -104,7 +107,8 @@
|
||||
selectedFilterIds,
|
||||
imageGenerationEnabled,
|
||||
webSearchEnabled,
|
||||
codeInterpreterEnabled
|
||||
codeInterpreterEnabled,
|
||||
thinkingEnabled
|
||||
});
|
||||
|
||||
let showTools = false;
|
||||
@ -159,6 +163,15 @@
|
||||
$models.find((m) => m.id === model)?.info?.meta?.capabilities?.code_interpreter ?? true
|
||||
);
|
||||
|
||||
// Add thinking capable models check
|
||||
let thinkingCapableModels = [];
|
||||
$: thinkingCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
|
||||
(model) => {
|
||||
const modelName = $models.find((m) => m.id === model)?.name?.toLowerCase() || model.toLowerCase();
|
||||
return modelName.includes('deepseek-r1') || modelName.includes('qwen3') || modelName.includes('magistral');
|
||||
}
|
||||
);
|
||||
|
||||
let toggleFilters = [];
|
||||
$: toggleFilters = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels)
|
||||
.map((id) => ($models.find((model) => model.id === id) || {})?.filters ?? [])
|
||||
@ -188,6 +201,12 @@
|
||||
$config?.features?.enable_code_interpreter &&
|
||||
($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter);
|
||||
|
||||
let showThinkingButton = false;
|
||||
$: showThinkingButton =
|
||||
(atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length ===
|
||||
thinkingCapableModels.length &&
|
||||
thinkingCapableModels.length > 0; // Only show if at least one model supports thinking
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTo({
|
||||
@ -744,6 +763,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if thinkingEnabled && showThinkingButton}
|
||||
<div class="mx-2.5 mt-2 -mb-1">
|
||||
<div class="flex items-center gap-2 px-2.5 py-1.5 bg-sky-50 dark:bg-sky-200/5 border border-sky-200 dark:border-sky-800 rounded-lg text-sky-600 dark:text-sky-300">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="size-4 animate-pulse"
|
||||
>
|
||||
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
|
||||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
|
||||
<path d="M2 12h20" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{$i18n.t('Thinking mode enabled - Model may take longer but respond better')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="px-2.5">
|
||||
{#if $settings?.richTextInput ?? true}
|
||||
<div
|
||||
@ -1268,7 +1309,7 @@
|
||||
</button>
|
||||
</InputMenu>
|
||||
|
||||
{#if $_user && (showToolsButton || (toggleFilters && toggleFilters.length > 0) || showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton)}
|
||||
{#if $_user && (showToolsButton || (toggleFilters && toggleFilters.length > 0) || showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showThinkingButton)}
|
||||
<div
|
||||
class="flex self-center w-[1px] h-4 mx-1.5 bg-gray-50 dark:bg-gray-800"
|
||||
/>
|
||||
@ -1394,6 +1435,38 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
{#if showThinkingButton}
|
||||
<Tooltip content={$i18n.t('Enable thinking mode - Model may take longer but respond better')} placement="top">
|
||||
<button
|
||||
on:click|preventDefault={() => (thinkingEnabled = !thinkingEnabled)}
|
||||
type="button"
|
||||
class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {thinkingEnabled
|
||||
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
|
||||
: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
|
||||
>
|
||||
<!-- Brain/Thinking icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="size-4"
|
||||
>
|
||||
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
|
||||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
|
||||
<path d="M2 12h20" />
|
||||
</svg>
|
||||
<span
|
||||
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis leading-none pr-0.5"
|
||||
>{$i18n.t('Thinking')}</span
|
||||
>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -50,7 +50,7 @@
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
videoInputDevices = devices.filter((device) => device.kind === 'videoinput');
|
||||
|
||||
if (!!navigator.mediaDevices.getDisplayMedia) {
|
||||
if (navigator.mediaDevices.getDisplayMedia) {
|
||||
videoInputDevices = [
|
||||
...videoInputDevices,
|
||||
{
|
||||
|
@ -39,6 +39,7 @@
|
||||
export let imageGenerationEnabled = false;
|
||||
export let codeInterpreterEnabled = false;
|
||||
export let webSearchEnabled = false;
|
||||
export let thinkingEnabled = false;
|
||||
|
||||
export let toolServers = [];
|
||||
|
||||
@ -213,6 +214,7 @@
|
||||
bind:imageGenerationEnabled
|
||||
bind:codeInterpreterEnabled
|
||||
bind:webSearchEnabled
|
||||
bind:thinkingEnabled
|
||||
bind:atSelectedModel
|
||||
{toolServers}
|
||||
{transparentBackground}
|
||||
|
@ -130,7 +130,7 @@
|
||||
|
||||
TTSModel = await KokoroTTS.from_pretrained(model_id, {
|
||||
dtype: TTSEngineConfig.dtype, // Options: "fp32", "fp16", "q8", "q4", "q4f16"
|
||||
device: !!navigator?.gpu ? 'webgpu' : 'wasm', // Detect WebGPU
|
||||
device: navigator?.gpu ? 'webgpu' : 'wasm', // Detect WebGPU
|
||||
progress_callback: (e) => {
|
||||
TTSModelProgress = e;
|
||||
console.log(e);
|
||||
|
@ -38,10 +38,10 @@ const createIsLoadingStore = (i18n: i18nType) => {
|
||||
};
|
||||
|
||||
export const initI18n = (defaultLocale?: string | undefined) => {
|
||||
let detectionOrder = defaultLocale
|
||||
const detectionOrder = defaultLocale
|
||||
? ['querystring', 'localStorage']
|
||||
: ['querystring', 'localStorage', 'navigator'];
|
||||
let fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US'];
|
||||
const fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US'];
|
||||
|
||||
const loadResource = (language: string, namespace: string) =>
|
||||
import(`./locales/${language}/${namespace}.json`);
|
||||
|
@ -221,6 +221,9 @@
|
||||
"Code Interpreter": "",
|
||||
"Code Interpreter Engine": "",
|
||||
"Code Interpreter Prompt Template": "",
|
||||
"Thinking": "Thinking",
|
||||
"Enable thinking mode - Model may take longer but respond better": "Enable thinking mode - Model may take longer but respond better",
|
||||
"Thinking mode enabled - Model may take longer but respond better": "Thinking mode enabled - Model may take longer but respond better",
|
||||
"Collapse": "",
|
||||
"Collection": "",
|
||||
"Color": "",
|
||||
|
@ -74,15 +74,15 @@ const readPngChunks = (data) => {
|
||||
|
||||
if (!isValidPng) throw new Error('Invalid PNG file');
|
||||
|
||||
let chunks = [];
|
||||
const chunks = [];
|
||||
let offset = 8; // Skip PNG signature
|
||||
|
||||
while (offset < data.length) {
|
||||
let length =
|
||||
const length =
|
||||
(data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
|
||||
let type = String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8));
|
||||
let chunkData = data.slice(offset + 8, offset + 8 + length);
|
||||
let crc =
|
||||
const type = String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8));
|
||||
const chunkData = data.slice(offset + 8, offset + 8 + length);
|
||||
const crc =
|
||||
(data[offset + 8 + length] << 24) |
|
||||
(data[offset + 8 + length + 1] << 16) |
|
||||
(data[offset + 8 + length + 2] << 8) |
|
||||
|
@ -23,8 +23,8 @@ const ALLOWED_SURROUNDING_CHARS =
|
||||
// const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/;
|
||||
// const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
|
||||
|
||||
let inlinePatterns = [];
|
||||
let blockPatterns = [];
|
||||
const inlinePatterns = [];
|
||||
const blockPatterns = [];
|
||||
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
@ -69,7 +69,7 @@ export default function (options = {}) {
|
||||
}
|
||||
|
||||
function katexStart(src, displayMode: boolean) {
|
||||
let ruleReg = displayMode ? blockRule : inlineRule;
|
||||
const ruleReg = displayMode ? blockRule : inlineRule;
|
||||
|
||||
let indexSrc = src;
|
||||
|
||||
@ -78,7 +78,7 @@ function katexStart(src, displayMode: boolean) {
|
||||
let startIndex = -1;
|
||||
let startDelimiter = '';
|
||||
let endDelimiter = '';
|
||||
for (let delimiter of DELIMITER_LIST) {
|
||||
for (const delimiter of DELIMITER_LIST) {
|
||||
if (delimiter.display !== displayMode) {
|
||||
continue;
|
||||
}
|
||||
@ -115,8 +115,8 @@ function katexStart(src, displayMode: boolean) {
|
||||
}
|
||||
|
||||
function katexTokenizer(src, tokens, displayMode: boolean) {
|
||||
let ruleReg = displayMode ? blockRule : inlineRule;
|
||||
let type = displayMode ? 'blockKatex' : 'inlineKatex';
|
||||
const ruleReg = displayMode ? blockRule : inlineRule;
|
||||
const type = displayMode ? 'blockKatex' : 'inlineKatex';
|
||||
|
||||
const match = src.match(ruleReg);
|
||||
|
||||
|
@ -20,7 +20,7 @@ self.onmessage = async (event) => {
|
||||
try {
|
||||
tts = await KokoroTTS.from_pretrained(model_id, {
|
||||
dtype,
|
||||
device: !!navigator?.gpu ? 'webgpu' : 'wasm' // Detect WebGPU
|
||||
device: navigator?.gpu ? 'webgpu' : 'wasm' // Detect WebGPU
|
||||
});
|
||||
isInitialized = true; // Mark as initialized after successful loading
|
||||
self.postMessage({ status: 'init:complete' });
|
||||
|
@ -40,7 +40,7 @@ async function loadPyodideAndPackages(packages: string[] = []) {
|
||||
packages: ['micropip']
|
||||
});
|
||||
|
||||
let mountDir = '/mnt';
|
||||
const mountDir = '/mnt';
|
||||
self.pyodide.FS.mkdirTree(mountDir);
|
||||
// self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, mountDir);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user