fix: should not rerender thread message components when typing

This commit is contained in:
Louis 2025-09-18 22:44:03 +07:00
parent f237936b0c
commit 508879e3ae
11 changed files with 67 additions and 47 deletions

View File

@ -47,23 +47,23 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
const [isFocused, setIsFocused] = useState(false) const [isFocused, setIsFocused] = useState(false)
const [rows, setRows] = useState(1) const [rows, setRows] = useState(1)
const serviceHub = useServiceHub() const serviceHub = useServiceHub()
const { const streamingContent = useAppState((state) => state.streamingContent)
streamingContent, const abortControllers = useAppState((state) => state.abortControllers)
abortControllers, const loadingModel = useAppState((state) => state.loadingModel)
loadingModel, const tools = useAppState((state) => state.tools)
tools, const cancelToolCall = useAppState((state) => state.cancelToolCall)
cancelToolCall, const prompt = usePrompt((state) => state.prompt)
} = useAppState() const setPrompt = usePrompt((state) => state.setPrompt)
const { prompt, setPrompt } = usePrompt() const currentThreadId = useThreads((state) => state.currentThreadId)
const { currentThreadId } = useThreads()
const { t } = useTranslation() const { t } = useTranslation()
const { spellCheckChatInput } = useGeneralSetting() const { spellCheckChatInput } = useGeneralSetting()
useTools() useTools()
const maxRows = 10 const maxRows = 10
const { selectedModel, selectedProvider } = useModelProvider() const selectedModel = useModelProvider((state) => state.selectedModel)
const { sendMessage } = useChat() const selectedProvider = useModelProvider((state) => state.selectedProvider)
const sendMessage = useChat()
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [dropdownToolsAvailable, setDropdownToolsAvailable] = useState(false) const [dropdownToolsAvailable, setDropdownToolsAvailable] = useState(false)
const [tooltipToolsAvailable, setTooltipToolsAvailable] = useState(false) const [tooltipToolsAvailable, setTooltipToolsAvailable] = useState(false)

View File

