mirror of
https://github.com/open-webui/open-webui
synced 2025-05-02 20:11:56 +00:00
enh: <think> tag support
This commit is contained in:
parent
8d3c73aed5
commit
c9dc7299c5
@ -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:
|
async for line in response.body_iterator:
|
||||||
line = line.decode("utf-8") if isinstance(line, bytes) else line
|
line = line.decode("utf-8") if isinstance(line, bytes) else line
|
||||||
data = line
|
data = line
|
||||||
@ -1098,7 +1104,6 @@ async def process_chat_response(
|
|||||||
"selectedModelId": data["selected_model_id"],
|
"selectedModelId": data["selected_model_id"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
value = (
|
value = (
|
||||||
data.get("choices", [])[0]
|
data.get("choices", [])[0]
|
||||||
@ -1109,6 +1114,39 @@ async def process_chat_response(
|
|||||||
if value:
|
if value:
|
||||||
content = f"{content}{value}"
|
content = f"{content}{value}"
|
||||||
|
|
||||||
|
if detect_reasoning:
|
||||||
|
if "<think>\n" in content:
|
||||||
|
reasoning_start_time = time.time()
|
||||||
|
reasoning_content = ""
|
||||||
|
content = content.replace("<think>\n", "")
|
||||||
|
|
||||||
|
if reasoning_start_time is not None:
|
||||||
|
reasoning_content += value
|
||||||
|
|
||||||
|
if "</think>\n" in reasoning_content:
|
||||||
|
reasoning_end_time = time.time()
|
||||||
|
reasoning_duration = int(
|
||||||
|
reasoning_end_time
|
||||||
|
- reasoning_start_time
|
||||||
|
)
|
||||||
|
|
||||||
|
reasoning_content = (
|
||||||
|
reasoning_content.strip("<think>\n")
|
||||||
|
.strip("</think>\n")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
if reasoning_content:
|
||||||
|
# Format reasoning with <details> tag
|
||||||
|
content = f"<details>\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_content}\n</details>\n"
|
||||||
|
else:
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
reasoning_start_time = None
|
||||||
|
else:
|
||||||
|
# Show ongoing thought process
|
||||||
|
content = f"<details>\n<summary>Thinking… <loading/></summary>\n{reasoning_content}\n</details>\n"
|
||||||
|
|
||||||
if ENABLE_REALTIME_CHAT_SAVE:
|
if ENABLE_REALTIME_CHAT_SAVE:
|
||||||
# Save message in the database
|
# Save message in the database
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
@ -1129,10 +1167,8 @@ async def process_chat_response(
|
|||||||
"data": data,
|
"data": data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
done = "data: [DONE]" in line
|
done = "data: [DONE]" in line
|
||||||
|
|
||||||
if done:
|
if done:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
44
src/app.css
44
src/app.css
@ -127,6 +127,50 @@ select {
|
|||||||
-webkit-appearance: none;
|
-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 {
|
.katex-mathml {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if token.type === 'details'}
|
{:else if token.type === 'details'}
|
||||||
<Collapsible title={token.summary} className="w-fit space-y-1">
|
<Collapsible
|
||||||
|
title={token.summary}
|
||||||
|
isLoading={token?.isLoading ?? false}
|
||||||
|
className="w-fit space-y-1"
|
||||||
|
>
|
||||||
<div class=" mb-1.5" slot="content">
|
<div class=" mb-1.5" slot="content">
|
||||||
<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
|
<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
<div class=" self-center font-semibold mb-0.5 line-clamp-1 flex gap-1 items-center">
|
<div class=" self-center font-semibold line-clamp-1 flex gap-1 items-center">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1234,48 +1234,4 @@
|
|||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -9,12 +9,14 @@
|
|||||||
|
|
||||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let buttonClassName =
|
export let buttonClassName =
|
||||||
'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
||||||
export let title = null;
|
export let title = null;
|
||||||
|
export let isLoading = false;
|
||||||
|
|
||||||
export let grow = false;
|
export let grow = false;
|
||||||
|
|
||||||
@ -34,12 +36,23 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" w-full font-medium flex items-center justify-between gap-2">
|
<div
|
||||||
|
class=" w-full font-medium flex items-center justify-between gap-2 {isLoading === true
|
||||||
|
? 'shimmer'
|
||||||
|
: ''}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{#if isLoading}
|
||||||
|
<div>
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="flex self-center translate-y-[1px]">
|
||||||
{#if open}
|
{#if open}
|
||||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -18,18 +18,26 @@ function findMatchingClosingTag(src, openTag, closeTag) {
|
|||||||
function detailsTokenizer(src) {
|
function detailsTokenizer(src) {
|
||||||
const detailsRegex = /^<details>\n/;
|
const detailsRegex = /^<details>\n/;
|
||||||
const summaryRegex = /^<summary>(.*?)<\/summary>\n/;
|
const summaryRegex = /^<summary>(.*?)<\/summary>\n/;
|
||||||
|
const loadingRegex = /<loading\s*\/>/; // Detect <loading/>
|
||||||
|
|
||||||
if (detailsRegex.test(src)) {
|
if (detailsRegex.test(src)) {
|
||||||
const endIndex = findMatchingClosingTag(src, '<details>', '</details>');
|
const endIndex = findMatchingClosingTag(src, '<details>', '</details>');
|
||||||
if (endIndex === -1) return;
|
if (endIndex === -1) return;
|
||||||
|
|
||||||
const fullMatch = src.slice(0, endIndex);
|
const fullMatch = src.slice(0, endIndex);
|
||||||
let content = fullMatch.slice(10, -10).trim(); // Remove <details> and </details>
|
let content = fullMatch.slice(10, -10).trim(); // Remove <details> and </details>
|
||||||
|
|
||||||
let summary = '';
|
let summary = '';
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
const summaryMatch = summaryRegex.exec(content);
|
const summaryMatch = summaryRegex.exec(content);
|
||||||
if (summaryMatch) {
|
if (summaryMatch) {
|
||||||
summary = summaryMatch[1].trim();
|
summary = summaryMatch[1].trim();
|
||||||
|
|
||||||
|
// Detect and remove <loading/>
|
||||||
|
if (loadingRegex.test(summary)) {
|
||||||
|
isLoading = true;
|
||||||
|
summary = summary.replace(loadingRegex, '').trim(); // Remove <loading/> from summary
|
||||||
|
}
|
||||||
|
|
||||||
content = content.slice(summaryMatch[0].length).trim();
|
content = content.slice(summaryMatch[0].length).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +45,8 @@ function detailsTokenizer(src) {
|
|||||||
type: 'details',
|
type: 'details',
|
||||||
raw: fullMatch,
|
raw: fullMatch,
|
||||||
summary: summary,
|
summary: summary,
|
||||||
text: content
|
text: content,
|
||||||
|
isLoading: isLoading // Include loading property to indicate if <loading/> was present
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user