Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions web-app/src/containers/ThinkingBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
import { create } from 'zustand'
import { RenderMarkdown } from './RenderMarkdown'
import { useAppState } from '@/hooks/useAppState'
import { useTranslation } from '@/i18n/react-i18next-compat'

Check warning on line 5 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

1-5 lines are not covered with tests

interface Props {
text: string
Expand All @@ -15,45 +15,62 @@
setThinkingState: (id: string, expanded: boolean) => void
}

const useThinkingStore = create<ThinkingBlockState>((set) => ({
thinkingState: {},
setThinkingState: (id, expanded) =>
set((state) => ({
thinkingState: {
...state.thinkingState,
[id]: expanded,
},
})),
}))

Check warning on line 27 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

18-27 lines are not covered with tests

const ThinkingBlock = ({ id, text }: Props) => {
const { thinkingState, setThinkingState } = useThinkingStore()
const { streamingContent } = useAppState()
const { t } = useTranslation()

Check warning on line 32 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

29-32 lines are not covered with tests
const loading = !text.includes('</think>') && streamingContent
// Check for thinking formats
const hasThinkTag = text.includes('<think>') && !text.includes('</think>')
const hasAnalysisChannel = text.includes('<|channel|>analysis<|message|>') && !text.includes('<|start|>assistant<|channel|>final<|message|>')
const loading = (hasThinkTag || hasAnalysisChannel) && streamingContent
const isExpanded = thinkingState[id] ?? (loading ? true : false)
const handleClick = () => {
const newExpandedState = !isExpanded
setThinkingState(id, newExpandedState)
}

Check warning on line 41 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

34-41 lines are not covered with tests

if (!text.replace(/<\/?think>/g, '').trim()) return null
// Extract thinking content from either format
const extractThinkingContent = (text: string) => {
return text
.replace(/<\/?think>/g, '')
.replace(/<\|channel\|>analysis<\|message\|>/g, '')
.replace(/<\|start\|>assistant<\|channel\|>final<\|message\|>/g, '')
.replace(/assistant<\|channel\|>final<\|message\|>/g, '')
.replace(/<\|channel\|>/g, '') // remove any remaining channel markers
.replace(/<\|message\|>/g, '') // remove any remaining message markers
.replace(/<\|start\|>/g, '') // remove any remaining start markers
.trim()
}

Check warning on line 54 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

44-54 lines are not covered with tests

const thinkingContent = extractThinkingContent(text)
if (!thinkingContent) return null

Check warning on line 57 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

56-57 lines are not covered with tests

return (
<div
className="mx-auto w-full cursor-pointer break-words"
onClick={handleClick}

Check warning on line 62 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

59-62 lines are not covered with tests
>
<div className="mb-4 rounded-lg bg-main-view-fg/4 border border-dashed border-main-view-fg/10 p-2">
<div className="flex items-center gap-3">
{loading && (
<Loader className="size-4 animate-spin text-main-view-fg/60" />

Check warning on line 67 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

64-67 lines are not covered with tests
)}
<button className="flex items-center gap-2 focus:outline-none">
{isExpanded ? (
<ChevronUp className="size-4 text-main-view-fg/60" />

Check warning on line 71 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

69-71 lines are not covered with tests
) : (
<ChevronDown className="size-4 text-main-view-fg/60" />

Check warning on line 73 in web-app/src/containers/ThinkingBlock.tsx

View workflow job for this annotation

GitHub Actions / coverage-check

73 line is not covered with tests
)}
<span className="font-medium">
{loading ? t('common:thinking') : t('common:thought')}
Expand All @@ -63,7 +80,7 @@

{isExpanded && (
<div className="mt-2 pl-6 pr-4 text-main-view-fg/60">
<RenderMarkdown content={text.replace(/<\/?think>/g, '').trim()} />
<RenderMarkdown content={thinkingContent} />
</div>
)}
</div>
Expand Down
33 changes: 24 additions & 9 deletions web-app/src/containers/ThreadContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,33 @@ export const ThreadContent = memo(
)

const { reasoningSegment, textSegment } = useMemo(() => {
const isThinking = text.includes('<think>') && !text.includes('</think>')
if (isThinking) return { reasoningSegment: text, textSegment: '' }
// Check for thinking formats
const hasThinkTag = text.includes('<think>') && !text.includes('</think>')
const hasAnalysisChannel = text.includes('<|channel|>analysis<|message|>') && !text.includes('<|start|>assistant<|channel|>final<|message|>')

if (hasThinkTag || hasAnalysisChannel) return { reasoningSegment: text, textSegment: '' }

const match = text.match(/<think>([\s\S]*?)<\/think>/)
if (match?.index === undefined)
return { reasoningSegment: undefined, textSegment: text }
// Check for completed think tag format
const thinkMatch = text.match(/<think>([\s\S]*?)<\/think>/)
if (thinkMatch?.index !== undefined) {
const splitIndex = thinkMatch.index + thinkMatch[0].length
return {
reasoningSegment: text.slice(0, splitIndex),
textSegment: text.slice(splitIndex),
}
}

const splitIndex = match.index + match[0].length
return {
reasoningSegment: text.slice(0, splitIndex),
textSegment: text.slice(splitIndex),
// Check for completed analysis channel format
const analysisMatch = text.match(/<\|channel\|>analysis<\|message\|>([\s\S]*?)<\|start\|>assistant<\|channel\|>final<\|message\|>/)
if (analysisMatch?.index !== undefined) {
const splitIndex = analysisMatch.index + analysisMatch[0].length
return {
reasoningSegment: text.slice(0, splitIndex),
textSegment: text.slice(splitIndex),
}
}

return { reasoningSegment: undefined, textSegment: text }
}, [text])

const { getMessages, deleteMessage } = useMessages()
Expand Down
Loading