fix: refresh should not create new thread (#3314)
This commit is contained in:
parent
a4f5fda104
commit
624d07703c
@ -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> {}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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) {
|
||||
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<Thread | undefined>((get) =>
|
||||
|
||||
26
web/hooks/useModelQuery.ts
Normal file
26
web/hooks/useModelQuery.ts
Normal 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
|
||||
58
web/hooks/useThreadCreateMutation.ts
Normal file
58
web/hooks/useThreadCreateMutation.ts
Normal 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
|
||||
26
web/hooks/useThreadQuery.ts
Normal file
26
web/hooks/useThreadQuery.ts
Normal 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
|
||||
@ -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,
|
||||
|
||||
@ -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, {
|
||||
...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,
|
||||
])
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -23,12 +23,16 @@ const ModalDeleteThread: React.FC<Props> = ({
|
||||
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 (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user