Download frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx from leowuming/rag: direct link, hf CLI and curl.
- Browser
- Download file 7.11 kB
-
https://huggingface.co/spaces/leowuming/rag/resolve/main/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx
- Command line
-
hf download hf://spaces/leowuming/rag/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx
-
curl -L -o index.jsx https://huggingface.co/spaces/leowuming/rag/resolve/main/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx
7.11 kB
| import Workspace from "@/models/workspace"; | |
| import paths from "@/utils/paths"; | |
| import showToast from "@/utils/toast"; | |
| import { Plus, CircleNotch, Trash } from "@phosphor-icons/react"; | |
| import { useEffect, useState } from "react"; | |
| import ThreadItem from "./ThreadItem"; | |
| import { useParams } from "react-router-dom"; | |
| export const THREAD_RENAME_EVENT = "renameThread"; | |
| export default function ThreadContainer({ workspace }) { | |
| const { threadSlug = null } = useParams(); | |
| const [threads, setThreads] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| const [ctrlPressed, setCtrlPressed] = useState(false); | |
| useEffect(() => { | |
| const chatHandler = (event) => { | |
| const { threadSlug, newName } = event.detail; | |
| setThreads((prevThreads) => | |
| prevThreads.map((thread) => { | |
| if (thread.slug === threadSlug) { | |
| return { ...thread, name: newName }; | |
| } | |
| return thread; | |
| }) | |
| ); | |
| }; | |
| window.addEventListener(THREAD_RENAME_EVENT, chatHandler); | |
| return () => { | |
| window.removeEventListener(THREAD_RENAME_EVENT, chatHandler); | |
| }; | |
| }, []); | |
| useEffect(() => { | |
| async function fetchThreads() { | |
| if (!workspace.slug) return; | |
| const { threads } = await Workspace.threads.all(workspace.slug); | |
| setLoading(false); | |
| setThreads(threads); | |
| } | |
| fetchThreads(); | |
| }, [workspace.slug]); | |
| // Enable toggling of bulk-deletion by holding meta-key (ctrl on win and cmd/fn on others) | |
| useEffect(() => { | |
| const handleKeyDown = (event) => { | |
| if (["Control", "Meta"].includes(event.key)) { | |
| setCtrlPressed(true); | |
| } | |
| }; | |
| const handleKeyUp = (event) => { | |
| if (["Control", "Meta"].includes(event.key)) { | |
| setCtrlPressed(false); | |
| // when toggling, unset bulk progress so | |
| // previously marked threads that were never deleted | |
| // come back to life. | |
| setThreads((prev) => | |
| prev.map((t) => { | |
| return { ...t, deleted: false }; | |
| }) | |
| ); | |
| } | |
| }; | |
| window.addEventListener("keydown", handleKeyDown); | |
| window.addEventListener("keyup", handleKeyUp); | |
| return () => { | |
| window.removeEventListener("keydown", handleKeyDown); | |
| window.removeEventListener("keyup", handleKeyUp); | |
| }; | |
| }, []); | |
| const toggleForDeletion = (id) => { | |
| setThreads((prev) => | |
| prev.map((t) => { | |
| if (t.id !== id) return t; | |
| return { ...t, deleted: !t.deleted }; | |
| }) | |
| ); | |
| }; | |
| const handleDeleteAll = async () => { | |
| const slugs = threads.filter((t) => t.deleted === true).map((t) => t.slug); | |
| await Workspace.threads.deleteBulk(workspace.slug, slugs); | |
| setThreads((prev) => prev.filter((t) => !t.deleted)); | |
| // Only redirect if current thread is being deleted | |
| if (slugs.includes(threadSlug)) { | |
| window.location.href = paths.workspace.chat(workspace.slug); | |
| } | |
| }; | |
| function removeThread(threadId) { | |
| setThreads((prev) => | |
| prev.map((_t) => { | |
| if (_t.id !== threadId) return _t; | |
| return { ..._t, deleted: true }; | |
| }) | |
| ); | |
| // Show thread was deleted, but then remove from threads entirely so it will | |
| // not appear in bulk-selection. | |
| setTimeout(() => { | |
| setThreads((prev) => prev.filter((t) => !t.deleted)); | |
| }, 500); | |
| } | |
| if (loading) { | |
| return ( | |
| <div className="flex flex-col bg-pulse w-full h-10 items-center justify-center"> | |
| <p className="text-xs text-white animate-pulse">loading threads....</p> | |
| </div> | |
| ); | |
| } | |
| const activeThreadIdx = !!threads.find( | |
| (thread) => thread?.slug === threadSlug | |
| ) | |
| ? threads.findIndex((thread) => thread?.slug === threadSlug) + 1 | |
| : 0; | |
| return ( | |
| <div className="flex flex-col" role="list" aria-label="Threads"> | |
| <ThreadItem | |
| idx={0} | |
| activeIdx={activeThreadIdx} | |
| isActive={activeThreadIdx === 0} | |
| thread={{ slug: null, name: "default" }} | |
| hasNext={threads.length > 0} | |
| /> | |
| {threads.map((thread, i) => ( | |
| <ThreadItem | |
| key={thread.slug} | |
| idx={i + 1} | |
| ctrlPressed={ctrlPressed} | |
| toggleMarkForDeletion={toggleForDeletion} | |
| activeIdx={activeThreadIdx} | |
| isActive={activeThreadIdx === i + 1} | |
| workspace={workspace} | |
| onRemove={removeThread} | |
| thread={thread} | |
| hasNext={i !== threads.length - 1} | |
| /> | |
| ))} | |
| <DeleteAllThreadButton | |
| ctrlPressed={ctrlPressed} | |
| threads={threads} | |
| onDelete={handleDeleteAll} | |
| /> | |
| <NewThreadButton workspace={workspace} /> | |
| </div> | |
| ); | |
| } | |
| function NewThreadButton({ workspace }) { | |
| const [loading, setLoading] = useState(false); | |
| const onClick = async () => { | |
| setLoading(true); | |
| const { thread, error } = await Workspace.threads.new(workspace.slug); | |
| if (!!error) { | |
| showToast(`Could not create thread - ${error}`, "error", { clear: true }); | |
| setLoading(false); | |
| return; | |
| } | |
| window.location.replace( | |
| paths.workspace.thread(workspace.slug, thread.slug) | |
| ); | |
| }; | |
| return ( | |
| <button | |
| onClick={onClick} | |
| className="w-full relative flex h-[40px] items-center border-none hover:bg-[var(--theme-sidebar-thread-selected)] hover:light:bg-theme-sidebar-subitem-hover rounded-lg" | |
| > | |
| <div className="flex w-full gap-x-2 items-center pl-4"> | |
| <div className="bg-white/20 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center"> | |
| {loading ? ( | |
| <CircleNotch | |
| weight="bold" | |
| size={14} | |
| className="shrink-0 animate-spin text-white light:text-theme-text-primary" | |
| /> | |
| ) : ( | |
| <Plus | |
| weight="bold" | |
| size={14} | |
| className="shrink-0 text-white light:text-theme-text-primary" | |
| /> | |
| )} | |
| </div> | |
| {loading ? ( | |
| <p className="text-left text-white light:text-theme-text-primary text-sm"> | |
| Starting Thread... | |
| </p> | |
| ) : ( | |
| <p className="text-left text-white light:text-theme-text-primary text-sm"> | |
| New Thread | |
| </p> | |
| )} | |
| </div> | |
| </button> | |
| ); | |
| } | |
| function DeleteAllThreadButton({ ctrlPressed, threads, onDelete }) { | |
| if (!ctrlPressed || threads.filter((t) => t.deleted).length === 0) | |
| return null; | |
| return ( | |
| <button | |
| type="button" | |
| onClick={onDelete} | |
| className="w-full relative flex h-[40px] items-center border-none hover:bg-red-400/20 rounded-lg group" | |
| > | |
| <div className="flex w-full gap-x-2 items-center pl-4"> | |
| <div className="bg-transparent p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center"> | |
| <Trash | |
| weight="bold" | |
| size={14} | |
| className="shrink-0 text-white light:text-red-500/50 group-hover:text-red-400" | |
| /> | |
| </div> | |
| <p className="text-white light:text-theme-text-secondary text-left text-sm group-hover:text-red-400"> | |
| Delete Selected | |
| </p> | |
| </div> | |
| </button> | |
| ); | |
| } | |