From b9b6cfd406814f453a1cfe46842cd7a7126b957b Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:47:02 +0400 Subject: [PATCH] refactor(frontend) Refactor and move components (#5290) --- frontend/README.md | 15 +- .../__tests__/components/browser.test.tsx | 2 +- .../components/chat-message.test.tsx | 2 +- .../components/chat/chat-input.test.tsx | 2 +- .../components/chat/chat-interface.test.tsx | 2 +- .../account-settings-context-menu.test.tsx | 2 +- .../context-menu-list-item.test.tsx | 2 +- .../components/feedback-actions.test.tsx | 2 +- .../components/feedback-form.test.tsx | 2 +- .../file-explorer/explorer-tree.test.tsx | 2 +- .../file-explorer/file-explorer.test.tsx | 2 +- .../file-explorer/tree-node.test.tsx | 2 +- .../components/image-preview.test.tsx | 2 +- .../components/interactive-chat-box.test.tsx | 2 +- .../modals/base-modal/base-modal.test.tsx | 2 +- .../modals/settings/model-selector.test.tsx | 2 +- .../components/suggestion-item.test.tsx | 2 +- .../__tests__/components/suggestions.test.tsx | 2 +- .../components/terminal/terminal.test.tsx | 2 +- .../components/upload-image-input.test.tsx | 2 +- .../components/user-actions.test.tsx | 2 +- .../__tests__/components/user-avatar.test.tsx | 2 +- frontend/src/components/agent-control-bar.tsx | 110 -------- frontend/src/components/agent-status-bar.tsx | 132 --------- .../components/agent-status-map.constant.ts | 64 +++++ .../components/chat/confirmation-buttons.tsx | 64 ----- .../extension-icon-map.constant.tsx | 20 ++ .../analytics-consent-form-modal.tsx | 8 +- .../features/browser/browser-snapshot.tsx | 14 + .../{ => features/browser}/browser.tsx | 23 +- .../browser/empty-browser-message.tsx | 14 + .../features/chat}/action-suggestions.tsx | 2 +- .../{ => features/chat}/chat-input.tsx | 24 +- .../features/chat}/chat-interface.tsx | 20 +- .../{ => features/chat}/chat-message.tsx | 28 +- .../features/chat}/chat-suggestions.tsx | 2 +- .../{ => features/chat}/error-message.tsx | 0 .../chat}/interactive-chat-box.tsx | 4 +- .../features/chat}/messages.tsx | 8 +- .../{ => features}/chat/typing-indicator.tsx | 6 +- .../account-settings-context-menu.tsx | 0 .../context-menu/context-menu-list-item.tsx | 0 .../context-menu/context-menu-separator.tsx | 0 .../context-menu/context-menu.tsx | 0 .../features/controls/agent-control-bar.tsx | 44 +++ .../features/controls/agent-status-bar.tsx | 53 ++++ .../{ => features/controls}/controls.tsx | 16 +- .../features/controls/security-lock.tsx | 17 ++ .../editor}/code-editor-component.tsx | 0 .../{ => features/editor}/editor-actions.tsx | 31 +- .../feedback}/feedback-actions.tsx | 24 +- .../{ => features/feedback}/feedback-form.tsx | 2 +- .../feedback}/feedback-modal.tsx | 8 +- .../features}/file-explorer/dropzone.tsx | 0 .../file-explorer/explorer-tree.tsx | 7 +- .../file-explorer/file-explorer-actions.tsx | 6 +- .../file-explorer/file-explorer-header.tsx | 0 .../features}/file-explorer/file-explorer.tsx | 4 +- .../features/file-explorer/file-icon.tsx | 12 + .../features/file-explorer/filename.tsx | 20 ++ .../file-explorer}/folder-icon.tsx | 4 +- .../file-explorer/tree-node.tsx | 24 +- .../features/github}/github-repo-selector.tsx | 0 .../github-repositories-suggestion-box.tsx | 10 +- .../images}/attach-image-label.tsx | 0 .../{ => features/images}/image-carousel.tsx | 0 .../features/images/image-preview.tsx | 21 ++ .../components/features/images/thumbnail.tsx | 21 ++ .../images}/upload-image-input.tsx | 0 .../features/jupyter/jupyter-cell-input.tsx | 22 ++ .../features/jupyter/jupyter-cell-output.tsx | 40 +++ .../features/jupyter/jupyter-cell.tsx | 23 ++ .../components/features/jupyter/jupyter.tsx | 37 +++ .../{ => features}/markdown/code.tsx | 0 .../{ => features}/markdown/list.tsx | 0 .../project-menu/ProjectMenuCard.tsx | 6 +- .../project-menu-details-placeholder.tsx | 0 .../project-menu/project-menu-details.tsx | 0 .../project.menu-card-context-menu.tsx | 0 .../components/features/sidebar/avatar.tsx | 9 + .../features/sidebar}/sidebar.tsx | 18 +- .../{ => features/sidebar}/user-actions.tsx | 2 +- .../{ => features/sidebar}/user-avatar.tsx | 11 +- .../import-project-suggestion-box.tsx | 0 .../features/suggestions}/suggestion-box.tsx | 0 .../suggestions}/suggestion-bubble.tsx | 17 +- .../suggestions}/suggestion-item.tsx | 0 .../suggestions}/suggestions.tsx | 0 .../{ => features}/terminal/terminal.tsx | 2 +- .../waitlist/join-waitlist-anchor.tsx | 12 + .../features/waitlist/waitlist-message.tsx | 35 +++ .../features/waitlist/waitlist-modal.tsx | 37 +++ frontend/src/components/file-icons.tsx | 33 --- frontend/src/components/image-preview.tsx | 41 --- frontend/src/components/jupyter.tsx | 128 --------- frontend/src/components/layout/beta-badge.tsx | 7 + .../src/components/{ => layout}/container.tsx | 28 +- frontend/src/components/layout/nav-tab.tsx | 32 +++ frontend/src/components/scroll-button.tsx | 26 -- .../src/components/shared/action-tooltip.tsx | 33 +++ .../shared/buttons/action-button.tsx | 33 +++ .../shared}/buttons/all-hands-logo-button.tsx | 0 .../shared/buttons/confirmation-buttons.tsx | 32 +++ .../{ => shared/buttons}/continue-button.tsx | 2 +- .../buttons/copy-to-clipboard-button.tsx | 30 ++ .../shared}/buttons/docs-button.tsx | 0 .../shared/buttons/editor-action-button.tsx | 29 ++ .../shared}/buttons/exit-project-button.tsx | 0 .../shared/buttons/feedback-action-button.tsx | 22 ++ .../{ => shared/buttons}/icon-button.tsx | 4 +- .../{ => shared}/buttons/modal-button.tsx | 4 +- .../shared}/buttons/open-vscode-button.tsx | 0 .../shared/buttons/refresh-button.tsx | 13 + .../shared}/buttons/refresh-icon-button.tsx | 2 +- .../shared/buttons/remove-button.tsx | 21 ++ .../buttons}/scroll-to-bottom-button.tsx | 2 +- .../shared}/buttons/settings-button.tsx | 0 .../components/shared/buttons/stop-button.tsx | 19 ++ .../shared/buttons/submit-button.tsx | 20 ++ .../buttons/toggle-workspace-icon-button.tsx | 2 +- .../shared}/buttons/upload-icon-button.tsx | 2 +- .../{form => shared}/custom-input.tsx | 0 .../components/{ => shared}/error-toast.tsx | 0 .../{form => shared}/form-fieldset.tsx | 4 +- .../shared}/hero-heading.tsx | 0 .../shared/inputs/advanced-option-switch.tsx | 40 +++ .../components/shared/inputs/agent-input.tsx | 46 +++ .../shared/inputs/api-key-input.tsx | 42 +++ .../shared/inputs/base-url-input.tsx | 30 ++ .../inputs/confirmation-mode-switch.tsx | 37 +++ .../shared/inputs/custom-model-input.tsx | 36 +++ .../inputs/security-analyzers-input.tsx | 47 ++++ .../loading-spinner.tsx} | 22 -- .../account-settings-form.tsx | 10 +- .../account-settings-modal.tsx | 4 +- .../modals/base-modal/base-modal.tsx | 4 +- .../modals/base-modal/footer-content.tsx | 0 .../modals/base-modal/header-content.tsx | 0 .../modals/confirmation-modals/base-modal.tsx | 8 +- .../confirmation-modals/danger-modal.tsx | 2 +- .../modals/connect-to-github-modal.tsx | 6 +- .../exit-project-confirmation-modal.tsx | 4 +- .../{ => shared}/modals/modal-backdrop.tsx | 0 .../{ => shared}/modals/modal-body.tsx | 4 +- .../modals/security/invariant/assets/logo.tsx | 0 .../modals/security/invariant/invariant.tsx | 0 .../{ => shared}/modals/security/security.tsx | 2 +- .../modals/settings/model-selector.tsx | 0 .../modals/settings}/settings-form.tsx | 266 +++++------------- .../modals/settings}/settings-modal.tsx | 6 +- .../shared}/task-form.tsx | 10 +- frontend/src/components/waitlist-modal.tsx | 69 ----- .../src/ignore-task-state-map.constant.ts | 26 ++ .../src/{components/chat => }/message.d.ts | 0 frontend/src/routes/_oh._index/route.tsx | 8 +- frontend/src/routes/_oh.app._index/route.tsx | 6 +- frontend/src/routes/_oh.app.browser.tsx | 2 +- frontend/src/routes/_oh.app.jupyter.tsx | 2 +- .../src/routes/_oh.app/loading-spinner.tsx | 7 - frontend/src/routes/_oh.app/route.tsx | 10 +- frontend/src/routes/_oh/route.tsx | 6 +- frontend/src/tailwind.css | 4 + frontend/src/utils/display-error-toast.tsx | 2 +- frontend/src/utils/parse-cell-content.ts | 24 ++ 164 files changed, 1390 insertions(+), 1166 deletions(-) delete mode 100644 frontend/src/components/agent-control-bar.tsx delete mode 100644 frontend/src/components/agent-status-bar.tsx create mode 100644 frontend/src/components/agent-status-map.constant.ts delete mode 100644 frontend/src/components/chat/confirmation-buttons.tsx create mode 100644 frontend/src/components/extension-icon-map.constant.tsx rename frontend/src/components/{ => features/analytics}/analytics-consent-form-modal.tsx (84%) create mode 100644 frontend/src/components/features/browser/browser-snapshot.tsx rename frontend/src/components/{ => features/browser}/browser.tsx (52%) create mode 100644 frontend/src/components/features/browser/empty-browser-message.tsx rename frontend/src/{routes/_oh.app => components/features/chat}/action-suggestions.tsx (96%) rename frontend/src/components/{ => features/chat}/chat-input.tsx (82%) rename frontend/src/{routes/_oh.app => components/features/chat}/chat-interface.tsx (88%) rename frontend/src/components/{ => features/chat}/chat-message.tsx (70%) rename frontend/src/{routes/_oh.app => components/features/chat}/chat-suggestions.tsx (92%) rename frontend/src/components/{ => features/chat}/error-message.tsx (100%) rename frontend/src/components/{ => features/chat}/interactive-chat-box.tsx (94%) rename frontend/src/{routes/_oh.app => components/features/chat}/messages.tsx (76%) rename frontend/src/components/{ => features}/chat/typing-indicator.tsx (85%) rename frontend/src/components/{ => features}/context-menu/account-settings-context-menu.tsx (100%) rename frontend/src/components/{ => features}/context-menu/context-menu-list-item.tsx (100%) rename frontend/src/components/{ => features}/context-menu/context-menu-separator.tsx (100%) rename frontend/src/components/{ => features}/context-menu/context-menu.tsx (100%) create mode 100644 frontend/src/components/features/controls/agent-control-bar.tsx create mode 100644 frontend/src/components/features/controls/agent-status-bar.tsx rename frontend/src/components/{ => features/controls}/controls.tsx (72%) create mode 100644 frontend/src/components/features/controls/security-lock.tsx rename frontend/src/{routes/_oh.app._index => components/features/editor}/code-editor-component.tsx (100%) rename frontend/src/components/{ => features/editor}/editor-actions.tsx (50%) rename frontend/src/components/{ => features/feedback}/feedback-actions.tsx (63%) rename frontend/src/components/{ => features/feedback}/feedback-form.tsx (98%) rename frontend/src/components/{ => features/feedback}/feedback-modal.tsx (78%) rename frontend/src/{routes/_oh.app._index => components/features}/file-explorer/dropzone.tsx (100%) rename frontend/src/components/{ => features}/file-explorer/explorer-tree.tsx (86%) rename frontend/src/{routes/_oh.app._index => components/features}/file-explorer/file-explorer-actions.tsx (71%) rename frontend/src/{routes/_oh.app._index => components/features}/file-explorer/file-explorer-header.tsx (100%) rename frontend/src/{routes/_oh.app._index => components/features}/file-explorer/file-explorer.tsx (96%) create mode 100644 frontend/src/components/features/file-explorer/file-icon.tsx create mode 100644 frontend/src/components/features/file-explorer/filename.tsx rename frontend/src/components/{ => features/file-explorer}/folder-icon.tsx (72%) rename frontend/src/components/{ => features}/file-explorer/tree-node.tsx (76%) rename frontend/src/{routes/_oh._index => components/features/github}/github-repo-selector.tsx (100%) rename frontend/src/components/{ => features/github}/github-repositories-suggestion-box.tsx (82%) rename frontend/src/components/{ => features/images}/attach-image-label.tsx (100%) rename frontend/src/components/{ => features/images}/image-carousel.tsx (100%) create mode 100644 frontend/src/components/features/images/image-preview.tsx create mode 100644 frontend/src/components/features/images/thumbnail.tsx rename frontend/src/components/{ => features/images}/upload-image-input.tsx (100%) create mode 100644 frontend/src/components/features/jupyter/jupyter-cell-input.tsx create mode 100644 frontend/src/components/features/jupyter/jupyter-cell-output.tsx create mode 100644 frontend/src/components/features/jupyter/jupyter-cell.tsx create mode 100644 frontend/src/components/features/jupyter/jupyter.tsx rename frontend/src/components/{ => features}/markdown/code.tsx (100%) rename frontend/src/components/{ => features}/markdown/list.tsx (100%) rename frontend/src/components/{ => features}/project-menu/ProjectMenuCard.tsx (93%) rename frontend/src/components/{ => features}/project-menu/project-menu-details-placeholder.tsx (100%) rename frontend/src/components/{ => features}/project-menu/project-menu-details.tsx (100%) rename frontend/src/components/{ => features}/project-menu/project.menu-card-context-menu.tsx (100%) create mode 100644 frontend/src/components/features/sidebar/avatar.tsx rename frontend/src/{routes/_oh => components/features/sidebar}/sidebar.tsx (79%) rename frontend/src/components/{ => features/sidebar}/user-actions.tsx (92%) rename frontend/src/components/{ => features/sidebar}/user-avatar.tsx (77%) rename frontend/src/{routes/_oh._index => components/features/suggestions}/import-project-suggestion-box.tsx (100%) rename frontend/src/{routes/_oh._index => components/features/suggestions}/suggestion-box.tsx (100%) rename frontend/src/components/{ => features/suggestions}/suggestion-bubble.tsx (71%) rename frontend/src/components/{ => features/suggestions}/suggestion-item.tsx (100%) rename frontend/src/components/{ => features/suggestions}/suggestions.tsx (100%) rename frontend/src/components/{ => features}/terminal/terminal.tsx (89%) create mode 100644 frontend/src/components/features/waitlist/join-waitlist-anchor.tsx create mode 100644 frontend/src/components/features/waitlist/waitlist-message.tsx create mode 100644 frontend/src/components/features/waitlist/waitlist-modal.tsx delete mode 100644 frontend/src/components/file-icons.tsx delete mode 100644 frontend/src/components/image-preview.tsx delete mode 100644 frontend/src/components/jupyter.tsx create mode 100644 frontend/src/components/layout/beta-badge.tsx rename frontend/src/components/{ => layout}/container.tsx (55%) create mode 100644 frontend/src/components/layout/nav-tab.tsx delete mode 100644 frontend/src/components/scroll-button.tsx create mode 100644 frontend/src/components/shared/action-tooltip.tsx create mode 100644 frontend/src/components/shared/buttons/action-button.tsx rename frontend/src/{routes/_oh => components/shared}/buttons/all-hands-logo-button.tsx (100%) create mode 100644 frontend/src/components/shared/buttons/confirmation-buttons.tsx rename frontend/src/components/{ => shared/buttons}/continue-button.tsx (88%) create mode 100644 frontend/src/components/shared/buttons/copy-to-clipboard-button.tsx rename frontend/src/{routes/_oh => components/shared}/buttons/docs-button.tsx (100%) create mode 100644 frontend/src/components/shared/buttons/editor-action-button.tsx rename frontend/src/{routes/_oh => components/shared}/buttons/exit-project-button.tsx (100%) create mode 100644 frontend/src/components/shared/buttons/feedback-action-button.tsx rename frontend/src/components/{ => shared/buttons}/icon-button.tsx (92%) rename frontend/src/components/{ => shared}/buttons/modal-button.tsx (95%) rename frontend/src/{routes/_oh.app._index/file-explorer => components/shared}/buttons/open-vscode-button.tsx (100%) create mode 100644 frontend/src/components/shared/buttons/refresh-button.tsx rename frontend/src/{routes/_oh.app._index/file-explorer => components/shared}/buttons/refresh-icon-button.tsx (89%) create mode 100644 frontend/src/components/shared/buttons/remove-button.tsx rename frontend/src/components/{ => shared/buttons}/scroll-to-bottom-button.tsx (78%) rename frontend/src/{routes/_oh => components/shared}/buttons/settings-button.tsx (100%) create mode 100644 frontend/src/components/shared/buttons/stop-button.tsx create mode 100644 frontend/src/components/shared/buttons/submit-button.tsx rename frontend/src/{routes/_oh.app._index/file-explorer => components/shared}/buttons/toggle-workspace-icon-button.tsx (93%) rename frontend/src/{routes/_oh.app._index/file-explorer => components/shared}/buttons/upload-icon-button.tsx (89%) rename frontend/src/components/{form => shared}/custom-input.tsx (100%) rename frontend/src/components/{ => shared}/error-toast.tsx (100%) rename frontend/src/components/{form => shared}/form-fieldset.tsx (94%) rename frontend/src/{routes/_oh._index => components/shared}/hero-heading.tsx (100%) create mode 100644 frontend/src/components/shared/inputs/advanced-option-switch.tsx create mode 100644 frontend/src/components/shared/inputs/agent-input.tsx create mode 100644 frontend/src/components/shared/inputs/api-key-input.tsx create mode 100644 frontend/src/components/shared/inputs/base-url-input.tsx create mode 100644 frontend/src/components/shared/inputs/confirmation-mode-switch.tsx create mode 100644 frontend/src/components/shared/inputs/custom-model-input.tsx create mode 100644 frontend/src/components/shared/inputs/security-analyzers-input.tsx rename frontend/src/components/{modals/loading-project.tsx => shared/loading-spinner.tsx} (52%) rename frontend/src/components/{modals => shared/modals/account-settings}/account-settings-form.tsx (94%) rename frontend/src/{routes/_oh/modals => components/shared/modals/account-settings}/account-settings-modal.tsx (83%) rename frontend/src/components/{ => shared}/modals/base-modal/base-modal.tsx (97%) rename frontend/src/components/{ => shared}/modals/base-modal/footer-content.tsx (100%) rename frontend/src/components/{ => shared}/modals/base-modal/header-content.tsx (100%) rename frontend/src/components/{ => shared}/modals/confirmation-modals/base-modal.tsx (86%) rename frontend/src/components/{ => shared}/modals/confirmation-modals/danger-modal.tsx (94%) rename frontend/src/components/{ => shared}/modals/connect-to-github-modal.tsx (93%) rename frontend/src/{routes/_oh => components/shared}/modals/exit-project-confirmation-modal.tsx (87%) rename frontend/src/components/{ => shared}/modals/modal-backdrop.tsx (100%) rename frontend/src/components/{ => shared}/modals/modal-body.tsx (81%) rename frontend/src/components/{ => shared}/modals/security/invariant/assets/logo.tsx (100%) rename frontend/src/components/{ => shared}/modals/security/invariant/invariant.tsx (100%) rename frontend/src/components/{ => shared}/modals/security/security.tsx (95%) rename frontend/src/components/{ => shared}/modals/settings/model-selector.tsx (100%) rename frontend/src/components/{form => shared/modals/settings}/settings-form.tsx (54%) rename frontend/src/{routes/_oh/modals => components/shared/modals/settings}/settings-modal.tsx (88%) rename frontend/src/{routes/_oh._index => components/shared}/task-form.tsx (92%) delete mode 100644 frontend/src/components/waitlist-modal.tsx create mode 100644 frontend/src/ignore-task-state-map.constant.ts rename frontend/src/{components/chat => }/message.d.ts (100%) delete mode 100644 frontend/src/routes/_oh.app/loading-spinner.tsx create mode 100644 frontend/src/utils/parse-cell-content.ts diff --git a/frontend/README.md b/frontend/README.md index 7d05e8a9ba3c..65493728aa39 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -9,6 +9,7 @@ This is the frontend of the OpenHands project. It is a React application that pr - Remix SPA Mode (React + Vite + React Router) - TypeScript - Redux +- TanStack Query - Tailwind CSS - i18next - React Testing Library @@ -85,7 +86,7 @@ frontend ├── src │ ├── api # API calls │ ├── assets -│ ├── components # Reusable components +│ ├── components │ ├── context # Local state management │ ├── hooks # Custom hooks │ ├── i18n # Internationalization @@ -99,6 +100,18 @@ frontend └── .env.sample # Sample environment variables ``` +#### Components + +Components are organized into folders based on their **domain**, **feature**, or **shared functionality**. + +```sh +components +├── features # Domain-specific components +├── layout +├── modals +└── ui # Shared UI components +``` + ### Features - Real-time updates with WebSockets diff --git a/frontend/__tests__/components/browser.test.tsx b/frontend/__tests__/components/browser.test.tsx index 6d533adf1c3b..9b9f0fa3ba47 100644 --- a/frontend/__tests__/components/browser.test.tsx +++ b/frontend/__tests__/components/browser.test.tsx @@ -1,7 +1,7 @@ import { screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; import { renderWithProviders } from "../../test-utils"; -import BrowserPanel from "#/components/browser"; +import { BrowserPanel } from "#/components/features/browser/browser"; describe("Browser", () => { it("renders a message if no screenshotSrc is provided", () => { diff --git a/frontend/__tests__/components/chat-message.test.tsx b/frontend/__tests__/components/chat-message.test.tsx index ae2f7ad81335..9b3156ee54fe 100644 --- a/frontend/__tests__/components/chat-message.test.tsx +++ b/frontend/__tests__/components/chat-message.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, test } from "vitest"; -import { ChatMessage } from "#/components/chat-message"; +import { ChatMessage } from "#/components/features/chat/chat-message"; describe("ChatMessage", () => { it("should render a user message", () => { diff --git a/frontend/__tests__/components/chat/chat-input.test.tsx b/frontend/__tests__/components/chat/chat-input.test.tsx index 8a5f0c3dd6b4..51cbe4df6674 100644 --- a/frontend/__tests__/components/chat/chat-input.test.tsx +++ b/frontend/__tests__/components/chat/chat-input.test.tsx @@ -1,7 +1,7 @@ import userEvent from "@testing-library/user-event"; import { fireEvent, render, screen } from "@testing-library/react"; import { describe, afterEach, vi, it, expect } from "vitest"; -import { ChatInput } from "#/components/chat-input"; +import { ChatInput } from "#/components/features/chat/chat-input"; describe("ChatInput", () => { const onSubmitMock = vi.fn(); diff --git a/frontend/__tests__/components/chat/chat-interface.test.tsx b/frontend/__tests__/components/chat/chat-interface.test.tsx index 8596d3acd3d7..e3e35a209f29 100644 --- a/frontend/__tests__/components/chat/chat-interface.test.tsx +++ b/frontend/__tests__/components/chat/chat-interface.test.tsx @@ -6,7 +6,7 @@ import { addUserMessage } from "#/state/chat-slice"; import { SUGGESTIONS } from "#/utils/suggestions"; import * as ChatSlice from "#/state/chat-slice"; import { WsClientProviderStatus } from "#/context/ws-client-provider"; -import { ChatInterface } from "#/routes/_oh.app/chat-interface"; +import { ChatInterface } from "#/components/features/chat/chat-interface"; // eslint-disable-next-line @typescript-eslint/no-unused-vars const renderChatInterface = (messages: (Message | ErrorMessage)[]) => diff --git a/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx b/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx index 57c2aea3724d..ad5b6a0a3443 100644 --- a/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx +++ b/frontend/__tests__/components/context-menu/account-settings-context-menu.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, test, vi } from "vitest"; -import { AccountSettingsContextMenu } from "#/components/context-menu/account-settings-context-menu"; +import { AccountSettingsContextMenu } from "#/components/features/context-menu/account-settings-context-menu"; describe("AccountSettingsContextMenu", () => { const user = userEvent.setup(); diff --git a/frontend/__tests__/components/context-menu/context-menu-list-item.test.tsx b/frontend/__tests__/components/context-menu/context-menu-list-item.test.tsx index 9f72aada2a24..55e19e099228 100644 --- a/frontend/__tests__/components/context-menu/context-menu-list-item.test.tsx +++ b/frontend/__tests__/components/context-menu/context-menu-list-item.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ContextMenuListItem } from "#/components/context-menu/context-menu-list-item"; +import { ContextMenuListItem } from "#/components/features/context-menu/context-menu-list-item"; describe("ContextMenuListItem", () => { it("should render the component with the children", () => { diff --git a/frontend/__tests__/components/feedback-actions.test.tsx b/frontend/__tests__/components/feedback-actions.test.tsx index 48d1ce5d8913..c4f8170eea33 100644 --- a/frontend/__tests__/components/feedback-actions.test.tsx +++ b/frontend/__tests__/components/feedback-actions.test.tsx @@ -1,7 +1,7 @@ import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { FeedbackActions } from "#/components/feedback-actions"; +import { FeedbackActions } from "#/components/features/feedback/feedback-actions"; describe("FeedbackActions", () => { const user = userEvent.setup(); diff --git a/frontend/__tests__/components/feedback-form.test.tsx b/frontend/__tests__/components/feedback-form.test.tsx index ba276885cd32..4ff87da4534c 100644 --- a/frontend/__tests__/components/feedback-form.test.tsx +++ b/frontend/__tests__/components/feedback-form.test.tsx @@ -2,7 +2,7 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils"; -import { FeedbackForm } from "#/components/feedback-form"; +import { FeedbackForm } from "#/components/features/feedback/feedback-form"; describe("FeedbackForm", () => { const user = userEvent.setup(); diff --git a/frontend/__tests__/components/file-explorer/explorer-tree.test.tsx b/frontend/__tests__/components/file-explorer/explorer-tree.test.tsx index a30594a1f046..84c1e911ac28 100644 --- a/frontend/__tests__/components/file-explorer/explorer-tree.test.tsx +++ b/frontend/__tests__/components/file-explorer/explorer-tree.test.tsx @@ -1,7 +1,7 @@ import { screen } from "@testing-library/react"; import { renderWithProviders } from "test-utils"; import { describe, afterEach, vi, it, expect } from "vitest"; -import ExplorerTree from "#/components/file-explorer/explorer-tree"; +import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree"; const FILES = ["file-1-1.ts", "folder-1-2"]; diff --git a/frontend/__tests__/components/file-explorer/file-explorer.test.tsx b/frontend/__tests__/components/file-explorer/file-explorer.test.tsx index 6b360520347d..5a68fa67876e 100644 --- a/frontend/__tests__/components/file-explorer/file-explorer.test.tsx +++ b/frontend/__tests__/components/file-explorer/file-explorer.test.tsx @@ -4,8 +4,8 @@ import { renderWithProviders } from "test-utils"; import { describe, it, expect, vi, Mock, afterEach } from "vitest"; import toast from "#/utils/toast"; import AgentState from "#/types/agent-state"; -import { FileExplorer } from "#/routes/_oh.app._index/file-explorer/file-explorer"; import OpenHands from "#/api/open-hands"; +import { FileExplorer } from "#/components/features/file-explorer/file-explorer"; const toastSpy = vi.spyOn(toast, "error"); const uploadFilesSpy = vi.spyOn(OpenHands, "uploadFiles"); diff --git a/frontend/__tests__/components/file-explorer/tree-node.test.tsx b/frontend/__tests__/components/file-explorer/tree-node.test.tsx index ebd8c029d433..5756df921d2e 100644 --- a/frontend/__tests__/components/file-explorer/tree-node.test.tsx +++ b/frontend/__tests__/components/file-explorer/tree-node.test.tsx @@ -2,7 +2,7 @@ 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 TreeNode from "#/components/file-explorer/tree-node"; +import TreeNode from "#/components/features/file-explorer/tree-node"; import OpenHands from "#/api/open-hands"; const getFileSpy = vi.spyOn(OpenHands, "getFile"); diff --git a/frontend/__tests__/components/image-preview.test.tsx b/frontend/__tests__/components/image-preview.test.tsx index ba33fb1f0a86..39d2f089fb9b 100644 --- a/frontend/__tests__/components/image-preview.test.tsx +++ b/frontend/__tests__/components/image-preview.test.tsx @@ -1,7 +1,7 @@ +import { ImagePreview } from "#/components/features/images/image-preview"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; -import { ImagePreview } from "#/components/image-preview"; describe("ImagePreview", () => { it("should render an image", () => { diff --git a/frontend/__tests__/components/interactive-chat-box.test.tsx b/frontend/__tests__/components/interactive-chat-box.test.tsx index d7e29bae86cc..fa0d3a1b8e30 100644 --- a/frontend/__tests__/components/interactive-chat-box.test.tsx +++ b/frontend/__tests__/components/interactive-chat-box.test.tsx @@ -1,7 +1,7 @@ import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import { InteractiveChatBox } from "#/components/interactive-chat-box"; +import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box"; describe("InteractiveChatBox", () => { const onSubmitMock = vi.fn(); diff --git a/frontend/__tests__/components/modals/base-modal/base-modal.test.tsx b/frontend/__tests__/components/modals/base-modal/base-modal.test.tsx index 59abb7e44a91..563cbca6c45a 100644 --- a/frontend/__tests__/components/modals/base-modal/base-modal.test.tsx +++ b/frontend/__tests__/components/modals/base-modal/base-modal.test.tsx @@ -1,7 +1,7 @@ import { render, screen, act } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, vi, expect } from "vitest"; -import BaseModal from "#/components/modals/base-modal/base-modal"; +import { BaseModal } from "#/components/shared/modals/base-modal/base-modal"; describe("BaseModal", () => { it("should render if the modal is open", () => { diff --git a/frontend/__tests__/components/modals/settings/model-selector.test.tsx b/frontend/__tests__/components/modals/settings/model-selector.test.tsx index 2823d988eca7..8da2fec14475 100644 --- a/frontend/__tests__/components/modals/settings/model-selector.test.tsx +++ b/frontend/__tests__/components/modals/settings/model-selector.test.tsx @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ModelSelector } from "#/components/modals/settings/model-selector"; +import { ModelSelector } from "#/components/shared/modals/settings/model-selector"; describe("ModelSelector", () => { const models = { diff --git a/frontend/__tests__/components/suggestion-item.test.tsx b/frontend/__tests__/components/suggestion-item.test.tsx index 076b29ca2144..23d2aaa41e5b 100644 --- a/frontend/__tests__/components/suggestion-item.test.tsx +++ b/frontend/__tests__/components/suggestion-item.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { SuggestionItem } from "#/components/suggestion-item"; +import { SuggestionItem } from "#/components/features/suggestions/suggestion-item"; describe("SuggestionItem", () => { const suggestionItem = { label: "suggestion1", value: "a long text value" }; diff --git a/frontend/__tests__/components/suggestions.test.tsx b/frontend/__tests__/components/suggestions.test.tsx index fcc45a6ad040..33290819c3bc 100644 --- a/frontend/__tests__/components/suggestions.test.tsx +++ b/frontend/__tests__/components/suggestions.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { Suggestions } from "#/components/suggestions"; +import { Suggestions } from "#/components/features/suggestions/suggestions"; describe("Suggestions", () => { const firstSuggestion = { diff --git a/frontend/__tests__/components/terminal/terminal.test.tsx b/frontend/__tests__/components/terminal/terminal.test.tsx index d660f83639a0..055aa4433c96 100644 --- a/frontend/__tests__/components/terminal/terminal.test.tsx +++ b/frontend/__tests__/components/terminal/terminal.test.tsx @@ -2,7 +2,7 @@ import { act, screen } from "@testing-library/react"; import { renderWithProviders } from "test-utils"; import { vi, describe, afterEach, it, expect } from "vitest"; import { Command, appendInput, appendOutput } from "#/state/command-slice"; -import Terminal from "#/components/terminal/terminal"; +import Terminal from "#/components/features/terminal/terminal"; global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), diff --git a/frontend/__tests__/components/upload-image-input.test.tsx b/frontend/__tests__/components/upload-image-input.test.tsx index 77f89ee8851a..e95b80c9697b 100644 --- a/frontend/__tests__/components/upload-image-input.test.tsx +++ b/frontend/__tests__/components/upload-image-input.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { UploadImageInput } from "#/components/upload-image-input"; +import { UploadImageInput } from "#/components/features/images/upload-image-input"; describe("UploadImageInput", () => { const user = userEvent.setup(); diff --git a/frontend/__tests__/components/user-actions.test.tsx b/frontend/__tests__/components/user-actions.test.tsx index b9bb65d0f3c9..a83b88a38923 100644 --- a/frontend/__tests__/components/user-actions.test.tsx +++ b/frontend/__tests__/components/user-actions.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, test, vi, afterEach } from "vitest"; import userEvent from "@testing-library/user-event"; -import { UserActions } from "#/components/user-actions"; +import { UserActions } from "#/components/features/sidebar/user-actions"; describe("UserActions", () => { const user = userEvent.setup(); diff --git a/frontend/__tests__/components/user-avatar.test.tsx b/frontend/__tests__/components/user-avatar.test.tsx index 07f3d44afe41..076eb75b49bf 100644 --- a/frontend/__tests__/components/user-avatar.test.tsx +++ b/frontend/__tests__/components/user-avatar.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { UserAvatar } from "#/components/user-avatar"; +import { UserAvatar } from "#/components/features/sidebar/user-avatar"; describe("UserAvatar", () => { const onClickMock = vi.fn(); diff --git a/frontend/src/components/agent-control-bar.tsx b/frontend/src/components/agent-control-bar.tsx deleted file mode 100644 index a043f2a6a0a8..000000000000 --- a/frontend/src/components/agent-control-bar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Tooltip } from "@nextui-org/react"; -import React from "react"; -import { useSelector } from "react-redux"; -import PauseIcon from "#/assets/pause"; -import PlayIcon from "#/assets/play"; -import { generateAgentStateChangeEvent } from "#/services/agent-state-service"; -import { RootState } from "#/store"; -import AgentState from "#/types/agent-state"; -import { useWsClient } from "#/context/ws-client-provider"; - -const IgnoreTaskStateMap: Record = { - [AgentState.PAUSED]: [ - AgentState.INIT, - AgentState.PAUSED, - AgentState.STOPPED, - AgentState.FINISHED, - AgentState.REJECTED, - AgentState.AWAITING_USER_INPUT, - AgentState.AWAITING_USER_CONFIRMATION, - ], - [AgentState.RUNNING]: [ - AgentState.INIT, - AgentState.RUNNING, - AgentState.STOPPED, - AgentState.FINISHED, - AgentState.REJECTED, - AgentState.AWAITING_USER_INPUT, - AgentState.AWAITING_USER_CONFIRMATION, - ], - [AgentState.STOPPED]: [AgentState.INIT, AgentState.STOPPED], - [AgentState.USER_CONFIRMED]: [AgentState.RUNNING], - [AgentState.USER_REJECTED]: [AgentState.RUNNING], - [AgentState.AWAITING_USER_CONFIRMATION]: [], -}; - -interface ActionButtonProps { - isDisabled?: boolean; - content: string; - action: AgentState; - handleAction: (action: AgentState) => void; - large?: boolean; -} - -function ActionButton({ - isDisabled = false, - content, - action, - handleAction, - children, - large = false, -}: React.PropsWithChildren) { - return ( - - - - ); -} - -function AgentControlBar() { - const { send } = useWsClient(); - const { curAgentState } = useSelector((state: RootState) => state.agent); - - const handleAction = (action: AgentState) => { - if (!IgnoreTaskStateMap[action].includes(curAgentState)) { - send(generateAgentStateChangeEvent(action)); - } - }; - - return ( -
- - {curAgentState === AgentState.PAUSED ? : } - -
- ); -} - -export default AgentControlBar; diff --git a/frontend/src/components/agent-status-bar.tsx b/frontend/src/components/agent-status-bar.tsx deleted file mode 100644 index e2f12b5e63af..000000000000 --- a/frontend/src/components/agent-status-bar.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; -import toast from "react-hot-toast"; -import { I18nKey } from "#/i18n/declaration"; -import { RootState } from "#/store"; -import AgentState from "#/types/agent-state"; -import beep from "#/utils/beep"; - -enum IndicatorColor { - BLUE = "bg-blue-500", - GREEN = "bg-green-500", - ORANGE = "bg-orange-500", - YELLOW = "bg-yellow-500", - RED = "bg-red-500", - DARK_ORANGE = "bg-orange-800", -} - -function AgentStatusBar() { - const { t, i18n } = useTranslation(); - const { curAgentState } = useSelector((state: RootState) => state.agent); - const { curStatusMessage } = useSelector((state: RootState) => state.status); - - const AgentStatusMap: { - [k: string]: { message: string; indicator: IndicatorColor }; - } = { - [AgentState.INIT]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_INIT_MESSAGE), - indicator: IndicatorColor.BLUE, - }, - [AgentState.RUNNING]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_RUNNING_MESSAGE), - indicator: IndicatorColor.GREEN, - }, - [AgentState.AWAITING_USER_INPUT]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE), - indicator: IndicatorColor.ORANGE, - }, - [AgentState.PAUSED]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE), - indicator: IndicatorColor.YELLOW, - }, - [AgentState.LOADING]: { - message: t(I18nKey.CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE), - indicator: IndicatorColor.DARK_ORANGE, - }, - [AgentState.STOPPED]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_STOPPED_MESSAGE), - indicator: IndicatorColor.RED, - }, - [AgentState.FINISHED]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_FINISHED_MESSAGE), - indicator: IndicatorColor.GREEN, - }, - [AgentState.REJECTED]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_REJECTED_MESSAGE), - indicator: IndicatorColor.YELLOW, - }, - [AgentState.ERROR]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE), - indicator: IndicatorColor.RED, - }, - [AgentState.AWAITING_USER_CONFIRMATION]: { - message: t( - I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE, - ), - indicator: IndicatorColor.ORANGE, - }, - [AgentState.USER_CONFIRMED]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE), - indicator: IndicatorColor.GREEN, - }, - [AgentState.USER_REJECTED]: { - message: t(I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE), - indicator: IndicatorColor.RED, - }, - }; - - // TODO: Extend the agent status, e.g.: - // - Agent is typing - // - Agent is initializing - // - Agent is thinking - // - Agent is ready - // - Agent is not available - useEffect(() => { - if ( - curAgentState === AgentState.AWAITING_USER_INPUT || - curAgentState === AgentState.ERROR || - curAgentState === AgentState.INIT - ) { - if (document.cookie.indexOf("audio") !== -1) beep(); - } - }, [curAgentState]); - - const [statusMessage, setStatusMessage] = React.useState(""); - - React.useEffect(() => { - let message = curStatusMessage.message || ""; - if (curStatusMessage?.id) { - const id = curStatusMessage.id.trim(); - if (i18n.exists(id)) { - message = t(curStatusMessage.id.trim()) || message; - } - } - if (curStatusMessage?.type === "error") { - toast.error(message); - return; - } - if (curAgentState === AgentState.LOADING && message.trim()) { - setStatusMessage(message); - } else { - setStatusMessage(AgentStatusMap[curAgentState].message); - } - }, [curStatusMessage.id]); - - React.useEffect(() => { - setStatusMessage(AgentStatusMap[curAgentState].message); - }, [curAgentState]); - - return ( -
-
-
- {statusMessage} -
-
- ); -} - -export default AgentStatusBar; diff --git a/frontend/src/components/agent-status-map.constant.ts b/frontend/src/components/agent-status-map.constant.ts new file mode 100644 index 000000000000..5de965a34196 --- /dev/null +++ b/frontend/src/components/agent-status-map.constant.ts @@ -0,0 +1,64 @@ +import { I18nKey } from "#/i18n/declaration"; +import AgentState from "#/types/agent-state"; + +enum IndicatorColor { + BLUE = "bg-blue-500", + GREEN = "bg-green-500", + ORANGE = "bg-orange-500", + YELLOW = "bg-yellow-500", + RED = "bg-red-500", + DARK_ORANGE = "bg-orange-800", +} + +export const AGENT_STATUS_MAP: { + [k: string]: { message: string; indicator: IndicatorColor }; +} = { + [AgentState.INIT]: { + message: I18nKey.CHAT_INTERFACE$AGENT_INIT_MESSAGE, + indicator: IndicatorColor.BLUE, + }, + [AgentState.RUNNING]: { + message: I18nKey.CHAT_INTERFACE$AGENT_RUNNING_MESSAGE, + indicator: IndicatorColor.GREEN, + }, + [AgentState.AWAITING_USER_INPUT]: { + message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE, + indicator: IndicatorColor.ORANGE, + }, + [AgentState.PAUSED]: { + message: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE, + indicator: IndicatorColor.YELLOW, + }, + [AgentState.LOADING]: { + message: I18nKey.CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE, + indicator: IndicatorColor.DARK_ORANGE, + }, + [AgentState.STOPPED]: { + message: I18nKey.CHAT_INTERFACE$AGENT_STOPPED_MESSAGE, + indicator: IndicatorColor.RED, + }, + [AgentState.FINISHED]: { + message: I18nKey.CHAT_INTERFACE$AGENT_FINISHED_MESSAGE, + indicator: IndicatorColor.GREEN, + }, + [AgentState.REJECTED]: { + message: I18nKey.CHAT_INTERFACE$AGENT_REJECTED_MESSAGE, + indicator: IndicatorColor.YELLOW, + }, + [AgentState.ERROR]: { + message: I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE, + indicator: IndicatorColor.RED, + }, + [AgentState.AWAITING_USER_CONFIRMATION]: { + message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE, + indicator: IndicatorColor.ORANGE, + }, + [AgentState.USER_CONFIRMED]: { + message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE, + indicator: IndicatorColor.GREEN, + }, + [AgentState.USER_REJECTED]: { + message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE, + indicator: IndicatorColor.RED, + }, +}; diff --git a/frontend/src/components/chat/confirmation-buttons.tsx b/frontend/src/components/chat/confirmation-buttons.tsx deleted file mode 100644 index 5e0eaf37c405..000000000000 --- a/frontend/src/components/chat/confirmation-buttons.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Tooltip } from "@nextui-org/react"; -import { useTranslation } from "react-i18next"; -import ConfirmIcon from "#/assets/confirm"; -import RejectIcon from "#/assets/reject"; -import { I18nKey } from "#/i18n/declaration"; -import AgentState from "#/types/agent-state"; -import { generateAgentStateChangeEvent } from "#/services/agent-state-service"; -import { useWsClient } from "#/context/ws-client-provider"; - -interface ActionTooltipProps { - type: "confirm" | "reject"; - onClick: () => void; -} - -function ActionTooltip({ type, onClick }: ActionTooltipProps) { - const { t } = useTranslation(); - - const content = - type === "confirm" - ? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED) - : t(I18nKey.CHAT_INTERFACE$USER_REJECTED); - - return ( - - - - ); -} - -function ConfirmationButtons() { - const { t } = useTranslation(); - const { send } = useWsClient(); - - const handleStateChange = (state: AgentState) => { - const event = generateAgentStateChangeEvent(state); - send(event); - }; - - return ( -
-

{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}

-
- handleStateChange(AgentState.USER_CONFIRMED)} - /> - handleStateChange(AgentState.USER_REJECTED)} - /> -
-
- ); -} - -export default ConfirmationButtons; diff --git a/frontend/src/components/extension-icon-map.constant.tsx b/frontend/src/components/extension-icon-map.constant.tsx new file mode 100644 index 000000000000..990a3061bd1c --- /dev/null +++ b/frontend/src/components/extension-icon-map.constant.tsx @@ -0,0 +1,20 @@ +import { DiJavascript } from "react-icons/di"; +import { + FaCss3, + FaHtml5, + FaList, + FaMarkdown, + FaNpm, + FaPython, +} from "react-icons/fa"; + +export const EXTENSION_ICON_MAP: Record = { + js: , + ts: , + py: , + css: , + json: , + npmignore: , + html: , + md: , +}; diff --git a/frontend/src/components/analytics-consent-form-modal.tsx b/frontend/src/components/features/analytics/analytics-consent-form-modal.tsx similarity index 84% rename from frontend/src/components/analytics-consent-form-modal.tsx rename to frontend/src/components/features/analytics/analytics-consent-form-modal.tsx index 84f336092023..02fabd81aedc 100644 --- a/frontend/src/components/analytics-consent-form-modal.tsx +++ b/frontend/src/components/features/analytics/analytics-consent-form-modal.tsx @@ -1,10 +1,10 @@ -import { ModalBackdrop } from "./modals/modal-backdrop"; -import ModalBody from "./modals/modal-body"; -import ModalButton from "./buttons/modal-button"; +import { ModalButton } from "#/components/shared/buttons/modal-button"; import { BaseModalTitle, BaseModalDescription, -} from "./modals/confirmation-modals/base-modal"; +} from "#/components/shared/modals/confirmation-modals/base-modal"; +import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; +import { ModalBody } from "#/components/shared/modals/modal-body"; import { handleCaptureConsent } from "#/utils/handle-capture-consent"; interface AnalyticsConsentFormModalProps { diff --git a/frontend/src/components/features/browser/browser-snapshot.tsx b/frontend/src/components/features/browser/browser-snapshot.tsx new file mode 100644 index 000000000000..e5fa5e0cc501 --- /dev/null +++ b/frontend/src/components/features/browser/browser-snapshot.tsx @@ -0,0 +1,14 @@ +interface BrowserSnaphsotProps { + src: string; +} + +export function BrowserSnapshot({ src }: BrowserSnaphsotProps) { + return ( + Browser Screenshot + ); +} diff --git a/frontend/src/components/browser.tsx b/frontend/src/components/features/browser/browser.tsx similarity index 52% rename from frontend/src/components/browser.tsx rename to frontend/src/components/features/browser/browser.tsx index 6541ff98ff9f..00bf8e196562 100644 --- a/frontend/src/components/browser.tsx +++ b/frontend/src/components/features/browser/browser.tsx @@ -1,12 +1,9 @@ -import { useTranslation } from "react-i18next"; -import { IoIosGlobe } from "react-icons/io"; import { useSelector } from "react-redux"; -import { I18nKey } from "#/i18n/declaration"; import { RootState } from "#/store"; +import { BrowserSnapshot } from "./browser-snapshot"; +import { EmptyBrowserMessage } from "./empty-browser-message"; -function BrowserPanel() { - const { t } = useTranslation(); - +export function BrowserPanel() { const { url, screenshotSrc } = useSelector( (state: RootState) => state.browser, ); @@ -23,21 +20,11 @@ function BrowserPanel() {
{screenshotSrc ? ( - Browser Screenshot + ) : ( -
- - {t(I18nKey.BROWSER$EMPTY_MESSAGE)} -
+ )}
); } - -export default BrowserPanel; diff --git a/frontend/src/components/features/browser/empty-browser-message.tsx b/frontend/src/components/features/browser/empty-browser-message.tsx new file mode 100644 index 000000000000..bf034bf2c171 --- /dev/null +++ b/frontend/src/components/features/browser/empty-browser-message.tsx @@ -0,0 +1,14 @@ +import { useTranslation } from "react-i18next"; +import { IoIosGlobe } from "react-icons/io"; +import { I18nKey } from "#/i18n/declaration"; + +export function EmptyBrowserMessage() { + const { t } = useTranslation(); + + return ( +
+ + {t(I18nKey.BROWSER$EMPTY_MESSAGE)} +
+ ); +} diff --git a/frontend/src/routes/_oh.app/action-suggestions.tsx b/frontend/src/components/features/chat/action-suggestions.tsx similarity index 96% rename from frontend/src/routes/_oh.app/action-suggestions.tsx rename to frontend/src/components/features/chat/action-suggestions.tsx index fc9442e99606..28feab537b83 100644 --- a/frontend/src/routes/_oh.app/action-suggestions.tsx +++ b/frontend/src/components/features/chat/action-suggestions.tsx @@ -1,6 +1,6 @@ import posthog from "posthog-js"; import React from "react"; -import { SuggestionItem } from "#/components/suggestion-item"; +import { SuggestionItem } from "#/components/features/suggestions/suggestion-item"; import { useAuth } from "#/context/auth-context"; import { downloadWorkspace } from "#/utils/download-workspace"; diff --git a/frontend/src/components/chat-input.tsx b/frontend/src/components/features/chat/chat-input.tsx similarity index 82% rename from frontend/src/components/chat-input.tsx rename to frontend/src/components/features/chat/chat-input.tsx index 96c27fc84d65..815eb1933720 100644 --- a/frontend/src/components/chat-input.tsx +++ b/frontend/src/components/features/chat/chat-input.tsx @@ -1,7 +1,8 @@ import React from "react"; import TextareaAutosize from "react-textarea-autosize"; -import ArrowSendIcon from "#/icons/arrow-send.svg?react"; import { cn } from "#/utils/utils"; +import { SubmitButton } from "#/components/shared/buttons/submit-button"; +import { StopButton } from "#/components/shared/buttons/stop-button"; interface ChatInputProps { name?: string; @@ -132,27 +133,10 @@ export function ChatInput({ {showButton && (
{button === "submit" && ( - + )} {button === "stop" && ( - + )}
)} diff --git a/frontend/src/routes/_oh.app/chat-interface.tsx b/frontend/src/components/features/chat/chat-interface.tsx similarity index 88% rename from frontend/src/routes/_oh.app/chat-interface.tsx rename to frontend/src/components/features/chat/chat-interface.tsx index 8b413c550fdd..d87ded048906 100644 --- a/frontend/src/routes/_oh.app/chat-interface.tsx +++ b/frontend/src/components/features/chat/chat-interface.tsx @@ -2,23 +2,23 @@ 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 "../../components/feedback-actions"; +import { FeedbackActions } from "../feedback/feedback-actions"; import { createChatMessage } from "#/services/chat-service"; -import { InteractiveChatBox } from "../../components/interactive-chat-box"; +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 "../../components/feedback-modal"; +import { FeedbackModal } from "../feedback/feedback-modal"; import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom"; -import TypingIndicator from "../../components/chat/typing-indicator"; -import { ContinueButton } from "../../components/continue-button"; -import { ScrollToBottomButton } from "../../components/scroll-to-bottom-button"; +import { TypingIndicator } from "./typing-indicator"; import { useWsClient } from "#/context/ws-client-provider"; import { Messages } from "./messages"; -import { LoadingSpinner } from "./loading-spinner"; 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"; export function ChatInterface() { const { send, isLoadingMessages } = useWsClient(); @@ -81,7 +81,11 @@ export function ChatInterface() { onScroll={(e) => onChatBodyScroll(e.currentTarget)} className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2" > - {isLoadingMessages && } + {isLoadingMessages && ( +
+ +
+ )} {!isLoadingMessages && ( - + mode={isCopy ? "copied" : "copy"} + /> ); } - -export default TypingIndicator; diff --git a/frontend/src/components/context-menu/account-settings-context-menu.tsx b/frontend/src/components/features/context-menu/account-settings-context-menu.tsx similarity index 100% rename from frontend/src/components/context-menu/account-settings-context-menu.tsx rename to frontend/src/components/features/context-menu/account-settings-context-menu.tsx diff --git a/frontend/src/components/context-menu/context-menu-list-item.tsx b/frontend/src/components/features/context-menu/context-menu-list-item.tsx similarity index 100% rename from frontend/src/components/context-menu/context-menu-list-item.tsx rename to frontend/src/components/features/context-menu/context-menu-list-item.tsx diff --git a/frontend/src/components/context-menu/context-menu-separator.tsx b/frontend/src/components/features/context-menu/context-menu-separator.tsx similarity index 100% rename from frontend/src/components/context-menu/context-menu-separator.tsx rename to frontend/src/components/features/context-menu/context-menu-separator.tsx diff --git a/frontend/src/components/context-menu/context-menu.tsx b/frontend/src/components/features/context-menu/context-menu.tsx similarity index 100% rename from frontend/src/components/context-menu/context-menu.tsx rename to frontend/src/components/features/context-menu/context-menu.tsx diff --git a/frontend/src/components/features/controls/agent-control-bar.tsx b/frontend/src/components/features/controls/agent-control-bar.tsx new file mode 100644 index 000000000000..dad718533f6e --- /dev/null +++ b/frontend/src/components/features/controls/agent-control-bar.tsx @@ -0,0 +1,44 @@ +import { useSelector } from "react-redux"; +import PauseIcon from "#/assets/pause"; +import PlayIcon from "#/assets/play"; +import { generateAgentStateChangeEvent } from "#/services/agent-state-service"; +import { RootState } from "#/store"; +import AgentState from "#/types/agent-state"; +import { useWsClient } from "#/context/ws-client-provider"; +import { IGNORE_TASK_STATE_MAP } from "#/ignore-task-state-map.constant"; +import { ActionButton } from "#/components/shared/buttons/action-button"; + +export function AgentControlBar() { + const { send } = useWsClient(); + const { curAgentState } = useSelector((state: RootState) => state.agent); + + const handleAction = (action: AgentState) => { + if (!IGNORE_TASK_STATE_MAP[action].includes(curAgentState)) { + send(generateAgentStateChangeEvent(action)); + } + }; + + return ( +
+ + {curAgentState === AgentState.PAUSED ? : } + +
+ ); +} diff --git a/frontend/src/components/features/controls/agent-status-bar.tsx b/frontend/src/components/features/controls/agent-status-bar.tsx new file mode 100644 index 000000000000..62511897151b --- /dev/null +++ b/frontend/src/components/features/controls/agent-status-bar.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import toast from "react-hot-toast"; +import { RootState } from "#/store"; +import AgentState from "#/types/agent-state"; +import { AGENT_STATUS_MAP } from "../../agent-status-map.constant"; + +export function AgentStatusBar() { + const { t, i18n } = useTranslation(); + const { curAgentState } = useSelector((state: RootState) => state.agent); + const { curStatusMessage } = useSelector((state: RootState) => state.status); + + const [statusMessage, setStatusMessage] = React.useState(""); + + const updateStatusMessage = () => { + let message = curStatusMessage.message || ""; + if (curStatusMessage?.id) { + const id = curStatusMessage.id.trim(); + if (i18n.exists(id)) { + message = t(curStatusMessage.id.trim()) || message; + } + } + if (curStatusMessage?.type === "error") { + toast.error(message); + return; + } + if (curAgentState === AgentState.LOADING && message.trim()) { + setStatusMessage(message); + } else { + setStatusMessage(AGENT_STATUS_MAP[curAgentState].message); + } + }; + + React.useEffect(() => { + updateStatusMessage(); + }, [curStatusMessage.id]); + + React.useEffect(() => { + setStatusMessage(AGENT_STATUS_MAP[curAgentState].message); + }, [curAgentState]); + + return ( +
+
+
+ {t(statusMessage)} +
+
+ ); +} diff --git a/frontend/src/components/controls.tsx b/frontend/src/components/features/controls/controls.tsx similarity index 72% rename from frontend/src/components/controls.tsx rename to frontend/src/components/features/controls/controls.tsx index 41ebc1328eac..d7c3fc7673e1 100644 --- a/frontend/src/components/controls.tsx +++ b/frontend/src/components/features/controls/controls.tsx @@ -1,11 +1,11 @@ -import { IoLockClosed } from "react-icons/io5"; import React from "react"; import { useSelector } from "react-redux"; -import AgentControlBar from "./agent-control-bar"; -import AgentStatusBar from "./agent-status-bar"; -import { ProjectMenuCard } from "./project-menu/ProjectMenuCard"; +import { AgentControlBar } from "./agent-control-bar"; +import { AgentStatusBar } from "./agent-status-bar"; +import { ProjectMenuCard } from "../project-menu/ProjectMenuCard"; import { useAuth } from "#/context/auth-context"; import { RootState } from "#/store"; +import { SecurityLock } from "./security-lock"; interface ControlsProps { setSecurityOpen: (isOpen: boolean) => void; @@ -42,13 +42,7 @@ export function Controls({ {showSecurityLock && ( -
setSecurityOpen(true)} - > - -
+ setSecurityOpen(true)} /> )}
diff --git a/frontend/src/components/features/controls/security-lock.tsx b/frontend/src/components/features/controls/security-lock.tsx new file mode 100644 index 000000000000..3fe4c8010abe --- /dev/null +++ b/frontend/src/components/features/controls/security-lock.tsx @@ -0,0 +1,17 @@ +import { IoLockClosed } from "react-icons/io5"; + +interface SecurityLockProps { + onClick: () => void; +} + +export function SecurityLock({ onClick }: SecurityLockProps) { + return ( +
+ +
+ ); +} diff --git a/frontend/src/routes/_oh.app._index/code-editor-component.tsx b/frontend/src/components/features/editor/code-editor-component.tsx similarity index 100% rename from frontend/src/routes/_oh.app._index/code-editor-component.tsx rename to frontend/src/components/features/editor/code-editor-component.tsx diff --git a/frontend/src/components/editor-actions.tsx b/frontend/src/components/features/editor/editor-actions.tsx similarity index 50% rename from frontend/src/components/editor-actions.tsx rename to frontend/src/components/features/editor/editor-actions.tsx index e66e9553e6f4..1f3accf6252a 100644 --- a/frontend/src/components/editor-actions.tsx +++ b/frontend/src/components/features/editor/editor-actions.tsx @@ -1,33 +1,4 @@ -import { cn } from "@nextui-org/react"; -import { HTMLAttributes } from "react"; - -interface EditorActionButtonProps { - onClick: () => void; - disabled: boolean; - className: HTMLAttributes["className"]; -} - -function EditorActionButton({ - onClick, - disabled, - className, - children, -}: React.PropsWithChildren) { - return ( - - ); -} +import { EditorActionButton } from "#/components/shared/buttons/editor-action-button"; interface EditorActionsProps { onSave: () => void; diff --git a/frontend/src/components/feedback-actions.tsx b/frontend/src/components/features/feedback/feedback-actions.tsx similarity index 63% rename from frontend/src/components/feedback-actions.tsx rename to frontend/src/components/features/feedback/feedback-actions.tsx index 05a503f0bdfd..2293789c201f 100644 --- a/frontend/src/components/feedback-actions.tsx +++ b/frontend/src/components/features/feedback/feedback-actions.tsx @@ -1,28 +1,6 @@ import ThumbsUpIcon from "#/icons/thumbs-up.svg?react"; import ThumbDownIcon from "#/icons/thumbs-down.svg?react"; - -interface FeedbackActionButtonProps { - testId?: string; - onClick: () => void; - icon: React.ReactNode; -} - -function FeedbackActionButton({ - testId, - onClick, - icon, -}: FeedbackActionButtonProps) { - return ( - - ); -} +import { FeedbackActionButton } from "#/components/shared/buttons/feedback-action-button"; interface FeedbackActionsProps { onPositiveFeedback: () => void; diff --git a/frontend/src/components/feedback-form.tsx b/frontend/src/components/features/feedback/feedback-form.tsx similarity index 98% rename from frontend/src/components/feedback-form.tsx rename to frontend/src/components/features/feedback/feedback-form.tsx index ca3645817b43..31705a101493 100644 --- a/frontend/src/components/feedback-form.tsx +++ b/frontend/src/components/features/feedback/feedback-form.tsx @@ -1,8 +1,8 @@ import React from "react"; import hotToast from "react-hot-toast"; -import ModalButton from "./buttons/modal-button"; import { Feedback } from "#/api/open-hands.types"; import { useSubmitFeedback } from "#/hooks/mutation/use-submit-feedback"; +import { ModalButton } from "#/components/shared/buttons/modal-button"; const FEEDBACK_VERSION = "1.0"; const VIEWER_PAGE = "https://www.all-hands.dev/share"; diff --git a/frontend/src/components/feedback-modal.tsx b/frontend/src/components/features/feedback/feedback-modal.tsx similarity index 78% rename from frontend/src/components/feedback-modal.tsx rename to frontend/src/components/features/feedback/feedback-modal.tsx index e28d072fa6cc..a3e9d6264f56 100644 --- a/frontend/src/components/feedback-modal.tsx +++ b/frontend/src/components/features/feedback/feedback-modal.tsx @@ -1,10 +1,10 @@ -import { FeedbackForm } from "./feedback-form"; import { BaseModalTitle, BaseModalDescription, -} from "./modals/confirmation-modals/base-modal"; -import { ModalBackdrop } from "./modals/modal-backdrop"; -import ModalBody from "./modals/modal-body"; +} from "#/components/shared/modals/confirmation-modals/base-modal"; +import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; +import { ModalBody } from "#/components/shared/modals/modal-body"; +import { FeedbackForm } from "./feedback-form"; interface FeedbackModalProps { onClose: () => void; diff --git a/frontend/src/routes/_oh.app._index/file-explorer/dropzone.tsx b/frontend/src/components/features/file-explorer/dropzone.tsx similarity index 100% rename from frontend/src/routes/_oh.app._index/file-explorer/dropzone.tsx rename to frontend/src/components/features/file-explorer/dropzone.tsx diff --git a/frontend/src/components/file-explorer/explorer-tree.tsx b/frontend/src/components/features/file-explorer/explorer-tree.tsx similarity index 86% rename from frontend/src/components/file-explorer/explorer-tree.tsx rename to frontend/src/components/features/file-explorer/explorer-tree.tsx index 30a04eb3699f..01ddea816d40 100644 --- a/frontend/src/components/file-explorer/explorer-tree.tsx +++ b/frontend/src/components/features/file-explorer/explorer-tree.tsx @@ -7,7 +7,10 @@ interface ExplorerTreeProps { defaultOpen?: boolean; } -function ExplorerTree({ files, defaultOpen = false }: ExplorerTreeProps) { +export function ExplorerTree({ + files, + defaultOpen = false, +}: ExplorerTreeProps) { const { t } = useTranslation(); if (!files?.length) { const message = !files @@ -23,5 +26,3 @@ function ExplorerTree({ files, defaultOpen = false }: ExplorerTreeProps) { ); } - -export default ExplorerTree; diff --git a/frontend/src/routes/_oh.app._index/file-explorer/file-explorer-actions.tsx b/frontend/src/components/features/file-explorer/file-explorer-actions.tsx similarity index 71% rename from frontend/src/routes/_oh.app._index/file-explorer/file-explorer-actions.tsx rename to frontend/src/components/features/file-explorer/file-explorer-actions.tsx index 7fb9552ba2f3..92698b87eabd 100644 --- a/frontend/src/routes/_oh.app._index/file-explorer/file-explorer-actions.tsx +++ b/frontend/src/components/features/file-explorer/file-explorer-actions.tsx @@ -1,7 +1,7 @@ +import { RefreshIconButton } from "#/components/shared/buttons/refresh-icon-button"; +import { ToggleWorkspaceIconButton } from "#/components/shared/buttons/toggle-workspace-icon-button"; +import { UploadIconButton } from "#/components/shared/buttons/upload-icon-button"; import { cn } from "#/utils/utils"; -import { RefreshIconButton } from "./buttons/refresh-icon-button"; -import { ToggleWorkspaceIconButton } from "./buttons/toggle-workspace-icon-button"; -import { UploadIconButton } from "./buttons/upload-icon-button"; interface ExplorerActionsProps { onRefresh: () => void; diff --git a/frontend/src/routes/_oh.app._index/file-explorer/file-explorer-header.tsx b/frontend/src/components/features/file-explorer/file-explorer-header.tsx similarity index 100% rename from frontend/src/routes/_oh.app._index/file-explorer/file-explorer-header.tsx rename to frontend/src/components/features/file-explorer/file-explorer-header.tsx diff --git a/frontend/src/routes/_oh.app._index/file-explorer/file-explorer.tsx b/frontend/src/components/features/file-explorer/file-explorer.tsx similarity index 96% rename from frontend/src/routes/_oh.app._index/file-explorer/file-explorer.tsx rename to frontend/src/components/features/file-explorer/file-explorer.tsx index a7979cacd381..b033cfb62126 100644 --- a/frontend/src/routes/_oh.app._index/file-explorer/file-explorer.tsx +++ b/frontend/src/components/features/file-explorer/file-explorer.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import AgentState from "#/types/agent-state"; -import ExplorerTree from "../../../components/file-explorer/explorer-tree"; +import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree"; import toast from "#/utils/toast"; import { RootState } from "#/store"; import { I18nKey } from "#/i18n/declaration"; @@ -10,10 +10,10 @@ import { useListFiles } from "#/hooks/query/use-list-files"; import { FileUploadSuccessResponse } from "#/api/open-hands.types"; import { useUploadFiles } from "#/hooks/mutation/use-upload-files"; import { cn } from "#/utils/utils"; -import { OpenVSCodeButton } from "./buttons/open-vscode-button"; import { Dropzone } from "./dropzone"; import { FileExplorerHeader } from "./file-explorer-header"; import { useVSCodeUrl } from "#/hooks/query/use-vscode-url"; +import { OpenVSCodeButton } from "#/components/shared/buttons/open-vscode-button"; interface FileExplorerProps { isOpen: boolean; diff --git a/frontend/src/components/features/file-explorer/file-icon.tsx b/frontend/src/components/features/file-explorer/file-icon.tsx new file mode 100644 index 000000000000..bd6486a03558 --- /dev/null +++ b/frontend/src/components/features/file-explorer/file-icon.tsx @@ -0,0 +1,12 @@ +import { FaFile } from "react-icons/fa"; +import { getExtension } from "#/utils/utils"; +import { EXTENSION_ICON_MAP } from "../../extension-icon-map.constant"; + +interface FileIconProps { + filename: string; +} + +export function FileIcon({ filename }: FileIconProps) { + const extension = getExtension(filename); + return EXTENSION_ICON_MAP[extension] || ; +} diff --git a/frontend/src/components/features/file-explorer/filename.tsx b/frontend/src/components/features/file-explorer/filename.tsx new file mode 100644 index 000000000000..179fe84d860b --- /dev/null +++ b/frontend/src/components/features/file-explorer/filename.tsx @@ -0,0 +1,20 @@ +import { FolderIcon } from "./folder-icon"; +import { FileIcon } from "./file-icon"; + +interface FilenameProps { + name: string; + type: "folder" | "file"; + isOpen: boolean; +} + +export function Filename({ name, type, isOpen }: FilenameProps) { + return ( +
+
+ {type === "folder" && } + {type === "file" && } +
+
{name}
+
+ ); +} diff --git a/frontend/src/components/folder-icon.tsx b/frontend/src/components/features/file-explorer/folder-icon.tsx similarity index 72% rename from frontend/src/components/folder-icon.tsx rename to frontend/src/components/features/file-explorer/folder-icon.tsx index 3491363ba6ed..e08597c21d19 100644 --- a/frontend/src/components/folder-icon.tsx +++ b/frontend/src/components/features/file-explorer/folder-icon.tsx @@ -4,12 +4,10 @@ interface FolderIconProps { isOpen: boolean; } -function FolderIcon({ isOpen }: FolderIconProps): JSX.Element { +export function FolderIcon({ isOpen }: FolderIconProps): JSX.Element { return isOpen ? ( ) : ( ); } - -export default FolderIcon; diff --git a/frontend/src/components/file-explorer/tree-node.tsx b/frontend/src/components/features/file-explorer/tree-node.tsx similarity index 76% rename from frontend/src/components/file-explorer/tree-node.tsx rename to frontend/src/components/features/file-explorer/tree-node.tsx index 5300cae19868..524f56578e1c 100644 --- a/frontend/src/components/file-explorer/tree-node.tsx +++ b/frontend/src/components/features/file-explorer/tree-node.tsx @@ -1,28 +1,10 @@ import React from "react"; -import FolderIcon from "../folder-icon"; -import FileIcon from "../file-icons"; + import { useFiles } from "#/context/files"; import { cn } from "#/utils/utils"; import { useListFiles } from "#/hooks/query/use-list-files"; import { useListFile } from "#/hooks/query/use-list-file"; - -interface TitleProps { - name: string; - type: "folder" | "file"; - isOpen: boolean; -} - -function Title({ name, type, isOpen }: TitleProps) { - return ( -
-
- {type === "folder" && } - {type === "file" && } -
-
{name}
-
- ); -} +import { Filename } from "./filename"; interface TreeNodeProps { path: string; @@ -83,7 +65,7 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) { onClick={handleClick} className="flex items-center justify-between w-full px-1" > - void; diff --git a/frontend/src/components/attach-image-label.tsx b/frontend/src/components/features/images/attach-image-label.tsx similarity index 100% rename from frontend/src/components/attach-image-label.tsx rename to frontend/src/components/features/images/attach-image-label.tsx diff --git a/frontend/src/components/image-carousel.tsx b/frontend/src/components/features/images/image-carousel.tsx similarity index 100% rename from frontend/src/components/image-carousel.tsx rename to frontend/src/components/features/images/image-carousel.tsx diff --git a/frontend/src/components/features/images/image-preview.tsx b/frontend/src/components/features/images/image-preview.tsx new file mode 100644 index 000000000000..37dc314a4be8 --- /dev/null +++ b/frontend/src/components/features/images/image-preview.tsx @@ -0,0 +1,21 @@ +import { RemoveButton } from "#/components/shared/buttons/remove-button"; +import { Thumbnail } from "./thumbnail"; + +interface ImagePreviewProps { + src: string; + onRemove?: () => void; + size?: "small" | "large"; +} + +export function ImagePreview({ + src, + onRemove, + size = "small", +}: ImagePreviewProps) { + return ( + <div data-testid="image-preview" className="relative w-fit shrink-0"> + <Thumbnail src={src} size={size} /> + {onRemove && <RemoveButton onClick={onRemove} />} + </div> + ); +} diff --git a/frontend/src/components/features/images/thumbnail.tsx b/frontend/src/components/features/images/thumbnail.tsx new file mode 100644 index 000000000000..5dc0818c2873 --- /dev/null +++ b/frontend/src/components/features/images/thumbnail.tsx @@ -0,0 +1,21 @@ +import { cn } from "#/utils/utils"; + +interface ThumbnailProps { + src: string; + size?: "small" | "large"; +} + +export function Thumbnail({ src, size = "small" }: ThumbnailProps) { + return ( + <img + role="img" + alt="" + src={src} + className={cn( + "rounded object-cover", + size === "small" && "w-[62px] h-[62px]", + size === "large" && "w-[100px] h-[100px]", + )} + /> + ); +} diff --git a/frontend/src/components/upload-image-input.tsx b/frontend/src/components/features/images/upload-image-input.tsx similarity index 100% rename from frontend/src/components/upload-image-input.tsx rename to frontend/src/components/features/images/upload-image-input.tsx diff --git a/frontend/src/components/features/jupyter/jupyter-cell-input.tsx b/frontend/src/components/features/jupyter/jupyter-cell-input.tsx new file mode 100644 index 000000000000..c69651d105d8 --- /dev/null +++ b/frontend/src/components/features/jupyter/jupyter-cell-input.tsx @@ -0,0 +1,22 @@ +import SyntaxHighlighter from "react-syntax-highlighter"; +import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; + +interface JupytrerCellInputProps { + code: string; +} + +export function JupytrerCellInput({ code }: JupytrerCellInputProps) { + return ( + <div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs"> + <div className="mb-1 text-gray-400">EXECUTE</div> + <pre + className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5" + style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }} + > + <SyntaxHighlighter language="python" style={atomOneDark} wrapLongLines> + {code} + </SyntaxHighlighter> + </pre> + </div> + ); +} diff --git a/frontend/src/components/features/jupyter/jupyter-cell-output.tsx b/frontend/src/components/features/jupyter/jupyter-cell-output.tsx new file mode 100644 index 000000000000..e1d80e68e94b --- /dev/null +++ b/frontend/src/components/features/jupyter/jupyter-cell-output.tsx @@ -0,0 +1,40 @@ +import Markdown from "react-markdown"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; +import { JupyterLine } from "#/utils/parse-cell-content"; + +interface JupyterCellOutputProps { + lines: JupyterLine[]; +} + +export function JupyterCellOutput({ lines }: JupyterCellOutputProps) { + return ( + <div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs"> + <div className="mb-1 text-gray-400">STDOUT/STDERR</div> + <pre + className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5 max-h-[60vh] bg-gray-800" + style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }} + > + {/* display the lines as plaintext or image */} + {lines.map((line, index) => { + if (line.type === "image") { + return ( + <div key={index}> + <Markdown urlTransform={(value: string) => value}> + {line.content} + </Markdown> + </div> + ); + } + return ( + <div key={index}> + <SyntaxHighlighter language="plaintext" style={atomOneDark}> + {line.content} + </SyntaxHighlighter> + </div> + ); + })} + </pre> + </div> + ); +} diff --git a/frontend/src/components/features/jupyter/jupyter-cell.tsx b/frontend/src/components/features/jupyter/jupyter-cell.tsx new file mode 100644 index 000000000000..a6a99837f1b7 --- /dev/null +++ b/frontend/src/components/features/jupyter/jupyter-cell.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { Cell } from "#/state/jupyter-slice"; +import { JupyterLine, parseCellContent } from "#/utils/parse-cell-content"; +import { JupytrerCellInput } from "./jupyter-cell-input"; +import { JupyterCellOutput } from "./jupyter-cell-output"; + +interface JupyterCellProps { + cell: Cell; +} + +export function JupyterCell({ cell }: JupyterCellProps) { + const [lines, setLines] = React.useState<JupyterLine[]>([]); + + React.useEffect(() => { + setLines(parseCellContent(cell.content)); + }, [cell.content]); + + if (cell.type === "input") { + return <JupytrerCellInput code={cell.content} />; + } + + return <JupyterCellOutput lines={lines} />; +} diff --git a/frontend/src/components/features/jupyter/jupyter.tsx b/frontend/src/components/features/jupyter/jupyter.tsx new file mode 100644 index 000000000000..8ae871c520d0 --- /dev/null +++ b/frontend/src/components/features/jupyter/jupyter.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "#/store"; +import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom"; +import { JupyterCell } from "./jupyter-cell"; +import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button"; + +interface JupyterEditorProps { + maxWidth: number; +} + +export function JupyterEditor({ maxWidth }: JupyterEditorProps) { + const { cells } = useSelector((state: RootState) => state.jupyter); + const jupyterRef = React.useRef<HTMLDivElement>(null); + + const { hitBottom, scrollDomToBottom, onChatBodyScroll } = + useScrollToBottom(jupyterRef); + + return ( + <div className="flex-1" style={{ maxWidth }}> + <div + className="overflow-y-auto h-full" + ref={jupyterRef} + onScroll={(e) => onChatBodyScroll(e.currentTarget)} + > + {cells.map((cell, index) => ( + <JupyterCell key={index} cell={cell} /> + ))} + </div> + {!hitBottom && ( + <div className="sticky bottom-2 flex items-center justify-center"> + <ScrollToBottomButton onClick={scrollDomToBottom} /> + </div> + )} + </div> + ); +} diff --git a/frontend/src/components/markdown/code.tsx b/frontend/src/components/features/markdown/code.tsx similarity index 100% rename from frontend/src/components/markdown/code.tsx rename to frontend/src/components/features/markdown/code.tsx diff --git a/frontend/src/components/markdown/list.tsx b/frontend/src/components/features/markdown/list.tsx similarity index 100% rename from frontend/src/components/markdown/list.tsx rename to frontend/src/components/features/markdown/list.tsx diff --git a/frontend/src/components/project-menu/ProjectMenuCard.tsx b/frontend/src/components/features/project-menu/ProjectMenuCard.tsx similarity index 93% rename from frontend/src/components/project-menu/ProjectMenuCard.tsx rename to frontend/src/components/features/project-menu/ProjectMenuCard.tsx index e61ec954c059..159edb9b5610 100644 --- a/frontend/src/components/project-menu/ProjectMenuCard.tsx +++ b/frontend/src/components/features/project-menu/ProjectMenuCard.tsx @@ -3,16 +3,16 @@ import { useDispatch } from "react-redux"; import toast from "react-hot-toast"; import posthog from "posthog-js"; import EllipsisH from "#/icons/ellipsis-h.svg?react"; -import { ModalBackdrop } from "../modals/modal-backdrop"; -import { ConnectToGitHubModal } from "../modals/connect-to-github-modal"; import { addUserMessage } from "#/state/chat-slice"; import { createChatMessage } from "#/services/chat-service"; import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu"; import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder"; import { ProjectMenuDetails } from "./project-menu-details"; import { downloadWorkspace } from "#/utils/download-workspace"; -import { LoadingSpinner } from "../modals/loading-project"; import { useWsClient } from "#/context/ws-client-provider"; +import { LoadingSpinner } from "#/components/shared/loading-spinner"; +import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal"; +import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; interface ProjectMenuCardProps { isConnectedToGitHub: boolean; diff --git a/frontend/src/components/project-menu/project-menu-details-placeholder.tsx b/frontend/src/components/features/project-menu/project-menu-details-placeholder.tsx similarity index 100% rename from frontend/src/components/project-menu/project-menu-details-placeholder.tsx rename to frontend/src/components/features/project-menu/project-menu-details-placeholder.tsx diff --git a/frontend/src/components/project-menu/project-menu-details.tsx b/frontend/src/components/features/project-menu/project-menu-details.tsx similarity index 100% rename from frontend/src/components/project-menu/project-menu-details.tsx rename to frontend/src/components/features/project-menu/project-menu-details.tsx diff --git a/frontend/src/components/project-menu/project.menu-card-context-menu.tsx b/frontend/src/components/features/project-menu/project.menu-card-context-menu.tsx similarity index 100% rename from frontend/src/components/project-menu/project.menu-card-context-menu.tsx rename to frontend/src/components/features/project-menu/project.menu-card-context-menu.tsx diff --git a/frontend/src/components/features/sidebar/avatar.tsx b/frontend/src/components/features/sidebar/avatar.tsx new file mode 100644 index 000000000000..394087c91052 --- /dev/null +++ b/frontend/src/components/features/sidebar/avatar.tsx @@ -0,0 +1,9 @@ +interface AvatarProps { + src: string; +} + +export function Avatar({ src }: AvatarProps) { + return ( + <img src={src} alt="user avatar" className="w-full h-full rounded-full" /> + ); +} diff --git a/frontend/src/routes/_oh/sidebar.tsx b/frontend/src/components/features/sidebar/sidebar.tsx similarity index 79% rename from frontend/src/routes/_oh/sidebar.tsx rename to frontend/src/components/features/sidebar/sidebar.tsx index b02974fa9e13..f5345b2af2a9 100644 --- a/frontend/src/routes/_oh/sidebar.tsx +++ b/frontend/src/components/features/sidebar/sidebar.tsx @@ -1,18 +1,18 @@ import React from "react"; import { useLocation } from "react-router-dom"; -import { LoadingSpinner } from "#/components/modals/loading-project"; -import { UserActions } from "#/components/user-actions"; import { useAuth } from "#/context/auth-context"; import { useUserPrefs } from "#/context/user-prefs-context"; import { useGitHubUser } from "#/hooks/query/use-github-user"; import { useIsAuthed } from "#/hooks/query/use-is-authed"; -import { SettingsModal } from "./modals/settings-modal"; -import { ExitProjectConfirmationModal } from "./modals/exit-project-confirmation-modal"; -import { AllHandsLogoButton } from "./buttons/all-hands-logo-button"; -import { SettingsButton } from "./buttons/settings-button"; -import { DocsButton } from "./buttons/docs-button"; -import { ExitProjectButton } from "./buttons/exit-project-button"; -import { AccountSettingsModal } from "./modals/account-settings-modal"; +import { UserActions } from "./user-actions"; +import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button"; +import { DocsButton } from "#/components/shared/buttons/docs-button"; +import { ExitProjectButton } from "#/components/shared/buttons/exit-project-button"; +import { SettingsButton } from "#/components/shared/buttons/settings-button"; +import { LoadingSpinner } from "#/components/shared/loading-spinner"; +import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal"; +import { ExitProjectConfirmationModal } from "#/components/shared/modals/exit-project-confirmation-modal"; +import { SettingsModal } from "#/components/shared/modals/settings/settings-modal"; export function Sidebar() { const location = useLocation(); diff --git a/frontend/src/components/user-actions.tsx b/frontend/src/components/features/sidebar/user-actions.tsx similarity index 92% rename from frontend/src/components/user-actions.tsx rename to frontend/src/components/features/sidebar/user-actions.tsx index c3bfc4bd02e4..359c8fbbb314 100644 --- a/frontend/src/components/user-actions.tsx +++ b/frontend/src/components/features/sidebar/user-actions.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { AccountSettingsContextMenu } from "./context-menu/account-settings-context-menu"; import { UserAvatar } from "./user-avatar"; +import { AccountSettingsContextMenu } from "../context-menu/account-settings-context-menu"; interface UserActionsProps { onClickAccountSettings: () => void; diff --git a/frontend/src/components/user-avatar.tsx b/frontend/src/components/features/sidebar/user-avatar.tsx similarity index 77% rename from frontend/src/components/user-avatar.tsx rename to frontend/src/components/features/sidebar/user-avatar.tsx index 17433cde463e..58a4af17386a 100644 --- a/frontend/src/components/user-avatar.tsx +++ b/frontend/src/components/features/sidebar/user-avatar.tsx @@ -1,6 +1,7 @@ -import { LoadingSpinner } from "./modals/loading-project"; +import { LoadingSpinner } from "#/components/shared/loading-spinner"; import DefaultUserAvatar from "#/icons/default-user.svg?react"; import { cn } from "#/utils/utils"; +import { Avatar } from "./avatar"; interface UserAvatarProps { onClick: () => void; @@ -19,13 +20,7 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) { isLoading && "bg-transparent", )} > - {!isLoading && avatarUrl && ( - <img - src={avatarUrl} - alt="user avatar" - className="w-full h-full rounded-full" - /> - )} + {!isLoading && avatarUrl && <Avatar src={avatarUrl} />} {!isLoading && !avatarUrl && ( <DefaultUserAvatar aria-label="user avatar placeholder" diff --git a/frontend/src/routes/_oh._index/import-project-suggestion-box.tsx b/frontend/src/components/features/suggestions/import-project-suggestion-box.tsx similarity index 100% rename from frontend/src/routes/_oh._index/import-project-suggestion-box.tsx rename to frontend/src/components/features/suggestions/import-project-suggestion-box.tsx diff --git a/frontend/src/routes/_oh._index/suggestion-box.tsx b/frontend/src/components/features/suggestions/suggestion-box.tsx similarity index 100% rename from frontend/src/routes/_oh._index/suggestion-box.tsx rename to frontend/src/components/features/suggestions/suggestion-box.tsx diff --git a/frontend/src/components/suggestion-bubble.tsx b/frontend/src/components/features/suggestions/suggestion-bubble.tsx similarity index 71% rename from frontend/src/components/suggestion-bubble.tsx rename to frontend/src/components/features/suggestions/suggestion-bubble.tsx index 00fc1b7d0d6b..5679799c6ed1 100644 --- a/frontend/src/components/suggestion-bubble.tsx +++ b/frontend/src/components/features/suggestions/suggestion-bubble.tsx @@ -1,5 +1,5 @@ +import { RefreshButton } from "#/components/shared/buttons/refresh-button"; import Lightbulb from "#/icons/lightbulb.svg?react"; -import Refresh from "#/icons/refresh.svg?react"; interface SuggestionBubbleProps { suggestion: string; @@ -12,6 +12,11 @@ export function SuggestionBubble({ onClick, onRefresh, }: SuggestionBubbleProps) { + const handleRefresh = (e: React.MouseEvent<HTMLButtonElement>) => { + e.stopPropagation(); + onRefresh(); + }; + return ( <div onClick={onClick} @@ -21,15 +26,7 @@ export function SuggestionBubble({ <Lightbulb width={18} height={18} /> <span className="text-sm">{suggestion}</span> </div> - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - onRefresh(); - }} - > - <Refresh width={14} height={14} /> - </button> + <RefreshButton onClick={handleRefresh} /> </div> ); } diff --git a/frontend/src/components/suggestion-item.tsx b/frontend/src/components/features/suggestions/suggestion-item.tsx similarity index 100% rename from frontend/src/components/suggestion-item.tsx rename to frontend/src/components/features/suggestions/suggestion-item.tsx diff --git a/frontend/src/components/suggestions.tsx b/frontend/src/components/features/suggestions/suggestions.tsx similarity index 100% rename from frontend/src/components/suggestions.tsx rename to frontend/src/components/features/suggestions/suggestions.tsx diff --git a/frontend/src/components/terminal/terminal.tsx b/frontend/src/components/features/terminal/terminal.tsx similarity index 89% rename from frontend/src/components/terminal/terminal.tsx rename to frontend/src/components/features/terminal/terminal.tsx index dac0dc73d8b6..bd8283b8903b 100644 --- a/frontend/src/components/terminal/terminal.tsx +++ b/frontend/src/components/features/terminal/terminal.tsx @@ -1,6 +1,6 @@ import { useSelector } from "react-redux"; import { RootState } from "#/store"; -import { useTerminal } from "../../hooks/use-terminal"; +import { useTerminal } from "#/hooks/use-terminal"; import "@xterm/xterm/css/xterm.css"; diff --git a/frontend/src/components/features/waitlist/join-waitlist-anchor.tsx b/frontend/src/components/features/waitlist/join-waitlist-anchor.tsx new file mode 100644 index 000000000000..b1ca5e0223c3 --- /dev/null +++ b/frontend/src/components/features/waitlist/join-waitlist-anchor.tsx @@ -0,0 +1,12 @@ +export function JoinWaitlistAnchor() { + return ( + <a + href="https://www.all-hands.dev/join-waitlist" + target="_blank" + rel="noreferrer" + className="rounded bg-[#FFE165] text-black text-sm font-bold py-[10px] w-full text-center hover:opacity-80" + > + Join Waitlist + </a> + ); +} diff --git a/frontend/src/components/features/waitlist/waitlist-message.tsx b/frontend/src/components/features/waitlist/waitlist-message.tsx new file mode 100644 index 000000000000..2c66453360c8 --- /dev/null +++ b/frontend/src/components/features/waitlist/waitlist-message.tsx @@ -0,0 +1,35 @@ +interface WaitlistMessageProps { + content: "waitlist" | "sign-in"; +} + +export function WaitlistMessage({ content }: WaitlistMessageProps) { + return ( + <div className="flex flex-col gap-2 w-full items-center text-center"> + <h1 className="text-2xl font-bold"> + {content === "sign-in" && "Sign in with GitHub"} + {content === "waitlist" && "Just a little longer!"} + </h1> + {content === "sign-in" && ( + <p> + or{" "} + <a + href="https://www.all-hands.dev/join-waitlist" + target="_blank" + rel="noreferrer noopener" + className="text-blue-500 hover:underline underline-offset-2" + > + join the waitlist + </a>{" "} + if you haven't already + </p> + )} + {content === "waitlist" && ( + <p className="text-sm"> + Thanks for your patience! We're accepting new members + progressively. If you haven't joined the waitlist yet, now's + the time! + </p> + )} + </div> + ); +} diff --git a/frontend/src/components/features/waitlist/waitlist-modal.tsx b/frontend/src/components/features/waitlist/waitlist-modal.tsx new file mode 100644 index 000000000000..c8ee583fbef0 --- /dev/null +++ b/frontend/src/components/features/waitlist/waitlist-modal.tsx @@ -0,0 +1,37 @@ +import { ModalBody } from "@nextui-org/react"; +import GitHubLogo from "#/assets/branding/github-logo.svg?react"; +import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react"; +import { JoinWaitlistAnchor } from "./join-waitlist-anchor"; +import { WaitlistMessage } from "./waitlist-message"; +import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; +import { ModalButton } from "#/components/shared/buttons/modal-button"; + +interface WaitlistModalProps { + ghToken: string | null; + githubAuthUrl: string | null; +} + +export function WaitlistModal({ ghToken, githubAuthUrl }: WaitlistModalProps) { + return ( + <ModalBackdrop> + <ModalBody> + <AllHandsLogo width={68} height={46} /> + <WaitlistMessage content={ghToken ? "waitlist" : "sign-in"} /> + + {!ghToken && ( + <ModalButton + text="Connect to GitHub" + icon={<GitHubLogo width={20} height={20} />} + className="bg-[#791B80] w-full" + onClick={() => { + if (githubAuthUrl) { + window.location.href = githubAuthUrl; + } + }} + /> + )} + {ghToken && <JoinWaitlistAnchor />} + </ModalBody> + </ModalBackdrop> + ); +} diff --git a/frontend/src/components/file-icons.tsx b/frontend/src/components/file-icons.tsx deleted file mode 100644 index 45da4eff0b92..000000000000 --- a/frontend/src/components/file-icons.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { DiJavascript } from "react-icons/di"; -import { - FaCss3, - FaFile, - FaHtml5, - FaList, - FaMarkdown, - FaNpm, - FaPython, -} from "react-icons/fa"; -import { getExtension } from "#/utils/utils"; - -const EXTENSION_ICON_MAP: Record<string, JSX.Element> = { - js: <DiJavascript />, - ts: <DiJavascript />, - py: <FaPython />, - css: <FaCss3 />, - json: <FaList />, - npmignore: <FaNpm />, - html: <FaHtml5 />, - md: <FaMarkdown />, -}; - -interface FileIconProps { - filename: string; -} - -function FileIcon({ filename }: FileIconProps) { - const extension = getExtension(filename); - return EXTENSION_ICON_MAP[extension] || <FaFile />; -} - -export default FileIcon; diff --git a/frontend/src/components/image-preview.tsx b/frontend/src/components/image-preview.tsx deleted file mode 100644 index a8be0b24d624..000000000000 --- a/frontend/src/components/image-preview.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import CloseIcon from "#/icons/close.svg?react"; -import { cn } from "#/utils/utils"; - -interface ImagePreviewProps { - src: string; - onRemove?: () => void; - size?: "small" | "large"; -} - -export function ImagePreview({ - src, - onRemove, - size = "small", -}: ImagePreviewProps) { - return ( - <div data-testid="image-preview" className="relative w-fit shrink-0"> - <img - role="img" - src={src} - alt="" - className={cn( - "rounded object-cover", - size === "small" && "w-[62px] h-[62px]", - size === "large" && "w-[100px] h-[100px]", - )} - /> - {onRemove && ( - <button - type="button" - onClick={onRemove} - className={cn( - "bg-neutral-400 rounded-full w-3 h-3 flex items-center justify-center", - "absolute right-[3px] top-[3px]", - )} - > - <CloseIcon width={10} height={10} /> - </button> - )} - </div> - ); -} diff --git a/frontend/src/components/jupyter.tsx b/frontend/src/components/jupyter.tsx deleted file mode 100644 index 5e6915b89e91..000000000000 --- a/frontend/src/components/jupyter.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from "react"; -import { useSelector } from "react-redux"; -import SyntaxHighlighter from "react-syntax-highlighter"; -import Markdown from "react-markdown"; -import { VscArrowDown } from "react-icons/vsc"; -import { useTranslation } from "react-i18next"; -import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; -import { RootState } from "#/store"; -import { Cell } from "#/state/jupyter-slice"; -import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom"; -import { I18nKey } from "#/i18n/declaration"; - -interface IJupyterCell { - cell: Cell; -} - -function JupyterCell({ cell }: IJupyterCell): JSX.Element { - const code = cell.content; - - if (cell.type === "input") { - return ( - <div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs"> - <div className="mb-1 text-gray-400">EXECUTE</div> - <pre - className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5" - style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }} - > - <SyntaxHighlighter - language="python" - style={atomOneDark} - wrapLongLines - > - {code} - </SyntaxHighlighter> - </pre> - </div> - ); - } - - // aggregate all the NON-image lines into a single plaintext. - const lines: { type: "plaintext" | "image"; content: string }[] = []; - let current = ""; - for (const line of code.split("\n")) { - if (line.startsWith("![image](data:image/png;base64,")) { - lines.push({ type: "plaintext", content: current }); - lines.push({ type: "image", content: line }); - current = ""; - } else { - current += `${line}\n`; - } - } - lines.push({ type: "plaintext", content: current }); - - return ( - <div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs"> - <div className="mb-1 text-gray-400">STDOUT/STDERR</div> - <pre - className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5 max-h-[60vh] bg-gray-800" - style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }} - > - {/* display the lines as plaintext or image */} - {lines.map((line, index) => { - if (line.type === "image") { - return ( - <div key={index}> - <Markdown urlTransform={(value: string) => value}> - {line.content} - </Markdown> - </div> - ); - } - return ( - <div key={index}> - <SyntaxHighlighter language="plaintext" style={atomOneDark}> - {line.content} - </SyntaxHighlighter> - </div> - ); - })} - </pre> - </div> - ); -} - -interface JupyterEditorProps { - maxWidth: number; -} - -function JupyterEditor({ maxWidth }: JupyterEditorProps) { - const { t } = useTranslation(); - - const { cells } = useSelector((state: RootState) => state.jupyter); - const jupyterRef = React.useRef<HTMLDivElement>(null); - - const { hitBottom, scrollDomToBottom, onChatBodyScroll } = - useScrollToBottom(jupyterRef); - - return ( - <div className="flex-1" style={{ maxWidth }}> - <div - className="overflow-y-auto h-full" - ref={jupyterRef} - onScroll={(e) => onChatBodyScroll(e.currentTarget)} - > - {cells.map((cell, index) => ( - <JupyterCell key={index} cell={cell} /> - ))} - </div> - {!hitBottom && ( - <div className="sticky bottom-2 flex items-center justify-center"> - <button - type="button" - className="relative border-1 text-sm rounded px-3 py-1 border-neutral-600 bg-neutral-700 cursor-pointer select-none" - > - <span className="flex items-center" onClick={scrollDomToBottom}> - <VscArrowDown className="inline mr-2 w-3 h-3" /> - <span className="inline-block" onClick={scrollDomToBottom}> - {t(I18nKey.CHAT_INTERFACE$TO_BOTTOM)} - </span> - </span> - </button> - </div> - )} - </div> - ); -} - -export default JupyterEditor; diff --git a/frontend/src/components/layout/beta-badge.tsx b/frontend/src/components/layout/beta-badge.tsx new file mode 100644 index 000000000000..e31903d138aa --- /dev/null +++ b/frontend/src/components/layout/beta-badge.tsx @@ -0,0 +1,7 @@ +export function BetaBadge() { + return ( + <span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl"> + Beta + </span> + ); +} diff --git a/frontend/src/components/container.tsx b/frontend/src/components/layout/container.tsx similarity index 55% rename from frontend/src/components/container.tsx rename to frontend/src/components/layout/container.tsx index 8a6ec0a4997a..15047cccd779 100644 --- a/frontend/src/components/container.tsx +++ b/frontend/src/components/layout/container.tsx @@ -1,14 +1,6 @@ -import { NavLink } from "@remix-run/react"; import clsx from "clsx"; import React from "react"; - -function BetaBadge() { - return ( - <span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl"> - Beta - </span> - ); -} +import { NavTab } from "./nav-tab"; interface ContainerProps { label?: string; @@ -38,23 +30,7 @@ export function Container({ {labels && ( <div className="flex text-xs h-[36px]"> {labels.map(({ label: l, to, icon, isBeta }) => ( - <NavLink - end - key={to} - to={to} - className={({ isActive }) => - clsx( - "px-2 border-b border-r border-neutral-600 bg-root-primary flex-1", - "first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0", - "flex items-center gap-2", - isActive && "bg-root-secondary", - ) - } - > - {icon} - {l} - {isBeta && <BetaBadge />} - </NavLink> + <NavTab key={to} to={to} label={l} icon={icon} isBeta={isBeta} /> ))} </div> )} diff --git a/frontend/src/components/layout/nav-tab.tsx b/frontend/src/components/layout/nav-tab.tsx new file mode 100644 index 000000000000..2f8657a8f667 --- /dev/null +++ b/frontend/src/components/layout/nav-tab.tsx @@ -0,0 +1,32 @@ +import { NavLink } from "react-router-dom"; +import { cn } from "#/utils/utils"; +import { BetaBadge } from "./beta-badge"; + +interface NavTabProps { + to: string; + label: string; + icon: React.ReactNode; + isBeta?: boolean; +} + +export function NavTab({ to, label, icon, isBeta }: NavTabProps) { + return ( + <NavLink + end + key={to} + to={to} + className={({ isActive }) => + cn( + "px-2 border-b border-r border-neutral-600 bg-root-primary flex-1", + "first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl last-of-type:border-r-0", + "flex items-center gap-2", + isActive && "bg-root-secondary", + ) + } + > + {icon} + {label} + {isBeta && <BetaBadge />} + </NavLink> + ); +} diff --git a/frontend/src/components/scroll-button.tsx b/frontend/src/components/scroll-button.tsx deleted file mode 100644 index 31b7f5311643..000000000000 --- a/frontend/src/components/scroll-button.tsx +++ /dev/null @@ -1,26 +0,0 @@ -interface ScrollButtonProps { - onClick: () => void; - icon: JSX.Element; - label: string; - disabled?: boolean; -} - -export function ScrollButton({ - onClick, - icon, - label, - disabled = false, -}: ScrollButtonProps): JSX.Element { - return ( - <button - type="button" - className="relative border-1 text-xs rounded px-2 py-1 border-neutral-600 bg-neutral-700 cursor-pointer select-none" - onClick={onClick} - disabled={disabled} - > - <div className="flex items-center"> - {icon} <span className="inline-block">{label}</span> - </div> - </button> - ); -} diff --git a/frontend/src/components/shared/action-tooltip.tsx b/frontend/src/components/shared/action-tooltip.tsx new file mode 100644 index 000000000000..eb2053a7bdf4 --- /dev/null +++ b/frontend/src/components/shared/action-tooltip.tsx @@ -0,0 +1,33 @@ +import { Tooltip } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import ConfirmIcon from "#/assets/confirm"; +import RejectIcon from "#/assets/reject"; +import { I18nKey } from "#/i18n/declaration"; + +interface ActionTooltipProps { + type: "confirm" | "reject"; + onClick: () => void; +} + +export function ActionTooltip({ type, onClick }: ActionTooltipProps) { + const { t } = useTranslation(); + + const content = + type === "confirm" + ? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED) + : t(I18nKey.CHAT_INTERFACE$USER_REJECTED); + + return ( + <Tooltip content={content} closeDelay={100}> + <button + data-testid={`action-${type}-button`} + type="button" + aria-label={type === "confirm" ? "Confirm action" : "Reject action"} + className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800" + onClick={onClick} + > + {type === "confirm" ? <ConfirmIcon /> : <RejectIcon />} + </button> + </Tooltip> + ); +} diff --git a/frontend/src/components/shared/buttons/action-button.tsx b/frontend/src/components/shared/buttons/action-button.tsx new file mode 100644 index 000000000000..57c9f62d1107 --- /dev/null +++ b/frontend/src/components/shared/buttons/action-button.tsx @@ -0,0 +1,33 @@ +import { Tooltip } from "@nextui-org/react"; +import AgentState from "#/types/agent-state"; + +interface ActionButtonProps { + isDisabled?: boolean; + content: string; + action: AgentState; + handleAction: (action: AgentState) => void; +} + +export function ActionButton({ + isDisabled = false, + content, + action, + handleAction, + children, +}: React.PropsWithChildren<ActionButtonProps>) { + return ( + <Tooltip content={content} closeDelay={100}> + <button + onClick={() => handleAction(action)} + disabled={isDisabled} + className="relative overflow-visible cursor-default hover:cursor-pointer group disabled:cursor-not-allowed transition-all duration-300 ease-in-out" + type="button" + > + <span className="relative z-10 group-hover:filter group-hover:drop-shadow-[0_0_5px_rgba(255,64,0,0.4)]"> + {children} + </span> + <span className="absolute -inset-[5px] border-2 border-red-400/40 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out" /> + </button> + </Tooltip> + ); +} diff --git a/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx b/frontend/src/components/shared/buttons/all-hands-logo-button.tsx similarity index 100% rename from frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx rename to frontend/src/components/shared/buttons/all-hands-logo-button.tsx diff --git a/frontend/src/components/shared/buttons/confirmation-buttons.tsx b/frontend/src/components/shared/buttons/confirmation-buttons.tsx new file mode 100644 index 000000000000..18ad6c50e8f9 --- /dev/null +++ b/frontend/src/components/shared/buttons/confirmation-buttons.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; +import AgentState from "#/types/agent-state"; +import { generateAgentStateChangeEvent } from "#/services/agent-state-service"; +import { useWsClient } from "#/context/ws-client-provider"; +import { ActionTooltip } from "../action-tooltip"; + +export function ConfirmationButtons() { + const { t } = useTranslation(); + const { send } = useWsClient(); + + const handleStateChange = (state: AgentState) => { + const event = generateAgentStateChangeEvent(state); + send(event); + }; + + return ( + <div className="flex justify-between items-center pt-4"> + <p>{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}</p> + <div className="flex items-center gap-3"> + <ActionTooltip + type="confirm" + onClick={() => handleStateChange(AgentState.USER_CONFIRMED)} + /> + <ActionTooltip + type="reject" + onClick={() => handleStateChange(AgentState.USER_REJECTED)} + /> + </div> + </div> + ); +} diff --git a/frontend/src/components/continue-button.tsx b/frontend/src/components/shared/buttons/continue-button.tsx similarity index 88% rename from frontend/src/components/continue-button.tsx rename to frontend/src/components/shared/buttons/continue-button.tsx index ade9b5dcd358..a9325c95f358 100644 --- a/frontend/src/components/continue-button.tsx +++ b/frontend/src/components/shared/buttons/continue-button.tsx @@ -11,7 +11,7 @@ export function ContinueButton({ onClick }: ContinueButtonProps) { type="button" onClick={onClick} className={cn( - "px-2 py-1 bg-neutral-700 border border-neutral-600 rounded", + "button-base px-2 py-1", "text-[11px] leading-4 tracking-[0.01em] font-[500]", "flex items-center gap-2", )} diff --git a/frontend/src/components/shared/buttons/copy-to-clipboard-button.tsx b/frontend/src/components/shared/buttons/copy-to-clipboard-button.tsx new file mode 100644 index 000000000000..8d1293732097 --- /dev/null +++ b/frontend/src/components/shared/buttons/copy-to-clipboard-button.tsx @@ -0,0 +1,30 @@ +import CheckmarkIcon from "#/icons/checkmark.svg?react"; +import CopyIcon from "#/icons/copy.svg?react"; + +interface CopyToClipboardButtonProps { + isHidden: boolean; + isDisabled: boolean; + onClick: () => void; + mode: "copy" | "copied"; +} + +export function CopyToClipboardButton({ + isHidden, + isDisabled, + onClick, + mode, +}: CopyToClipboardButtonProps) { + return ( + <button + hidden={isHidden} + disabled={isDisabled} + data-testid="copy-to-clipboard" + type="button" + onClick={onClick} + className="button-base p-1 absolute top-1 right-1" + > + {mode === "copy" && <CopyIcon width={15} height={15} />} + {mode === "copied" && <CheckmarkIcon width={15} height={15} />} + </button> + ); +} diff --git a/frontend/src/routes/_oh/buttons/docs-button.tsx b/frontend/src/components/shared/buttons/docs-button.tsx similarity index 100% rename from frontend/src/routes/_oh/buttons/docs-button.tsx rename to frontend/src/components/shared/buttons/docs-button.tsx diff --git a/frontend/src/components/shared/buttons/editor-action-button.tsx b/frontend/src/components/shared/buttons/editor-action-button.tsx new file mode 100644 index 000000000000..e5b29f0a6e73 --- /dev/null +++ b/frontend/src/components/shared/buttons/editor-action-button.tsx @@ -0,0 +1,29 @@ +import { cn } from "#/utils/utils"; + +interface EditorActionButtonProps { + onClick: () => void; + disabled: boolean; + className: React.HTMLAttributes<HTMLButtonElement>["className"]; +} + +export function EditorActionButton({ + onClick, + disabled, + className, + children, +}: React.PropsWithChildren<EditorActionButtonProps>) { + return ( + <button + type="button" + onClick={onClick} + disabled={disabled} + className={cn( + "text-sm py-0.5 rounded w-20", + "hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed", + className, + )} + > + {children} + </button> + ); +} diff --git a/frontend/src/routes/_oh/buttons/exit-project-button.tsx b/frontend/src/components/shared/buttons/exit-project-button.tsx similarity index 100% rename from frontend/src/routes/_oh/buttons/exit-project-button.tsx rename to frontend/src/components/shared/buttons/exit-project-button.tsx diff --git a/frontend/src/components/shared/buttons/feedback-action-button.tsx b/frontend/src/components/shared/buttons/feedback-action-button.tsx new file mode 100644 index 000000000000..cef1d9184f12 --- /dev/null +++ b/frontend/src/components/shared/buttons/feedback-action-button.tsx @@ -0,0 +1,22 @@ +interface FeedbackActionButtonProps { + testId?: string; + onClick: () => void; + icon: React.ReactNode; +} + +export function FeedbackActionButton({ + testId, + onClick, + icon, +}: FeedbackActionButtonProps) { + return ( + <button + type="button" + data-testid={testId} + onClick={onClick} + className="button-base p-1 hover:bg-neutral-500" + > + {icon} + </button> + ); +} diff --git a/frontend/src/components/icon-button.tsx b/frontend/src/components/shared/buttons/icon-button.tsx similarity index 92% rename from frontend/src/components/icon-button.tsx rename to frontend/src/components/shared/buttons/icon-button.tsx index ad19a4f35ca6..66c86e2a58e3 100644 --- a/frontend/src/components/icon-button.tsx +++ b/frontend/src/components/shared/buttons/icon-button.tsx @@ -8,7 +8,7 @@ export interface IconButtonProps { testId?: string; } -function IconButton({ +export function IconButton({ icon, onClick, ariaLabel, @@ -27,5 +27,3 @@ function IconButton({ </Button> ); } - -export default IconButton; diff --git a/frontend/src/components/buttons/modal-button.tsx b/frontend/src/components/shared/buttons/modal-button.tsx similarity index 95% rename from frontend/src/components/buttons/modal-button.tsx rename to frontend/src/components/shared/buttons/modal-button.tsx index e011fef76075..706e7a481b7f 100644 --- a/frontend/src/components/buttons/modal-button.tsx +++ b/frontend/src/components/shared/buttons/modal-button.tsx @@ -13,7 +13,7 @@ interface ModalButtonProps { intent?: string; } -function ModalButton({ +export function ModalButton({ testId, variant = "default", onClick, @@ -45,5 +45,3 @@ function ModalButton({ </button> ); } - -export default ModalButton; diff --git a/frontend/src/routes/_oh.app._index/file-explorer/buttons/open-vscode-button.tsx b/frontend/src/components/shared/buttons/open-vscode-button.tsx similarity index 100% rename from frontend/src/routes/_oh.app._index/file-explorer/buttons/open-vscode-button.tsx rename to frontend/src/components/shared/buttons/open-vscode-button.tsx diff --git a/frontend/src/components/shared/buttons/refresh-button.tsx b/frontend/src/components/shared/buttons/refresh-button.tsx new file mode 100644 index 000000000000..4fe67a0489ff --- /dev/null +++ b/frontend/src/components/shared/buttons/refresh-button.tsx @@ -0,0 +1,13 @@ +import Refresh from "#/icons/refresh.svg?react"; + +interface RefreshButtonProps { + onClick: (event: React.MouseEvent<HTMLButtonElement>) => void; +} + +export function RefreshButton({ onClick }: RefreshButtonProps) { + return ( + <button type="button" onClick={onClick}> + <Refresh width={14} height={14} /> + </button> + ); +} diff --git a/frontend/src/routes/_oh.app._index/file-explorer/buttons/refresh-icon-button.tsx b/frontend/src/components/shared/buttons/refresh-icon-button.tsx similarity index 89% rename from frontend/src/routes/_oh.app._index/file-explorer/buttons/refresh-icon-button.tsx rename to frontend/src/components/shared/buttons/refresh-icon-button.tsx index d21ab4160b35..3d0080ba0c60 100644 --- a/frontend/src/routes/_oh.app._index/file-explorer/buttons/refresh-icon-button.tsx +++ b/frontend/src/components/shared/buttons/refresh-icon-button.tsx @@ -1,5 +1,5 @@ import { IoIosRefresh } from "react-icons/io"; -import IconButton from "#/components/icon-button"; +import { IconButton } from "./icon-button"; interface RefreshIconButtonProps { onClick: () => void; diff --git a/frontend/src/components/shared/buttons/remove-button.tsx b/frontend/src/components/shared/buttons/remove-button.tsx new file mode 100644 index 000000000000..afcf2ffc8c32 --- /dev/null +++ b/frontend/src/components/shared/buttons/remove-button.tsx @@ -0,0 +1,21 @@ +import { cn } from "#/utils/utils"; +import CloseIcon from "#/icons/close.svg?react"; + +interface RemoveButtonProps { + onClick: () => void; +} + +export function RemoveButton({ onClick }: RemoveButtonProps) { + return ( + <button + type="button" + onClick={onClick} + className={cn( + "bg-neutral-400 rounded-full w-3 h-3 flex items-center justify-center", + "absolute right-[3px] top-[3px]", + )} + > + <CloseIcon width={10} height={10} /> + </button> + ); +} diff --git a/frontend/src/components/scroll-to-bottom-button.tsx b/frontend/src/components/shared/buttons/scroll-to-bottom-button.tsx similarity index 78% rename from frontend/src/components/scroll-to-bottom-button.tsx rename to frontend/src/components/shared/buttons/scroll-to-bottom-button.tsx index 49c5542d7b7a..25db4240166d 100644 --- a/frontend/src/components/scroll-to-bottom-button.tsx +++ b/frontend/src/components/shared/buttons/scroll-to-bottom-button.tsx @@ -10,7 +10,7 @@ export function ScrollToBottomButton({ onClick }: ScrollToBottomButtonProps) { type="button" onClick={onClick} data-testid="scroll-to-bottom" - className="p-1 bg-neutral-700 border border-neutral-600 rounded hover:bg-neutral-500 rotate-180" + className="button-base p-1 hover:bg-neutral-500 rotate-180" > <ArrowSendIcon width={15} height={15} /> </button> diff --git a/frontend/src/routes/_oh/buttons/settings-button.tsx b/frontend/src/components/shared/buttons/settings-button.tsx similarity index 100% rename from frontend/src/routes/_oh/buttons/settings-button.tsx rename to frontend/src/components/shared/buttons/settings-button.tsx diff --git a/frontend/src/components/shared/buttons/stop-button.tsx b/frontend/src/components/shared/buttons/stop-button.tsx new file mode 100644 index 000000000000..dbb99ebd100f --- /dev/null +++ b/frontend/src/components/shared/buttons/stop-button.tsx @@ -0,0 +1,19 @@ +interface StopButtonProps { + isDisabled?: boolean; + onClick?: () => void; +} + +export function StopButton({ isDisabled, onClick }: StopButtonProps) { + return ( + <button + data-testid="stop-button" + aria-label="Stop" + disabled={isDisabled} + onClick={onClick} + type="button" + className="border border-white rounded-lg w-6 h-6 hover:bg-neutral-500 focus:bg-neutral-500 flex items-center justify-center" + > + <div className="w-[10px] h-[10px] bg-white" /> + </button> + ); +} diff --git a/frontend/src/components/shared/buttons/submit-button.tsx b/frontend/src/components/shared/buttons/submit-button.tsx new file mode 100644 index 000000000000..b65fa25775dd --- /dev/null +++ b/frontend/src/components/shared/buttons/submit-button.tsx @@ -0,0 +1,20 @@ +import ArrowSendIcon from "#/icons/arrow-send.svg?react"; + +interface SubmitButtonProps { + isDisabled?: boolean; + onClick: () => void; +} + +export function SubmitButton({ isDisabled, onClick }: SubmitButtonProps) { + return ( + <button + aria-label="Send" + disabled={isDisabled} + onClick={onClick} + type="submit" + className="border border-white rounded-lg w-6 h-6 hover:bg-neutral-500 focus:bg-neutral-500 flex items-center justify-center" + > + <ArrowSendIcon /> + </button> + ); +} diff --git a/frontend/src/routes/_oh.app._index/file-explorer/buttons/toggle-workspace-icon-button.tsx b/frontend/src/components/shared/buttons/toggle-workspace-icon-button.tsx similarity index 93% rename from frontend/src/routes/_oh.app._index/file-explorer/buttons/toggle-workspace-icon-button.tsx rename to frontend/src/components/shared/buttons/toggle-workspace-icon-button.tsx index 5d42eeba0495..5eedd1ae8f1b 100644 --- a/frontend/src/routes/_oh.app._index/file-explorer/buttons/toggle-workspace-icon-button.tsx +++ b/frontend/src/components/shared/buttons/toggle-workspace-icon-button.tsx @@ -1,5 +1,5 @@ import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io"; -import IconButton from "#/components/icon-button"; +import { IconButton } from "./icon-button"; interface ToggleWorkspaceIconButtonProps { onClick: () => void; diff --git a/frontend/src/routes/_oh.app._index/file-explorer/buttons/upload-icon-button.tsx b/frontend/src/components/shared/buttons/upload-icon-button.tsx similarity index 89% rename from frontend/src/routes/_oh.app._index/file-explorer/buttons/upload-icon-button.tsx rename to frontend/src/components/shared/buttons/upload-icon-button.tsx index b10ede48d806..0e8bdeab29d1 100644 --- a/frontend/src/routes/_oh.app._index/file-explorer/buttons/upload-icon-button.tsx +++ b/frontend/src/components/shared/buttons/upload-icon-button.tsx @@ -1,5 +1,5 @@ import { IoIosCloudUpload } from "react-icons/io"; -import IconButton from "#/components/icon-button"; +import { IconButton } from "./icon-button"; interface UploadIconButtonProps { onClick: () => void; diff --git a/frontend/src/components/form/custom-input.tsx b/frontend/src/components/shared/custom-input.tsx similarity index 100% rename from frontend/src/components/form/custom-input.tsx rename to frontend/src/components/shared/custom-input.tsx diff --git a/frontend/src/components/error-toast.tsx b/frontend/src/components/shared/error-toast.tsx similarity index 100% rename from frontend/src/components/error-toast.tsx rename to frontend/src/components/shared/error-toast.tsx diff --git a/frontend/src/components/form/form-fieldset.tsx b/frontend/src/components/shared/form-fieldset.tsx similarity index 94% rename from frontend/src/components/form/form-fieldset.tsx rename to frontend/src/components/shared/form-fieldset.tsx index df40b45511f7..ef0879a77c7f 100644 --- a/frontend/src/components/form/form-fieldset.tsx +++ b/frontend/src/components/shared/form-fieldset.tsx @@ -8,7 +8,7 @@ interface FormFieldsetProps { isClearable?: boolean; } -function FormFieldset({ +export function FormFieldset({ id, label, items, @@ -41,5 +41,3 @@ function FormFieldset({ </fieldset> ); } - -export default FormFieldset; diff --git a/frontend/src/routes/_oh._index/hero-heading.tsx b/frontend/src/components/shared/hero-heading.tsx similarity index 100% rename from frontend/src/routes/_oh._index/hero-heading.tsx rename to frontend/src/components/shared/hero-heading.tsx diff --git a/frontend/src/components/shared/inputs/advanced-option-switch.tsx b/frontend/src/components/shared/inputs/advanced-option-switch.tsx new file mode 100644 index 000000000000..9b5368b4ab65 --- /dev/null +++ b/frontend/src/components/shared/inputs/advanced-option-switch.tsx @@ -0,0 +1,40 @@ +import { Switch } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; +import { cn } from "#/utils/utils"; + +interface AdvancedOptionSwitchProps { + isDisabled: boolean; + showAdvancedOptions: boolean; + setShowAdvancedOptions: (value: boolean) => void; +} + +export function AdvancedOptionSwitch({ + isDisabled, + showAdvancedOptions, + setShowAdvancedOptions, +}: AdvancedOptionSwitchProps) { + const { t } = useTranslation(); + + return ( + <Switch + isDisabled={isDisabled} + name="use-advanced-options" + isSelected={showAdvancedOptions} + onValueChange={setShowAdvancedOptions} + classNames={{ + thumb: cn( + "bg-[#5D5D5D] w-3 h-3 z-0", + "group-data-[selected=true]:bg-white", + ), + wrapper: cn( + "border border-[#D4D4D4] bg-white px-[6px] w-12 h-6", + "group-data-[selected=true]:border-transparent group-data-[selected=true]:bg-[#4465DB]", + ), + label: "text-[#A3A3A3] text-xs", + }} + > + {t(I18nKey.SETTINGS_FORM$ADVANCED_OPTIONS_LABEL)} + </Switch> + ); +} diff --git a/frontend/src/components/shared/inputs/agent-input.tsx b/frontend/src/components/shared/inputs/agent-input.tsx new file mode 100644 index 000000000000..0dd21f4b0462 --- /dev/null +++ b/frontend/src/components/shared/inputs/agent-input.tsx @@ -0,0 +1,46 @@ +import { Autocomplete, AutocompleteItem } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; + +interface AgentInputProps { + isDisabled: boolean; + defaultValue: string; + agents: string[]; +} + +export function AgentInput({ + isDisabled, + defaultValue, + agents, +}: AgentInputProps) { + const { t } = useTranslation(); + + return ( + <fieldset data-testid="agent-selector" className="flex flex-col gap-2"> + <label htmlFor="agent" className="font-[500] text-[#A3A3A3] text-xs"> + {t(I18nKey.SETTINGS_FORM$AGENT_LABEL)} + </label> + <Autocomplete + isDisabled={isDisabled} + isRequired + id="agent" + aria-label="Agent" + data-testid="agent-input" + name="agent" + defaultSelectedKey={defaultValue} + isClearable={false} + inputProps={{ + classNames: { + inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", + }, + }} + > + {agents.map((agent) => ( + <AutocompleteItem key={agent} value={agent}> + {agent} + </AutocompleteItem> + ))} + </Autocomplete> + </fieldset> + ); +} diff --git a/frontend/src/components/shared/inputs/api-key-input.tsx b/frontend/src/components/shared/inputs/api-key-input.tsx new file mode 100644 index 000000000000..ea6e553adbe9 --- /dev/null +++ b/frontend/src/components/shared/inputs/api-key-input.tsx @@ -0,0 +1,42 @@ +import { Input } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; + +interface APIKeyInputProps { + isDisabled: boolean; + defaultValue: string; +} + +export function APIKeyInput({ isDisabled, defaultValue }: APIKeyInputProps) { + const { t } = useTranslation(); + + return ( + <fieldset data-testid="api-key-input" className="flex flex-col gap-2"> + <label htmlFor="api-key" className="font-[500] text-[#A3A3A3] text-xs"> + {t(I18nKey.SETTINGS_FORM$API_KEY_LABEL)} + </label> + <Input + isDisabled={isDisabled} + id="api-key" + name="api-key" + aria-label="API Key" + type="password" + defaultValue={defaultValue} + classNames={{ + inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", + }} + /> + <p className="text-sm text-[#A3A3A3]"> + {t(I18nKey.SETTINGS_FORM$DONT_KNOW_API_KEY_LABEL)}{" "} + <a + href="https://docs.all-hands.dev/modules/usage/llms" + rel="noreferrer noopener" + target="_blank" + className="underline underline-offset-2" + > + {t(I18nKey.SETTINGS_FORM$CLICK_HERE_FOR_INSTRUCTIONS_LABEL)} + </a> + </p> + </fieldset> + ); +} diff --git a/frontend/src/components/shared/inputs/base-url-input.tsx b/frontend/src/components/shared/inputs/base-url-input.tsx new file mode 100644 index 000000000000..d70d1eb9ea13 --- /dev/null +++ b/frontend/src/components/shared/inputs/base-url-input.tsx @@ -0,0 +1,30 @@ +import { Input } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; + +interface BaseUrlInputProps { + isDisabled: boolean; + defaultValue: string; +} + +export function BaseUrlInput({ isDisabled, defaultValue }: BaseUrlInputProps) { + const { t } = useTranslation(); + + return ( + <fieldset className="flex flex-col gap-2"> + <label htmlFor="base-url" className="font-[500] text-[#A3A3A3] text-xs"> + {t(I18nKey.SETTINGS_FORM$BASE_URL_LABEL)} + </label> + <Input + isDisabled={isDisabled} + id="base-url" + name="base-url" + defaultValue={defaultValue} + aria-label="Base URL" + classNames={{ + inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", + }} + /> + </fieldset> + ); +} diff --git a/frontend/src/components/shared/inputs/confirmation-mode-switch.tsx b/frontend/src/components/shared/inputs/confirmation-mode-switch.tsx new file mode 100644 index 000000000000..39fd11ab6eb5 --- /dev/null +++ b/frontend/src/components/shared/inputs/confirmation-mode-switch.tsx @@ -0,0 +1,37 @@ +import { Switch } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; +import { cn } from "#/utils/utils"; + +interface ConfirmationModeSwitchProps { + isDisabled: boolean; + defaultSelected: boolean; +} + +export function ConfirmationModeSwitch({ + isDisabled, + defaultSelected, +}: ConfirmationModeSwitchProps) { + const { t } = useTranslation(); + + return ( + <Switch + isDisabled={isDisabled} + name="confirmation-mode" + defaultSelected={defaultSelected} + classNames={{ + thumb: cn( + "bg-[#5D5D5D] w-3 h-3", + "group-data-[selected=true]:bg-white", + ), + wrapper: cn( + "border border-[#D4D4D4] bg-white px-[6px] w-12 h-6", + "group-data-[selected=true]:border-transparent group-data-[selected=true]:bg-[#4465DB]", + ), + label: "text-[#A3A3A3] text-xs", + }} + > + {t(I18nKey.SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL)} + </Switch> + ); +} diff --git a/frontend/src/components/shared/inputs/custom-model-input.tsx b/frontend/src/components/shared/inputs/custom-model-input.tsx new file mode 100644 index 000000000000..b40fd2256566 --- /dev/null +++ b/frontend/src/components/shared/inputs/custom-model-input.tsx @@ -0,0 +1,36 @@ +import { Input } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; + +interface CustomModelInputProps { + isDisabled: boolean; + defaultValue: string; +} + +export function CustomModelInput({ + isDisabled, + defaultValue, +}: CustomModelInputProps) { + const { t } = useTranslation(); + + return ( + <fieldset className="flex flex-col gap-2"> + <label + htmlFor="custom-model" + className="font-[500] text-[#A3A3A3] text-xs" + > + {t(I18nKey.SETTINGS_FORM$CUSTOM_MODEL_LABEL)} + </label> + <Input + isDisabled={isDisabled} + id="custom-model" + name="custom-model" + defaultValue={defaultValue} + aria-label="Custom Model" + classNames={{ + inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", + }} + /> + </fieldset> + ); +} diff --git a/frontend/src/components/shared/inputs/security-analyzers-input.tsx b/frontend/src/components/shared/inputs/security-analyzers-input.tsx new file mode 100644 index 000000000000..e5e6d159cf9c --- /dev/null +++ b/frontend/src/components/shared/inputs/security-analyzers-input.tsx @@ -0,0 +1,47 @@ +import { Autocomplete, AutocompleteItem } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; + +interface SecurityAnalyzerInputProps { + isDisabled: boolean; + defaultValue: string; + securityAnalyzers: string[]; +} + +export function SecurityAnalyzerInput({ + isDisabled, + defaultValue, + securityAnalyzers, +}: SecurityAnalyzerInputProps) { + const { t } = useTranslation(); + + return ( + <fieldset className="flex flex-col gap-2"> + <label + htmlFor="security-analyzer" + className="font-[500] text-[#A3A3A3] text-xs" + > + {t(I18nKey.SETTINGS_FORM$SECURITY_ANALYZER_LABEL)} + </label> + <Autocomplete + isDisabled={isDisabled} + isRequired + id="security-analyzer" + name="security-analyzer" + aria-label="Security Analyzer" + defaultSelectedKey={defaultValue} + inputProps={{ + classNames: { + inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", + }, + }} + > + {securityAnalyzers.map((analyzer) => ( + <AutocompleteItem key={analyzer} value={analyzer}> + {analyzer} + </AutocompleteItem> + ))} + </Autocomplete> + </fieldset> + ); +} diff --git a/frontend/src/components/modals/loading-project.tsx b/frontend/src/components/shared/loading-spinner.tsx similarity index 52% rename from frontend/src/components/modals/loading-project.tsx rename to frontend/src/components/shared/loading-spinner.tsx index c0723ccaf815..5f19d0fe4de6 100644 --- a/frontend/src/components/modals/loading-project.tsx +++ b/frontend/src/components/shared/loading-spinner.tsx @@ -1,8 +1,5 @@ -import { useTranslation } from "react-i18next"; import LoadingSpinnerOuter from "#/icons/loading-outer.svg?react"; import { cn } from "#/utils/utils"; -import ModalBody from "./modal-body"; -import { I18nKey } from "#/i18n/declaration"; interface LoadingSpinnerProps { size: "small" | "large"; @@ -24,22 +21,3 @@ export function LoadingSpinner({ size }: LoadingSpinnerProps) { </div> ); } - -interface LoadingProjectModalProps { - message?: string; -} - -function LoadingProjectModal({ message }: LoadingProjectModalProps) { - const { t } = useTranslation(); - - return ( - <ModalBody> - <span className="text-xl leading-6 -tracking-[0.01em] font-semibold"> - {message || t(I18nKey.LOADING_PROJECT$LOADING)} - </span> - <LoadingSpinner size="large" /> - </ModalBody> - ); -} - -export default LoadingProjectModal; diff --git a/frontend/src/components/modals/account-settings-form.tsx b/frontend/src/components/shared/modals/account-settings/account-settings-form.tsx similarity index 94% rename from frontend/src/components/modals/account-settings-form.tsx rename to frontend/src/components/shared/modals/account-settings/account-settings-form.tsx index 3b41ab4332f7..3da9b97e2c8d 100644 --- a/frontend/src/components/modals/account-settings-form.tsx +++ b/frontend/src/components/shared/modals/account-settings/account-settings-form.tsx @@ -3,16 +3,16 @@ import { useTranslation } from "react-i18next"; import { BaseModalDescription, BaseModalTitle, -} from "./confirmation-modals/base-modal"; -import ModalBody from "./modal-body"; -import ModalButton from "../buttons/modal-button"; -import FormFieldset from "../form/form-fieldset"; -import { CustomInput } from "../form/custom-input"; +} from "../confirmation-modals/base-modal"; +import { ModalBody } from "../modal-body"; import { AvailableLanguages } from "#/i18n"; import { I18nKey } from "#/i18n/declaration"; import { useAuth } from "#/context/auth-context"; import { useUserPrefs } from "#/context/user-prefs-context"; import { handleCaptureConsent } from "#/utils/handle-capture-consent"; +import { ModalButton } from "../../buttons/modal-button"; +import { CustomInput } from "../../custom-input"; +import { FormFieldset } from "../../form-fieldset"; interface AccountSettingsFormProps { onClose: () => void; diff --git a/frontend/src/routes/_oh/modals/account-settings-modal.tsx b/frontend/src/components/shared/modals/account-settings/account-settings-modal.tsx similarity index 83% rename from frontend/src/routes/_oh/modals/account-settings-modal.tsx rename to frontend/src/components/shared/modals/account-settings/account-settings-modal.tsx index d1568940486c..f6bcdb48abfe 100644 --- a/frontend/src/routes/_oh/modals/account-settings-modal.tsx +++ b/frontend/src/components/shared/modals/account-settings/account-settings-modal.tsx @@ -1,7 +1,7 @@ -import { AccountSettingsForm } from "#/components/modals/account-settings-form"; -import { ModalBackdrop } from "#/components/modals/modal-backdrop"; import { useUserPrefs } from "#/context/user-prefs-context"; import { useGitHubUser } from "#/hooks/query/use-github-user"; +import { ModalBackdrop } from "../modal-backdrop"; +import { AccountSettingsForm } from "./account-settings-form"; interface AccountSettingsModalProps { onClose: () => void; diff --git a/frontend/src/components/modals/base-modal/base-modal.tsx b/frontend/src/components/shared/modals/base-modal/base-modal.tsx similarity index 97% rename from frontend/src/components/modals/base-modal/base-modal.tsx rename to frontend/src/components/shared/modals/base-modal/base-modal.tsx index 128461fa0cc3..ccb6d17a180a 100644 --- a/frontend/src/components/modals/base-modal/base-modal.tsx +++ b/frontend/src/components/shared/modals/base-modal/base-modal.tsx @@ -22,7 +22,7 @@ interface BaseModalProps { testID?: string; } -function BaseModal({ +export function BaseModal({ isOpen, onOpenChange, title, @@ -67,5 +67,3 @@ function BaseModal({ </Modal> ); } - -export default BaseModal; diff --git a/frontend/src/components/modals/base-modal/footer-content.tsx b/frontend/src/components/shared/modals/base-modal/footer-content.tsx similarity index 100% rename from frontend/src/components/modals/base-modal/footer-content.tsx rename to frontend/src/components/shared/modals/base-modal/footer-content.tsx diff --git a/frontend/src/components/modals/base-modal/header-content.tsx b/frontend/src/components/shared/modals/base-modal/header-content.tsx similarity index 100% rename from frontend/src/components/modals/base-modal/header-content.tsx rename to frontend/src/components/shared/modals/base-modal/header-content.tsx diff --git a/frontend/src/components/modals/confirmation-modals/base-modal.tsx b/frontend/src/components/shared/modals/confirmation-modals/base-modal.tsx similarity index 86% rename from frontend/src/components/modals/confirmation-modals/base-modal.tsx rename to frontend/src/components/shared/modals/confirmation-modals/base-modal.tsx index 3f7b6a80b533..2cb79b45c8c0 100644 --- a/frontend/src/components/modals/confirmation-modals/base-modal.tsx +++ b/frontend/src/components/shared/modals/confirmation-modals/base-modal.tsx @@ -1,6 +1,6 @@ import React from "react"; -import ModalButton from "#/components/buttons/modal-button"; -import ModalBody from "../modal-body"; +import { ModalBody } from "../modal-body"; +import { ModalButton } from "../../buttons/modal-button"; interface ButtonConfig { text: string; @@ -40,7 +40,7 @@ interface BaseModalProps { buttons: ButtonConfig[]; } -function BaseModal({ title, description, buttons }: BaseModalProps) { +export function BaseModal({ title, description, buttons }: BaseModalProps) { return ( <ModalBody> <div className="flex flex-col gap-2 self-start"> @@ -61,5 +61,3 @@ function BaseModal({ title, description, buttons }: BaseModalProps) { </ModalBody> ); } - -export default BaseModal; diff --git a/frontend/src/components/modals/confirmation-modals/danger-modal.tsx b/frontend/src/components/shared/modals/confirmation-modals/danger-modal.tsx similarity index 94% rename from frontend/src/components/modals/confirmation-modals/danger-modal.tsx rename to frontend/src/components/shared/modals/confirmation-modals/danger-modal.tsx index 0f14ff76f4ef..fa6d468d0072 100644 --- a/frontend/src/components/modals/confirmation-modals/danger-modal.tsx +++ b/frontend/src/components/shared/modals/confirmation-modals/danger-modal.tsx @@ -1,4 +1,4 @@ -import BaseModal from "./base-modal"; +import { BaseModal } from "./base-modal"; interface DangerModalProps { title: string; diff --git a/frontend/src/components/modals/connect-to-github-modal.tsx b/frontend/src/components/shared/modals/connect-to-github-modal.tsx similarity index 93% rename from frontend/src/components/modals/connect-to-github-modal.tsx rename to frontend/src/components/shared/modals/connect-to-github-modal.tsx index 0e6d52fc2517..130ca4e19929 100644 --- a/frontend/src/components/modals/connect-to-github-modal.tsx +++ b/frontend/src/components/shared/modals/connect-to-github-modal.tsx @@ -1,13 +1,13 @@ import { useTranslation } from "react-i18next"; -import ModalBody from "./modal-body"; -import { CustomInput } from "../form/custom-input"; -import ModalButton from "../buttons/modal-button"; +import { ModalBody } from "./modal-body"; import { BaseModalDescription, BaseModalTitle, } from "./confirmation-modals/base-modal"; import { I18nKey } from "#/i18n/declaration"; import { useAuth } from "#/context/auth-context"; +import { ModalButton } from "../buttons/modal-button"; +import { CustomInput } from "../custom-input"; interface ConnectToGitHubModalProps { onClose: () => void; diff --git a/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx b/frontend/src/components/shared/modals/exit-project-confirmation-modal.tsx similarity index 87% rename from frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx rename to frontend/src/components/shared/modals/exit-project-confirmation-modal.tsx index 5d425fd4a6a1..c29477f3e098 100644 --- a/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx +++ b/frontend/src/components/shared/modals/exit-project-confirmation-modal.tsx @@ -1,9 +1,9 @@ import { useDispatch } from "react-redux"; -import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal"; -import { ModalBackdrop } from "#/components/modals/modal-backdrop"; import { useEndSession } from "#/hooks/use-end-session"; import { setCurrentAgentState } from "#/state/agent-slice"; import AgentState from "#/types/agent-state"; +import { DangerModal } from "./confirmation-modals/danger-modal"; +import { ModalBackdrop } from "./modal-backdrop"; interface ExitProjectConfirmationModalProps { onClose: () => void; diff --git a/frontend/src/components/modals/modal-backdrop.tsx b/frontend/src/components/shared/modals/modal-backdrop.tsx similarity index 100% rename from frontend/src/components/modals/modal-backdrop.tsx rename to frontend/src/components/shared/modals/modal-backdrop.tsx diff --git a/frontend/src/components/modals/modal-body.tsx b/frontend/src/components/shared/modals/modal-body.tsx similarity index 81% rename from frontend/src/components/modals/modal-body.tsx rename to frontend/src/components/shared/modals/modal-body.tsx index 4eb835e0911f..32e107b36772 100644 --- a/frontend/src/components/modals/modal-body.tsx +++ b/frontend/src/components/shared/modals/modal-body.tsx @@ -7,7 +7,7 @@ interface ModalBodyProps { className?: React.HTMLProps<HTMLDivElement>["className"]; } -function ModalBody({ testID, children, className }: ModalBodyProps) { +export function ModalBody({ testID, children, className }: ModalBodyProps) { return ( <div data-testid={testID} @@ -20,5 +20,3 @@ function ModalBody({ testID, children, className }: ModalBodyProps) { </div> ); } - -export default ModalBody; diff --git a/frontend/src/components/modals/security/invariant/assets/logo.tsx b/frontend/src/components/shared/modals/security/invariant/assets/logo.tsx similarity index 100% rename from frontend/src/components/modals/security/invariant/assets/logo.tsx rename to frontend/src/components/shared/modals/security/invariant/assets/logo.tsx diff --git a/frontend/src/components/modals/security/invariant/invariant.tsx b/frontend/src/components/shared/modals/security/invariant/invariant.tsx similarity index 100% rename from frontend/src/components/modals/security/invariant/invariant.tsx rename to frontend/src/components/shared/modals/security/invariant/invariant.tsx diff --git a/frontend/src/components/modals/security/security.tsx b/frontend/src/components/shared/modals/security/security.tsx similarity index 95% rename from frontend/src/components/modals/security/security.tsx rename to frontend/src/components/shared/modals/security/security.tsx index f5001fa9a8f9..ba30bc08c851 100644 --- a/frontend/src/components/modals/security/security.tsx +++ b/frontend/src/components/shared/modals/security/security.tsx @@ -1,8 +1,8 @@ import React from "react"; import { useTranslation } from "react-i18next"; import SecurityInvariant from "./invariant/invariant"; -import BaseModal from "../base-modal/base-modal"; import { I18nKey } from "#/i18n/declaration"; +import { BaseModal } from "../base-modal/base-modal"; interface SecurityProps { isOpen: boolean; diff --git a/frontend/src/components/modals/settings/model-selector.tsx b/frontend/src/components/shared/modals/settings/model-selector.tsx similarity index 100% rename from frontend/src/components/modals/settings/model-selector.tsx rename to frontend/src/components/shared/modals/settings/model-selector.tsx diff --git a/frontend/src/components/form/settings-form.tsx b/frontend/src/components/shared/modals/settings/settings-form.tsx similarity index 54% rename from frontend/src/components/form/settings-form.tsx rename to frontend/src/components/shared/modals/settings/settings-form.tsx index 4a464b4cef81..4b50a85e783b 100644 --- a/frontend/src/components/form/settings-form.tsx +++ b/frontend/src/components/shared/modals/settings/settings-form.tsx @@ -1,21 +1,11 @@ -import { - Autocomplete, - AutocompleteItem, - Input, - Switch, -} from "@nextui-org/react"; import { useLocation } from "@remix-run/react"; import { useTranslation } from "react-i18next"; -import clsx from "clsx"; import React from "react"; import posthog from "posthog-js"; import { organizeModelsAndProviders } from "#/utils/organize-models-and-providers"; -import { ModelSelector } from "#/components/modals/settings/model-selector"; import { getDefaultSettings, Settings } from "#/services/settings"; -import { ModalBackdrop } from "#/components/modals/modal-backdrop"; import { extractModelAndProvider } from "#/utils/extract-model-and-provider"; -import ModalButton from "../buttons/modal-button"; -import { DangerModal } from "../modals/confirmation-modals/danger-modal"; +import { DangerModal } from "../confirmation-modals/danger-modal"; import { I18nKey } from "#/i18n/declaration"; import { extractSettings, @@ -24,6 +14,16 @@ import { } from "#/utils/settings-utils"; import { useEndSession } from "#/hooks/use-end-session"; import { useUserPrefs } from "#/context/user-prefs-context"; +import { ModalButton } from "../../buttons/modal-button"; +import { AdvancedOptionSwitch } from "../../inputs/advanced-option-switch"; +import { AgentInput } from "../../inputs/agent-input"; +import { APIKeyInput } from "../../inputs/api-key-input"; +import { BaseUrlInput } from "../../inputs/base-url-input"; +import { ConfirmationModeSwitch } from "../../inputs/confirmation-mode-switch"; +import { CustomModelInput } from "../../inputs/custom-model-input"; +import { SecurityAnalyzerInput } from "../../inputs/security-analyzers-input"; +import { ModalBackdrop } from "../modal-backdrop"; +import { ModelSelector } from "./model-selector"; interface SettingsFormProps { disabled?: boolean; @@ -50,29 +50,6 @@ export function SettingsForm({ const formRef = React.useRef<HTMLFormElement>(null); - const resetOngoingSession = () => { - if (location.pathname.startsWith("/app")) { - endSession(); - onClose(); - } - }; - - const handleFormSubmission = (formData: FormData) => { - const keys = Array.from(formData.keys()); - const isUsingAdvancedOptions = keys.includes("use-advanced-options"); - const newSettings = extractSettings(formData); - - saveSettings(newSettings); - saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic"); - updateSettingsVersion(); - resetOngoingSession(); - - posthog.capture("settings_saved", { - LLM_MODEL: newSettings.LLM_MODEL, - LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET", - }); - }; - const advancedAlreadyInUse = React.useMemo(() => { if (models.length > 0) { const organizedModels = organizeModelsAndProviders(models); @@ -107,6 +84,29 @@ export function SettingsForm({ React.useState(false); const [showWarningModal, setShowWarningModal] = React.useState(false); + const resetOngoingSession = () => { + if (location.pathname.startsWith("/app")) { + endSession(); + onClose(); + } + }; + + const handleFormSubmission = (formData: FormData) => { + const keys = Array.from(formData.keys()); + const isUsingAdvancedOptions = keys.includes("use-advanced-options"); + const newSettings = extractSettings(formData); + + saveSettings(newSettings); + saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic"); + updateSettingsVersion(); + resetOngoingSession(); + + posthog.capture("settings_saved", { + LLM_MODEL: newSettings.LLM_MODEL, + LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET", + }); + }; + const handleConfirmResetSettings = () => { saveSettings(getDefaultSettings()); resetOngoingSession(); @@ -164,66 +164,23 @@ export function SettingsForm({ onSubmit={handleSubmit} > <div className="flex flex-col gap-2"> - <Switch - isDisabled={disabled} - name="use-advanced-options" - isSelected={showAdvancedOptions} - onValueChange={setShowAdvancedOptions} - classNames={{ - thumb: clsx( - "bg-[#5D5D5D] w-3 h-3 z-0", - "group-data-[selected=true]:bg-white", - ), - wrapper: clsx( - "border border-[#D4D4D4] bg-white px-[6px] w-12 h-6", - "group-data-[selected=true]:border-transparent group-data-[selected=true]:bg-[#4465DB]", - ), - label: "text-[#A3A3A3] text-xs", - }} - > - {t(I18nKey.SETTINGS_FORM$ADVANCED_OPTIONS_LABEL)} - </Switch> + <AdvancedOptionSwitch + isDisabled={!!disabled} + showAdvancedOptions={showAdvancedOptions} + setShowAdvancedOptions={setShowAdvancedOptions} + /> {showAdvancedOptions && ( <> - <fieldset className="flex flex-col gap-2"> - <label - htmlFor="custom-model" - className="font-[500] text-[#A3A3A3] text-xs" - > - {t(I18nKey.SETTINGS_FORM$CUSTOM_MODEL_LABEL)} - </label> - <Input - isDisabled={disabled} - id="custom-model" - name="custom-model" - defaultValue={settings.LLM_MODEL} - aria-label="Custom Model" - classNames={{ - inputWrapper: - "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", - }} - /> - </fieldset> - <fieldset className="flex flex-col gap-2"> - <label - htmlFor="base-url" - className="font-[500] text-[#A3A3A3] text-xs" - > - {t(I18nKey.SETTINGS_FORM$BASE_URL_LABEL)} - </label> - <Input - isDisabled={disabled} - id="base-url" - name="base-url" - defaultValue={settings.LLM_BASE_URL} - aria-label="Base URL" - classNames={{ - inputWrapper: - "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", - }} - /> - </fieldset> + <CustomModelInput + isDisabled={!!disabled} + defaultValue={settings.LLM_MODEL} + /> + + <BaseUrlInput + isDisabled={!!disabled} + defaultValue={settings.LLM_BASE_URL} + /> </> )} @@ -235,122 +192,31 @@ export function SettingsForm({ /> )} - <fieldset data-testid="api-key-input" className="flex flex-col gap-2"> - <label - htmlFor="api-key" - className="font-[500] text-[#A3A3A3] text-xs" - > - {t(I18nKey.SETTINGS_FORM$API_KEY_LABEL)} - </label> - <Input - isDisabled={disabled} - id="api-key" - name="api-key" - aria-label="API Key" - type="password" - defaultValue={settings.LLM_API_KEY} - classNames={{ - inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", - }} - /> - <p className="text-sm text-[#A3A3A3]"> - {t(I18nKey.SETTINGS_FORM$DONT_KNOW_API_KEY_LABEL)}{" "} - <a - href="https://docs.all-hands.dev/modules/usage/llms" - rel="noreferrer noopener" - target="_blank" - className="underline underline-offset-2" - > - {t(I18nKey.SETTINGS_FORM$CLICK_HERE_FOR_INSTRUCTIONS_LABEL)} - </a> - </p> - </fieldset> + <APIKeyInput + isDisabled={!!disabled} + defaultValue={settings.LLM_API_KEY} + /> {showAdvancedOptions && ( - <fieldset - data-testid="agent-selector" - className="flex flex-col gap-2" - > - <label - htmlFor="agent" - className="font-[500] text-[#A3A3A3] text-xs" - > - {t(I18nKey.SETTINGS_FORM$AGENT_LABEL)} - </label> - <Autocomplete - isDisabled={disabled} - isRequired - id="agent" - aria-label="Agent" - data-testid="agent-input" - name="agent" - defaultSelectedKey={settings.AGENT} - isClearable={false} - inputProps={{ - classNames: { - inputWrapper: - "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", - }, - }} - > - {agents.map((agent) => ( - <AutocompleteItem key={agent} value={agent}> - {agent} - </AutocompleteItem> - ))} - </Autocomplete> - </fieldset> + <AgentInput + isDisabled={!!disabled} + defaultValue={settings.AGENT} + agents={agents} + /> )} {showAdvancedOptions && ( <> - <fieldset className="flex flex-col gap-2"> - <label - htmlFor="security-analyzer" - className="font-[500] text-[#A3A3A3] text-xs" - > - {t(I18nKey.SETTINGS_FORM$SECURITY_ANALYZER_LABEL)} - </label> - <Autocomplete - isDisabled={disabled} - isRequired - id="security-analyzer" - name="security-analyzer" - aria-label="Security Analyzer" - defaultSelectedKey={settings.SECURITY_ANALYZER} - inputProps={{ - classNames: { - inputWrapper: - "bg-[#27272A] rounded-md text-sm px-3 py-[10px]", - }, - }} - > - {securityAnalyzers.map((analyzer) => ( - <AutocompleteItem key={analyzer} value={analyzer}> - {analyzer} - </AutocompleteItem> - ))} - </Autocomplete> - </fieldset> - - <Switch - isDisabled={disabled} - name="confirmation-mode" + <SecurityAnalyzerInput + isDisabled={!!disabled} + defaultValue={settings.SECURITY_ANALYZER} + securityAnalyzers={securityAnalyzers} + /> + + <ConfirmationModeSwitch + isDisabled={!!disabled} defaultSelected={settings.CONFIRMATION_MODE} - classNames={{ - thumb: clsx( - "bg-[#5D5D5D] w-3 h-3", - "group-data-[selected=true]:bg-white", - ), - wrapper: clsx( - "border border-[#D4D4D4] bg-white px-[6px] w-12 h-6", - "group-data-[selected=true]:border-transparent group-data-[selected=true]:bg-[#4465DB]", - ), - label: "text-[#A3A3A3] text-xs", - }} - > - {t(I18nKey.SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL)} - </Switch> + /> </> )} </div> diff --git a/frontend/src/routes/_oh/modals/settings-modal.tsx b/frontend/src/components/shared/modals/settings/settings-modal.tsx similarity index 88% rename from frontend/src/routes/_oh/modals/settings-modal.tsx rename to frontend/src/components/shared/modals/settings/settings-modal.tsx index d09b73a33eb1..4cd0dd45a719 100644 --- a/frontend/src/routes/_oh/modals/settings-modal.tsx +++ b/frontend/src/components/shared/modals/settings/settings-modal.tsx @@ -1,8 +1,8 @@ -import { SettingsForm } from "#/components/form/settings-form"; -import { LoadingSpinner } from "#/components/modals/loading-project"; -import { ModalBackdrop } from "#/components/modals/modal-backdrop"; import { useUserPrefs } from "#/context/user-prefs-context"; import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options"; +import { LoadingSpinner } from "../../loading-spinner"; +import { ModalBackdrop } from "../modal-backdrop"; +import { SettingsForm } from "./settings-form"; interface SettingsModalProps { onClose: () => void; diff --git a/frontend/src/routes/_oh._index/task-form.tsx b/frontend/src/components/shared/task-form.tsx similarity index 92% rename from frontend/src/routes/_oh._index/task-form.tsx rename to frontend/src/components/shared/task-form.tsx index 99691d1ece00..3fcf6fc1f75a 100644 --- a/frontend/src/routes/_oh._index/task-form.tsx +++ b/frontend/src/components/shared/task-form.tsx @@ -8,15 +8,15 @@ import { removeFile, setInitialQuery, } from "#/state/initial-query-slice"; -import { SuggestionBubble } from "#/components/suggestion-bubble"; +import { SuggestionBubble } from "#/components/features/suggestions/suggestion-bubble"; import { SUGGESTIONS } from "#/utils/suggestions"; import { convertImageToBase64 } from "#/utils/convert-image-to-base-64"; -import { ChatInput } from "#/components/chat-input"; -import { UploadImageInput } from "#/components/upload-image-input"; -import { ImageCarousel } from "#/components/image-carousel"; +import { ChatInput } from "#/components/features/chat/chat-input"; import { getRandomKey } from "#/utils/get-random-key"; -import { AttachImageLabel } from "#/components/attach-image-label"; import { cn } from "#/utils/utils"; +import { AttachImageLabel } from "../features/images/attach-image-label"; +import { ImageCarousel } from "../features/images/image-carousel"; +import { UploadImageInput } from "../features/images/upload-image-input"; export const TaskForm = React.forwardRef<HTMLFormElement>((_, ref) => { const dispatch = useDispatch(); diff --git a/frontend/src/components/waitlist-modal.tsx b/frontend/src/components/waitlist-modal.tsx deleted file mode 100644 index 9f8b4f454809..000000000000 --- a/frontend/src/components/waitlist-modal.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import ModalButton from "./buttons/modal-button"; -import { ModalBackdrop } from "./modals/modal-backdrop"; -import GitHubLogo from "#/assets/branding/github-logo.svg?react"; -import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react"; -import ModalBody from "./modals/modal-body"; - -interface WaitlistModalProps { - ghToken: string | null; - githubAuthUrl: string | null; -} - -export function WaitlistModal({ ghToken, githubAuthUrl }: WaitlistModalProps) { - return ( - <ModalBackdrop> - <ModalBody> - <AllHandsLogo width={68} height={46} /> - <div className="flex flex-col gap-2 w-full items-center text-center"> - <h1 className="text-2xl font-bold"> - {ghToken ? "Just a little longer!" : "Sign in with GitHub"} - </h1> - {!ghToken && ( - <p> - or{" "} - <a - href="https://www.all-hands.dev/join-waitlist" - target="_blank" - rel="noreferrer noopener" - className="text-blue-500 hover:underline underline-offset-2" - > - join the waitlist - </a>{" "} - if you haven't already - </p> - )} - {ghToken && ( - <p className="text-sm"> - Thanks for your patience! We're accepting new members - progressively. If you haven't joined the waitlist yet, - now's the time! - </p> - )} - </div> - - {!ghToken && ( - <ModalButton - text="Connect to GitHub" - icon={<GitHubLogo width={20} height={20} />} - className="bg-[#791B80] w-full" - onClick={() => { - if (githubAuthUrl) { - window.location.href = githubAuthUrl; - } - }} - /> - )} - {ghToken && ( - <a - href="https://www.all-hands.dev/join-waitlist" - target="_blank" - rel="noreferrer" - className="rounded bg-[#FFE165] text-black text-sm font-bold py-[10px] w-full text-center hover:opacity-80" - > - Join Waitlist - </a> - )} - </ModalBody> - </ModalBackdrop> - ); -} diff --git a/frontend/src/ignore-task-state-map.constant.ts b/frontend/src/ignore-task-state-map.constant.ts new file mode 100644 index 000000000000..2fc50e7d5495 --- /dev/null +++ b/frontend/src/ignore-task-state-map.constant.ts @@ -0,0 +1,26 @@ +import AgentState from "./types/agent-state"; + +export const IGNORE_TASK_STATE_MAP: Record<string, AgentState[]> = { + [AgentState.PAUSED]: [ + AgentState.INIT, + AgentState.PAUSED, + AgentState.STOPPED, + AgentState.FINISHED, + AgentState.REJECTED, + AgentState.AWAITING_USER_INPUT, + AgentState.AWAITING_USER_CONFIRMATION, + ], + [AgentState.RUNNING]: [ + AgentState.INIT, + AgentState.RUNNING, + AgentState.STOPPED, + AgentState.FINISHED, + AgentState.REJECTED, + AgentState.AWAITING_USER_INPUT, + AgentState.AWAITING_USER_CONFIRMATION, + ], + [AgentState.STOPPED]: [AgentState.INIT, AgentState.STOPPED], + [AgentState.USER_CONFIRMED]: [AgentState.RUNNING], + [AgentState.USER_REJECTED]: [AgentState.RUNNING], + [AgentState.AWAITING_USER_CONFIRMATION]: [], +}; diff --git a/frontend/src/components/chat/message.d.ts b/frontend/src/message.d.ts similarity index 100% rename from frontend/src/components/chat/message.d.ts rename to frontend/src/message.d.ts diff --git a/frontend/src/routes/_oh._index/route.tsx b/frontend/src/routes/_oh._index/route.tsx index 3e5db919c0f0..1c4cb3efba0f 100644 --- a/frontend/src/routes/_oh._index/route.tsx +++ b/frontend/src/routes/_oh._index/route.tsx @@ -1,17 +1,17 @@ import { useLocation, useNavigate } from "@remix-run/react"; import React from "react"; import { useDispatch } from "react-redux"; -import { TaskForm } from "./task-form"; -import { HeroHeading } from "./hero-heading"; import { setImportedProjectZip } from "#/state/initial-query-slice"; -import { GitHubRepositoriesSuggestionBox } from "#/components/github-repositories-suggestion-box"; import { convertZipToBase64 } from "#/utils/convert-zip-to-base64"; import { useUserRepositories } from "#/hooks/query/use-user-repositories"; import { useGitHubUser } from "#/hooks/query/use-github-user"; import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url"; import { useConfig } from "#/hooks/query/use-config"; import { useAuth } from "#/context/auth-context"; -import { ImportProjectSuggestionBox } from "./import-project-suggestion-box"; +import { ImportProjectSuggestionBox } from "../../components/features/suggestions/import-project-suggestion-box"; +import { GitHubRepositoriesSuggestionBox } from "#/components/features/github/github-repositories-suggestion-box"; +import { HeroHeading } from "#/components/shared/hero-heading"; +import { TaskForm } from "#/components/shared/task-form"; function Home() { const { token, gitHubToken } = useAuth(); diff --git a/frontend/src/routes/_oh.app._index/route.tsx b/frontend/src/routes/_oh.app._index/route.tsx index 99f583ecf74e..169665100535 100644 --- a/frontend/src/routes/_oh.app._index/route.tsx +++ b/frontend/src/routes/_oh.app._index/route.tsx @@ -5,12 +5,12 @@ import { editor } from "monaco-editor"; import { EditorProps } from "@monaco-editor/react"; import { RootState } from "#/store"; import AgentState from "#/types/agent-state"; -import { FileExplorer } from "#/routes/_oh.app._index/file-explorer/file-explorer"; -import CodeEditorComponent from "./code-editor-component"; +import CodeEditorComponent from "../../components/features/editor/code-editor-component"; import { useFiles } from "#/context/files"; -import { EditorActions } from "#/components/editor-actions"; import { useSaveFile } from "#/hooks/mutation/use-save-file"; import { ASSET_FILE_TYPES } from "./constants"; +import { EditorActions } from "#/components/features/editor/editor-actions"; +import { FileExplorer } from "#/components/features/file-explorer/file-explorer"; export function ErrorBoundary() { const error = useRouteError(); diff --git a/frontend/src/routes/_oh.app.browser.tsx b/frontend/src/routes/_oh.app.browser.tsx index 4b4908506805..3c953c00ea03 100644 --- a/frontend/src/routes/_oh.app.browser.tsx +++ b/frontend/src/routes/_oh.app.browser.tsx @@ -1,4 +1,4 @@ -import BrowserPanel from "#/components/browser"; +import { BrowserPanel } from "#/components/features/browser/browser"; function Browser() { return <BrowserPanel />; diff --git a/frontend/src/routes/_oh.app.jupyter.tsx b/frontend/src/routes/_oh.app.jupyter.tsx index 71de5941366a..6565ad2c31f6 100644 --- a/frontend/src/routes/_oh.app.jupyter.tsx +++ b/frontend/src/routes/_oh.app.jupyter.tsx @@ -1,5 +1,5 @@ import React from "react"; -import JupyterEditor from "#/components/jupyter"; +import { JupyterEditor } from "#/components/features/jupyter/jupyter"; function Jupyter() { const parentRef = React.useRef<HTMLDivElement>(null); diff --git a/frontend/src/routes/_oh.app/loading-spinner.tsx b/frontend/src/routes/_oh.app/loading-spinner.tsx deleted file mode 100644 index ba119cb20c6b..000000000000 --- a/frontend/src/routes/_oh.app/loading-spinner.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function LoadingSpinner() { - return ( - <div className="flex justify-center"> - <div className="w-6 h-6 border-2 border-t-[4px] border-primary-500 rounded-full animate-spin" /> - </div> - ); -} diff --git a/frontend/src/routes/_oh.app/route.tsx b/frontend/src/routes/_oh.app/route.tsx index 5a0fc655e9c9..5d257b169495 100644 --- a/frontend/src/routes/_oh.app/route.tsx +++ b/frontend/src/routes/_oh.app/route.tsx @@ -2,10 +2,8 @@ import { useDisclosure } from "@nextui-org/react"; import React from "react"; import { Outlet } from "@remix-run/react"; import { useDispatch, useSelector } from "react-redux"; -import Security from "#/components/modals/security/security"; -import { Controls } from "#/components/controls"; +import { Controls } from "#/components/features/controls/controls"; import { RootState } from "#/store"; -import { Container } from "#/components/container"; import { clearMessages } from "#/state/chat-slice"; import { clearTerminal } from "#/state/command-slice"; import { useEffectOnce } from "#/utils/use-effect-once"; @@ -14,13 +12,15 @@ import GlobeIcon from "#/icons/globe.svg?react"; import ListIcon from "#/icons/list-type-number.svg?react"; import { clearJupyter } from "#/state/jupyter-slice"; import { FilesProvider } from "#/context/files"; -import { ChatInterface } from "./chat-interface"; +import { ChatInterface } from "../../components/features/chat/chat-interface"; import { WsClientProvider } from "#/context/ws-client-provider"; import { EventHandler } from "./event-handler"; import { useLatestRepoCommit } from "#/hooks/query/use-latest-repo-commit"; import { useAuth } from "#/context/auth-context"; import { useUserPrefs } from "#/context/user-prefs-context"; import { useConversationConfig } from "#/hooks/query/use-conversation-config"; +import { Container } from "#/components/layout/container"; +import Security from "#/components/shared/modals/security/security"; function App() { const { token, gitHubToken } = useAuth(); @@ -43,7 +43,7 @@ function App() { ); const Terminal = React.useMemo( - () => React.lazy(() => import("#/components/terminal/terminal")), + () => React.lazy(() => import("#/components/features/terminal/terminal")), [], ); diff --git a/frontend/src/routes/_oh/route.tsx b/frontend/src/routes/_oh/route.tsx index 328c271add97..ecd6e84dd9f4 100644 --- a/frontend/src/routes/_oh/route.tsx +++ b/frontend/src/routes/_oh/route.tsx @@ -1,14 +1,14 @@ import React from "react"; import { useRouteError, isRouteErrorResponse, Outlet } from "@remix-run/react"; import i18n from "#/i18n"; -import { WaitlistModal } from "#/components/waitlist-modal"; -import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal"; import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url"; import { useIsAuthed } from "#/hooks/query/use-is-authed"; import { useAuth } from "#/context/auth-context"; import { useUserPrefs } from "#/context/user-prefs-context"; -import { Sidebar } from "./sidebar"; import { useConfig } from "#/hooks/query/use-config"; +import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal"; +import { Sidebar } from "#/components/features/sidebar/sidebar"; +import { WaitlistModal } from "#/components/features/waitlist/waitlist-modal"; export function ErrorBoundary() { const error = useRouteError(); diff --git a/frontend/src/tailwind.css b/frontend/src/tailwind.css index b5c61c956711..a48cf2cb6a6a 100644 --- a/frontend/src/tailwind.css +++ b/frontend/src/tailwind.css @@ -1,3 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; + +.button-base { + @apply bg-neutral-700 border border-neutral-600 rounded; +} diff --git a/frontend/src/utils/display-error-toast.tsx b/frontend/src/utils/display-error-toast.tsx index d5952d979cd0..d481ccd932fd 100644 --- a/frontend/src/utils/display-error-toast.tsx +++ b/frontend/src/utils/display-error-toast.tsx @@ -1,5 +1,5 @@ import toast from "react-hot-toast"; -import { ErrorToast } from "#/components/error-toast"; +import { ErrorToast } from "#/components/shared/error-toast"; export const displayErrorToast = (error: string) => toast((t) => <ErrorToast id={t.id} error={error} />, { diff --git a/frontend/src/utils/parse-cell-content.ts b/frontend/src/utils/parse-cell-content.ts new file mode 100644 index 000000000000..98cd1e4b6086 --- /dev/null +++ b/frontend/src/utils/parse-cell-content.ts @@ -0,0 +1,24 @@ +export type JupyterLine = { type: "plaintext" | "image"; content: string }; + +export const parseCellContent = (content: string) => { + const lines: JupyterLine[] = []; + let currentText = ""; + + for (const line of content.split("\n")) { + if (line.startsWith("![image](data:image/png;base64,")) { + if (currentText) { + lines.push({ type: "plaintext", content: currentText }); + currentText = ""; // Reset after pushing plaintext + } + lines.push({ type: "image", content: line }); + } else { + currentText += `${line}\n`; + } + } + + if (currentText) { + lines.push({ type: "plaintext", content: currentText }); + } + + return lines; +};