fix: refresh should not create new thread (#3314)

This commit is contained in:
NamH 2024-08-09 10:28:49 +07:00 committed by GitHub
parent a4f5fda104
commit 624d07703c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 240 additions and 119 deletions

View File

@ -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<HTMLTextAreaElement> {}

View File

@ -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,36 @@ const DataLoader: React.FC = () => {
}
}, [assistants, assistantCreateMutation])
// automatically create new thread if thread list 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,
})
}
}, [
assistants,
models,
isFetchingThread,
threads,
createThreadMutation,
allThreads,
selectedModel,
])
useModelHub()
useLoadTheme()
useEngineQuery()
useEffect(() => {
getThreadList()
getModels()
}, [getThreadList, getModels])
console.debug('Load Data...')
return null
}

View File

@ -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'

View File

@ -70,17 +70,14 @@ export const isGeneratingResponseAtom = atom<boolean>(false)
*/
export const threadsAtom = atom<Thread[]>([])
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) {
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)
}
return allThreads
})
set(threadsAtom, filteredThreads)
})
export const activeThreadAtom = atom<Thread | undefined>((get) =>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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<DownloadContainerProps> = ({
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<DownloadContainerProps> = ({
return
}
await createThread(persistModelId, {
await createThreadMutation.mutateAsync({
modelId: persistModelId,
assistant: {
...assistants[0],
model: persistModelId,
},
})
setDownloadLocalModelModalStage('NONE', undefined)
setMainViewState(MainViewState.Thread)
}, [
setDownloadLocalModelModalStage,
setMainViewState,
createThread,
createThreadMutation,
persistModelId,
assistants,
])

View File

@ -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<HTMLInputElement>) => {
setCopyOverInstructionEnabled(e.target.checked)
},
[setCopyOverInstructionEnabled]
)
return (
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
<div className="flex-shrink-0 space-y-1">
<div className="flex gap-x-2">
<h6 className="font-semibold capitalize">Copy Over Instruction</h6>
</div>
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
Enable instruction to be copied to new thread
</p>
</div>
{/**/}
<Switch checked={copyOverInstructionEnabled} onChange={onSwitchToggled} />
</div>
)
}
export default CopyOverInstructionItem

View File

@ -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 */}
{/* <FactoryReset /> */}
{experimentalEnabled && <CopyOverInstructionItem />}
{experimentalEnabled && <DataMigration />}
</div>
</ScrollArea>

View File

@ -32,9 +32,7 @@ const ChatTextInput: React.FC<Props> = ({
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
const disabled = useMemo(() => {
return !activeThreadId
}, [activeThreadId])
const disabled = useMemo(() => !activeThreadId, [activeThreadId])
const onChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -44,10 +42,8 @@ const ChatTextInput: React.FC<Props> = ({
)
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus()
}
}, [activeThreadId])
textareaRef.current?.focus()
})
useEffect(() => {
if (textareaRef.current?.clientHeight) {

View File

@ -23,12 +23,16 @@ const ModalDeleteThread: React.FC<Props> = ({
const { deleteThread } = useThreads()
const onDeleteThreadClick = useCallback(async () => {
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 (

View File

@ -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,

View File

@ -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<HTMLInputElement>) => {
setCopyOverInstructionEnabled(e.target.checked)
},
[setCopyOverInstructionEnabled]
)
return (
<div className="flex w-full flex-row items-center justify-center gap-x-2">
<h6 className="flex-1 font-medium">Save instructions for all threads</h6>
<Switch
checked={copyOverInstructionEnabled}
onChange={onSwitchToggled}
className="flex-shrink-0"
/>
</div>
)
}
export default CopyOverInstruction

View File

@ -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
</label>
<TextArea
rows={5}
id="assistant-instructions"
placeholder="Eg. You are a helpful assistant."
value={instructions}
onChange={onInstructionChanged}
/>
{experimentalEnabled && <CopyOverInstruction />}
</div>
)
}