112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import { useCallback, useEffect, useRef } from 'react'
|
|
|
|
import { Button } from '@janhq/joi'
|
|
import { AnimatePresence } from 'framer-motion'
|
|
|
|
import { useAtomValue } from 'jotai'
|
|
import { PenSquareIcon } from 'lucide-react'
|
|
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
import LeftPanelContainer from '@/containers/LeftPanelContainer'
|
|
import { toaster } from '@/containers/Toast'
|
|
|
|
import useAssistantQuery from '@/hooks/useAssistantQuery'
|
|
|
|
import useThreads from '@/hooks/useThreads'
|
|
|
|
import { copyOverInstructionEnabledAtom } from '@/screens/Settings/Advanced/components/CopyOverInstruction'
|
|
|
|
import ThreadItem from './ThreadItem'
|
|
|
|
import {
|
|
downloadedModelsAtom,
|
|
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 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
|
|
setActiveThread(threads[0].id)
|
|
}, [activeThread?.id, setActiveThread, threads])
|
|
|
|
const onCreateThreadClicked = useCallback(async () => {
|
|
if (!assistants || assistants.length === 0) {
|
|
toaster({
|
|
title: 'No assistant available.',
|
|
description: `Could not create a new thread. Please add an assistant.`,
|
|
type: 'error',
|
|
})
|
|
return
|
|
}
|
|
if (!selectedModel) return
|
|
let instructions: string | undefined = undefined
|
|
if (copyOverInstructionEnabled) {
|
|
instructions = activeThread?.assistants[0]?.instructions ?? undefined
|
|
}
|
|
createThread(selectedModel.model, assistants[0], instructions)
|
|
}, [
|
|
createThread,
|
|
selectedModel,
|
|
assistants,
|
|
activeThread,
|
|
copyOverInstructionEnabled,
|
|
])
|
|
|
|
return (
|
|
<LeftPanelContainer>
|
|
<div className={twMerge('pl-1.5 pt-3', reduceTransparent && 'pr-1.5')}>
|
|
<Button
|
|
className="mb-2"
|
|
data-testid="btn-create-thread"
|
|
onClick={onCreateThreadClicked}
|
|
theme="icon"
|
|
>
|
|
<PenSquareIcon
|
|
size={16}
|
|
className="cursor-pointer text-[hsla(var(--text-secondary))]"
|
|
/>
|
|
</Button>
|
|
<AnimatePresence>
|
|
{threads.map((thread) => (
|
|
<ThreadItem key={thread.id} thread={thread} />
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
</LeftPanelContainer>
|
|
)
|
|
}
|
|
|
|
export default ThreadLeftPanel
|