import { useSelector } from "react-redux"; import React from "react"; import posthog from "posthog-js"; import { useParams } from "react-router"; import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import { convertImageToBase64 } from "#/utils/convert-image-to-base-64"; import { TrajectoryActions } from "../trajectory/trajectory-actions"; import { createChatMessage } from "#/services/chat-service"; import { InteractiveChatBox } from "./interactive-chat-box"; 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 { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button"; import { LoadingSpinner } from "#/components/shared/loading-spinner"; import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory"; import { downloadTrajectory } from "#/utils/download-trajectory"; import { displayErrorToast } from "#/utils/custom-toast-handlers"; import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message"; import { useWSErrorMessage } from "#/hooks/use-ws-error-message"; import { ErrorMessageBanner } from "./error-message-banner"; import { shouldRenderEvent } from "./event-content-helpers/should-render-event"; function getEntryPoint( hasRepository: boolean | null, hasReplayJson: boolean | null, ): string { if (hasRepository) return "github"; if (hasReplayJson) return "replay"; return "direct"; } export function ChatInterface() { const { getErrorMessage } = useWSErrorMessage(); const { send, isLoadingMessages, parsedEvents } = useWsClient(); const { setOptimisticUserMessage, getOptimisticUserMessage } = useOptimisticUserMessage(); const { t } = useTranslation(); const scrollRef = React.useRef(null); const { scrollDomToBottom, onChatBodyScroll, hitBottom } = useScrollToBottom(scrollRef); 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(null); const { selectedRepository, replayJson } = useSelector( (state: RootState) => state.initialQuery, ); const params = useParams(); const { mutate: getTrajectory } = useGetTrajectory(); const optimisticUserMessage = getOptimisticUserMessage(); const errorMessage = getErrorMessage(); const events = parsedEvents.filter(shouldRenderEvent); const handleSendMessage = async (content: string, files: File[]) => { if (events.length === 0) { posthog.capture("initial_query_submitted", { entry_point: getEntryPoint( selectedRepository !== null, replayJson !== null, ), query_character_length: content.length, replay_json_size: replayJson?.length, }); } else { posthog.capture("user_message_sent", { session_message_count: events.length, current_message_length: content.length, }); } const promises = files.map((file) => convertImageToBase64(file)); const imageUrls = await Promise.all(promises); const timestamp = new Date().toISOString(); send(createChatMessage(content, imageUrls, timestamp)); setOptimisticUserMessage(content); setMessageToSend(null); }; const handleStop = () => { posthog.capture("stop_button_clicked"); send(generateAgentStateChangeEvent(AgentState.STOPPED)); }; const onClickShareFeedbackActionButton = async ( polarity: "positive" | "negative", ) => { setFeedbackModalIsOpen(true); setFeedbackPolarity(polarity); }; const onClickExportTrajectoryButton = () => { if (!params.conversationId) { displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR)); return; } getTrajectory(params.conversationId, { onSuccess: async (data) => { await downloadTrajectory( params.conversationId ?? t(I18nKey.CONVERSATION$UNKNOWN), data.trajectory, ); }, onError: () => { displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR)); }, }); }; const isWaitingForUserInput = curAgentState === AgentState.AWAITING_USER_INPUT || curAgentState === AgentState.FINISHED; return (
{events.length === 0 && !optimisticUserMessage && ( )}
onChatBodyScroll(e.currentTarget)} className="scrollbar scrollbar-thin scrollbar-thumb-gray-400 scrollbar-thumb-rounded-full scrollbar-track-gray-800 hover:scrollbar-thumb-gray-300 flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2 fast-smooth-scroll" > {isLoadingMessages && (
)} {!isLoadingMessages && ( )} {isWaitingForUserInput && events.length > 0 && !optimisticUserMessage && ( handleSendMessage(value, [])} /> )}
onClickShareFeedbackActionButton("positive") } onNegativeFeedback={() => onClickShareFeedbackActionButton("negative") } onExportTrajectory={() => onClickExportTrajectoryButton()} />
{curAgentState === AgentState.RUNNING && }
{!hitBottom && }
{errorMessage && }
setFeedbackModalIsOpen(false)} polarity={feedbackPolarity} />
); }