- Need to support documents in agents
- Need to support images in agent mode
This reverts commit 4c69960dca.
404 lines
13 KiB
JavaScript
404 lines
13 KiB
JavaScript
import { useState, useEffect, useContext, useRef } from "react";
|
|
import ChatHistory from "./ChatHistory";
|
|
import { CLEAR_ATTACHMENTS_EVENT, DndUploaderContext } from "./DnDWrapper";
|
|
import PromptInput, {
|
|
PROMPT_INPUT_EVENT,
|
|
PROMPT_INPUT_ID,
|
|
} from "./PromptInput";
|
|
import Workspace from "@/models/workspace";
|
|
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
|
|
import { isMobile } from "react-device-detect";
|
|
import { SidebarMobileHeader } from "../../Sidebar";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { v4 } from "uuid";
|
|
import handleSocketResponse, {
|
|
websocketURI,
|
|
AGENT_SESSION_END,
|
|
AGENT_SESSION_START,
|
|
} from "@/utils/chat/agent";
|
|
import DnDFileUploaderWrapper from "./DnDWrapper";
|
|
import SpeechRecognition, {
|
|
useSpeechRecognition,
|
|
} from "react-speech-recognition";
|
|
import { ChatTooltips } from "./ChatTooltips";
|
|
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
|
|
import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll";
|
|
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
|
|
import { safeJsonParse } from "@/utils/request";
|
|
import { useTranslation } from "react-i18next";
|
|
import paths from "@/utils/paths";
|
|
import QuickActions from "@/components/lib/QuickActions";
|
|
import SuggestedMessages from "@/components/lib/SuggestedMessages";
|
|
|
|
export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|
const navigate = useNavigate();
|
|
const { t } = useTranslation();
|
|
const { threadSlug = null } = useParams();
|
|
const [loadingResponse, setLoadingResponse] = useState(false);
|
|
const [chatHistory, setChatHistory] = useState(knownHistory);
|
|
const [socketId, setSocketId] = useState(null);
|
|
const [websocket, setWebsocket] = useState(null);
|
|
const { files, parseAttachments } = useContext(DndUploaderContext);
|
|
const { chatHistoryRef } = useChatContainerQuickScroll();
|
|
const pendingMessageChecked = useRef(false);
|
|
|
|
const { listening, resetTranscript } = useSpeechRecognition({
|
|
clearTranscriptOnListen: true,
|
|
});
|
|
|
|
/**
|
|
* Emit an update to the state of the prompt input without directly
|
|
* passing a prop in so that it does not re-render constantly.
|
|
* @param {string} messageContent - The message content to set
|
|
* @param {'replace' | 'append'} writeMode - Replace current text or append to existing text (default: replace)
|
|
*/
|
|
function setMessageEmit(messageContent = "", writeMode = "replace") {
|
|
window.dispatchEvent(
|
|
new CustomEvent(PROMPT_INPUT_EVENT, {
|
|
detail: { messageContent, writeMode },
|
|
})
|
|
);
|
|
}
|
|
|
|
const handleSubmit = async (event) => {
|
|
event.preventDefault();
|
|
const currentMessage =
|
|
document.getElementById(PROMPT_INPUT_ID)?.value || "";
|
|
if (!currentMessage) return false;
|
|
|
|
const prevChatHistory = [
|
|
...chatHistory,
|
|
{
|
|
content: currentMessage,
|
|
role: "user",
|
|
attachments: parseAttachments(),
|
|
},
|
|
{
|
|
content: "",
|
|
role: "assistant",
|
|
pending: true,
|
|
userMessage: currentMessage,
|
|
animate: true,
|
|
},
|
|
];
|
|
|
|
if (listening) {
|
|
// Stop the mic if the send button is clicked
|
|
endSTTSession();
|
|
}
|
|
setChatHistory(prevChatHistory);
|
|
setMessageEmit("");
|
|
setLoadingResponse(true);
|
|
};
|
|
|
|
function endSTTSession() {
|
|
SpeechRecognition.stopListening();
|
|
resetTranscript();
|
|
}
|
|
|
|
const regenerateAssistantMessage = (chatId) => {
|
|
const updatedHistory = chatHistory.slice(0, -1);
|
|
const lastUserMessage = updatedHistory.slice(-1)[0];
|
|
Workspace.deleteChats(workspace.slug, [chatId])
|
|
.then(() =>
|
|
sendCommand({
|
|
text: lastUserMessage.content,
|
|
autoSubmit: true,
|
|
history: updatedHistory,
|
|
attachments: lastUserMessage?.attachments,
|
|
})
|
|
)
|
|
.catch((e) => console.error(e));
|
|
};
|
|
|
|
/**
|
|
* Send a command to the LLM prompt input.
|
|
* @param {Object} options - Arguments to send to the LLM
|
|
* @param {string} options.text - The text to send to the LLM
|
|
* @param {boolean} options.autoSubmit - Determines if the text should be sent immediately or if it should be added to the message state (default: false)
|
|
* @param {Object[]} options.history - The history of the chat prior to this message for overriding the current chat history
|
|
* @param {Object[import("./DnDWrapper").Attachment]} options.attachments - The attachments to send to the LLM for this message
|
|
* @param {'replace' | 'append'} options.writeMode - Replace current text or append to existing text (default: replace)
|
|
* @returns {void}
|
|
*/
|
|
const sendCommand = async ({
|
|
text = "",
|
|
autoSubmit = false,
|
|
history = [],
|
|
attachments = [],
|
|
writeMode = "replace",
|
|
} = {}) => {
|
|
// If we are not auto-submitting, we can just emit the text to the prompt input.
|
|
if (!autoSubmit) {
|
|
setMessageEmit(text, writeMode);
|
|
return;
|
|
}
|
|
|
|
// If we are auto-submitting in append mode
|
|
// than we need to update text with whatever is in the prompt input + the text we are sending.
|
|
// @note: `message` will not work here since it is not updated yet.
|
|
// If text is still empty, after this, then we should just return.
|
|
if (writeMode === "append") {
|
|
const currentText = document.getElementById(PROMPT_INPUT_ID)?.value ?? "";
|
|
text = currentText + text;
|
|
}
|
|
|
|
if (!text || text === "") return false;
|
|
// If we are auto-submitting
|
|
// Then we can replace the current text since this is not accumulating.
|
|
let prevChatHistory;
|
|
if (history.length > 0) {
|
|
// use pre-determined history chain.
|
|
prevChatHistory = [
|
|
...history,
|
|
{
|
|
content: "",
|
|
role: "assistant",
|
|
pending: true,
|
|
userMessage: text,
|
|
attachments,
|
|
animate: true,
|
|
},
|
|
];
|
|
} else {
|
|
prevChatHistory = [
|
|
...chatHistory,
|
|
{
|
|
content: text,
|
|
role: "user",
|
|
attachments,
|
|
},
|
|
{
|
|
content: "",
|
|
role: "assistant",
|
|
pending: true,
|
|
userMessage: text,
|
|
attachments,
|
|
animate: true,
|
|
},
|
|
];
|
|
}
|
|
|
|
setChatHistory(prevChatHistory);
|
|
setMessageEmit("");
|
|
setLoadingResponse(true);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (pendingMessageChecked.current || !workspace?.slug) return;
|
|
pendingMessageChecked.current = true;
|
|
|
|
const pending = safeJsonParse(sessionStorage.getItem(PENDING_HOME_MESSAGE));
|
|
if (pending?.message) {
|
|
setTimeout(() => {
|
|
sessionStorage.removeItem(PENDING_HOME_MESSAGE);
|
|
sendCommand({
|
|
text: pending.message,
|
|
attachments: pending.attachments || [],
|
|
autoSubmit: true,
|
|
});
|
|
}, 100);
|
|
}
|
|
}, [workspace?.slug]);
|
|
|
|
useEffect(() => {
|
|
async function fetchReply() {
|
|
const promptMessage =
|
|
chatHistory.length > 0 ? chatHistory[chatHistory.length - 1] : null;
|
|
const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : [];
|
|
var _chatHistory = [...remHistory];
|
|
|
|
// Override hook for new messages to now go to agents until the connection closes
|
|
if (!!websocket) {
|
|
if (!promptMessage || !promptMessage?.userMessage) return false;
|
|
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
|
|
websocket.send(
|
|
JSON.stringify({
|
|
type: "awaitingFeedback",
|
|
feedback: promptMessage?.userMessage,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!promptMessage || !promptMessage?.userMessage) return false;
|
|
|
|
// If running and edit or regeneration, this history will already have attachments
|
|
// so no need to parse the current state.
|
|
const attachments = promptMessage?.attachments ?? parseAttachments();
|
|
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
|
|
|
|
await Workspace.multiplexStream({
|
|
workspaceSlug: workspace.slug,
|
|
threadSlug,
|
|
prompt: promptMessage.userMessage,
|
|
chatHandler: (chatResult) =>
|
|
handleChat(
|
|
chatResult,
|
|
setLoadingResponse,
|
|
setChatHistory,
|
|
remHistory,
|
|
_chatHistory,
|
|
setSocketId
|
|
),
|
|
attachments,
|
|
});
|
|
return;
|
|
}
|
|
loadingResponse === true && fetchReply();
|
|
}, [loadingResponse, chatHistory, workspace]);
|
|
|
|
// TODO: Simplify this WSS stuff
|
|
useEffect(() => {
|
|
function handleWSS() {
|
|
try {
|
|
if (!socketId || !!websocket) return;
|
|
const socket = new WebSocket(
|
|
`${websocketURI()}/api/agent-invocation/${socketId}`
|
|
);
|
|
socket.supportsAgentStreaming = false;
|
|
|
|
window.addEventListener(ABORT_STREAM_EVENT, () => {
|
|
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
|
websocket.close();
|
|
});
|
|
|
|
socket.addEventListener("message", (event) => {
|
|
setLoadingResponse(true);
|
|
try {
|
|
handleSocketResponse(socket, event, setChatHistory);
|
|
} catch {
|
|
console.error("Failed to parse data");
|
|
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
|
socket.close();
|
|
}
|
|
setLoadingResponse(false);
|
|
});
|
|
|
|
socket.addEventListener("close", (_event) => {
|
|
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
|
|
setChatHistory((prev) => [
|
|
...prev.filter((msg) => !!msg.content),
|
|
{
|
|
uuid: v4(),
|
|
type: "statusResponse",
|
|
content: "Agent session complete.",
|
|
role: "assistant",
|
|
sources: [],
|
|
closed: true,
|
|
error: null,
|
|
animate: false,
|
|
pending: false,
|
|
},
|
|
]);
|
|
setLoadingResponse(false);
|
|
setWebsocket(null);
|
|
setSocketId(null);
|
|
});
|
|
setWebsocket(socket);
|
|
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
|
|
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
|
|
} catch (e) {
|
|
setChatHistory((prev) => [
|
|
...prev.filter((msg) => !!msg.content),
|
|
{
|
|
uuid: v4(),
|
|
type: "abort",
|
|
content: e.message,
|
|
role: "assistant",
|
|
sources: [],
|
|
closed: true,
|
|
error: e.message,
|
|
animate: false,
|
|
pending: false,
|
|
},
|
|
]);
|
|
setLoadingResponse(false);
|
|
setWebsocket(null);
|
|
setSocketId(null);
|
|
}
|
|
}
|
|
handleWSS();
|
|
}, [socketId]);
|
|
|
|
const isEmpty =
|
|
chatHistory.length === 0 && !sessionStorage.getItem(PENDING_HOME_MESSAGE);
|
|
|
|
if (isEmpty) {
|
|
return (
|
|
<div
|
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
|
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 overflow-hidden"
|
|
>
|
|
{isMobile && <SidebarMobileHeader />}
|
|
<DnDFileUploaderWrapper>
|
|
<div className="flex flex-col h-full w-full items-center justify-center">
|
|
<div className="flex flex-col items-center w-full max-w-[750px]">
|
|
<h1 className="text-white text-xl md:text-2xl mb-11 text-center">
|
|
{t("main-page.greeting")}
|
|
</h1>
|
|
<PromptInput
|
|
submit={handleSubmit}
|
|
isStreaming={loadingResponse}
|
|
sendCommand={sendCommand}
|
|
attachments={files}
|
|
centered={true}
|
|
/>
|
|
<QuickActions
|
|
hasAvailableWorkspace={!!workspace}
|
|
onCreateAgent={() => navigate(paths.settings.agentSkills())}
|
|
onEditWorkspace={() =>
|
|
navigate(
|
|
paths.workspace.settings.generalAppearance(workspace.slug)
|
|
)
|
|
}
|
|
onUploadDocument={() =>
|
|
document.getElementById("dnd-chat-file-uploader")?.click()
|
|
}
|
|
/>
|
|
</div>
|
|
<SuggestedMessages
|
|
suggestedMessages={workspace?.suggestedMessages}
|
|
sendCommand={sendCommand}
|
|
/>
|
|
</div>
|
|
</DnDFileUploaderWrapper>
|
|
<ChatTooltips />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
|
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 overflow-y-scroll no-scroll z-[2]"
|
|
>
|
|
{isMobile && <SidebarMobileHeader />}
|
|
<DnDFileUploaderWrapper>
|
|
<div className="flex flex-col h-full w-full">
|
|
<div className="contents">
|
|
<MetricsProvider>
|
|
<ChatHistory
|
|
ref={chatHistoryRef}
|
|
history={chatHistory}
|
|
workspace={workspace}
|
|
sendCommand={sendCommand}
|
|
updateHistory={setChatHistory}
|
|
regenerateAssistantMessage={regenerateAssistantMessage}
|
|
/>
|
|
</MetricsProvider>
|
|
<PromptInput
|
|
submit={handleSubmit}
|
|
isStreaming={loadingResponse}
|
|
sendCommand={sendCommand}
|
|
attachments={files}
|
|
centered={false}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</DnDFileUploaderWrapper>
|
|
<ChatTooltips />
|
|
</div>
|
|
);
|
|
}
|