Compare commits

...

3 Commits

Author SHA1 Message Date
Louis
804b0f0116
fix: should not include reasoning text in the chat completion request 2025-08-06 17:34:34 +07:00
Faisal Amir
4727132d3c
fix: gpt-oss thinking block (#6071) 2025-08-06 16:10:45 +07:00
Faisal Amir
4f5bde4964
fix: react state loop from hooks useMediaQuery (#6031)
* fix: react state loop from hooks useMediaQuerry

* chore: update test cases hooks media query
2025-08-06 14:01:12 +07:00
5 changed files with 57 additions and 48 deletions

View File

@ -30,14 +30,31 @@ const ThinkingBlock = ({ id, text }: Props) => {
const { thinkingState, setThinkingState } = useThinkingStore() const { thinkingState, setThinkingState } = useThinkingStore()
const { streamingContent } = useAppState() const { streamingContent } = useAppState()
const { t } = useTranslation() const { t } = useTranslation()
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 isExpanded = thinkingState[id] ?? (loading ? true : false)
const handleClick = () => { const handleClick = () => {
const newExpandedState = !isExpanded const newExpandedState = !isExpanded
setThinkingState(id, newExpandedState) setThinkingState(id, newExpandedState)
} }
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()
}
const thinkingContent = extractThinkingContent(text)
if (!thinkingContent) return null
return ( return (
<div <div
@ -63,7 +80,7 @@ const ThinkingBlock = ({ id, text }: Props) => {
{isExpanded && ( {isExpanded && (
<div className="mt-2 pl-6 pr-4 text-main-view-fg/60"> <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>
)} )}
</div> </div>

View File

@ -170,18 +170,33 @@ export const ThreadContent = memo(
) )
const { reasoningSegment, textSegment } = useMemo(() => { const { reasoningSegment, textSegment } = useMemo(() => {
const isThinking = text.includes('<think>') && !text.includes('</think>') // Check for thinking formats
if (isThinking) return { reasoningSegment: text, textSegment: '' } 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>/) // Check for completed think tag format
if (match?.index === undefined) const thinkMatch = text.match(/<think>([\s\S]*?)<\/think>/)
return { reasoningSegment: undefined, textSegment: text } if (thinkMatch?.index !== undefined) {
const splitIndex = thinkMatch.index + thinkMatch[0].length
const splitIndex = match.index + match[0].length return {
return { reasoningSegment: text.slice(0, splitIndex),
reasoningSegment: text.slice(0, splitIndex), textSegment: text.slice(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]) }, [text])
const { getMessages, deleteMessage } = useMessages() const { getMessages, deleteMessage } = useMessages()

View File

@ -266,14 +266,7 @@ describe('useSmallScreenStore', () => {
}) })
describe('useSmallScreen', () => { describe('useSmallScreen', () => {
beforeEach(() => { it('should return small screen state', () => {
// Reset the store state before each test
act(() => {
useSmallScreenStore.getState().setIsSmallScreen(false)
})
})
it('should return small screen state and update store', () => {
const mockMediaQueryList = { const mockMediaQueryList = {
matches: true, matches: true,
addEventListener: vi.fn(), addEventListener: vi.fn(),
@ -285,7 +278,6 @@ describe('useSmallScreen', () => {
const { result } = renderHook(() => useSmallScreen()) const { result } = renderHook(() => useSmallScreen())
expect(result.current).toBe(true) expect(result.current).toBe(true)
expect(useSmallScreenStore.getState().isSmallScreen).toBe(true)
}) })
it('should update when media query changes', () => { it('should update when media query changes', () => {
@ -309,7 +301,6 @@ describe('useSmallScreen', () => {
}) })
expect(result.current).toBe(true) expect(result.current).toBe(true)
expect(useSmallScreenStore.getState().isSmallScreen).toBe(true)
}) })
it('should use correct media query for small screen detection', () => { it('should use correct media query for small screen detection', () => {
@ -325,20 +316,4 @@ describe('useSmallScreen', () => {
expect(mockMatchMedia).toHaveBeenCalledWith('(max-width: 768px)') expect(mockMatchMedia).toHaveBeenCalledWith('(max-width: 768px)')
}) })
it('should persist state across multiple hook instances', () => {
const mockMediaQueryList = {
matches: true,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
mockMatchMedia.mockReturnValue(mockMediaQueryList)
const { result: result1 } = renderHook(() => useSmallScreen())
const { result: result2 } = renderHook(() => useSmallScreen())
expect(result1.current).toBe(true)
expect(result2.current).toBe(true)
})
}) })

View File

@ -77,14 +77,7 @@ export function useMediaQuery(
return matches || false return matches || false
} }
// Specific hook for small screen detection with state management // Specific hook for small screen detection
export const useSmallScreen = (): boolean => { export const useSmallScreen = (): boolean => {
const { isSmallScreen, setIsSmallScreen } = useSmallScreenStore() return useMediaQuery('(max-width: 768px)')
const mediaQuery = useMediaQuery('(max-width: 768px)')
useEffect(() => {
setIsSmallScreen(mediaQuery)
}, [mediaQuery, setIsSmallScreen])
return isSmallScreen
} }

View File

@ -102,6 +102,15 @@ export class CompletionMessagesBuilder {
content = content.slice(splitIndex).trim() content = content.slice(splitIndex).trim()
} }
} }
if (content.includes('<|channel|>analysis<|message|>')) {
const match = content.match(
/<\|channel\|>analysis<\|message\|>([\s\S]*?)<\|start\|>assistant<\|channel\|>final<\|message\|>/
)
if (match?.index !== undefined) {
const splitIndex = match.index + match[0].length
content = content.slice(splitIndex).trim()
}
}
return content return content
} }
} }