diff --git a/.eslintrc.js b/.eslintrc.js index cbb34c74f2..92e259cf38 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -120,8 +120,6 @@ module.exports = { ], rules: { '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unnecessary-condition': 'warn', - '@typescript-eslint/strict-boolean-expressions': 'warn', }, }, { diff --git a/client/src/components/Chat/Messages/MessagesView.tsx b/client/src/components/Chat/Messages/MessagesView.tsx index 9acdbc6e2b..ded7cd2f8c 100644 --- a/client/src/components/Chat/Messages/MessagesView.tsx +++ b/client/src/components/Chat/Messages/MessagesView.tsx @@ -33,7 +33,7 @@ export default function MessagesView({ return (
-
+
)}
diff --git a/client/src/data-provider/connection.ts b/client/src/data-provider/connection.ts new file mode 100644 index 0000000000..4ef7876d76 --- /dev/null +++ b/client/src/data-provider/connection.ts @@ -0,0 +1,48 @@ +import { useCallback, useRef } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { QueryKeys, Time, dataService } from 'librechat-data-provider'; +import { logger } from '~/utils'; + +export const useHealthCheck = () => { + useQuery([QueryKeys.health], () => dataService.healthCheck(), { + refetchInterval: Time.TEN_MINUTES, + retry: false, + onError: (error) => { + console.error('Health check failed:', error); + }, + cacheTime: 0, + staleTime: 0, + refetchOnWindowFocus: (query) => { + if (!query.state.dataUpdatedAt) { + return true; + } + + const lastUpdated = new Date(query.state.dataUpdatedAt); + const tenMinutesAgo = new Date(Date.now() - Time.TEN_MINUTES); + + logger.log(`Last health check: ${lastUpdated.toISOString()}`); + logger.log(`Ten minutes ago: ${tenMinutesAgo.toISOString()}`); + + return lastUpdated < tenMinutesAgo; + }, + }); +}; + +export const useInteractionHealthCheck = () => { + const queryClient = useQueryClient(); + const lastInteractionTimeRef = useRef(Date.now()); + + const checkHealthOnInteraction = useCallback(() => { + const currentTime = Date.now(); + if (currentTime - lastInteractionTimeRef.current > Time.FIVE_MINUTES) { + logger.log( + 'Checking health on interaction. Time elapsed:', + currentTime - lastInteractionTimeRef.current, + ); + queryClient.invalidateQueries([QueryKeys.health]); + lastInteractionTimeRef.current = currentTime; + } + }, [queryClient]); + + return checkHealthOnInteraction; +}; diff --git a/client/src/data-provider/index.ts b/client/src/data-provider/index.ts index 7da93a279e..14f82f312e 100644 --- a/client/src/data-provider/index.ts +++ b/client/src/data-provider/index.ts @@ -1,3 +1,4 @@ +export * from './connection'; export * from './mutations'; export * from './prompts'; export * from './queries'; diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 07c00c80ec..e230113b2f 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -18,8 +18,8 @@ import type { import type { SetterOrUpdater } from 'recoil'; import type { TAskFunction, ExtendedFile } from '~/common'; import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete'; +import { getEndpointField, logger, scrollToEnd } from '~/utils'; import useGetSender from '~/hooks/Conversations/useGetSender'; -import { getEndpointField, logger } from '~/utils'; import useUserKey from '~/hooks/Input/useUserKey'; import store from '~/store'; @@ -249,6 +249,8 @@ export default function useChatFunctions({ if (index === 0 && setLatestMessage) { setLatestMessage(initialResponse); } + + scrollToEnd(); setSubmission(submission); logger.log('Submission:'); logger.dir(submission, { depth: null }); diff --git a/client/src/hooks/Input/useTextarea.ts b/client/src/hooks/Input/useTextarea.ts index ff2114fc90..e90bca54e0 100644 --- a/client/src/hooks/Input/useTextarea.ts +++ b/client/src/hooks/Input/useTextarea.ts @@ -8,6 +8,7 @@ import { forceResize, insertTextAtCursor, getAssistantName } from '~/utils'; import { useAssistantsMapContext } from '~/Providers/AssistantsMapContext'; import useGetSender from '~/hooks/Conversations/useGetSender'; import useFileHandling from '~/hooks/Files/useFileHandling'; +import { useInteractionHealthCheck } from '~/data-provider'; import { useChatContext } from '~/Providers/ChatContext'; import useLocalize from '~/hooks/useLocalize'; import { globalAudioId } from '~/common'; @@ -29,6 +30,7 @@ export default function useTextarea({ const isComposing = useRef(false); const { handleFiles } = useFileHandling(); const assistantMap = useAssistantsMapContext(); + const checkHealth = useInteractionHealthCheck(); const enterToSend = useRecoilValue(store.enterToSend); const { @@ -152,6 +154,8 @@ export default function useTextarea({ return; } + checkHealth(); + const isNonShiftEnter = e.key === 'Enter' && !e.shiftKey; const isCtrlEnter = e.key === 'Enter' && e.ctrlKey; @@ -185,7 +189,7 @@ export default function useTextarea({ submitButtonRef.current?.click(); } }, - [isSubmitting, filesLoading, enterToSend, textAreaRef, submitButtonRef], + [isSubmitting, checkHealth, filesLoading, enterToSend, textAreaRef, submitButtonRef], ); const handleCompositionStart = () => { diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index bb293b38e7..7474ac0275 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; import { useParams } from 'react-router-dom'; -import { EModelEndpoint } from 'librechat-data-provider'; +import { Constants, EModelEndpoint } from 'librechat-data-provider'; import { useGetModelsQuery, useGetStartupConfig, @@ -8,14 +8,15 @@ import { } from 'librechat-data-provider/react-query'; import type { TPreset } from 'librechat-data-provider'; import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks'; +import { useGetConvoIdQuery, useHealthCheck } from '~/data-provider'; import { getDefaultModelSpec, getModelSpecIconURL } from '~/utils'; -import { useGetConvoIdQuery } from '~/data-provider'; import ChatView from '~/components/Chat/ChatView'; import useAuthRedirect from './useAuthRedirect'; import { Spinner } from '~/components/svg'; import store from '~/store'; export default function ChatRoute() { + useHealthCheck(); const { data: startupConfig } = useGetStartupConfig(); const { isAuthenticated, user } = useAuthRedirect(); useAppStartup({ startupConfig, user }); @@ -32,7 +33,7 @@ export default function ChatRoute() { refetchOnMount: 'always', }); const initialConvoQuery = useGetConvoIdQuery(conversationId ?? '', { - enabled: isAuthenticated && conversationId !== 'new', + enabled: isAuthenticated && conversationId !== Constants.NEW_CONVO, }); const endpointsQuery = useGetEndpointsQuery({ enabled: isAuthenticated }); const assistantListMap = useAssistantListMap(); @@ -45,7 +46,7 @@ export default function ChatRoute() { return; } - if (conversationId === 'new' && endpointsQuery.data && modelsQuery.data) { + if (conversationId === Constants.NEW_CONVO && endpointsQuery.data && modelsQuery.data) { const spec = getDefaultModelSpec(startupConfig.modelSpecs?.list); newConversation({ @@ -73,7 +74,7 @@ export default function ChatRoute() { }); hasSetConversation.current = true; } else if ( - conversationId === 'new' && + conversationId === Constants.NEW_CONVO && assistantListMap[EModelEndpoint.assistants] && assistantListMap[EModelEndpoint.azureAssistants] ) { diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts index 16d35a518c..e7abf1320b 100644 --- a/client/src/utils/messages.ts +++ b/client/src/utils/messages.ts @@ -43,3 +43,12 @@ export const getTextKey = (message?: TMessage | null, convoId?: string | null) = Constants.COMMON_DIVIDER }${message.conversationId ?? convoId}`; }; + +export const scrollToEnd = () => { + setTimeout(() => { + const messagesEndElement = document.getElementById('messages-end'); + if (messagesEndElement) { + messagesEndElement.scrollIntoView({ behavior: 'instant' }); + } + }, 500); +}; diff --git a/client/tailwind.config.cjs b/client/tailwind.config.cjs index 4b1b9a47b5..f28699919f 100644 --- a/client/tailwind.config.cjs +++ b/client/tailwind.config.cjs @@ -6,9 +6,6 @@ module.exports = { // darkMode: 'class', darkMode: ['class'], theme: { - // colors: { - // 'gpt-dark-gray': '#171717', - // }, fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['Roboto Mono', 'monospace'], diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index 5403740cee..f91d768ec9 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -1,5 +1,6 @@ import type { AssistantsEndpoint } from './schemas'; +export const health = () => '/api/health'; export const user = () => '/api/user'; export const balance = () => '/api/balance'; diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 4251cb9377..49288f6cf7 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -573,3 +573,7 @@ export function addTagToConversation( export function rebuildConversationTags(): Promise { return request.post(endpoints.conversationTags('rebuild')); } + +export function healthCheck(): Promise { + return request.get(endpoints.health()); +} diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index 8535c475bc..a446b84d20 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -37,6 +37,7 @@ export enum QueryKeys { randomPrompts = 'randomPrompts', roles = 'roles', conversationTags = 'conversationTags', + health = 'health', } export enum MutationKeys {