diff --git a/Makefile b/Makefile
index d87ea72296..6c89b04586 100644
--- a/Makefile
+++ b/Makefile
@@ -195,7 +195,7 @@ start-backend:
# Start frontend
start-frontend:
@echo "$(YELLOW)Starting frontend...$(RESET)"
- @cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start
+ @cd frontend && VITE_BACKEND_HOST=$(BACKEND_HOST_PORT) VITE_FRONTEND_PORT=$(FRONTEND_PORT) npm run start -- --port $(FRONTEND_PORT)
# Common setup for running the app (non-callable)
_run_setup:
diff --git a/frontend/.env.sample b/frontend/.env.sample
index 217d8c526a..5178426762 100644
--- a/frontend/.env.sample
+++ b/frontend/.env.sample
@@ -1,7 +1,4 @@
-VITE_BACKEND_HOST="127.0.0.1:3000"
-VITE_USE_TLS="false"
-VITE_INSECURE_SKIP_VERIFY="false"
-VITE_FRONTEND_PORT="3001"
+VITE_BACKEND_BASE_URL="localhost:3000" # Backend URL without protocol (e.g. localhost:3000)
# GitHub OAuth
VITE_GITHUB_CLIENT_ID=""
diff --git a/frontend/README.md b/frontend/README.md
index 5a9a5b9771..7d05e8a9ba 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1,63 +1,123 @@
# Getting Started with the OpenHands Frontend
-The frontend code can be run against the docker image defined in the [Main README](../README.md) as a backend
+## Overview
-## Prerequisites
+This is the frontend of the OpenHands project. It is a React application that provides a web interface for the OpenHands project.
-A recent version of NodeJS / NPM (`brew install node`)
+## Tech Stack
-## Available Scripts
+- Remix SPA Mode (React + Vite + React Router)
+- TypeScript
+- Redux
+- Tailwind CSS
+- i18next
+- React Testing Library
+- Vitest
+- Mock Service Worker
-In the project directory, you can run:
+## Getting Started
-### `npm run start -- --port 3001`
+### Prerequisites
-Runs the app in development mode.\
-Open [http://localhost:3001](http://localhost:3001) to view it in the browser.
+- Node.js 20.x or later
+- `npm`, `bun`, or any other package manager that supports the `package.json` file
-The page will reload if you make edits.\
-You will also see any lint errors in the console.
+### Installation
-### `npm run make-i18n`
+```sh
+# Clone the repository
+git clone https://github.com/All-Hands-AI/OpenHands.git
-Generates the i18n declaration file.\
-Run this when first setting up the repository or when updating translations.
+# Change the directory to the frontend
+cd OpenHands/frontend
-### `npm run test`
-
-Runs the available test suites for the application.\
-It launches the test runner in interactive watch mode, allowing you to see the results of your tests in real time.
-
-In order to skip all but one specific test file, like the one for the ChatInterface, the following command might be used: `npm run test -- -t "ChatInterface"`
-
-### `npm run build`
-
-Builds the app for production to the `dist` folder.\
-It correctly bundles React in production mode and optimizes the build for the best performance.
-
-The build is minified and the filenames include the hashes.\
-Your app is ready to be deployed!
-
-## Environment Variables
-
-You can set the environment variables in `frontend/.env` to configure the frontend.
-The following variables are available:
-
-```javascript
-VITE_BACKEND_HOST="127.0.0.1:3000" // The host of the backend
-VITE_USE_TLS="false" // Whether to use TLS for the backend (includes HTTPS and WSS)
-VITE_INSECURE_SKIP_VERIFY="false" // Whether to skip verifying the backend's certificate. Only takes effect if `VITE_USE_TLS` is true. Don't use this in production!
-VITE_FRONTEND_PORT="3001" // The port of the frontend
+# Install the dependencies
+npm install
```
-You can also set the environment variables from outside the project, like `export VITE_BACKEND_HOST="127.0.0.1:3000"`.
+### Running the Application in Development Mode
-The outside environment variables will override the ones in the `.env` file.
+We use `msw` to mock the backend API. To start the application with the mocked backend, run the following command:
-## Learn More
+```sh
+npm run dev
+```
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+This will start the application in development mode. Open [http://localhost:3001](http://localhost:3001) to view it in the browser.
-To learn React, check out the [React documentation](https://reactjs.org/).
+**NOTE: The backend is _partially_ mocked using `msw`. Therefore, some features may not work as they would with the actual backend.**
-For more information on tests, you can refer to the official documentation of [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/).
+### Running the Application with the Actual Backend (Production Mode)
+
+There are two ways to run the application with the actual backend:
+
+```sh
+# Build the application from the root directory
+make build
+
+# Start the application
+make start
+```
+
+OR
+
+```sh
+# Start the backend from the root directory
+make start-backend
+
+# Build the frontend
+cd frontend && npm run build
+
+# Serve the frontend
+npm start -- --port 3001
+```
+
+### Environment Variables
+
+TODO
+
+### Project Structure
+
+```sh
+frontend
+├── __tests__ # Tests
+├── public
+├── src
+│ ├── api # API calls
+│ ├── assets
+│ ├── components # Reusable components
+│ ├── context # Local state management
+│ ├── hooks # Custom hooks
+│ ├── i18n # Internationalization
+│ ├── mocks # MSW mocks for development
+│ ├── routes # React Router file-based routes
+│ ├── services
+│ ├── state # Redux state management
+│ ├── types
+│ ├── utils # Utility/helper functions
+│ └── root.tsx # Entry point
+└── .env.sample # Sample environment variables
+```
+
+### Features
+
+- Real-time updates with WebSockets
+- Internationalization
+- Router data loading with Remix
+- User authentication with GitHub OAuth (if saas mode is enabled)
+
+## Testing
+
+We use `Vitest` for testing. To run the tests, run the following command:
+
+```sh
+npm run test
+```
+
+## Contributing
+
+Please read the [CONTRIBUTING.md](../CONTRIBUTING.md) file for details on our code of conduct, and the process for submitting pull requests to us.
+
+## Troubleshooting
+
+TODO
diff --git a/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx b/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx
index 2d9a5f166b..fcdd05a423 100644
--- a/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx
+++ b/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx
@@ -2,24 +2,16 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { describe, it, expect, vi, Mock, afterEach } from "vitest";
-import { uploadFiles, listFiles } from "#/services/fileService";
import toast from "#/utils/toast";
import AgentState from "#/types/AgentState";
import FileExplorer from "#/components/file-explorer/FileExplorer";
+import OpenHands from "#/api/open-hands";
const toastSpy = vi.spyOn(toast, "error");
+const uploadFilesSpy = vi.spyOn(OpenHands, "uploadFiles");
+const getFilesSpy = vi.spyOn(OpenHands, "getFiles");
vi.mock("../../services/fileService", async () => ({
- listFiles: vi.fn(async (path: string = "/") => {
- if (path === "/") {
- return Promise.resolve(["folder1/", "file1.ts"]);
- }
- if (path === "/folder1/" || path === "folder1/") {
- return Promise.resolve(["file2.ts"]);
- }
- return Promise.resolve([]);
- }),
-
uploadFiles: vi.fn(),
}));
@@ -42,7 +34,7 @@ describe.skip("FileExplorer", () => {
expect(await screen.findByText("folder1")).toBeInTheDocument();
expect(await screen.findByText("file1.ts")).toBeInTheDocument();
- expect(listFiles).toHaveBeenCalledTimes(1); // once for root
+ expect(getFilesSpy).toHaveBeenCalledTimes(1); // once for root
});
it.todo("should render an empty workspace");
@@ -53,12 +45,12 @@ describe.skip("FileExplorer", () => {
expect(await screen.findByText("folder1")).toBeInTheDocument();
expect(await screen.findByText("file1.ts")).toBeInTheDocument();
- expect(listFiles).toHaveBeenCalledTimes(1); // once for root
+ expect(getFilesSpy).toHaveBeenCalledTimes(1); // once for root
const refreshButton = screen.getByTestId("refresh");
await user.click(refreshButton);
- expect(listFiles).toHaveBeenCalledTimes(2); // once for root, once for refresh button
+ expect(getFilesSpy).toHaveBeenCalledTimes(2); // once for root, once for refresh button
});
it("should toggle the explorer visibility when clicking the toggle button", async () => {
@@ -84,15 +76,15 @@ describe.skip("FileExplorer", () => {
await user.upload(uploadFileInput, file);
// TODO: Improve this test by passing expected argument to `uploadFiles`
- expect(uploadFiles).toHaveBeenCalledOnce();
- expect(listFiles).toHaveBeenCalled();
+ expect(uploadFilesSpy).toHaveBeenCalledOnce();
+ expect(getFilesSpy).toHaveBeenCalled();
const file2 = new File([""], "file-name-2");
const uploadDirInput = await screen.findByTestId("file-input");
await user.upload(uploadDirInput, [file, file2]);
- expect(uploadFiles).toHaveBeenCalledTimes(2);
- expect(listFiles).toHaveBeenCalled();
+ expect(uploadFilesSpy).toHaveBeenCalledTimes(2);
+ expect(getFilesSpy).toHaveBeenCalled();
});
it.todo("should upload files when dragging them to the explorer", () => {
@@ -104,7 +96,7 @@ describe.skip("FileExplorer", () => {
it.todo("should download a file");
it("should display an error toast if file upload fails", async () => {
- (uploadFiles as Mock).mockRejectedValue(new Error());
+ (uploadFilesSpy as Mock).mockRejectedValue(new Error());
const user = userEvent.setup();
renderFileExplorerWithRunningAgentState();
@@ -113,7 +105,7 @@ describe.skip("FileExplorer", () => {
await user.upload(uploadFileInput, file);
- expect(uploadFiles).rejects.toThrow();
+ expect(uploadFilesSpy).rejects.toThrow();
expect(toastSpy).toHaveBeenCalledWith(
expect.stringContaining("upload-error"),
expect.any(String),
diff --git a/frontend/__tests__/components/file-explorer/TreeNode.test.tsx b/frontend/__tests__/components/file-explorer/TreeNode.test.tsx
index 2c465fbf80..42919b3660 100644
--- a/frontend/__tests__/components/file-explorer/TreeNode.test.tsx
+++ b/frontend/__tests__/components/file-explorer/TreeNode.test.tsx
@@ -2,20 +2,13 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { vi, describe, afterEach, it, expect } from "vitest";
-import { selectFile, listFiles } from "#/services/fileService";
import TreeNode from "#/components/file-explorer/TreeNode";
+import OpenHands from "#/api/open-hands";
+
+const getFileSpy = vi.spyOn(OpenHands, "getFile");
+const getFilesSpy = vi.spyOn(OpenHands, "getFiles");
vi.mock("../../services/fileService", async () => ({
- listFiles: vi.fn(async (path: string = "/") => {
- if (path === "/") {
- return Promise.resolve(["folder1/", "file1.ts"]);
- }
- if (path === "/folder1/" || path === "folder1/") {
- return Promise.resolve(["file2.ts"]);
- }
- return Promise.resolve([]);
- }),
- selectFile: vi.fn(async () => Promise.resolve({ code: "Hello world!" })),
uploadFile: vi.fn(),
}));
@@ -31,7 +24,7 @@ describe.skip("TreeNode", () => {
it("should render a folder if it's in a subdir", async () => {
renderWithProviders();
- expect(listFiles).toHaveBeenCalledWith("/folder1/");
+ expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
expect(await screen.findByText("folder1")).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
@@ -63,27 +56,27 @@ describe.skip("TreeNode", () => {
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
await user.click(folder1);
- expect(listFiles).toHaveBeenCalledWith("/folder1/");
+ expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
expect(folder1).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
});
- it("should call `selectFile` and return the full path of a file when clicking on a file", async () => {
+ it("should call `OpenHands.getFile` and return the full path of a file when clicking on a file", async () => {
const user = userEvent.setup();
renderWithProviders();
const file2 = screen.getByText("file2.ts");
await user.click(file2);
- expect(selectFile).toHaveBeenCalledWith("/folder1/file2.ts");
+ expect(getFileSpy).toHaveBeenCalledWith("/folder1/file2.ts");
});
it("should render the full explorer given the defaultOpen prop", async () => {
const user = userEvent.setup();
renderWithProviders();
- expect(listFiles).toHaveBeenCalledWith("/");
+ expect(getFilesSpy).toHaveBeenCalledWith("/");
const file1 = await screen.findByText("file1.ts");
const folder1 = await screen.findByText("folder1");
@@ -93,7 +86,7 @@ describe.skip("TreeNode", () => {
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
await user.click(folder1);
- expect(listFiles).toHaveBeenCalledWith("folder1/");
+ expect(getFilesSpy).toHaveBeenCalledWith("folder1/");
expect(file1).toBeInTheDocument();
expect(folder1).toBeInTheDocument();
@@ -109,7 +102,7 @@ describe.skip("TreeNode", () => {
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
await userEvent.click(folder1);
- expect(listFiles).toHaveBeenCalledWith("/folder1/");
+ expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
expect(folder1).toBeInTheDocument();
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts
index 8788ec1d15..c881b70a38 100644
--- a/frontend/src/api/open-hands.ts
+++ b/frontend/src/api/open-hands.ts
@@ -1,36 +1,20 @@
-interface ErrorResponse {
- error: string;
-}
+import {
+ SaveFileSuccessResponse,
+ FileUploadSuccessResponse,
+ Feedback,
+ FeedbackResponse,
+ GitHubAccessTokenResponse,
+ ErrorResponse,
+} from "./open-hands.types";
-interface SaveFileSuccessResponse {
- message: string;
-}
-
-interface FileUploadSuccessResponse {
- message: string;
- uploaded_files: string[];
- skipped_files: { name: string; reason: string }[];
-}
-
-interface FeedbackBodyResponse {
- message: string;
- feedback_id: string;
- password: string;
-}
-
-interface FeedbackResponse {
- statusCode: number;
- body: FeedbackBodyResponse;
-}
-
-export interface Feedback {
- version: string;
- email: string;
- token: string;
- feedback: "positive" | "negative";
- permissions: "public" | "private";
- trajectory: unknown[];
-}
+/**
+ * Generate the base URL of the OpenHands API
+ * @returns Base URL of the OpenHands API
+ */
+const generateBaseURL = () => {
+ const baseUrl = import.meta.env.VITE_BACKEND_BASE_URL || "localhost:3000";
+ return `http://${baseUrl}`;
+};
/**
* Class to interact with the OpenHands API
@@ -39,7 +23,7 @@ class OpenHands {
/**
* Base URL of the OpenHands API
*/
- static BASE_URL = "http://localhost:3000";
+ static BASE_URL = generateBaseURL();
/**
* Retrieve the list of models available
@@ -73,12 +57,17 @@ class OpenHands {
/**
* Retrieve the list of files available in the workspace
* @param token User token provided by the server
- * @returns List of files available in the workspace
+ * @param path Path to list files from
+ * @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
- static async getFiles(token: string): Promise {
- const response = await fetch(`${OpenHands.BASE_URL}/api/list-files`, {
+ static async getFiles(token: string, path?: string): Promise {
+ const url = new URL(`${OpenHands.BASE_URL}/api/list-files`);
+ if (path) url.searchParams.append("path", encodeURIComponent(path));
+
+ const response = await fetch(url.toString(), {
headers: OpenHands.generateHeaders(token),
});
+
return response.json();
}
@@ -126,12 +115,12 @@ class OpenHands {
* @param file File to upload
* @returns Success message or error message
*/
- static async uploadFile(
+ static async uploadFiles(
token: string,
- file: File,
+ file: File[],
): Promise {
const formData = new FormData();
- formData.append("files", file);
+ file.forEach((f) => formData.append("files", f));
const response = await fetch(`${OpenHands.BASE_URL}/api/upload-files`, {
method: "POST",
@@ -174,6 +163,22 @@ class OpenHands {
return response.json();
}
+ /**
+ * Get the GitHub access token
+ * @param code Code provided by GitHub
+ * @returns GitHub access token
+ */
+ static async getGitHubAccessToken(
+ code: string,
+ ): Promise {
+ const response = await fetch(`${OpenHands.BASE_URL}/github/callback`, {
+ method: "POST",
+ body: JSON.stringify({ code }),
+ });
+
+ return response.json();
+ }
+
/**
* Generate the headers for the request
* @param token User token provided by the server
diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts
new file mode 100644
index 0000000000..63edc9b034
--- /dev/null
+++ b/frontend/src/api/open-hands.types.ts
@@ -0,0 +1,37 @@
+export interface ErrorResponse {
+ error: string;
+}
+
+export interface SaveFileSuccessResponse {
+ message: string;
+}
+
+export interface FileUploadSuccessResponse {
+ message: string;
+ uploaded_files: string[];
+ skipped_files: { name: string; reason: string }[];
+}
+
+export interface FeedbackBodyResponse {
+ message: string;
+ feedback_id: string;
+ password: string;
+}
+
+export interface FeedbackResponse {
+ statusCode: number;
+ body: FeedbackBodyResponse;
+}
+
+export interface GitHubAccessTokenResponse {
+ access_token: string;
+}
+
+export interface Feedback {
+ version: string;
+ email: string;
+ token: string;
+ feedback: "positive" | "negative";
+ permissions: "public" | "private";
+ trajectory: unknown[];
+}
diff --git a/frontend/src/api/open-hands.utils.ts b/frontend/src/api/open-hands.utils.ts
new file mode 100644
index 0000000000..c2460d0636
--- /dev/null
+++ b/frontend/src/api/open-hands.utils.ts
@@ -0,0 +1,6 @@
+import { ErrorResponse, FileUploadSuccessResponse } from "./open-hands.types";
+
+export const isOpenHandsErrorResponse = (
+ data: ErrorResponse | FileUploadSuccessResponse,
+): data is ErrorResponse =>
+ typeof data === "object" && data !== null && "error" in data;
diff --git a/frontend/src/components/file-explorer/FileExplorer.tsx b/frontend/src/components/file-explorer/FileExplorer.tsx
index 7172dadfd3..8362723b53 100644
--- a/frontend/src/components/file-explorer/FileExplorer.tsx
+++ b/frontend/src/components/file-explorer/FileExplorer.tsx
@@ -12,7 +12,6 @@ import { useTranslation } from "react-i18next";
import { twMerge } from "tailwind-merge";
import AgentState from "#/types/AgentState";
import { setRefreshID } from "#/state/codeSlice";
-import { uploadFiles } from "#/services/fileService";
import IconButton from "../IconButton";
import ExplorerTree from "./ExplorerTree";
import toast from "#/utils/toast";
@@ -20,6 +19,7 @@ import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import OpenHands from "#/api/open-hands";
import { useFiles } from "#/context/files";
+import { isOpenHandsErrorResponse } from "#/api/open-hands.utils";
interface ExplorerActionsProps {
onRefresh: () => void;
@@ -118,43 +118,46 @@ function FileExplorer() {
revalidate();
};
- const uploadFileData = async (toAdd: FileList) => {
+ const uploadFileData = async (files: FileList) => {
try {
- const result = await uploadFiles(toAdd);
+ const token = localStorage.getItem("token");
+ if (token) {
+ const result = await OpenHands.uploadFiles(token, Array.from(files));
- if (result.error) {
- // Handle error response
- toast.error(
- `upload-error-${new Date().getTime()}`,
- result.error || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
- );
- return;
+ if (isOpenHandsErrorResponse(result)) {
+ // Handle error response
+ toast.error(
+ `upload-error-${new Date().getTime()}`,
+ result.error || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE),
+ );
+ return;
+ }
+
+ const uploadedCount = result.uploaded_files.length;
+ const skippedCount = result.skipped_files.length;
+
+ if (uploadedCount > 0) {
+ toast.success(
+ `upload-success-${new Date().getTime()}`,
+ t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, {
+ count: uploadedCount,
+ }),
+ );
+ }
+
+ if (skippedCount > 0) {
+ const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, {
+ count: skippedCount,
+ });
+ toast.info(message);
+ }
+
+ if (uploadedCount === 0 && skippedCount === 0) {
+ toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE));
+ }
+
+ refreshWorkspace();
}
-
- const uploadedCount = result.uploadedFiles.length;
- const skippedCount = result.skippedFiles.length;
-
- if (uploadedCount > 0) {
- toast.success(
- `upload-success-${new Date().getTime()}`,
- t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, {
- count: uploadedCount,
- }),
- );
- }
-
- if (skippedCount > 0) {
- const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, {
- count: skippedCount,
- });
- toast.info(message);
- }
-
- if (uploadedCount === 0 && skippedCount === 0) {
- toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE));
- }
-
- refreshWorkspace();
} catch (error) {
// Handle unexpected errors (network issues, etc.)
toast.error(
diff --git a/frontend/src/components/file-explorer/TreeNode.tsx b/frontend/src/components/file-explorer/TreeNode.tsx
index fed4c4b624..fddcc0288d 100644
--- a/frontend/src/components/file-explorer/TreeNode.tsx
+++ b/frontend/src/components/file-explorer/TreeNode.tsx
@@ -3,7 +3,6 @@ import { useSelector } from "react-redux";
import { RootState } from "#/store";
import FolderIcon from "../FolderIcon";
import FileIcon from "../FileIcons";
-import { listFiles } from "#/services/fileService";
import OpenHands from "#/api/open-hands";
import { useFiles } from "#/context/files";
import { cn } from "#/utils/utils";
@@ -58,7 +57,12 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
setChildren(null);
return;
}
- setChildren(await listFiles(path));
+
+ const token = localStorage.getItem("token");
+ if (token) {
+ const newChildren = await OpenHands.getFiles(token, path);
+ setChildren(newChildren);
+ }
};
React.useEffect(() => {
diff --git a/frontend/src/components/modals/feedback/FeedbackModal.tsx b/frontend/src/components/modals/feedback/FeedbackModal.tsx
index 1149c87865..e7051da694 100644
--- a/frontend/src/components/modals/feedback/FeedbackModal.tsx
+++ b/frontend/src/components/modals/feedback/FeedbackModal.tsx
@@ -8,7 +8,8 @@ import toast from "#/utils/toast";
import { getToken } from "#/services/auth";
import { removeApiKey, removeUnwantedKeys } from "#/utils/utils";
import { useSocket } from "#/context/socket";
-import OpenHands, { Feedback } from "#/api/open-hands";
+import OpenHands from "#/api/open-hands";
+import { Feedback } from "#/api/open-hands.types";
const isEmailValid = (email: string) => {
// Regular expression to validate email format
diff --git a/frontend/src/context/socket.tsx b/frontend/src/context/socket.tsx
index 523f1e601d..723a6478b9 100644
--- a/frontend/src/context/socket.tsx
+++ b/frontend/src/context/socket.tsx
@@ -45,15 +45,9 @@ function SocketProvider({ children }: SocketProviderProps) {
);
}
- /*
- const wsUrl = new URL("/", document.baseURI);
- wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
- if (options?.token) wsUrl.searchParams.set("token", options.token);
- const ws = new WebSocket(`${wsUrl.origin}/ws`);
- */
- // TODO: Remove hardcoded URL; may have to use a proxy
+ const baseUrl = import.meta.env.VITE_BACKEND_BASE_URL || "localhost:3000";
const ws = new WebSocket(
- `ws://localhost:3000/ws${options?.token ? `?token=${options.token}` : ""}`,
+ `ws://${baseUrl}/ws${options?.token ? `?token=${options.token}` : ""}`,
);
ws.addEventListener("open", (event) => {
diff --git a/frontend/src/root.tsx b/frontend/src/root.tsx
index a079c69b05..82dc29b54e 100644
--- a/frontend/src/root.tsx
+++ b/frontend/src/root.tsx
@@ -1,6 +1,7 @@
import {
Links,
Meta,
+ MetaFunction,
Outlet,
Scripts,
ScrollRestoration,
@@ -49,6 +50,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}
+export const meta: MetaFunction = () => [
+ { title: "OpenHands" },
+ { name: "description", content: "Let's Start Building!" },
+];
+
export const clientLoader = async () => {
let token = localStorage.getItem("token");
const ghToken = localStorage.getItem("ghToken");
@@ -157,7 +163,7 @@ export default function App() {
};
return (
-
+