gaojintao01
Add files using Git LFS
f8b5d42
import { useState, useEffect, useContext } 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 { 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";
export default function ChatContainer({ workspace, knownHistory = [] }) {
const { threadSlug = null } = useParams();
const [message, setMessage] = useState("");
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);
// Maintain state of message from whatever is in PromptInput
const handleMessageChange = (event) => {
setMessage(event.target.value);
};
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") {
if (writeMode === "append") setMessage((prev) => prev + messageContent);
else setMessage(messageContent ?? "");
// Push the update to the PromptInput component (same logic as above to keep in sync)
window.dispatchEvent(
new CustomEvent(PROMPT_INPUT_EVENT, {
detail: { messageContent, writeMode },
})
);
}
const handleSubmit = async (event) => {
event.preventDefault();
if (!message || message === "") return false;
const prevChatHistory = [
...chatHistory,
{
content: message,
role: "user",
attachments: parseAttachments(),
},
{
content: "",
role: "assistant",
pending: true,
userMessage: message,
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,
animate: true,
},
];
}
setChatHistory(prevChatHistory);
setMessageEmit("");
setLoadingResponse(true);
};
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}`
);
window.addEventListener(ABORT_STREAM_EVENT, () => {
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
websocket.close();
});
socket.addEventListener("message", (event) => {
setLoadingResponse(true);
try {
handleSocketResponse(event, setChatHistory);
} catch (e) {
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]);
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>
<MetricsProvider>
<ChatHistory
history={chatHistory}
workspace={workspace}
sendCommand={sendCommand}
updateHistory={setChatHistory}
regenerateAssistantMessage={regenerateAssistantMessage}
hasAttachments={files.length > 0}
/>
</MetricsProvider>
<PromptInput
submit={handleSubmit}
onChange={handleMessageChange}
isStreaming={loadingResponse}
sendCommand={sendCommand}
attachments={files}
/>
</DnDFileUploaderWrapper>
<ChatTooltips />
</div>
);
}