* remove legacy home page components, update home page to new layout * update PromptInput component styles to match new designs, make quick action buttons functional * home page chat creates new thread in last used workspace * fix slash commands and agent popup on home page * disable llm workspace selector action in home page * add drag and drop file support to home page * fix behavior of drag and drop on home page * handle pasting attachments in home page * update empty state of workspace chat to use new ui * update empty workspace ui to match home page design, fix flickering loading states * convert quick action buttons to component, add to empty state ws chat * fix hover state light mode in quick actions * add suggested messages subcomponent to empty ws/thread * adjust width, rounded edges of prompt input * only show quick actions for admin/manager role * fix hover states for quick actions and suggested messages component * make upload document quick action trigger parsed document upload * fix mic behavior in homepage, ws chat, ws thread chat * fix margin between prompt input and quick actions * Simplify message presets by removing heading input (#4915) * Remove heading input from message presets, merge legacy headings on edit * filter out empty messages from state after saving * mark form as dirty on input change * styling --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> * convert SuggestedMessages to component, render SuggestedMessages in home page to target ws * fix broken handleMessageChange reference * add translations for QuickActions * lint * fix home page chat submission broken by PromptInput onChange removal * fix prompt input remount race condition, home page suggested message flicker * remove unused handleSendSuggestedMessage from ChatHistory * add greeting text to main-page translations, remove defaults * fix file deletion in parsed files menu on home page * add virtual thread sidebar state and workspace indicator on home page * show workspace llm selector on home page when workspace exists * show home page for all user roles with rbac quick actions * fix positioning of agent and slash command popups * remove workspace indicator from home page, match empty state spacing * Normalize translations for home page redesign (#4986) * normalize translations * update translations with DMR * accidentally changed es translation * normalize translations for main-page.greeting * update translations with DMR --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> * update translations * create new workspace in native language Cleanup workspace page from empty state handling * update quick action show logic * fix send button --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
250 lines
7.5 KiB
JavaScript
250 lines
7.5 KiB
JavaScript
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,
|
|
isVirtualThread = false,
|
|
}) {
|
|
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);
|
|
}
|
|
|
|
function getActiveThreadIdx() {
|
|
if (isVirtualThread) return threads.length + 1;
|
|
const idx = threads.findIndex((t) => t?.slug === threadSlug);
|
|
return idx >= 0 ? idx + 1 : 0;
|
|
}
|
|
|
|
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 = getActiveThreadIdx();
|
|
|
|
return (
|
|
<div className="flex flex-col" role="list" aria-label="Threads">
|
|
<ThreadItem
|
|
idx={0}
|
|
activeIdx={activeThreadIdx}
|
|
isActive={activeThreadIdx === 0}
|
|
workspace={workspace}
|
|
thread={{ slug: null, name: "default" }}
|
|
hasNext={threads.length > 0 || isVirtualThread}
|
|
/>
|
|
{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 || isVirtualThread}
|
|
/>
|
|
))}
|
|
{isVirtualThread && (
|
|
<ThreadItem
|
|
idx={activeThreadIdx}
|
|
activeIdx={activeThreadIdx}
|
|
isActive={true}
|
|
workspace={workspace}
|
|
thread={{ slug: null, name: "*New Thread", virtual: true }}
|
|
hasNext={false}
|
|
/>
|
|
)}
|
|
<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)] light:hover:bg-slate-300 hover:light:bg-theme-sidebar-subitem-hover rounded-lg"
|
|
>
|
|
<div className="flex w-full gap-x-2 items-center pl-4">
|
|
<div className="bg-zinc-800 light:bg-slate-50 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 font-semibold">
|
|
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>
|
|
);
|
|
}
|