From e5db31f82a384f12a0d25613c59527cf2323105e Mon Sep 17 00:00:00 2001 From: James Date: Fri, 9 Aug 2024 02:52:38 +0700 Subject: [PATCH] fix: refresh should not create new thread --- joi/src/core/TextArea/index.tsx | 3 +- web/containers/Providers/DataLoader.tsx | 52 +++++++++++++---- web/containers/Providers/KeyListener.tsx | 2 +- web/helpers/atoms/Thread.atom.ts | 19 +++--- web/hooks/useModelQuery.ts | 26 +++++++++ web/hooks/useThreadCreateMutation.ts | 58 +++++++++++++++++++ web/hooks/useThreadQuery.ts | 26 +++++++++ web/hooks/useThreads.ts | 7 --- .../HubScreen2/components/SliderItem.tsx | 16 +++-- .../components/CopyOverInstruction.tsx | 41 ------------- web/screens/Settings/Advanced/index.tsx | 2 - .../ChatInput/ChatTextInput/index.tsx | 10 +--- .../ModalDeleteThread/index.tsx | 16 +++-- web/screens/Thread/ThreadLeftPanel/index.tsx | 39 +++++-------- .../components/CopyOverInstruction.tsx | 38 ++++++++++++ .../AssistantSettingContainer/index.tsx | 7 +++ 16 files changed, 243 insertions(+), 119 deletions(-) create mode 100644 web/hooks/useModelQuery.ts create mode 100644 web/hooks/useThreadCreateMutation.ts create mode 100644 web/hooks/useThreadQuery.ts delete mode 100644 web/screens/Settings/Advanced/components/CopyOverInstruction.tsx create mode 100644 web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/components/CopyOverInstruction.tsx diff --git a/joi/src/core/TextArea/index.tsx b/joi/src/core/TextArea/index.tsx index 33d6744ada..791ff1430f 100644 --- a/joi/src/core/TextArea/index.tsx +++ b/joi/src/core/TextArea/index.tsx @@ -1,8 +1,7 @@ -import React, { ReactNode, forwardRef } from 'react' +import React, { forwardRef } from 'react' import { twMerge } from 'tailwind-merge' import './styles.scss' -import { ScrollArea } from '../ScrollArea' export interface TextAreaProps extends React.TextareaHTMLAttributes {} diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx index a3fe7d227c..dbcb36c582 100644 --- a/web/containers/Providers/DataLoader.tsx +++ b/web/containers/Providers/DataLoader.tsx @@ -2,18 +2,27 @@ import { useEffect } from 'react' +import { useAtomValue } from 'jotai' + import useAssistantCreate, { janAssistant } from '@/hooks/useAssistantCreate' import useAssistantQuery from '@/hooks/useAssistantQuery' import useEngineQuery from '@/hooks/useEngineQuery' import { useLoadTheme } from '@/hooks/useLoadTheme' import useModelHub from '@/hooks/useModelHub' -import useModels from '@/hooks/useModels' -import useThreads from '@/hooks/useThreads' +import useModelQuery from '@/hooks/useModelQuery' +import useThreadCreateMutation from '@/hooks/useThreadCreateMutation' +import useThreadQuery from '@/hooks/useThreadQuery' + +import { getSelectedModelAtom } from '@/helpers/atoms/Model.atom' +import { threadsAtom } from '@/helpers/atoms/Thread.atom' const DataLoader: React.FC = () => { - const { getThreadList } = useThreads() - const { getModels } = useModels() + const selectedModel = useAtomValue(getSelectedModelAtom) + const allThreads = useAtomValue(threadsAtom) const { data: assistants } = useAssistantQuery() + const { data: models } = useModelQuery() + const { data: threads, isLoading: isFetchingThread } = useThreadQuery() + const createThreadMutation = useThreadCreateMutation() const assistantCreateMutation = useAssistantCreate() useEffect(() => { @@ -25,16 +34,39 @@ const DataLoader: React.FC = () => { } }, [assistants, assistantCreateMutation]) + // automatically create new thread if thread is empty + useEffect(() => { + if (isFetchingThread) return + if (allThreads.length > 0) return + if (!assistants || assistants.length === 0) return + if (!models || models.length === 0) return + if (allThreads.length === 0 && !createThreadMutation.isPending) { + const model = selectedModel ?? models[0] + const assistant = assistants[0] + + console.log('Create new thread because user have no thread') + createThreadMutation.mutate({ + modelId: model.id, + assistant: { + ...assistant, + model: model.id, + }, + }) + } + }, [ + assistants, + models, + isFetchingThread, + threads, + createThreadMutation, + allThreads, + selectedModel, + ]) + useModelHub() useLoadTheme() useEngineQuery() - useEffect(() => { - getThreadList() - getModels() - }, [getThreadList, getModels]) - - console.debug('Load Data...') return null } diff --git a/web/containers/Providers/KeyListener.tsx b/web/containers/Providers/KeyListener.tsx index 12ce826aa3..1e012501ca 100644 --- a/web/containers/Providers/KeyListener.tsx +++ b/web/containers/Providers/KeyListener.tsx @@ -7,7 +7,7 @@ import { useAtomValue, useSetAtom } from 'jotai' import useAssistantQuery from '@/hooks/useAssistantQuery' import useThreads from '@/hooks/useThreads' -import { copyOverInstructionEnabledAtom } from '@/screens/Settings/Advanced/components/CopyOverInstruction' +import { copyOverInstructionEnabledAtom } from '@/screens/Thread/ThreadRightPanel/AssistantSettingContainer/components/CopyOverInstruction' import { toaster } from '../Toast' diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index af5598e213..dc45018b45 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -70,17 +70,14 @@ export const isGeneratingResponseAtom = atom(false) */ export const threadsAtom = atom([]) -export const deleteThreadAtom = atom(null, (_get, set, threadId: string) => { - set(threadsAtom, (threads) => { - // set active thread to the latest - const allThreads = threads.filter((c) => c.id !== threadId) - if (allThreads.length > 0) { - const latestThread = allThreads[0] - set(activeThreadIdAtom, latestThread.id) - } - - return allThreads - }) +export const deleteThreadAtom = atom(null, (get, set, threadId: string) => { + const allThreads = get(threadsAtom) + const filteredThreads = allThreads.filter((t) => t.id !== threadId) + if (filteredThreads.length > 0) { + const latestThread = allThreads[0] + set(activeThreadIdAtom, latestThread.id) + } + set(threadsAtom, filteredThreads) }) export const activeThreadAtom = atom((get) => diff --git a/web/hooks/useModelQuery.ts b/web/hooks/useModelQuery.ts new file mode 100644 index 0000000000..ddc77ad638 --- /dev/null +++ b/web/hooks/useModelQuery.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query' + +import { useSetAtom } from 'jotai' + +import useCortex from './useCortex' + +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' + +export const modelQueryKey = ['getModels'] + +const useModelQuery = () => { + const { fetchModels } = useCortex() + const setDownloadedModels = useSetAtom(downloadedModelsAtom) + + return useQuery({ + queryKey: modelQueryKey, + queryFn: async () => { + const models = await fetchModels() + setDownloadedModels(models) + return models + }, + staleTime: 30 * 1000, + }) +} + +export default useModelQuery diff --git a/web/hooks/useThreadCreateMutation.ts b/web/hooks/useThreadCreateMutation.ts new file mode 100644 index 0000000000..4cff3c045b --- /dev/null +++ b/web/hooks/useThreadCreateMutation.ts @@ -0,0 +1,58 @@ +import { Assistant } from '@janhq/core' +import { useMutation } from '@tanstack/react-query' + +import { useSetAtom } from 'jotai' + +import { toaster } from '@/containers/Toast' + +import useCortex from './useCortex' + +import { setThreadMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' +import { setActiveThreadIdAtom, threadsAtom } from '@/helpers/atoms/Thread.atom' + +export type ThreadCreateMutationVariables = { + modelId: string + assistant: Assistant + instructions?: string +} + +const useThreadCreateMutation = () => { + const { createThread } = useCortex() + const setThreads = useSetAtom(threadsAtom) + const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) + const setThreadMessage = useSetAtom(setThreadMessagesAtom) + + return useMutation({ + mutationFn: async (variables: ThreadCreateMutationVariables) => { + const { assistant, modelId, instructions } = variables + if (instructions) { + assistant.instructions = instructions + } + + return createThread({ + ...assistant, + model: modelId, + }) + }, + + onSuccess: (thread, variables, context) => { + console.log('New thread created', thread, variables, context) + setThreads((threads) => [thread, ...threads]) + setActiveThreadId(thread.id) + setThreadMessage(thread.id, []) + }, + + onError: (error, variables) => { + console.error( + `Failed to create new thread: ${JSON.stringify(variables)}, error: ${error}` + ) + toaster({ + title: 'Failed to create thread', + description: `Unexpected error while creating thread. Please try again!`, + type: 'error', + }) + }, + }) +} + +export default useThreadCreateMutation diff --git a/web/hooks/useThreadQuery.ts b/web/hooks/useThreadQuery.ts new file mode 100644 index 0000000000..034490e9e8 --- /dev/null +++ b/web/hooks/useThreadQuery.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query' + +import { useSetAtom } from 'jotai' + +import useCortex from './useCortex' + +import { threadsAtom } from '@/helpers/atoms/Thread.atom' + +export const threadQueryKey = ['getThreads'] + +const useThreadQuery = () => { + const { fetchThreads } = useCortex() + const setThreads = useSetAtom(threadsAtom) + + return useQuery({ + queryKey: threadQueryKey, + queryFn: async () => { + const threads = await fetchThreads() + setThreads(threads) + return threads + }, + staleTime: 30 * 1000, + }) +} + +export default useThreadQuery diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts index 39dd6df1a3..828bf82c53 100644 --- a/web/hooks/useThreads.ts +++ b/web/hooks/useThreads.ts @@ -27,18 +27,12 @@ const useThreads = () => { const deleteThreadState = useSetAtom(deleteThreadAtom) const cleanMessages = useSetAtom(cleanChatMessageAtom) const { - fetchThreads, createThread, fetchMessages, deleteThread: deleteCortexThread, cleanThread: cleanCortexThread, } = useCortex() - const getThreadList = useCallback(async () => { - const threads = await fetchThreads() - setThreads(threads) - }, [setThreads, fetchThreads]) - const setActiveThread = useCallback( async (threadId: string) => { const messages = await fetchMessages(threadId) @@ -85,7 +79,6 @@ const useThreads = () => { ) return { - getThreadList, createThread: createNewThread, setActiveThread, deleteThread, diff --git a/web/screens/HubScreen2/components/SliderItem.tsx b/web/screens/HubScreen2/components/SliderItem.tsx index 93a1169962..43c2d40597 100644 --- a/web/screens/HubScreen2/components/SliderItem.tsx +++ b/web/screens/HubScreen2/components/SliderItem.tsx @@ -13,7 +13,8 @@ import useAssistantQuery from '@/hooks/useAssistantQuery' import { downloadStateListAtom } from '@/hooks/useDownloadState' import useModelDownloadMutation from '@/hooks/useModelDownloadMutation' import { QuickStartModel } from '@/hooks/useModelHub' -import useThreads from '@/hooks/useThreads' + +import useThreadCreateMutation from '@/hooks/useThreadCreateMutation' import { formatDownloadPercentage, toGibibytes } from '@/utils/converter' import { downloadProgress } from '@/utils/download' @@ -75,8 +76,8 @@ const DownloadContainer: React.FC = ({ fileName, }) => { const downloadModelMutation = useModelDownloadMutation() + const createThreadMutation = useThreadCreateMutation() const setMainViewState = useSetAtom(mainViewStateAtom) - const { createThread } = useThreads() const { data: assistants } = useAssistantQuery() const { abortDownload } = useAbortDownload() @@ -116,16 +117,19 @@ const DownloadContainer: React.FC = ({ return } - await createThread(persistModelId, { - ...assistants[0], - model: persistModelId, + await createThreadMutation.mutateAsync({ + modelId: persistModelId, + assistant: { + ...assistants[0], + model: persistModelId, + }, }) setDownloadLocalModelModalStage('NONE', undefined) setMainViewState(MainViewState.Thread) }, [ setDownloadLocalModelModalStage, setMainViewState, - createThread, + createThreadMutation, persistModelId, assistants, ]) diff --git a/web/screens/Settings/Advanced/components/CopyOverInstruction.tsx b/web/screens/Settings/Advanced/components/CopyOverInstruction.tsx deleted file mode 100644 index b4102994a4..0000000000 --- a/web/screens/Settings/Advanced/components/CopyOverInstruction.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ChangeEvent, useCallback } from 'react' - -import { Switch } from '@janhq/joi' -import { useAtom } from 'jotai' -import { atomWithStorage } from 'jotai/utils' - -const COPY_OVER_INSTRUCTION_ENABLED = 'copy_over_instruction_enabled' -export const copyOverInstructionEnabledAtom = atomWithStorage( - COPY_OVER_INSTRUCTION_ENABLED, - false -) - -const CopyOverInstructionItem: React.FC = () => { - const [copyOverInstructionEnabled, setCopyOverInstructionEnabled] = useAtom( - copyOverInstructionEnabledAtom - ) - - const onSwitchToggled = useCallback( - (e: ChangeEvent) => { - setCopyOverInstructionEnabled(e.target.checked) - }, - [setCopyOverInstructionEnabled] - ) - - return ( -
-
-
-
Copy Over Instruction
-
-

- Enable instruction to be copied to new thread -

-
- {/**/} - -
- ) -} - -export default CopyOverInstructionItem diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 7f26947c1b..2de42ebb3f 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -14,7 +14,6 @@ import useModelStop from '@/hooks/useModelStop' import { useSettings } from '@/hooks/useSettings' import DataFolder from './DataFolder' -import CopyOverInstructionItem from './components/CopyOverInstruction' import DataMigration from './components/DataMigration' @@ -459,7 +458,6 @@ const Advanced = () => { {/* Factory Reset */} {/* */} - {experimentalEnabled && } {experimentalEnabled && } diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/ChatTextInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/ChatTextInput/index.tsx index e9d7a93410..f43bf68924 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/ChatTextInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/ChatTextInput/index.tsx @@ -32,9 +32,7 @@ const ChatTextInput: React.FC = ({ const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom) - const disabled = useMemo(() => { - return !activeThreadId - }, [activeThreadId]) + const disabled = useMemo(() => !activeThreadId, [activeThreadId]) const onChange = useCallback( (e: React.ChangeEvent) => { @@ -44,10 +42,8 @@ const ChatTextInput: React.FC = ({ ) useEffect(() => { - if (textareaRef.current) { - textareaRef.current.focus() - } - }, [activeThreadId]) + textareaRef.current?.focus() + }) useEffect(() => { if (textareaRef.current?.clientHeight) { diff --git a/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx index 25a48602d2..f1af0966a3 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx @@ -23,12 +23,16 @@ const ModalDeleteThread: React.FC = ({ const { deleteThread } = useThreads() const onDeleteThreadClick = useCallback(async () => { - await deleteThread(id) - toaster({ - title: 'Thread successfully deleted.', - description: `Thread ${title} has been successfully deleted.`, - type: 'success', - }) + try { + await deleteThread(id) + toaster({ + title: 'Thread successfully deleted.', + description: `Thread ${title} has been successfully deleted.`, + type: 'success', + }) + } catch (err) { + console.log(err) + } }, [deleteThread, id, title]) return ( diff --git a/web/screens/Thread/ThreadLeftPanel/index.tsx b/web/screens/Thread/ThreadLeftPanel/index.tsx index 0af866d4fc..4e35e9f00f 100644 --- a/web/screens/Thread/ThreadLeftPanel/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect } from 'react' import { Button } from '@janhq/joi' import { AnimatePresence } from 'framer-motion' @@ -13,48 +13,31 @@ import { toaster } from '@/containers/Toast' import useAssistantQuery from '@/hooks/useAssistantQuery' +import useThreadCreateMutation from '@/hooks/useThreadCreateMutation' + import useThreads from '@/hooks/useThreads' -import { copyOverInstructionEnabledAtom } from '@/screens/Settings/Advanced/components/CopyOverInstruction' +import { copyOverInstructionEnabledAtom } from '../ThreadRightPanel/AssistantSettingContainer/components/CopyOverInstruction' import ThreadItem from './ThreadItem' -import { - downloadedModelsAtom, - getSelectedModelAtom, -} from '@/helpers/atoms/Model.atom' +import { getSelectedModelAtom } from '@/helpers/atoms/Model.atom' import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom' import { activeThreadAtom, threadsAtom } from '@/helpers/atoms/Thread.atom' const ThreadLeftPanel: React.FC = () => { - const { createThread, setActiveThread } = useThreads() + const { setActiveThread } = useThreads() + const createThreadMutation = useThreadCreateMutation() const reduceTransparent = useAtomValue(reduceTransparentAtom) - const downloadedModels = useAtomValue(downloadedModelsAtom) const selectedModel = useAtomValue(getSelectedModelAtom) const threads = useAtomValue(threadsAtom) const activeThread = useAtomValue(activeThreadAtom) const { data: assistants } = useAssistantQuery() - const isCreatingThread = useRef(false) const copyOverInstructionEnabled = useAtomValue( copyOverInstructionEnabledAtom ) - useEffect(() => { - // if user does not have any threads, we should create one - const createThreadIfEmpty = async () => { - if (!assistants || assistants.length === 0) return - if (downloadedModels.length === 0) return - if (threads.length > 0) return - if (isCreatingThread.current) return - isCreatingThread.current = true - // user have models but does not have any thread. Let's create one - await createThread(downloadedModels[0].model, assistants[0]) - isCreatingThread.current = false - } - createThreadIfEmpty() - }, [threads, assistants, downloadedModels, createThread]) - useEffect(() => { if (activeThread?.id) return if (threads.length === 0) return @@ -75,9 +58,13 @@ const ThreadLeftPanel: React.FC = () => { if (copyOverInstructionEnabled) { instructions = activeThread?.assistants[0]?.instructions ?? undefined } - createThread(selectedModel.model, assistants[0], instructions) + await createThreadMutation.mutateAsync({ + modelId: selectedModel.model, + assistant: assistants[0], + instructions, + }) }, [ - createThread, + createThreadMutation, selectedModel, assistants, activeThread, diff --git a/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/components/CopyOverInstruction.tsx b/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/components/CopyOverInstruction.tsx new file mode 100644 index 0000000000..57c0d68a4b --- /dev/null +++ b/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/components/CopyOverInstruction.tsx @@ -0,0 +1,38 @@ +import { ChangeEvent, useCallback } from 'react' + +import { Switch } from '@janhq/joi' +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +const COPY_OVER_INSTRUCTION_ENABLED = 'copy_over_instruction_enabled' + +export const copyOverInstructionEnabledAtom = atomWithStorage( + COPY_OVER_INSTRUCTION_ENABLED, + false +) + +const CopyOverInstruction: React.FC = () => { + const [copyOverInstructionEnabled, setCopyOverInstructionEnabled] = useAtom( + copyOverInstructionEnabledAtom + ) + + const onSwitchToggled = useCallback( + (e: ChangeEvent) => { + setCopyOverInstructionEnabled(e.target.checked) + }, + [setCopyOverInstructionEnabled] + ) + + return ( +
+
Save instructions for all threads
+ +
+ ) +} + +export default CopyOverInstruction diff --git a/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/index.tsx b/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/index.tsx index 74b9560425..ca1951f6ce 100644 --- a/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/index.tsx +++ b/web/screens/Thread/ThreadRightPanel/AssistantSettingContainer/index.tsx @@ -7,9 +7,14 @@ import { useDebouncedCallback } from 'use-debounce' import useUpdateInstruction from '@/hooks/useUpdateInstruction' +import CopyOverInstruction from './components/CopyOverInstruction' + +import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' + import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const AssistantSettingContainer: React.FC = () => { + const experimentalEnabled = useAtomValue(experimentalFeatureEnabledAtom) const activeThread = useAtomValue(activeThreadAtom) const [instructions, setInstructions] = useState( activeThread?.assistants[0]?.instructions || '' @@ -43,11 +48,13 @@ const AssistantSettingContainer: React.FC = () => { Instructions