diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index 7737652eb..35a71912d 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -35,7 +35,6 @@ import { ModelLoader } from '@/containers/loaders/ModelLoader' import DropdownToolsAvailable from '@/containers/DropdownToolsAvailable' import { getConnectedServers } from '@/services/mcp' import { stopAllModels } from '@/services/models' -import { useOutOfContextPromiseModal } from './dialogs/OutOfContextDialog' type ChatInputProps = { className?: string @@ -55,8 +54,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const { t } = useTranslation() const { spellCheckChatInput } = useGeneralSetting() - const { showModal, PromiseModal: OutOfContextModal } = - useOutOfContextPromiseModal() const maxRows = 10 const { selectedModel } = useModelProvider() @@ -107,7 +104,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { return } setMessage('') - sendMessage(prompt, showModal) + sendMessage(prompt) } useEffect(() => { @@ -599,7 +596,6 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { )} - ) } diff --git a/web-app/src/containers/ThreadContent.tsx b/web-app/src/containers/ThreadContent.tsx index 076327ea6..0ee166b7d 100644 --- a/web-app/src/containers/ThreadContent.tsx +++ b/web-app/src/containers/ThreadContent.tsx @@ -83,7 +83,6 @@ export const ThreadContent = memo( // eslint-disable-next-line @typescript-eslint/no-explicit-any streamTools?: any contextOverflowModal?: React.ReactNode | null - showContextOverflowModal?: () => Promise } ) => { const [message, setMessage] = useState(item.content?.[0]?.text?.value || '') @@ -134,10 +133,7 @@ export const ThreadContent = memo( } if (toSendMessage) { deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '') - sendMessage( - toSendMessage.content?.[0]?.text?.value || '', - item.showContextOverflowModal - ) + sendMessage(toSendMessage.content?.[0]?.text?.value || '') } }, [deleteMessage, getMessages, item, sendMessage]) @@ -179,16 +175,9 @@ export const ThreadContent = memo( deleteMessage(threadMessages[i].thread_id, threadMessages[i].id) } - sendMessage(message, item.showContextOverflowModal) + sendMessage(message) }, - [ - deleteMessage, - getMessages, - item.thread_id, - message, - sendMessage, - item.showContextOverflowModal, - ] + [deleteMessage, getMessages, item.thread_id, message, sendMessage] ) const isToolCalls = diff --git a/web-app/src/containers/dialogs/OutOfContextDialog.tsx b/web-app/src/containers/dialogs/OutOfContextDialog.tsx index 92e72950a..d1d6317a8 100644 --- a/web-app/src/containers/dialogs/OutOfContextDialog.tsx +++ b/web-app/src/containers/dialogs/OutOfContextDialog.tsx @@ -8,108 +8,76 @@ import { DialogTitle, } from '@/components/ui/dialog' -import { ReactNode, useCallback, useState } from 'react' import { Button } from '@/components/ui/button' +import { useContextSizeApproval } from '@/hooks/useModelContextApproval' -export function useOutOfContextPromiseModal() { - const [isOpen, setIsOpen] = useState(false) - const [modalProps, setModalProps] = useState<{ - resolveRef: - | ((value: 'ctx_len' | 'context_shift' | undefined) => void) - | null - }>({ - resolveRef: null, - }) - // Function to open the modal and return a Promise - const showModal = useCallback(() => { - return new Promise((resolve) => { - setModalProps({ - resolveRef: resolve, - }) - setIsOpen(true) - }) - }, []) +export default function OutOfContextPromiseModal() { + const { isModalOpen, modalProps, setModalOpen } = useContextSizeApproval() + if (!modalProps) { + return null + } + const { onApprove, onDeny } = modalProps - const PromiseModal = useCallback((): ReactNode => { - if (!isOpen) { - return null + const handleContextLength = () => { + onApprove('ctx_len') + } + + const handleContextShift = () => { + onApprove('context_shift') + } + + const handleDialogOpen = (open: boolean) => { + setModalOpen(open) + if (!open) { + onDeny() } + } - const handleContextLength = () => { - setIsOpen(false) - if (modalProps.resolveRef) { - modalProps.resolveRef('ctx_len') - } - } - - const handleContextShift = () => { - setIsOpen(false) - if (modalProps.resolveRef) { - modalProps.resolveRef('context_shift') - } - } - const handleCancel = () => { - setIsOpen(false) - if (modalProps.resolveRef) { - modalProps.resolveRef(undefined) - } - } - - return ( - { - setIsOpen(open) - if (!open) handleCancel() - }} - > - - - - {t('outOfContextError.title', 'Out of context error')} - - - - {t( - 'outOfContextError.description', - 'This chat is reaching the AI’s memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computer’s memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.' - )} -
-
- {t( - 'outOfContextError.increaseContextSizeDescription', - 'Do you want to increase the context size?' - )} -
- - - - -
-
- ) - }, [isOpen, modalProps]) - return { showModal, PromiseModal } + return ( + + + + + {t('outOfContextError.title', 'Out of context error')} + + + + {t( + 'outOfContextError.description', + 'This chat is reaching the AI’s memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computer’s memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.' + )} +
+
+ {t( + 'outOfContextError.increaseContextSizeDescription', + 'Do you want to increase the context size?' + )} +
+ + + + +
+
+ ) } diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 2c8f9fd2a..3e9dd6363 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -30,6 +30,7 @@ import { useToolApproval } from '@/hooks/useToolApproval' import { useToolAvailable } from '@/hooks/useToolAvailable' import { OUT_OF_CONTEXT_SIZE } from '@/utils/error' import { updateSettings } from '@/services/providers' +import { useContextSizeApproval } from './useModelContextApproval' export const useChat = () => { const { prompt, setPrompt } = usePrompt() @@ -47,6 +48,8 @@ export const useChat = () => { const { approvedTools, showApprovalModal, allowAllMCPPermissions } = useToolApproval() + const { showApprovalModal: showIncreaseContextSizeModal } = + useContextSizeApproval() const { getDisabledToolsForThread } = useToolAvailable() const { getProviderByName, selectedModel, selectedProvider } = @@ -223,11 +226,7 @@ export const useChat = () => { ) const sendMessage = useCallback( - async ( - message: string, - showModal?: () => Promise, - troubleshooting = true - ) => { + async (message: string, troubleshooting = true) => { const activeThread = await getCurrentThread() resetTokenSpeed() @@ -361,7 +360,7 @@ export const useChat = () => { selectedModel && troubleshooting ) { - const method = await showModal?.() + const method = await showIncreaseContextSizeModal() if (method === 'ctx_len') { /// Increase context size activeProvider = await increaseModelContextSize( @@ -447,8 +446,7 @@ export const useChat = () => { updateThreadTimestamp, setPrompt, selectedModel, - currentAssistant?.instructions, - currentAssistant.parameters, + currentAssistant, tools, updateLoadingModel, getDisabledToolsForThread, @@ -456,6 +454,7 @@ export const useChat = () => { allowAllMCPPermissions, showApprovalModal, updateTokenSpeed, + showIncreaseContextSizeModal, increaseModelContextSize, toggleOnContextShifting, ] diff --git a/web-app/src/hooks/useModelContextApproval.ts b/web-app/src/hooks/useModelContextApproval.ts new file mode 100644 index 000000000..92abe6ba6 --- /dev/null +++ b/web-app/src/hooks/useModelContextApproval.ts @@ -0,0 +1,53 @@ +import { create } from 'zustand' + +export type ApprovalModalProps = { + onApprove: (method: 'ctx_len' | 'context_shift') => void + onDeny: () => void +} + +type ApprovalState = { + // Modal state + isModalOpen: boolean + modalProps: ApprovalModalProps | null + + showApprovalModal: () => Promise<'ctx_len' | 'context_shift' | undefined> + closeModal: () => void + setModalOpen: (open: boolean) => void +} + +export const useContextSizeApproval = create()((set, get) => ({ + isModalOpen: false, + modalProps: null, + + showApprovalModal: async () => { + return new Promise<'ctx_len' | 'context_shift' | undefined>((resolve) => { + set({ + isModalOpen: true, + modalProps: { + onApprove: (method) => { + get().closeModal() + resolve(method) + }, + onDeny: () => { + get().closeModal() + resolve(undefined) + }, + }, + }) + }) + }, + + closeModal: () => { + set({ + isModalOpen: false, + modalProps: null, + }) + }, + + setModalOpen: (open: boolean) => { + set({ isModalOpen: open }) + if (!open) { + get().closeModal() + } + }, +})) diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 67e88ed90..c4efdbf72 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -18,6 +18,7 @@ import { AnalyticProvider } from '@/providers/AnalyticProvider' import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import ToolApproval from '@/containers/dialogs/ToolApproval' +import OutOfContextPromiseModal from '@/containers/dialogs/OutOfContextDialog' export const Route = createRootRoute({ component: RootLayout, @@ -94,6 +95,7 @@ function RootLayout() { {/* */} + ) } diff --git a/web-app/src/routes/threads/$threadId.tsx b/web-app/src/routes/threads/$threadId.tsx index bb8e9a72c..ae426af0b 100644 --- a/web-app/src/routes/threads/$threadId.tsx +++ b/web-app/src/routes/threads/$threadId.tsx @@ -18,7 +18,6 @@ import { useAppState } from '@/hooks/useAppState' import DropdownAssistant from '@/containers/DropdownAssistant' import { useAssistant } from '@/hooks/useAssistant' import { useAppearance } from '@/hooks/useAppearance' -import { useOutOfContextPromiseModal } from '@/containers/dialogs/OutOfContextDialog' // as route.threadsDetail export const Route = createFileRoute('/threads/$threadId')({ @@ -48,8 +47,6 @@ function ThreadDetail() { const scrollContainerRef = useRef(null) const isFirstRender = useRef(true) const messagesCount = useMemo(() => messages?.length ?? 0, [messages]) - const { showModal, PromiseModal: OutOfContextModal } = - useOutOfContextPromiseModal() // Function to check scroll position and scrollbar presence const checkScrollState = () => { @@ -196,8 +193,6 @@ function ThreadDetail() { if (!messages || !threadModel) return null - const contextOverflowModalComponent = - return (
@@ -243,8 +238,6 @@ function ThreadDetail() { )) } index={index} - showContextOverflowModal={showModal} - contextOverflowModal={contextOverflowModalComponent} />
)