|
import { useDispatch, useSelector } from "react-redux";
|
|
import React from "react";
|
|
import posthog from "posthog-js";
|
|
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
|
import { FeedbackActions } from "../feedback/feedback-actions";
|
|
import { createChatMessage } from "#/services/chat-service";
|
|
import { InteractiveChatBox } from "./interactive-chat-box";
|
|
import { addUserMessage } from "#/state/chat-slice";
|
|
import { RootState } from "#/store";
|
|
import { AgentState } from "#/types/agent-state";
|
|
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
|
import { FeedbackModal } from "../feedback/feedback-modal";
|
|
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
|
import { TypingIndicator } from "./typing-indicator";
|
|
import { useWsClient } from "#/context/ws-client-provider";
|
|
import { Messages } from "./messages";
|
|
import { ChatSuggestions } from "./chat-suggestions";
|
|
import { ActionSuggestions } from "./action-suggestions";
|
|
import { ContinueButton } from "#/components/shared/buttons/continue-button";
|
|
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
|
|
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
|
|
|
function getEntryPoint(
|
|
hasRepository: boolean | null,
|
|
hasImportedProjectZip: boolean | null,
|
|
): string {
|
|
if (hasRepository) return "github";
|
|
if (hasImportedProjectZip) return "zip";
|
|
return "direct";
|
|
}
|
|
|
|
export function ChatInterface() {
|
|
const { send, isLoadingMessages } = useWsClient();
|
|
const dispatch = useDispatch();
|
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
|
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
|
useScrollToBottom(scrollRef);
|
|
|
|
const { messages } = useSelector((state: RootState) => state.chat);
|
|
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
|
|
|
const [feedbackPolarity, setFeedbackPolarity] = React.useState<
|
|
"positive" | "negative"
|
|
>("positive");
|
|
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
|
|
const [messageToSend, setMessageToSend] = React.useState<string | null>(null);
|
|
const { selectedRepository, importedProjectZip } = useSelector(
|
|
(state: RootState) => state.initialQuery,
|
|
);
|
|
|
|
const handleSendMessage = async (content: string, files: File[]) => {
|
|
if (messages.length === 0) {
|
|
posthog.capture("initial_query_submitted", {
|
|
entry_point: getEntryPoint(
|
|
selectedRepository !== null,
|
|
importedProjectZip !== null,
|
|
),
|
|
query_character_length: content.length,
|
|
uploaded_zip_size: importedProjectZip?.length,
|
|
});
|
|
} else {
|
|
posthog.capture("user_message_sent", {
|
|
session_message_count: messages.length,
|
|
current_message_length: content.length,
|
|
});
|
|
}
|
|
const promises = files.map((file) => convertImageToBase64(file));
|
|
const imageUrls = await Promise.all(promises);
|
|
|
|
const timestamp = new Date().toISOString();
|
|
const pending = true;
|
|
dispatch(addUserMessage({ content, imageUrls, timestamp, pending }));
|
|
send(createChatMessage(content, imageUrls, timestamp));
|
|
setMessageToSend(null);
|
|
};
|
|
|
|
const handleStop = () => {
|
|
posthog.capture("stop_button_clicked");
|
|
send(generateAgentStateChangeEvent(AgentState.STOPPED));
|
|
};
|
|
|
|
const handleSendContinueMsg = () => {
|
|
handleSendMessage("Continue", []);
|
|
};
|
|
|
|
const onClickShareFeedbackActionButton = async (
|
|
polarity: "positive" | "negative",
|
|
) => {
|
|
setFeedbackModalIsOpen(true);
|
|
setFeedbackPolarity(polarity);
|
|
};
|
|
|
|
const isWaitingForUserInput =
|
|
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
|
curAgentState === AgentState.FINISHED;
|
|
|
|
return (
|
|
<div className="h-full flex flex-col justify-between">
|
|
{messages.length === 0 && (
|
|
<ChatSuggestions onSuggestionsClick={setMessageToSend} />
|
|
)}
|
|
|
|
<div
|
|
ref={scrollRef}
|
|
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
|
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
|
|
>
|
|
{isLoadingMessages && (
|
|
<div className="flex justify-center">
|
|
<LoadingSpinner size="small" />
|
|
</div>
|
|
)}
|
|
|
|
{!isLoadingMessages && (
|
|
<Messages
|
|
messages={messages}
|
|
isAwaitingUserConfirmation={
|
|
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{isWaitingForUserInput && (
|
|
<ActionSuggestions
|
|
onSuggestionsClick={(value) => handleSendMessage(value, [])}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-[6px] px-4 pb-4">
|
|
<div className="flex justify-between relative">
|
|
<FeedbackActions
|
|
onPositiveFeedback={() =>
|
|
onClickShareFeedbackActionButton("positive")
|
|
}
|
|
onNegativeFeedback={() =>
|
|
onClickShareFeedbackActionButton("negative")
|
|
}
|
|
/>
|
|
|
|
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0">
|
|
{messages.length > 2 &&
|
|
curAgentState === AgentState.AWAITING_USER_INPUT && (
|
|
<ContinueButton onClick={handleSendContinueMsg} />
|
|
)}
|
|
{curAgentState === AgentState.RUNNING && <TypingIndicator />}
|
|
</div>
|
|
|
|
{!hitBottom && <ScrollToBottomButton onClick={scrollDomToBottom} />}
|
|
</div>
|
|
|
|
<InteractiveChatBox
|
|
onSubmit={handleSendMessage}
|
|
onStop={handleStop}
|
|
isDisabled={
|
|
curAgentState === AgentState.LOADING ||
|
|
curAgentState === AgentState.AWAITING_USER_CONFIRMATION ||
|
|
curAgentState === AgentState.RATE_LIMITED
|
|
}
|
|
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
|
|
value={messageToSend ?? undefined}
|
|
onChange={setMessageToSend}
|
|
/>
|
|
</div>
|
|
|
|
<FeedbackModal
|
|
isOpen={feedbackModalIsOpen}
|
|
onClose={() => setFeedbackModalIsOpen(false)}
|
|
polarity={feedbackPolarity}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|