* fix: white flash when switching between threads * nest thread route under workspace * fix: white flash when switching between workspaces * simplify Link usage in workspace/thread sidebar items * smooth workspace and thread switching * fix race condition on send during thread/workspace switch --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
146 lines
5.1 KiB
JavaScript
146 lines
5.1 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import Workspace from "@/models/workspace";
|
|
import LoadingChat from "./LoadingChat";
|
|
import ChatContainer from "./ChatContainer";
|
|
import paths from "@/utils/paths";
|
|
import ModalWrapper from "../ModalWrapper";
|
|
import { useParams } from "react-router-dom";
|
|
import { DnDFileUploaderProvider } from "./ChatContainer/DnDWrapper";
|
|
import { WarningCircle } from "@phosphor-icons/react";
|
|
import {
|
|
TTSProvider,
|
|
useWatchForAutoPlayAssistantTTSResponse,
|
|
} from "../contexts/TTSProvider";
|
|
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
|
|
|
|
export default function WorkspaceChat({ loading, workspace }) {
|
|
useWatchForAutoPlayAssistantTTSResponse();
|
|
const { threadSlug = null } = useParams();
|
|
// Stores { key, workspace, history } currently rendered. Lags the props so
|
|
// the previous chat stays mounted until the next one's history is ready,
|
|
// avoiding a skeleton/loader flash on workspace/thread switches.
|
|
const [loaded, setLoaded] = useState(null);
|
|
|
|
useEffect(() => {
|
|
async function getHistory() {
|
|
if (loading) return;
|
|
if (!workspace?.slug) {
|
|
setLoaded({ key: "none", workspace: null, history: [] });
|
|
return false;
|
|
}
|
|
|
|
const chatHistory = threadSlug
|
|
? await Workspace.threads.chatHistory(workspace.slug, threadSlug)
|
|
: await Workspace.chatHistory(workspace.slug);
|
|
|
|
setLoaded({
|
|
key: `${workspace.slug}:${threadSlug ?? "default"}`,
|
|
workspace,
|
|
threadSlug,
|
|
history: chatHistory,
|
|
});
|
|
}
|
|
getHistory();
|
|
}, [workspace, loading, threadSlug]);
|
|
|
|
const hasPendingMessage = !!sessionStorage.getItem(PENDING_HOME_MESSAGE);
|
|
if (loaded === null) {
|
|
if (hasPendingMessage) {
|
|
return (
|
|
<div className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full" />
|
|
);
|
|
}
|
|
return <LoadingChat />;
|
|
}
|
|
if (!loading && !workspace) {
|
|
return (
|
|
<>
|
|
{loading === false && !workspace && (
|
|
<ModalWrapper isOpen={true}>
|
|
<div className="w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden">
|
|
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
|
<div className="w-full flex gap-x-2 items-center">
|
|
<WarningCircle
|
|
className="text-red-500 w-6 h-6"
|
|
weight="fill"
|
|
/>
|
|
<h3 className="text-xl font-semibold text-red-500 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
|
Workspace not found
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div className="py-7 px-9 space-y-2 flex-col">
|
|
<p className="text-white text-sm">
|
|
The workspace you're looking for is not available. It may have
|
|
been deleted or you may not have access to it.
|
|
</p>
|
|
</div>
|
|
<div className="flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b">
|
|
<a
|
|
href={paths.home()}
|
|
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
|
>
|
|
Return to homepage
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</ModalWrapper>
|
|
)}
|
|
<LoadingChat />
|
|
</>
|
|
);
|
|
}
|
|
|
|
setEventDelegatorForCodeSnippets();
|
|
return (
|
|
<TTSProvider>
|
|
<DnDFileUploaderProvider
|
|
workspace={loaded.workspace}
|
|
threadSlug={loaded.threadSlug}
|
|
>
|
|
<ChatContainer
|
|
key={loaded.key}
|
|
workspace={loaded.workspace}
|
|
threadSlug={loaded.threadSlug}
|
|
knownHistory={loaded.history}
|
|
/>
|
|
</DnDFileUploaderProvider>
|
|
</TTSProvider>
|
|
);
|
|
}
|
|
|
|
// Enables us to safely markdown and sanitize all responses without risk of injection
|
|
// but still be able to attach a handler to copy code snippets on all elements
|
|
// that are code snippets.
|
|
function copyCodeSnippet(uuid) {
|
|
const target = document.querySelector(`[data-code="${uuid}"]`);
|
|
if (!target) return false;
|
|
const markdown =
|
|
target.parentElement?.parentElement?.querySelector(
|
|
"pre:first-of-type"
|
|
)?.innerText;
|
|
if (!markdown) return false;
|
|
|
|
window.navigator.clipboard.writeText(markdown);
|
|
target.classList.add("text-green-500");
|
|
const originalText = target.innerHTML;
|
|
target.innerText = "Copied!";
|
|
target.setAttribute("disabled", true);
|
|
|
|
setTimeout(() => {
|
|
target.classList.remove("text-green-500");
|
|
target.innerHTML = originalText;
|
|
target.removeAttribute("disabled");
|
|
}, 2500);
|
|
}
|
|
|
|
// Listens and hunts for all data-code-snippet clicks.
|
|
export function setEventDelegatorForCodeSnippets() {
|
|
document?.addEventListener("click", function (e) {
|
|
const target = e.target.closest("[data-code-snippet]");
|
|
const uuidCode = target?.dataset?.code;
|
|
if (!uuidCode) return false;
|
|
copyCodeSnippet(uuidCode);
|
|
});
|
|
}
|