| 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> | |
| ); | |
| } | |