diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index f46bd88de..cf43aaa8f 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1072,6 +1072,12 @@ async def process_chat_response( }, ) + # We might want to disable this by default + detect_reasoning = True + + reasoning_start_time = None + reasoning_content = "" + async for line in response.body_iterator: line = line.decode("utf-8") if isinstance(line, bytes) else line data = line @@ -1098,7 +1104,6 @@ async def process_chat_response( "selectedModelId": data["selected_model_id"], }, ) - else: value = ( data.get("choices", [])[0] @@ -1109,6 +1114,39 @@ async def process_chat_response( if value: content = f"{content}{value}" + if detect_reasoning: + if "\n" in content: + reasoning_start_time = time.time() + reasoning_content = "" + content = content.replace("\n", "") + + if reasoning_start_time is not None: + reasoning_content += value + + if "\n" in reasoning_content: + reasoning_end_time = time.time() + reasoning_duration = int( + reasoning_end_time + - reasoning_start_time + ) + + reasoning_content = ( + reasoning_content.strip("\n") + .strip("\n") + .strip() + ) + + if reasoning_content: + # Format reasoning with
tag + content = f"
\nThought for {reasoning_duration} seconds\n{reasoning_content}\n
\n" + else: + content = "" + + reasoning_start_time = None + else: + # Show ongoing thought process + content = f"
\nThinking… \n{reasoning_content}\n
\n" + if ENABLE_REALTIME_CHAT_SAVE: # Save message in the database Chats.upsert_message_to_chat_by_id_and_message_id( @@ -1129,10 +1167,8 @@ async def process_chat_response( "data": data, } ) - except Exception as e: done = "data: [DONE]" in line - if done: pass else: diff --git a/src/app.css b/src/app.css index dad382818..f0ddfb522 100644 --- a/src/app.css +++ b/src/app.css @@ -127,6 +127,50 @@ select { -webkit-appearance: none; } +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.shimmer { + background: linear-gradient(90deg, #9a9b9e 25%, #2a2929 50%, #9a9b9e 75%); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer 4s linear infinite; + color: #818286; /* Fallback color */ +} + +:global(.dark) .shimmer { + background: linear-gradient(90deg, #818286 25%, #eae5e5 50%, #818286 75%); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer 4s linear infinite; + color: #a1a3a7; /* Darker fallback color for dark mode */ +} + +@keyframes smoothFadeIn { + 0% { + opacity: 0; + transform: translateY(-10px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.status-description { + animation: smoothFadeIn 0.2s forwards; +} + .katex-mathml { display: none; } diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte index 3106cfd5c..1ea7644d0 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte @@ -195,7 +195,11 @@ {/if} {:else if token.type === 'details'} - +
diff --git a/src/lib/components/chat/Messages/Name.svelte b/src/lib/components/chat/Messages/Name.svelte index 43084609b..82f2c5b21 100644 --- a/src/lib/components/chat/Messages/Name.svelte +++ b/src/lib/components/chat/Messages/Name.svelte @@ -1,3 +1,3 @@ -
+
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 4959be342..4c0a0996c 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -1234,48 +1234,4 @@ -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } - - @keyframes shimmer { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } - } - - .shimmer { - background: linear-gradient(90deg, #9a9b9e 25%, #2a2929 50%, #9a9b9e 75%); - background-size: 200% 100%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: shimmer 4s linear infinite; - color: #818286; /* Fallback color */ - } - - :global(.dark) .shimmer { - background: linear-gradient(90deg, #818286 25%, #eae5e5 50%, #818286 75%); - background-size: 200% 100%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: shimmer 4s linear infinite; - color: #a1a3a7; /* Darker fallback color for dark mode */ - } - - @keyframes smoothFadeIn { - 0% { - opacity: 0; - transform: translateY(-10px); - } - 100% { - opacity: 1; - transform: translateY(0); - } - } - - .status-description { - animation: smoothFadeIn 0.2s forwards; - } diff --git a/src/lib/components/common/Collapsible.svelte b/src/lib/components/common/Collapsible.svelte index a167e3cf2..579e10260 100644 --- a/src/lib/components/common/Collapsible.svelte +++ b/src/lib/components/common/Collapsible.svelte @@ -9,12 +9,14 @@ import ChevronUp from '../icons/ChevronUp.svelte'; import ChevronDown from '../icons/ChevronDown.svelte'; + import Spinner from './Spinner.svelte'; export let open = false; export let className = ''; export let buttonClassName = 'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition'; export let title = null; + export let isLoading = false; export let grow = false; @@ -34,12 +36,23 @@ } }} > -
+
+ {#if isLoading} +
+ +
+ {/if} +
{title}
-
+
{#if open} {:else} diff --git a/src/lib/utils/marked/extension.ts b/src/lib/utils/marked/extension.ts index 394b6906a..7a6fea8f0 100644 --- a/src/lib/utils/marked/extension.ts +++ b/src/lib/utils/marked/extension.ts @@ -18,18 +18,26 @@ function findMatchingClosingTag(src, openTag, closeTag) { function detailsTokenizer(src) { const detailsRegex = /^
\n/; const summaryRegex = /^(.*?)<\/summary>\n/; + const loadingRegex = //; // Detect if (detailsRegex.test(src)) { const endIndex = findMatchingClosingTag(src, '
', '
'); if (endIndex === -1) return; - const fullMatch = src.slice(0, endIndex); let content = fullMatch.slice(10, -10).trim(); // Remove
and
- let summary = ''; + let isLoading = false; + const summaryMatch = summaryRegex.exec(content); if (summaryMatch) { summary = summaryMatch[1].trim(); + + // Detect and remove + if (loadingRegex.test(summary)) { + isLoading = true; + summary = summary.replace(loadingRegex, '').trim(); // Remove from summary + } + content = content.slice(summaryMatch[0].length).trim(); } @@ -37,7 +45,8 @@ function detailsTokenizer(src) { type: 'details', raw: fullMatch, summary: summary, - text: content + text: content, + isLoading: isLoading // Include loading property to indicate if was present }; } }