* chore: initial new FE setup * chore: update namespace text-left-panel foreground variable * chore: enable dynamic mainview color * chore: remove greetings new chat * chore: fix chat input style * chore: simplify hook useAppearance * chore: enable internationalization * chore: prepare vn locale * chore: keyboardshortcut layout * chore: update keyboard shortcut exclude pathname * chore: update state active setting route * chore: fix update theme by system * chore: handle dynamic primary color * chore: fix left panel navigation active state and styled item privacy analytic * chore: reorder general setting being a first * chore: add function reset appearance * chore: update scrollbar * chore: update delete thread with dialog confirmation * chore: update state dialog inside dropdown menu * chore: wip thread detail or chat page * chore: wip model dropdown * chore: prepare model dropdown select * chore: update model providers setting * chore: show provider on model dropdown based isActive toogle * chore: update layout model provider * chore: update state active on storage * chore: update gap of item dropdown model * chore: update select model base on id * chore: update edit model capabilities * chore: add dialog to add model * chore: update sheet for model setting * chore: add sheet setting each model * chore: make dynamic syntax highlight * chore: fix menu setting appearance theme * chore: markdown render support emoji * chore: markdown support latex * chore: change codeblock default theme * chore: update ui codeblock * chore: custom render link taget new window * chore: fix copy button codeblock * chore: update accent and desctructive color * chore: setup user chat message * chore: prepare some page settings * chore: simple list extension and prepare mcp, local api, and hardware * chore: mcp-serve * chore: MCP server UI * chore: update local api server config * chore: adjust chat input * chore: update local api server log * chore: prepare hub page * chore: remove help page * chore: update mock * chore: prepare http proxy setting UI * chore: adjust local api server and title every action * fix: chore FE package (#4962) * fix: update command which referred to non-existent web app * fix: added commented out macos platform for now * fix: remove the platform name as macos * fix: remove unnecessary line for platform name in HeaderPage component * fix: update dev script to specify port 3000 for Vite * feat: model providers and chat completion * enhancement: threads performance * fix: thread content update * chore: clean up threads * fix: performance issue with streaming and state loop * fix: streaming * fix: react markdow * feat: extension manager * chore: add nodePolyfills include path * chore: improve performance avoid unhandle rejection * chore: update pre margin bottom * chore: swith thread should be deafult scroll to bottom * chore: wip scroll to bottom * chore: add model loader * chore: add platform utils * feat: threads functionality * chore: setup toaster * chore: persist threads deletion * fix: create thread with new message * chore: create new thread should change route path * chore: navigate after delet dialog thread * chore: thread favorites and orders * chore: dismiss deleting modal on delete * chore: remove undefined properties * chore: remove deprecated run step * chore: fix delete thread * chore: create empty thread content on started streaming * chore: correct messages store key * chore: stuck at generating state * chore: preapre chat toolbar * chore: introduce in-memory app state * chore: update extensions migration logic * chore: remove redundant extensions migration gate * chore: message toolbar user and assistant * chore: add logo gemini * feat: remote providers with model capabilities * chore: maintain provider settings * chore: move speed token into chat input * chore: temp harcoded model loader * chore: make chat text selectable and truncate model list * chore: update shortcut UI * Feat/implement threads (#4977) * chore: add fuse.js library for enhanced search functionality * feat: implement thread filtering with Fuse.js for improved search capabilities * fix: update the fuseOptions * feat: add search functionality to LeftPanel and refactor thread retrieval logic * refactor: optimize thread filtering and improve search functionality in LeftPanel * fix: more edits * refactor: remove duplicate import of useAppState in StreamingContent component * chore: update navigate after delete all thread * chore: pass prop speedToken from new chat input * chore: persist provider general settings * chore: styling search left panel * chore: cleanup margin * chore: update size icon * chore: improve chat input * chore: imprve list markdown * chore: animate border * feat: local model provider work * chore: persist manually added model * chore: prepare download management ui and show version on general setting * chore: improve pre tag * chore: remove buton install extension and improve light theme download * chore: add missing hardware information handler * chore: cleanup small ui * chore: update default provider settings * fix: missing fs commands * chore: correct provider models * chore: prepare delete model * chore: handle thinking block * chore: fix conditional message toolbar * chore: pophover download select none * enhancement: add prune mode * chore: model settings * chore: bump engine version tauri * chore: update style thinking * chore: add indicator and toogle mcp server * chore: wip hub * chore: update model settings * chore: mvp hub * chore: add function rename title * chore: update function delete message * chore: update rename title * chore: update model settings * chore: persist MCP configs * refactor: clean up utils * chore: add tools to completion request * chore: clean up * chore: ignore assets --------- Co-authored-by: Ivan Leo <ivanleomk@gmail.com> Co-authored-by: Louis <louis@jan.ai>
325 lines
10 KiB
TypeScript
325 lines
10 KiB
TypeScript
import {
|
|
DndContext,
|
|
closestCenter,
|
|
useSensor,
|
|
useSensors,
|
|
PointerSensor,
|
|
KeyboardSensor,
|
|
} from '@dnd-kit/core'
|
|
import {
|
|
SortableContext,
|
|
verticalListSortingStrategy,
|
|
arrayMove,
|
|
useSortable,
|
|
} from '@dnd-kit/sortable'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
import {
|
|
IconDots,
|
|
IconStarFilled,
|
|
IconTrash,
|
|
IconEdit,
|
|
IconStar,
|
|
} from '@tabler/icons-react'
|
|
import { useThreads } from '@/hooks/useThreads'
|
|
import { cn } from '@/lib/utils'
|
|
import { route } from '@/constants/routes'
|
|
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { DialogClose, DialogFooter, DialogHeader } from '@/components/ui/dialog'
|
|
import {
|
|
Dialog,
|
|
DialogTrigger,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { memo, useMemo, useState } from 'react'
|
|
import { useNavigate, useMatches } from '@tanstack/react-router'
|
|
import { toast } from 'sonner'
|
|
import { Input } from '@/components/ui/input'
|
|
|
|
const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|
const {
|
|
attributes,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
transition,
|
|
isDragging,
|
|
} = useSortable({ id: thread.id })
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
opacity: isDragging ? 0.5 : 1,
|
|
}
|
|
const { toggleFavorite, deleteThread, renameThread } = useThreads()
|
|
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 = () => {
|
|
if (!isDragging) {
|
|
navigate({ to: route.threadsDetail, params: { threadId: thread.id } })
|
|
}
|
|
}
|
|
|
|
const [title, setTitle] = useState(thread.title || 'New Thread')
|
|
|
|
return (
|
|
<div
|
|
ref={setNodeRef}
|
|
style={style}
|
|
{...attributes}
|
|
{...listeners}
|
|
onClick={handleClick}
|
|
className={cn(
|
|
'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 group/thread-list transition-all',
|
|
isDragging ? 'cursor-move' : 'cursor-pointer',
|
|
isActive && 'bg-left-panel-fg/10'
|
|
)}
|
|
>
|
|
<div className="py-1 pr-2 truncate">
|
|
<span className="text-left-panel-fg/90">
|
|
{thread.title || 'New Thread'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<DropdownMenu
|
|
open={openDropdown}
|
|
onOpenChange={(open) => setOpenDropdown(open)}
|
|
>
|
|
<DropdownMenuTrigger asChild>
|
|
<IconDots
|
|
size={14}
|
|
className="text-left-panel-fg/60 shrink-0 cursor-pointer px-0.5 -mr-1 data-[state=open]:bg-left-panel-fg/10 rounded group-hover/thread-list:data-[state=closed]:size-5 size-5 data-[state=closed]:size-0"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}}
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="bottom" align="end">
|
|
{thread.isFavorite ? (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
toggleFavorite(thread.id)
|
|
}}
|
|
>
|
|
<IconStarFilled />
|
|
<span>{t('common.unstar')}</span>
|
|
</DropdownMenuItem>
|
|
) : (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
toggleFavorite(thread.id)
|
|
}}
|
|
>
|
|
<IconStar />
|
|
<span>{t('common.star')}</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<Dialog
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setOpenDropdown(false)
|
|
setTitle(thread.title)
|
|
}
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
|
<IconEdit />
|
|
<span>{t('common.rename')}</span>
|
|
</DropdownMenuItem>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Rename Title</DialogTitle>
|
|
<Input
|
|
value={title}
|
|
onChange={(e) => {
|
|
setTitle(e.target.value)
|
|
}}
|
|
className="mt-2"
|
|
onKeyDown={(e) => {
|
|
// Prevent key from being captured by parent components
|
|
e.stopPropagation()
|
|
}}
|
|
/>
|
|
<DialogFooter className="mt-2 flex items-center">
|
|
<DialogClose asChild>
|
|
<Button
|
|
variant="link"
|
|
size="sm"
|
|
className="hover:no-underline"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</DialogClose>
|
|
<Button
|
|
disabled={!title}
|
|
onClick={() => {
|
|
renameThread(thread.id, title)
|
|
setOpenDropdown(false)
|
|
toast.success('Renema Title', {
|
|
id: 'rename-thread',
|
|
description:
|
|
"Thread title has been renamed to '" + title + "'",
|
|
})
|
|
}}
|
|
>
|
|
Rename
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogHeader>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<DropdownMenuSeparator />
|
|
<Dialog
|
|
onOpenChange={(open) => {
|
|
if (!open) setOpenDropdown(false)
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
|
<IconTrash />
|
|
<span>{t('common.delete')}</span>
|
|
</DropdownMenuItem>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Thread</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete this thread? This action
|
|
cannot be undone.
|
|
</DialogDescription>
|
|
<DialogFooter className="mt-2 flex items-center">
|
|
<DialogClose asChild>
|
|
<Button
|
|
variant="link"
|
|
size="sm"
|
|
className="hover:no-underline"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</DialogClose>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() => {
|
|
deleteThread(thread.id)
|
|
setOpenDropdown(false)
|
|
toast.success('Delete Thread', {
|
|
id: 'delete-thread',
|
|
description:
|
|
'This thread has been permanently deleted.',
|
|
})
|
|
setTimeout(() => {
|
|
navigate({ to: route.home })
|
|
}, 0)
|
|
}}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogHeader>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
type ThreadListProps = {
|
|
threads: Thread[]
|
|
isFavoriteSection?: boolean
|
|
}
|
|
|
|
function ThreadList({ threads, isFavoriteSection = false }: ThreadListProps) {
|
|
const { setThreads } = useThreads()
|
|
|
|
const sortedThreads = useMemo(() => {
|
|
return threads.sort((a, b) => {
|
|
if (a.order && b.order) return a.order - b.order
|
|
|
|
// Later on top
|
|
return (b.updated || 0) - (a.updated || 0)
|
|
})
|
|
}, [threads])
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
delay: 200,
|
|
tolerance: 5,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor)
|
|
)
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={(event) => {
|
|
const { active, over } = event
|
|
if (active.id !== over?.id) {
|
|
const oldIndex = threads.findIndex((t) => t.id === active.id)
|
|
const newIndex = threads.findIndex((t) => t.id === over?.id)
|
|
|
|
// Create a new array with the reordered threads from this section only
|
|
const reorderedSectionThreads = arrayMove(threads, oldIndex, newIndex)
|
|
|
|
// Split all threads into favorites and non-favorites
|
|
const favThreads = sortedThreads.filter((t) => t.isFavorite)
|
|
const nonFavThreads = sortedThreads.filter((t) => !t.isFavorite)
|
|
|
|
// Replace the appropriate section with the reordered threads
|
|
let updatedThreads
|
|
if (isFavoriteSection) {
|
|
// If we're in the favorites section, update favorites and keep non-favorites as is
|
|
updatedThreads = [...reorderedSectionThreads, ...nonFavThreads]
|
|
} else {
|
|
// If we're in the non-favorites section, update non-favorites and keep favorites as is
|
|
updatedThreads = [...favThreads, ...reorderedSectionThreads]
|
|
}
|
|
|
|
setThreads(updatedThreads)
|
|
}
|
|
}}
|
|
>
|
|
<SortableContext
|
|
items={sortedThreads.map((t) => t.id)}
|
|
strategy={verticalListSortingStrategy}
|
|
>
|
|
{sortedThreads.map((thread, index) => (
|
|
<SortableItem key={index} thread={thread} />
|
|
))}
|
|
</SortableContext>
|
|
</DndContext>
|
|
)
|
|
}
|
|
|
|
export default memo(ThreadList)
|