diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 9460ed98f..f82d17f52 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -47,23 +47,23 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const [isFocused, setIsFocused] = useState(false) const [rows, setRows] = useState(1) const serviceHub = useServiceHub() - const { - streamingContent, - abortControllers, - loadingModel, - tools, - cancelToolCall, - } = useAppState() - const { prompt, setPrompt } = usePrompt() - const { currentThreadId } = useThreads() + const streamingContent = useAppState((state) => state.streamingContent) + const abortControllers = useAppState((state) => state.abortControllers) + const loadingModel = useAppState((state) => state.loadingModel) + const tools = useAppState((state) => state.tools) + const cancelToolCall = useAppState((state) => state.cancelToolCall) + const prompt = usePrompt((state) => state.prompt) + const setPrompt = usePrompt((state) => state.setPrompt) + const currentThreadId = useThreads((state) => state.currentThreadId) const { t } = useTranslation() const { spellCheckChatInput } = useGeneralSetting() useTools() const maxRows = 10 - const { selectedModel, selectedProvider } = useModelProvider() - const { sendMessage } = useChat() + const selectedModel = useModelProvider((state) => state.selectedModel) + const selectedProvider = useModelProvider((state) => state.selectedProvider) + const sendMessage = useChat() const [message, setMessage] = useState('') const [dropdownToolsAvailable, setDropdownToolsAvailable] = useState(false) const [tooltipToolsAvailable, setTooltipToolsAvailable] = useState(false) diff --git a/web-app/src/containers/GenerateResponseButton.tsx b/web-app/src/containers/GenerateResponseButton.tsx index 0d1ab339e..9f6df11f8 100644 --- a/web-app/src/containers/GenerateResponseButton.tsx +++ b/web-app/src/containers/GenerateResponseButton.tsx @@ -12,7 +12,7 @@ export const GenerateResponseButton = ({ threadId }: { threadId: string }) => { messages: state.messages[threadId], })) ) - const { sendMessage } = useChat() + const sendMessage = useChat() const generateAIResponse = () => { const latestUserMessage = messages[messages.length - 1] if ( diff --git a/web-app/src/containers/StreamingContent.tsx b/web-app/src/containers/StreamingContent.tsx index 573dc29c9..57aebe61e 100644 --- a/web-app/src/containers/StreamingContent.tsx +++ b/web-app/src/containers/StreamingContent.tsx @@ -21,7 +21,7 @@ function extractReasoningSegment(text: string) { // Use memo with no dependencies to allow re-renders when props change // Avoid duplicate reasoning segments after tool calls export const StreamingContent = memo(({ threadId }: Props) => { - const { streamingContent } = useAppState() + const streamingContent = useAppState((state) => state.streamingContent) const { getMessages } = useMessages() const messages = getMessages(threadId) @@ -68,6 +68,7 @@ export const StreamingContent = memo(({ threadId }: Props) => { }} {...streamingContent} isLastMessage={true} + streamingThread={streamingContent.thread_id} showAssistant={ messages.length > 0 ? messages[messages.length - 1].role !== 'assistant' diff --git a/web-app/src/containers/ThinkingBlock.tsx b/web-app/src/containers/ThinkingBlock.tsx index 7a1e7b540..be33ae70e 100644 --- a/web-app/src/containers/ThinkingBlock.tsx +++ b/web-app/src/containers/ThinkingBlock.tsx @@ -27,12 +27,15 @@ const useThinkingStore = create((set) => ({ })) const ThinkingBlock = ({ id, text }: Props) => { - const { thinkingState, setThinkingState } = useThinkingStore() - const { streamingContent } = useAppState() + const thinkingState = useThinkingStore((state) => state.thinkingState) + const setThinkingState = useThinkingStore((state) => state.setThinkingState) + const streamingContent = useAppState((state) => state.streamingContent) const { t } = useTranslation() // Check for thinking formats const hasThinkTag = text.includes('') && !text.includes('') - 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 isExpanded = thinkingState[id] ?? (loading ? true : false) const handleClick = () => { @@ -48,7 +51,7 @@ const ThinkingBlock = ({ id, text }: Props) => { .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(/<\|message\|>/g, '') // remove any remaining message markers .replace(/<\|start\|>/g, '') // remove any remaining start markers .trim() } diff --git a/web-app/src/containers/ThreadContent.tsx b/web-app/src/containers/ThreadContent.tsx index 0316ee764..8d76e9671 100644 --- a/web-app/src/containers/ThreadContent.tsx +++ b/web-app/src/containers/ThreadContent.tsx @@ -68,6 +68,7 @@ export const ThreadContent = memo( isLastMessage?: boolean index?: number showAssistant?: boolean + streamingThread?: string streamTools?: any contextOverflowModal?: React.ReactNode | null @@ -75,7 +76,7 @@ export const ThreadContent = memo( } ) => { const { t } = useTranslation() - const { selectedModel } = useModelProvider() + const selectedModel = useModelProvider((state) => state.selectedModel) // Use useMemo to stabilize the components prop const linkComponents = useMemo( @@ -87,7 +88,6 @@ export const ThreadContent = memo( [] ) const image = useMemo(() => item.content?.[0]?.image_url, [item]) - const { streamingContent } = useAppState() const text = useMemo( () => item.content.find((e) => e.type === 'text')?.text?.value ?? '', @@ -129,8 +129,9 @@ export const ThreadContent = memo( return { reasoningSegment: undefined, textSegment: text } }, [text]) - const { getMessages, deleteMessage } = useMessages() - const { sendMessage } = useChat() + const getMessages = useMessages((state) => state.getMessages) + const deleteMessage = useMessages((state) => state.deleteMessage) + const sendMessage = useChat() const regenerate = useCallback(() => { // Only regenerate assistant message is allowed @@ -361,8 +362,8 @@ export const ThreadContent = memo( className={cn( 'flex items-center gap-2', item.isLastMessage && - streamingContent && - streamingContent.thread_id === item.thread_id && + item.streamingThread && + item.streamingThread === item.thread_id && 'hidden' )} > @@ -395,9 +396,10 @@ export const ThreadContent = memo( diff --git a/web-app/src/containers/ThreadList.tsx b/web-app/src/containers/ThreadList.tsx index 112f41b2d..672fc3ebc 100644 --- a/web-app/src/containers/ThreadList.tsx +++ b/web-app/src/containers/ThreadList.tsx @@ -46,14 +46,16 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => { } = useSortable({ id: thread.id, disabled: true }) const isSmallScreen = useSmallScreen() - const { setLeftPanel } = useLeftPanel() + const setLeftPanel = useLeftPanel(state => state.setLeftPanel) const style = { transform: CSS.Transform.toString(transform), transition, 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 [openDropdown, setOpenDropdown] = useState(false) const navigate = useNavigate() diff --git a/web-app/src/containers/TokenSpeedIndicator.tsx b/web-app/src/containers/TokenSpeedIndicator.tsx index ea9f91be0..262ea32a2 100644 --- a/web-app/src/containers/TokenSpeedIndicator.tsx +++ b/web-app/src/containers/TokenSpeedIndicator.tsx @@ -1,6 +1,7 @@ import { useAppState } from '@/hooks/useAppState' import { toNumber } from '@/utils/number' import { Gauge } from 'lucide-react' +import { memo } from 'react' interface TokenSpeedIndicatorProps { metadata?: Record @@ -11,7 +12,7 @@ export const TokenSpeedIndicator = ({ metadata, streaming, }: TokenSpeedIndicatorProps) => { - const { tokenSpeed } = useAppState() + const tokenSpeed = useAppState((state) => state.tokenSpeed) const persistedTokenSpeed = (metadata?.tokenSpeed as { tokenSpeed: number })?.tokenSpeed || 0 @@ -40,4 +41,4 @@ export const TokenSpeedIndicator = ({ ) } -export default TokenSpeedIndicator +export default memo(TokenSpeedIndicator) diff --git a/web-app/src/containers/dialogs/ErrorDialog.tsx b/web-app/src/containers/dialogs/ErrorDialog.tsx index cd6ca879a..27a6c8bf2 100644 --- a/web-app/src/containers/dialogs/ErrorDialog.tsx +++ b/web-app/src/containers/dialogs/ErrorDialog.tsx @@ -16,7 +16,8 @@ import { useAppState } from '@/hooks/useAppState' export default function ErrorDialog() { 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 [isDetailExpanded, setIsDetailExpanded] = useState(true) diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 45cfde26c..e3022c168 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -33,7 +33,6 @@ import { } from '@/utils/reasoning' export const useChat = () => { - const prompt = usePrompt((state) => state.prompt) const setPrompt = usePrompt((state) => state.setPrompt) const tools = useAppState((state) => state.tools) const updateTokenSpeed = useAppState((state) => state.updateTokenSpeed) @@ -84,7 +83,7 @@ export const useChat = () => { const selectedAssistant = assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] - const getCurrentThread = useCallback(async () => { + const getCurrentThread = useCallback(async (prompt: string) => { let currentThread = retrieveThread() if (!currentThread) { @@ -226,7 +225,7 @@ export const useChat = () => { dataUrl: string }> ) => { - const activeThread = await getCurrentThread() + const activeThread = await getCurrentThread(message) resetTokenSpeed() let activeProvider = currentProviderId @@ -572,5 +571,5 @@ export const useChat = () => { ] ) - return useMemo(() => ({ sendMessage }), [sendMessage]) + return useMemo(() => (sendMessage), [sendMessage]) } diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx index a734cd39f..2b94276a4 100644 --- a/web-app/src/providers/DataProvider.tsx +++ b/web-app/src/providers/DataProvider.tsx @@ -38,13 +38,18 @@ export function DataProvider() { verboseLogs, proxyTimeout, } = useLocalApiServer() - const { setServerStatus } = useAppState() + const setServerStatus = useAppState((state) => state.setServerStatus) useEffect(() => { console.log('Initializing DataProvider...') serviceHub.providers().getProviders().then(setProviders) - serviceHub.mcp().getMCPConfig().then((data) => setServers(data.mcpServers ?? {})) - serviceHub.assistants().getAssistants() + serviceHub + .mcp() + .getMCPConfig() + .then((data) => setServers(data.mcpServers ?? {})) + serviceHub + .assistants() + .getAssistants() .then((data) => { // Only update assistants if we have valid data if (data && Array.isArray(data) && data.length > 0) { @@ -61,14 +66,18 @@ export function DataProvider() { }, [serviceHub]) useEffect(() => { - serviceHub.threads().fetchThreads().then((threads) => { - setThreads(threads) - threads.forEach((thread) => - serviceHub.messages().fetchMessages(thread.id).then((messages) => - setMessages(thread.id, messages) + serviceHub + .threads() + .fetchThreads() + .then((threads) => { + setThreads(threads) + threads.forEach((thread) => + serviceHub + .messages() + .fetchMessages(thread.id) + .then((messages) => setMessages(thread.id, messages)) ) - ) - }) + }) }, [serviceHub, setThreads, setMessages]) // Check for app updates @@ -157,7 +166,9 @@ export function DataProvider() { setServerStatus('pending') // Start the model first - serviceHub.models().startModel(modelToStart.provider, modelToStart.model) + serviceHub + .models() + .startModel(modelToStart.provider, modelToStart.model) .then(() => { console.log(`Model ${modelToStart.model} started successfully`) diff --git a/web-app/src/routes/settings/mcp-servers.tsx b/web-app/src/routes/settings/mcp-servers.tsx index 0b95cf7ce..242d4f217 100644 --- a/web-app/src/routes/settings/mcp-servers.tsx +++ b/web-app/src/routes/settings/mcp-servers.tsx @@ -132,7 +132,7 @@ function MCPServersDesktop() { const [loadingServers, setLoadingServers] = useState<{ [key: string]: boolean }>({}) - const { setErrorMessage } = useAppState() + const setErrorMessage = useAppState((state) => state.setErrorMessage) const handleOpenDialog = (serverKey?: string) => { if (serverKey) {