From 161dc5f901b03af8d0b93acf4eeb9d5b5f1278d1 Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Wed, 14 Feb 2024 12:31:43 -0800 Subject: [PATCH] Implement thread UI update (#722) * Implement thread UI update * remove comment --- .../ThreadContainer/ThreadItem/index.jsx | 58 +++++++++++++------ .../ThreadContainer/index.jsx | 43 +++++++++++--- .../Sidebar/ActiveWorkspaces/index.jsx | 8 +-- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx index 29e4f67e0..5e9a66f40 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx @@ -7,13 +7,20 @@ import { useParams } from "react-router-dom"; import truncate from "truncate"; const THREAD_CALLOUT_DETAIL_WIDTH = 26; -export default function ThreadItem({ workspace, thread, onRemove, hasNext }) { +export default function ThreadItem({ + idx, + activeIdx, + isActive, + workspace, + thread, + onRemove, + hasNext, +}) { const optionsContainer = useRef(null); - const { slug, threadSlug = null } = useParams(); + const { slug } = useParams(); const [showOptions, setShowOptions] = useState(false); const [name, setName] = useState(thread.name); - const isActive = threadSlug === thread.slug; const linkTo = !thread.slug ? paths.workspace.chat(slug) : paths.workspace.thread(slug, thread.slug); @@ -23,33 +30,48 @@ export default function ThreadItem({ workspace, thread, onRemove, hasNext }) { {/* Curved line Element and leader if required */} <div style={{ width: THREAD_CALLOUT_DETAIL_WIDTH / 2 }} - className="border-l border-b border-slate-300 h-[50%] absolute top-0 left-2 rounded-bl-lg" + className={`${ + isActive + ? "border-l-2 border-b-2 border-white" + : "border-l border-b border-slate-300" + } h-[50%] absolute top-0 left-2 rounded-bl-lg`} ></div> + {/* Downstroke border for next item */} {hasNext && ( <div style={{ width: THREAD_CALLOUT_DETAIL_WIDTH / 2 }} - className="border-l border-slate-300 h-[100%] absolute top-0 left-2" + className={`${ + idx <= activeIdx && !isActive + ? "border-l-2 border-white" + : "border-l border-slate-300" + } h-[100%] absolute top-0 left-2`} ></div> )} - {/* Curved line inline placeholder for spacing */} + {/* Curved line inline placeholder for spacing - not visible */} <div style={{ width: THREAD_CALLOUT_DETAIL_WIDTH }} className="w-[26px] h-full" /> <div className="flex w-full items-center justify-between pr-2 group relative"> - <a href={isActive ? "#" : linkTo} className="w-full"> - <p - className={`text-left text-sm ${ - isActive - ? "font-semibold text-slate-300" - : "text-slate-400 italic" - }`} - > - {truncate(name, 25)} - </p> - </a> - {!!thread.slug && ( + {thread.deleted ? ( + <a className="w-full"> + <p className={`text-left text-sm text-slate-400/50 italic`}> + deleted thread + </p> + </a> + ) : ( + <a href={isActive ? "#" : linkTo} className="w-full"> + <p + className={`text-left text-sm ${ + isActive ? "font-bold text-white" : "text-slate-400" + }`} + > + {truncate(name, 25)} + </p> + </a> + )} + {!!thread.slug && !thread.deleted && ( <div ref={optionsContainer}> <div className="flex items-center w-fit group-hover:visible md:invisible gap-x-1"> <button diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx index 5667d639e..50205ef9e 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx @@ -4,8 +4,10 @@ import showToast from "@/utils/toast"; import { Plus, CircleNotch } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; import ThreadItem from "./ThreadItem"; +import { useParams } from "react-router-dom"; export default function ThreadContainer({ workspace }) { + const { threadSlug = null } = useParams(); const [threads, setThreads] = useState([]); const [loading, setLoading] = useState(true); @@ -20,7 +22,12 @@ export default function ThreadContainer({ workspace }) { }, [workspace.slug]); function removeThread(threadId) { - setThreads((prev) => prev.filter((thread) => thread.id !== threadId)); + setThreads((prev) => + prev.map((_t) => { + if (_t.id !== threadId) return _t; + return { ..._t, deleted: true }; + }) + ); } if (loading) { @@ -33,15 +40,26 @@ export default function ThreadContainer({ workspace }) { ); } + const activeThreadIdx = !!threads.find( + (thread) => thread?.slug === threadSlug + ) + ? threads.findIndex((thread) => thread?.slug === threadSlug) + 1 + : 0; return ( <div className="flex flex-col"> <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} + activeIdx={activeThreadIdx} + isActive={activeThreadIdx === i + 1} workspace={workspace} onRemove={removeThread} thread={thread} @@ -54,7 +72,7 @@ export default function ThreadContainer({ workspace }) { } function NewThreadButton({ workspace }) { - const [loading, setLoading] = useState(); + const [loading, setLoading] = useState(false); const onClick = async () => { setLoading(true); const { thread, error } = await Workspace.threads.new(workspace.slug); @@ -74,15 +92,22 @@ function NewThreadButton({ workspace }) { className="w-full relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg" > <div className="flex w-full gap-x-2 items-center pl-4"> + <div className="bg-zinc-600 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-slate-100" + /> + ) : ( + <Plus weight="bold" size={14} className="shrink-0 text-slate-100" /> + )} + </div> + {loading ? ( - <CircleNotch className="animate-spin text-slate-300" /> + <p className="text-left text-slate-100 text-sm">Starting Thread...</p> ) : ( - <Plus className="text-slate-300" /> - )} - {loading ? ( - <p className="text-left text-slate-300 text-sm">starting thread...</p> - ) : ( - <p className="text-left text-slate-300 text-sm">new thread</p> + <p className="text-left text-slate-100 text-sm">New Thread</p> )} </div> </button> diff --git a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx index 4603009bd..54580a2af 100644 --- a/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx +++ b/frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx @@ -80,12 +80,12 @@ export default function ActiveWorkspaces() { href={isActive ? null : paths.workspace.chat(workspace.slug)} className={` transition-all duration-[200ms] - flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-lg text-slate-200 justify-start items-center border - hover:bg-workspace-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 + flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-lg text-slate-200 justify-start items-center + hover:bg-workspace-item-selected-gradient ${ isActive - ? "bg-workspace-item-selected-gradient border-slate-100 border-opacity-50" - : "bg-workspace-item-gradient bg-opacity-60 border-transparent" + ? "border-2 bg-workspace-item-selected-gradient border-white" + : "border bg-workspace-item-gradient bg-opacity-60 border-transparent hover:border-slate-100 hover:border-opacity-50" }`} > <div className="flex flex-row justify-between w-full">