event-handler.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import React from "react";
  2. import {
  3. useFetcher,
  4. useLoaderData,
  5. useRouteLoaderData,
  6. } from "@remix-run/react";
  7. import { useDispatch, useSelector } from "react-redux";
  8. import toast from "react-hot-toast";
  9. import posthog from "posthog-js";
  10. import {
  11. useWsClient,
  12. WsClientProviderStatus,
  13. } from "#/context/ws-client-provider";
  14. import { ErrorObservation } from "#/types/core/observations";
  15. import { addErrorMessage, addUserMessage } from "#/state/chatSlice";
  16. import { handleAssistantMessage } from "#/services/actions";
  17. import {
  18. getCloneRepoCommand,
  19. getGitHubTokenCommand,
  20. } from "#/services/terminalService";
  21. import {
  22. clearFiles,
  23. clearSelectedRepository,
  24. setImportedProjectZip,
  25. } from "#/state/initial-query-slice";
  26. import { clientLoader as appClientLoader } from "#/routes/_oh.app";
  27. import store, { RootState } from "#/store";
  28. import { createChatMessage } from "#/services/chatService";
  29. import { clientLoader as rootClientLoader } from "#/routes/_oh";
  30. import { isGitHubErrorReponse } from "#/api/github";
  31. import OpenHands from "#/api/open-hands";
  32. import { base64ToBlob } from "#/utils/base64-to-blob";
  33. import { setCurrentAgentState } from "#/state/agentSlice";
  34. import AgentState from "#/types/AgentState";
  35. import { getSettings } from "#/services/settings";
  36. import { generateAgentStateChangeEvent } from "#/services/agentStateService";
  37. interface ServerError {
  38. error: boolean | string;
  39. message: string;
  40. [key: string]: unknown;
  41. }
  42. const isServerError = (data: object): data is ServerError => "error" in data;
  43. const isErrorObservation = (data: object): data is ErrorObservation =>
  44. "observation" in data && data.observation === "error";
  45. export function EventHandler({ children }: React.PropsWithChildren) {
  46. const { events, status, send } = useWsClient();
  47. const statusRef = React.useRef<WsClientProviderStatus | null>(null);
  48. const runtimeActive = status === WsClientProviderStatus.ACTIVE;
  49. const fetcher = useFetcher();
  50. const dispatch = useDispatch();
  51. const { files, importedProjectZip } = useSelector(
  52. (state: RootState) => state.initalQuery,
  53. );
  54. const { ghToken, repo } = useLoaderData<typeof appClientLoader>();
  55. const initialQueryRef = React.useRef<string | null>(
  56. store.getState().initalQuery.initialQuery,
  57. );
  58. const sendInitialQuery = (query: string, base64Files: string[]) => {
  59. const timestamp = new Date().toISOString();
  60. send(createChatMessage(query, base64Files, timestamp));
  61. };
  62. const data = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
  63. const userId = React.useMemo(() => {
  64. if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id;
  65. return null;
  66. }, [data?.user]);
  67. const userSettings = getSettings();
  68. React.useEffect(() => {
  69. if (!events.length) {
  70. return;
  71. }
  72. const event = events[events.length - 1];
  73. if (event.token) {
  74. fetcher.submit({ token: event.token as string }, { method: "post" });
  75. return;
  76. }
  77. if (isServerError(event)) {
  78. if (event.error_code === 401) {
  79. toast.error("Session expired.");
  80. fetcher.submit({}, { method: "POST", action: "/end-session" });
  81. return;
  82. }
  83. if (typeof event.error === "string") {
  84. toast.error(event.error);
  85. } else {
  86. toast.error(event.message);
  87. }
  88. return;
  89. }
  90. if (event.type === "error") {
  91. const message: string = `${event.message}`;
  92. if (message.startsWith("Agent reached maximum")) {
  93. // We set the agent state to paused here - if the user clicks resume, it auto updates the max iterations
  94. send(generateAgentStateChangeEvent(AgentState.PAUSED));
  95. }
  96. }
  97. if (isErrorObservation(event)) {
  98. dispatch(
  99. addErrorMessage({
  100. id: event.extras?.error_id,
  101. message: event.message,
  102. }),
  103. );
  104. return;
  105. }
  106. handleAssistantMessage(event);
  107. }, [events.length]);
  108. React.useEffect(() => {
  109. if (statusRef.current === status) {
  110. return; // This is a check because of strict mode - if the status did not change, don't do anything
  111. }
  112. statusRef.current = status;
  113. const initialQuery = initialQueryRef.current;
  114. if (status === WsClientProviderStatus.ACTIVE) {
  115. let additionalInfo = "";
  116. if (ghToken && repo) {
  117. send(getCloneRepoCommand(ghToken, repo));
  118. additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`;
  119. dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'?
  120. }
  121. // if there's an uploaded project zip, add it to the chat
  122. else if (importedProjectZip) {
  123. additionalInfo = `Files have been uploaded. Please check the /workspace for files.`;
  124. }
  125. if (initialQuery) {
  126. if (additionalInfo) {
  127. sendInitialQuery(`${initialQuery}\n\n[${additionalInfo}]`, files);
  128. } else {
  129. sendInitialQuery(initialQuery, files);
  130. }
  131. dispatch(clearFiles()); // reset selected files
  132. initialQueryRef.current = null;
  133. }
  134. }
  135. if (status === WsClientProviderStatus.OPENING && initialQuery) {
  136. dispatch(
  137. addUserMessage({
  138. content: initialQuery,
  139. imageUrls: files,
  140. timestamp: new Date().toISOString(),
  141. }),
  142. );
  143. }
  144. if (status === WsClientProviderStatus.STOPPED) {
  145. store.dispatch(setCurrentAgentState(AgentState.STOPPED));
  146. }
  147. }, [status]);
  148. React.useEffect(() => {
  149. if (runtimeActive && userId && ghToken) {
  150. // Export if the user valid, this could happen mid-session so it is handled here
  151. send(getGitHubTokenCommand(ghToken));
  152. }
  153. }, [userId, ghToken, runtimeActive]);
  154. React.useEffect(() => {
  155. (async () => {
  156. if (runtimeActive && importedProjectZip) {
  157. // upload files action
  158. try {
  159. const blob = base64ToBlob(importedProjectZip);
  160. const file = new File([blob], "imported-project.zip", {
  161. type: blob.type,
  162. });
  163. await OpenHands.uploadFiles([file]);
  164. dispatch(setImportedProjectZip(null));
  165. } catch (error) {
  166. toast.error("Failed to upload project files.");
  167. }
  168. }
  169. })();
  170. }, [runtimeActive, importedProjectZip]);
  171. React.useEffect(() => {
  172. if (userSettings.LLM_API_KEY) {
  173. posthog.capture("user_activated");
  174. }
  175. }, [userSettings.LLM_API_KEY]);
  176. return children;
  177. }