feat: Improve the display of thinking content in the desktop UI (#8414)
Signed-off-by: jh-block <jhugo@block.xyz>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useMemo, useRef } from 'react';
|
|||||||
import ImagePreview from './ImagePreview';
|
import ImagePreview from './ImagePreview';
|
||||||
import { formatMessageTimestamp } from '../utils/timeUtils';
|
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||||
import MarkdownContent from './MarkdownContent';
|
import MarkdownContent from './MarkdownContent';
|
||||||
|
import ThinkingContent from './ThinkingContent';
|
||||||
import ToolCallWithResponse from './ToolCallWithResponse';
|
import ToolCallWithResponse from './ToolCallWithResponse';
|
||||||
import {
|
import {
|
||||||
getTextAndImageContent,
|
getTextAndImageContent,
|
||||||
@@ -47,27 +48,9 @@ export default function GooseMessage({
|
|||||||
}: GooseMessageProps) {
|
}: GooseMessageProps) {
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
let { textContent, imagePaths } = getTextAndImageContent(message);
|
const { textContent: displayText, imagePaths } = getTextAndImageContent(message);
|
||||||
const thinkingContent = getThinkingContent(message);
|
const thinkingContent = getThinkingContent(message);
|
||||||
|
|
||||||
const splitChainOfThought = (text: string): { displayText: string; cotText: string | null } => {
|
|
||||||
const regex = /<think>([\s\S]*?)<\/think>/i;
|
|
||||||
const match = text.match(regex);
|
|
||||||
if (!match) {
|
|
||||||
return { displayText: text, cotText: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const cotRaw = match[1].trim();
|
|
||||||
const displayText = text.replace(regex, '').trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
displayText,
|
|
||||||
cotText: cotRaw || null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const { displayText, cotText } = splitChainOfThought(textContent);
|
|
||||||
|
|
||||||
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
|
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
|
||||||
const toolRequests = getToolRequests(message);
|
const toolRequests = getToolRequests(message);
|
||||||
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
|
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
|
||||||
@@ -132,15 +115,10 @@ export default function GooseMessage({
|
|||||||
<div className="goose-message flex w-[90%] justify-start min-w-0">
|
<div className="goose-message flex w-[90%] justify-start min-w-0">
|
||||||
<div className="flex flex-col w-full min-w-0">
|
<div className="flex flex-col w-full min-w-0">
|
||||||
{thinkingContent && (
|
{thinkingContent && (
|
||||||
<div className="mb-2 text-xs text-gray-400/70 italic">
|
<ThinkingContent
|
||||||
<MarkdownContent content={thinkingContent} />
|
content={thinkingContent}
|
||||||
</div>
|
isExpanded={isStreaming && !displayText.trim() && imagePaths.length === 0 && toolRequests.length === 0}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
{cotText && (
|
|
||||||
<div className="mb-2 text-sm text-gray-400 italic">
|
|
||||||
<MarkdownContent content={cotText} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(displayText.trim() || imagePaths.length > 0) && (
|
{(displayText.trim() || imagePaths.length > 0) && (
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import MarkdownContent from './MarkdownContent';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
||||||
|
import Expand from './ui/Expand';
|
||||||
|
|
||||||
|
interface ThinkingContentProps {
|
||||||
|
content: string;
|
||||||
|
isExpanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThinkingContent({ content, isExpanded }: ThinkingContentProps) {
|
||||||
|
const [manualToggle, setManualToggle] = useState<boolean | null>(null);
|
||||||
|
const prevIsExpanded = useRef(isExpanded);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevIsExpanded.current && !isExpanded) {
|
||||||
|
setManualToggle(null);
|
||||||
|
}
|
||||||
|
prevIsExpanded.current = isExpanded;
|
||||||
|
}, [isExpanded]);
|
||||||
|
|
||||||
|
const expanded = manualToggle !== null ? manualToggle : isExpanded;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible open={expanded} onOpenChange={(open) => setManualToggle(open)} className="mb-2">
|
||||||
|
<CollapsibleTrigger className="flex items-center gap-1.5 text-xs text-text-secondary hover:text-text-primary transition-colors cursor-pointer">
|
||||||
|
<Expand size={3} isExpanded={expanded} />
|
||||||
|
<span className="italic">Thinking</span>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="mt-1 ml-[18px] text-xs text-text-secondary italic">
|
||||||
|
<MarkdownContent content={content} />
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -94,19 +94,39 @@ export function getTextAndImageContent(message: Message): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip <think> tags from assistant text — the thinking is surfaced via getThinkingContent
|
||||||
|
if (message.role === 'assistant') {
|
||||||
|
textContent = textContent.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
||||||
|
}
|
||||||
|
|
||||||
return { textContent, imagePaths };
|
return { textContent, imagePaths };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThinkingContent(message: Message): string | null {
|
export function getThinkingContent(message: Message): string | null {
|
||||||
const thinkingContents = message.content
|
const parts: string[] = [];
|
||||||
.filter((content) => content.type === 'thinking')
|
|
||||||
.map((content) => {
|
|
||||||
if ('thinking' in content) return content.thinking;
|
|
||||||
return '';
|
|
||||||
})
|
|
||||||
.filter((text) => text.length > 0);
|
|
||||||
|
|
||||||
return thinkingContents.length > 0 ? thinkingContents.join('') : null;
|
// Structured thinking content blocks
|
||||||
|
for (const content of message.content) {
|
||||||
|
if (content.type === 'thinking' && 'thinking' in content && content.thinking) {
|
||||||
|
parts.push(content.thinking);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline <think> tags in assistant text content
|
||||||
|
if (message.role === 'assistant') {
|
||||||
|
for (const content of message.content) {
|
||||||
|
if (content.type === 'text') {
|
||||||
|
const regex = /<think>([\s\S]*?)<\/think>/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content.text)) !== null) {
|
||||||
|
const text = match[1].trim();
|
||||||
|
if (text) parts.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join('') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {
|
export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user