浏览代码

refac: think tag

Timothy Jaeryang Baek 3 月之前
父节点
当前提交
9feed97f22

+ 2 - 2
backend/open_webui/utils/middleware.py

@@ -1166,7 +1166,7 @@ async def process_chat_response(
                                                 )
                                                 )
 
 
                                                 # Format reasoning with <details> tag
                                                 # Format reasoning with <details> tag
-                                                content = f"{ongoing_content}<details>\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n"
+                                                content = f'{ongoing_content}<details type="reasoning" done="true">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
                                             else:
                                             else:
                                                 content = ""
                                                 content = ""
 
 
@@ -1183,7 +1183,7 @@ async def process_chat_response(
                                             )
                                             )
 
 
                                             # Show ongoing thought process
                                             # Show ongoing thought process
-                                            content = f"{ongoing_content}<details>\n<summary>Thinking… <loading/></summary>\n{reasoning_display_content}\n</details>\n"
+                                            content = f'{ongoing_content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
 
 
                                 if ENABLE_REALTIME_CHAT_SAVE:
                                 if ENABLE_REALTIME_CHAT_SAVE:
                                     # Save message in the database
                                     # Save message in the database

+ 1 - 5
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

@@ -195,11 +195,7 @@
 			</ul>
 			</ul>
 		{/if}
 		{/if}
 	{:else if token.type === 'details'}
 	{:else if token.type === 'details'}
-		<Collapsible
-			title={token.summary}
-			isLoading={token?.isLoading ?? false}
-			className="w-fit space-y-1"
-		>
+		<Collapsible title={token.summary} attributes={token?.attributes} 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>

+ 4 - 3
src/lib/components/common/Collapsible.svelte

@@ -16,7 +16,7 @@
 	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 attributes = null;
 
 
 	export let grow = false;
 	export let grow = false;
 
 
@@ -37,12 +37,13 @@
 			}}
 			}}
 		>
 		>
 			<div
 			<div
-				class=" w-full font-medium flex items-center justify-between gap-2 {isLoading === true
+				class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done !==
+				'true'
 					? 'shimmer'
 					? 'shimmer'
 					: ''}
 					: ''}
 			"
 			"
 			>
 			>
-				{#if isLoading}
+				{#if attributes?.done !== 'true'}
 					<div>
 					<div>
 						<Spinner className="size-4" />
 						<Spinner className="size-4" />
 					</div>
 					</div>

+ 34 - 19
src/lib/utils/marked/extension.ts

@@ -1,5 +1,5 @@
 // Helper function to find matching closing tag
 // Helper function to find matching closing tag
-function findMatchingClosingTag(src, openTag, closeTag) {
+function findMatchingClosingTag(src: string, openTag: string, closeTag: string): number {
 	let depth = 1;
 	let depth = 1;
 	let index = openTag.length;
 	let index = openTag.length;
 	while (depth > 0 && index < src.length) {
 	while (depth > 0 && index < src.length) {
@@ -15,29 +15,37 @@ function findMatchingClosingTag(src, openTag, closeTag) {
 	return depth === 0 ? index + closeTag.length : -1;
 	return depth === 0 ? index + closeTag.length : -1;
 }
 }
 
 
-function detailsTokenizer(src) {
-	const detailsRegex = /^<details>\n/;
+// Function to parse attributes from tag
+function parseAttributes(tag: string): { [key: string]: string } {
+	const attributes: { [key: string]: string } = {};
+	const attrRegex = /(\w+)="(.*?)"/g;
+	let match;
+	while ((match = attrRegex.exec(tag)) !== null) {
+		attributes[match[1]] = match[2];
+	}
+	return attributes;
+}
+
+function detailsTokenizer(src: string) {
+	// Updated regex to capture attributes inside <details>
+	const detailsRegex = /^<details(\s+[^>]*)?>\n/;
 	const summaryRegex = /^<summary>(.*?)<\/summary>\n/;
 	const summaryRegex = /^<summary>(.*?)<\/summary>\n/;
-	const loadingRegex = /<loading\s*\/>/; // Detect <loading/>
 
 
-	if (detailsRegex.test(src)) {
-		const endIndex = findMatchingClosingTag(src, '<details>', '</details>');
+	const detailsMatch = detailsRegex.exec(src);
+	if (detailsMatch) {
+		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>
+		const detailsTag = detailsMatch[0];
+		const attributes = parseAttributes(detailsTag); // Parse attributes from <details>
+
+		let content = fullMatch.slice(detailsTag.length, -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();
 		}
 		}
 
 
@@ -46,22 +54,29 @@ function detailsTokenizer(src) {
 			raw: fullMatch,
 			raw: fullMatch,
 			summary: summary,
 			summary: summary,
 			text: content,
 			text: content,
-			isLoading: isLoading // Include loading property to indicate if <loading/> was present
+			attributes: attributes // Include extracted attributes from <details>
 		};
 		};
 	}
 	}
 }
 }
 
 
-function detailsStart(src) {
+function detailsStart(src: string) {
 	return src.match(/^<details>/) ? 0 : -1;
 	return src.match(/^<details>/) ? 0 : -1;
 }
 }
 
 
-function detailsRenderer(token) {
-	return `<details>
+function detailsRenderer(token: any) {
+	const attributesString = token.attributes
+		? Object.entries(token.attributes)
+				.map(([key, value]) => `${key}="${value}"`)
+				.join(' ')
+		: '';
+
+	return `<details ${attributesString}>
   ${token.summary ? `<summary>${token.summary}</summary>` : ''}
   ${token.summary ? `<summary>${token.summary}</summary>` : ''}
   ${token.text}
   ${token.text}
   </details>`;
   </details>`;
 }
 }
 
 
+// Extension wrapper function
 function detailsExtension() {
 function detailsExtension() {
 	return {
 	return {
 		name: 'details',
 		name: 'details',