import { DndContext, closestCenter, useSensor, useSensors, PointerSensor, KeyboardSensor, } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { IconDots, IconStarFilled, IconStar, IconFolder, IconX, } from '@tabler/icons-react' import { useThreads } from '@/hooks/useThreads' import { useThreadManagement } from '@/hooks/useThreadManagement' import { useLeftPanel } from '@/hooks/useLeftPanel' import { useMessages } from '@/hooks/useMessages' import { cn, extractThinkingContent } from '@/lib/utils' import { useSmallScreen } from '@/hooks/useMediaQuery' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from '@/components/ui/dropdown-menu' import { useTranslation } from '@/i18n/react-i18next-compat' import { memo, MouseEvent, useMemo, useState } from 'react' import { useNavigate, useMatches } from '@tanstack/react-router' import { RenameThreadDialog, DeleteThreadDialog } from '@/containers/dialogs' import { route } from '@/constants/routes' import { toast } from 'sonner' const SortableItem = memo( ({ thread, variant, }: { thread: Thread variant?: 'default' | 'project' }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: thread.id, disabled: true }) const isSmallScreen = useSmallScreen() const setLeftPanel = useLeftPanel((state) => state.setLeftPanel) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } const toggleFavorite = useThreads((state) => state.toggleFavorite) const deleteThread = useThreads((state) => state.deleteThread) const renameThread = useThreads((state) => state.renameThread) const updateThread = useThreads((state) => state.updateThread) const getFolderById = useThreadManagement().getFolderById const { folders } = useThreadManagement() const getMessages = useMessages((state) => state.getMessages) const { t } = useTranslation() const [openDropdown, setOpenDropdown] = useState(false) const navigate = useNavigate() // Check if current route matches this thread's detail page const matches = useMatches() const isActive = matches.some( (match) => match.routeId === '/threads/$threadId' && 'threadId' in match.params && match.params.threadId === thread.id ) const handleClick = (e: MouseEvent) => { if (openDropdown) { e.stopPropagation() e.preventDefault() return } if (!isDragging) { // Only close panel and navigate if the thread is not already active if (!isActive) { if (isSmallScreen) setLeftPanel(false) navigate({ to: route.threadsDetail, params: { threadId: thread.id } }) } } } const plainTitleForRename = useMemo(() => { // Basic HTML stripping for simple span tags. // If thread.title is undefined or null, treat as empty string before replace. return (thread.title || '').replace(/]*>|<\/span>/g, '') }, [thread.title]) const assignThreadToProject = (threadId: string, projectId: string) => { const project = getFolderById(projectId) if (project && updateThread) { const projectMetadata = { id: project.id, name: project.name, updated_at: project.updated_at, } updateThread(threadId, { metadata: { ...thread.metadata, project: projectMetadata, }, }) toast.success(`Thread assigned to "${project.name}" successfully`) } } const getLastMessageInfo = useMemo(() => { const messages = getMessages(thread.id) if (messages.length === 0) return null const lastMessage = messages[messages.length - 1] return { date: new Date(lastMessage.created_at || 0), content: lastMessage.content?.[0]?.text?.value || '', } }, [getMessages, thread.id]) return (
handleClick(e)} onContextMenu={(e) => { e.preventDefault() e.stopPropagation() setOpenDropdown(true) }} >
{thread.title || t('common:newThread')} {variant === 'project' && getLastMessageInfo?.content && ( {extractThinkingContent(getLastMessageInfo.content)} )}
setOpenDropdown(open)} > { e.preventDefault() e.stopPropagation() }} /> {thread.isFavorite ? ( { e.stopPropagation() toggleFavorite(thread.id) }} > {t('common:unstar')} ) : ( { e.stopPropagation() toggleFavorite(thread.id) }} > {t('common:star')} )} setOpenDropdown(false)} /> Add to project {folders.length === 0 ? ( No projects available ) : ( folders .sort((a, b) => b.updated_at - a.updated_at) .map((folder) => ( { e.stopPropagation() assignThreadToProject(thread.id, folder.id) }} > {folder.name} )) )} {thread.metadata?.project && ( <> { e.stopPropagation() // Remove project from metadata const projectName = thread.metadata?.project?.name updateThread(thread.id, { metadata: { ...thread.metadata, project: undefined, }, }) toast.success( `Thread removed from "${projectName}" successfully` ) }} > Remove from project )} setOpenDropdown(false)} variant={variant} />
) } ) type ThreadListProps = { threads: Thread[] isFavoriteSection?: boolean variant?: 'default' | 'project' showDate?: boolean } function ThreadList({ threads, variant = 'default' }: ThreadListProps) { const sortedThreads = useMemo(() => { return threads.sort((a, b) => { return (b.updated || 0) - (a.updated || 0) }) }, [threads]) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { delay: 200, tolerance: 5, }, }), useSensor(KeyboardSensor) ) return ( t.id)} strategy={verticalListSortingStrategy} > {sortedThreads.map((thread, index) => ( ))} ) } export default memo(ThreadList)