@ -12,7 +12,7 @@ export const GenerateResponseButton = ({ threadId }: { threadId: string }) => {
messages: state.messages[threadId], messages: state.messages[threadId],
})) }))
) )
const { sendMessage } = useChat() const sendMessage = useChat()
const generateAIResponse = () => { const generateAIResponse = () => {
const latestUserMessage = messages[messages.length - 1] const latestUserMessage = messages[messages.length - 1]
if ( if (

View File

@ -21,7 +21,7 @@ function extractReasoningSegment(text: string) {
// Use memo with no dependencies to allow re-renders when props change // Use memo with no dependencies to allow re-renders when props change
// Avoid duplicate reasoning segments after tool calls // Avoid duplicate reasoning segments after tool calls
export const StreamingContent = memo(({ threadId }: Props) => { export const StreamingContent = memo(({ threadId }: Props) => {
const { streamingContent } = useAppState() const streamingContent = useAppState((state) => state.streamingContent)
const { getMessages } = useMessages() const { getMessages } = useMessages()
const messages = getMessages(threadId) const messages = getMessages(threadId)
@ -68,6 +68,7 @@ export const StreamingContent = memo(({ threadId }: Props) => {
}} }}
{...streamingContent} {...streamingContent}
isLastMessage={true} isLastMessage={true}
streamingThread={streamingContent.thread_id}
showAssistant={ showAssistant={
messages.length > 0 messages.length > 0
? messages[messages.length - 1].role !== 'assistant' ? messages[messages.length - 1].role !== 'assistant'

View File

@ -27,12 +27,15 @@ const useThinkingStore = create<ThinkingBlockState>((set) => ({
})) }))
const ThinkingBlock = ({ id, text }: Props) => { const ThinkingBlock = ({ id, text }: Props) => {
const { thinkingState, setThinkingState } = useThinkingStore() const thinkingState = useThinkingStore((state) => state.thinkingState)
const { streamingContent } = useAppState() const setThinkingState = useThinkingStore((state) => state.setThinkingState)
const streamingContent = useAppState((state) => state.streamingContent)
const { t } = useTranslation() const { t } = useTranslation()
// Check for thinking formats // Check for thinking formats
const hasThinkTag = text.includes('<think>') && !text.includes('</think>') const hasThinkTag = text.includes('<think>') && !text.includes('</think>')
const hasAnalysisChannel = text.includes('<|channel|>analysis<|message|>') && !text.includes('<|start|>assistant<|channel|>final<|message|>') const hasAnalysisChannel =
text.includes('<|channel|>analysis<|message|>') &&
!text.includes('<|start|>assistant<|channel|>final<|message|>')
const loading = (hasThinkTag || hasAnalysisChannel) && streamingContent const loading = (hasThinkTag || hasAnalysisChannel) && streamingContent
const isExpanded = thinkingState[id] ?? (loading ? true : false) const isExpanded = thinkingState[id] ?? (loading ? true : false)
const handleClick = () => { const handleClick = () => {

View File

@ -68,6 +68,7 @@ export const ThreadContent = memo(
isLastMessage?: boolean isLastMessage?: boolean
index?: number index?: number
showAssistant?: boolean showAssistant?: boolean
streamingThread?: string
streamTools?: any streamTools?: any
contextOverflowModal?: React.ReactNode | null contextOverflowModal?: React.ReactNode | null
@ -75,7 +76,7 @@ export const ThreadContent = memo(
} }
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
const { selectedModel } = useModelProvider() const selectedModel = useModelProvider((state) => state.selectedModel)
// Use useMemo to stabilize the components prop // Use useMemo to stabilize the components prop
const linkComponents = useMemo( const linkComponents = useMemo(
@ -87,7 +88,6 @@ export const ThreadContent = memo(
[] []
) )
const image = useMemo(() => item.content?.[0]?.image_url, [item]) const image = useMemo(() => item.content?.[0]?.image_url, [item])
const { streamingContent } = useAppState()
const text = useMemo( const text = useMemo(
() => item.content.find((e) => e.type === 'text')?.text?.value ?? '', () => item.content.find((e) => e.type === 'text')?.text?.value ?? '',
@ -129,8 +129,9 @@ export const ThreadContent = memo(
return { reasoningSegment: undefined, textSegment: text } return { reasoningSegment: undefined, textSegment: text }
}, [text]) }, [text])
const { getMessages, deleteMessage } = useMessages() const getMessages = useMessages((state) => state.getMessages)
const { sendMessage } = useChat() const deleteMessage = useMessages((state) => state.deleteMessage)
const sendMessage = useChat()
const regenerate = useCallback(() => { const regenerate = useCallback(() => {
// Only regenerate assistant message is allowed // Only regenerate assistant message is allowed
@ -361,8 +362,8 @@ export const ThreadContent = memo(
className={cn( className={cn(
'flex items-center gap-2', 'flex items-center gap-2',
item.isLastMessage && item.isLastMessage &&
streamingContent && item.streamingThread &&
streamingContent.thread_id === item.thread_id && item.streamingThread === item.thread_id &&
'hidden' 'hidden'
)} )}
> >
@ -395,9 +396,10 @@ export const ThreadContent = memo(
<TokenSpeedIndicator <TokenSpeedIndicator
streaming={Boolean( streaming={Boolean(
item.isLastMessage && item.isLastMessage
streamingContent && &&
streamingContent.thread_id === item.thread_id item.streamingThread &&
item.streamingThread === item.thread_id
)} )}
metadata={item.metadata} metadata={item.metadata}
/> />

View File

@ -46,14 +46,16 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
} = useSortable({ id: thread.id, disabled: true }) } = useSortable({ id: thread.id, disabled: true })
const isSmallScreen = useSmallScreen() const isSmallScreen = useSmallScreen()
const { setLeftPanel } = useLeftPanel() const setLeftPanel = useLeftPanel(state => state.setLeftPanel)
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
opacity: isDragging ? 0.5 : 1, opacity: isDragging ? 0.5 : 1,
} }
const { toggleFavorite, deleteThread, renameThread } = useThreads() const toggleFavorite = useThreads((state) => state.toggleFavorite)
const deleteThread = useThreads((state) => state.deleteThread)
const renameThread = useThreads((state) => state.renameThread)
const { t } = useTranslation() const { t } = useTranslation()
const [openDropdown, setOpenDropdown] = useState(false) const [openDropdown, setOpenDropdown] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()

View File

@ -1,6 +1,7 @@
import { useAppState } from '@/hooks/useAppState' import { useAppState } from '@/hooks/useAppState'
import { toNumber } from '@/utils/number' import { toNumber } from '@/utils/number'
import { Gauge } from 'lucide-react' import { Gauge } from 'lucide-react'
import { memo } from 'react'
interface TokenSpeedIndicatorProps { interface TokenSpeedIndicatorProps {
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
@ -11,7 +12,7 @@ export const TokenSpeedIndicator = ({
metadata, metadata,
streaming, streaming,
}: TokenSpeedIndicatorProps) => { }: TokenSpeedIndicatorProps) => {
const { tokenSpeed } = useAppState() const tokenSpeed = useAppState((state) => state.tokenSpeed)
const persistedTokenSpeed = const persistedTokenSpeed =
(metadata?.tokenSpeed as { tokenSpeed: number })?.tokenSpeed || 0 (metadata?.tokenSpeed as { tokenSpeed: number })?.tokenSpeed || 0
@ -40,4 +41,4 @@ export const TokenSpeedIndicator = ({
) )
} }
export default TokenSpeedIndicator export default memo(TokenSpeedIndicator)

View File

@ -16,7 +16,8 @@ import { useAppState } from '@/hooks/useAppState'
export default function ErrorDialog() { export default function ErrorDialog() {
const { t } = useTranslation() const { t } = useTranslation()
const { errorMessage, setErrorMessage } = useAppState() const errorMessage = useAppState((state) => state.errorMessage)
const setErrorMessage = useAppState((state) => state.setErrorMessage)
const [isCopying, setIsCopying] = useState(false) const [isCopying, setIsCopying] = useState(false)
const [isDetailExpanded, setIsDetailExpanded] = useState(true) const [isDetailExpanded, setIsDetailExpanded] = useState(true)

View File

@ -33,7 +33,6 @@ import {
} from '@/utils/reasoning' } from '@/utils/reasoning'
export const useChat = () => { export const useChat = () => {
const prompt = usePrompt((state) => state.prompt)
const setPrompt = usePrompt((state) => state.setPrompt) const setPrompt = usePrompt((state) => state.setPrompt)
const tools = useAppState((state) => state.tools) const tools = useAppState((state) => state.tools)
const updateTokenSpeed = useAppState((state) => state.updateTokenSpeed) const updateTokenSpeed = useAppState((state) => state.updateTokenSpeed)
@ -84,7 +83,7 @@ export const useChat = () => {
const selectedAssistant = const selectedAssistant =
assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] assistants.find((a) => a.id === currentAssistant?.id) || assistants[0]
const getCurrentThread = useCallback(async () => { const getCurrentThread = useCallback(async (prompt: string) => {
let currentThread = retrieveThread() let currentThread = retrieveThread()
if (!currentThread) { if (!currentThread) {
@ -226,7 +225,7 @@ export const useChat = () => {
dataUrl: string dataUrl: string
}> }>
) => { ) => {
const activeThread = await getCurrentThread() const activeThread = await getCurrentThread(message)
resetTokenSpeed() resetTokenSpeed()
let activeProvider = currentProviderId let activeProvider = currentProviderId
@ -572,5 +571,5 @@ export const useChat = () => {
] ]
) )
return useMemo(() => ({ sendMessage }), [sendMessage]) return useMemo(() => (sendMessage), [sendMessage])
} }

View File

@ -38,13 +38,18 @@ export function DataProvider() {
verboseLogs, verboseLogs,
proxyTimeout, proxyTimeout,
} = useLocalApiServer() } = useLocalApiServer()
const { setServerStatus } = useAppState() const setServerStatus = useAppState((state) => state.setServerStatus)
useEffect(() => { useEffect(() => {
console.log('Initializing DataProvider...') console.log('Initializing DataProvider...')
serviceHub.providers().getProviders().then(setProviders) serviceHub.providers().getProviders().then(setProviders)
serviceHub.mcp().getMCPConfig().then((data) => setServers(data.mcpServers ?? {})) serviceHub
serviceHub.assistants().getAssistants() .mcp()
.getMCPConfig()
.then((data) => setServers(data.mcpServers ?? {}))
serviceHub
.assistants()
.getAssistants()
.then((data) => { .then((data) => {
// Only update assistants if we have valid data // Only update assistants if we have valid data
if (data && Array.isArray(data) && data.length > 0) { if (data && Array.isArray(data) && data.length > 0) {
@ -61,14 +66,18 @@ export function DataProvider() {
}, [serviceHub]) }, [serviceHub])
useEffect(() => { useEffect(() => {
serviceHub.threads().fetchThreads().then((threads) => { serviceHub
setThreads(threads) .threads()
threads.forEach((thread) => .fetchThreads()
serviceHub.messages().fetchMessages(thread.id).then((messages) => .then((threads) => {
setMessages(thread.id, messages) setThreads(threads)
threads.forEach((thread) =>
serviceHub
.messages()
.fetchMessages(thread.id)
.then((messages) => setMessages(thread.id, messages))
) )
) })
})
}, [serviceHub, setThreads, setMessages]) }, [serviceHub, setThreads, setMessages])
// Check for app updates // Check for app updates
@ -157,7 +166,9 @@ export function DataProvider() {
setServerStatus('pending') setServerStatus('pending')
// Start the model first // Start the model first
serviceHub.models().startModel(modelToStart.provider, modelToStart.model) serviceHub
.models()
.startModel(modelToStart.provider, modelToStart.model)
.then(() => { .then(() => {
console.log(`Model ${modelToStart.model} started successfully`) console.log(`Model ${modelToStart.model} started successfully`)

View File

@ -132,7 +132,7 @@ function MCPServersDesktop() {
const [loadingServers, setLoadingServers] = useState<{ const [loadingServers, setLoadingServers] = useState<{
[key: string]: boolean [key: string]: boolean
}>({}) }>({})
const { setErrorMessage } = useAppState() const setErrorMessage = useAppState((state) => state.setErrorMessage)
const handleOpenDialog = (serverKey?: string) => { const handleOpenDialog = (serverKey?: string) => {
if (serverKey) { if (serverKey) {