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