Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): Keep prompt after project upload or repo selection #4925

Merged
merged 5 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions frontend/src/components/event-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "#/services/terminalService";
import {
clearFiles,
clearInitialQuery,
clearSelectedRepository,
setImportedProjectZip,
} from "#/state/initial-query-slice";
Expand Down Expand Up @@ -52,13 +53,10 @@ export function EventHandler({ children }: React.PropsWithChildren) {
const runtimeActive = status === WsClientProviderStatus.ACTIVE;
const fetcher = useFetcher();
const dispatch = useDispatch();
const { files, importedProjectZip } = useSelector(
const { files, importedProjectZip, initialQuery } = useSelector(
(state: RootState) => state.initalQuery,
);
const { ghToken, repo } = useLoaderData<typeof appClientLoader>();
const initialQueryRef = React.useRef<string | null>(
store.getState().initalQuery.initialQuery,
);

const sendInitialQuery = (query: string, base64Files: string[]) => {
const timestamp = new Date().toISOString();
Expand Down Expand Up @@ -119,7 +117,6 @@ export function EventHandler({ children }: React.PropsWithChildren) {
return; // This is a check because of strict mode - if the status did not change, don't do anything
}
statusRef.current = status;
const initialQuery = initialQueryRef.current;

if (status === WsClientProviderStatus.ACTIVE) {
let additionalInfo = "";
Expand All @@ -140,7 +137,7 @@ export function EventHandler({ children }: React.PropsWithChildren) {
sendInitialQuery(initialQuery, files);
}
dispatch(clearFiles()); // reset selected files
initialQueryRef.current = null;
dispatch(clearInitialQuery()); // reset initial query
}
}

Expand Down
47 changes: 17 additions & 30 deletions frontend/src/components/github-repositories-suggestion-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,8 @@ import { GitHubRepositorySelector } from "#/routes/_oh._index/github-repo-select
import ModalButton from "./buttons/ModalButton";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";

interface GitHubAuthProps {
onConnectToGitHub: () => void;
repositories: GitHubRepository[];
isLoggedIn: boolean;
}

function GitHubAuth({
onConnectToGitHub,
repositories,
isLoggedIn,
}: GitHubAuthProps) {
if (isLoggedIn) {
return <GitHubRepositorySelector repositories={repositories} />;
}

return (
<ModalButton
text="Connect to GitHub"
icon={<GitHubLogo width={20} height={20} />}
className="bg-[#791B80] w-full"
onClick={onConnectToGitHub}
/>
);
}

interface GitHubRepositoriesSuggestionBoxProps {
handleSubmit: () => void;
repositories: Awaited<
ReturnType<typeof retrieveAllGitHubUserRepositories>
> | null;
Expand All @@ -44,6 +20,7 @@ interface GitHubRepositoriesSuggestionBoxProps {
}

export function GitHubRepositoriesSuggestionBox({
handleSubmit,
repositories,
gitHubAuthUrl,
user,
Expand All @@ -70,16 +47,26 @@ export function GitHubRepositoriesSuggestionBox({
);
}

const isLoggedIn = !!user && !isGitHubErrorReponse(user);

return (
<>
<SuggestionBox
title="Open a Repo"
content={
<GitHubAuth
isLoggedIn={!!user && !isGitHubErrorReponse(user)}
repositories={repositories || []}
onConnectToGitHub={handleConnectToGitHub}
/>
isLoggedIn ? (
<GitHubRepositorySelector
onSelect={handleSubmit}
repositories={repositories || []}
/>
) : (
<ModalButton
text="Connect to GitHub"
icon={<GitHubLogo width={20} height={20} />}
className="bg-[#791B80] w-full"
onClick={handleConnectToGitHub}
/>
)
}
/>
{connectToGitHubModalOpen && (
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/routes/_oh._index/github-repo-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Autocomplete, AutocompleteItem } from "@nextui-org/react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { setSelectedRepository } from "#/state/initial-query-slice";

interface GitHubRepositorySelectorProps {
onSelect: () => void;
repositories: GitHubRepository[];
}

export function GitHubRepositorySelector({
onSelect,
repositories,
}: GitHubRepositorySelectorProps) {
const navigate = useNavigate();
const dispatch = useDispatch();

const handleRepoSelection = (id: string | null) => {
const repo = repositories.find((r) => r.id.toString() === id);
if (repo) {
// set query param
dispatch(setSelectedRepository(repo.full_name));
navigate("/app");
onSelect();
}
};

Expand Down
8 changes: 4 additions & 4 deletions frontend/src/routes/_oh._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
defer,
redirect,
useLoaderData,
useNavigate,
useRouteLoaderData,
} from "@remix-run/react";
import React from "react";
Expand Down Expand Up @@ -73,10 +72,10 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
};

function Home() {
const navigate = useNavigate();
const dispatch = useDispatch();
const rootData = useRouteLoaderData<typeof rootClientLoader>("routes/_oh");
const { repositories, githubAuthUrl } = useLoaderData<typeof clientLoader>();
const formRef = React.useRef<HTMLFormElement>(null);

return (
<div
Expand All @@ -86,7 +85,7 @@ function Home() {
<HeroHeading />
<div className="flex flex-col gap-16 w-[600px] items-center">
<div className="flex flex-col gap-2 w-full">
<TaskForm />
<TaskForm ref={formRef} />
</div>
<div className="flex gap-4 w-full">
<React.Suspense
Expand All @@ -100,6 +99,7 @@ function Home() {
<Await resolve={repositories}>
{(resolvedRepositories) => (
<GitHubRepositoriesSuggestionBox
handleSubmit={() => formRef.current?.requestSubmit()}
repositories={resolvedRepositories}
gitHubAuthUrl={githubAuthUrl}
user={rootData?.user || null}
Expand Down Expand Up @@ -129,7 +129,7 @@ function Home() {
dispatch(
setImportedProjectZip(await convertZipToBase64(zip)),
);
navigate("/app");
formRef.current?.requestSubmit();
} else {
// TODO: handle error
}
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/routes/_oh._index/task-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import { getRandomKey } from "#/utils/get-random-key";
import { AttachImageLabel } from "#/components/attach-image-label";
import { cn } from "#/utils/utils";

export function TaskForm() {
export const TaskForm = React.forwardRef<HTMLFormElement>((_, ref) => {
const dispatch = useDispatch();
const navigation = useNavigation();

const { selectedRepository, files } = useSelector(
(state: RootState) => state.initalQuery,
);

const formRef = React.useRef<HTMLFormElement>(null);
const [text, setText] = React.useState("");
const [suggestion, setSuggestion] = React.useState(
getRandomKey(SUGGESTIONS["non-repo"]),
Expand Down Expand Up @@ -55,7 +54,7 @@ export function TaskForm() {
return (
<div className="flex flex-col gap-2 w-full">
<Form
ref={formRef}
ref={ref}
method="post"
className="flex flex-col items-center gap-2"
replace
Expand All @@ -75,7 +74,7 @@ export function TaskForm() {
<ChatInput
name="q"
onSubmit={() => {
formRef.current?.requestSubmit();
if (typeof ref !== "function") ref?.current?.requestSubmit();
}}
onChange={(message) => setText(message)}
onFocus={() => setInputIsFocused(true)}
Expand Down Expand Up @@ -116,4 +115,6 @@ export function TaskForm() {
)}
</div>
);
}
});

TaskForm.displayName = "TaskForm";
8 changes: 4 additions & 4 deletions frontend/src/routes/_oh.app._index/code-editor-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { I18nKey } from "#/i18n/declaration";
import { useFiles } from "#/context/files";
import OpenHands from "#/api/open-hands";

interface CodeEditorCompoonentProps {
interface CodeEditorComponentProps {
onMount: EditorProps["onMount"];
isReadOnly: boolean;
}

function CodeEditorCompoonent({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂

function CodeEditorComponent({
onMount,
isReadOnly,
}: CodeEditorCompoonentProps) {
}: CodeEditorComponentProps) {
const { t } = useTranslation();
const {
files,
Expand Down Expand Up @@ -107,4 +107,4 @@ function CodeEditorCompoonent({
);
}

export default React.memo(CodeEditorCompoonent);
export default React.memo(CodeEditorComponent);
4 changes: 2 additions & 2 deletions frontend/src/routes/_oh.app._index/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RootState } from "#/store";
import AgentState from "#/types/AgentState";
import FileExplorer from "#/components/file-explorer/FileExplorer";
import OpenHands from "#/api/open-hands";
import CodeEditorCompoonent from "./code-editor-component";
import CodeEditorComponent from "./code-editor-component";
import { useFiles } from "#/context/files";
import { EditorActions } from "#/components/editor-actions";

Expand Down Expand Up @@ -138,7 +138,7 @@ function CodeEditor() {
/>
</div>
)}
<CodeEditorCompoonent
<CodeEditorComponent
onMount={handleEditorDidMount}
isReadOnly={!isEditingAllowed}
/>
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/routes/_oh.app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import CodeIcon from "#/icons/code.svg?react";
import GlobeIcon from "#/icons/globe.svg?react";
import ListIcon from "#/icons/list-type-number.svg?react";
import { clearInitialQuery } from "#/state/initial-query-slice";
import { isGitHubErrorReponse, retrieveLatestGitHubCommit } from "#/api/github";
import { clearJupyter } from "#/state/jupyterSlice";
import { FilesProvider } from "#/context/files";
Expand All @@ -28,8 +27,6 @@

export const clientLoader = async () => {
const ghToken = localStorage.getItem("ghToken");

const q = store.getState().initalQuery.initialQuery;
const repo =
store.getState().initalQuery.selectedRepository ||
localStorage.getItem("repo");
Expand All @@ -44,7 +41,7 @@
const data = await retrieveLatestGitHubCommit(ghToken, repo);
if (isGitHubErrorReponse(data)) {
// TODO: Handle error
console.error("Failed to retrieve latest commit", data);

Check warning on line 44 in frontend/src/routes/_oh.app.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

Unexpected console statement
} else {
[lastCommit] = data;
}
Expand All @@ -55,7 +52,6 @@
token,
ghToken,
repo,
q,
lastCommit,
});
};
Expand Down Expand Up @@ -91,7 +87,6 @@
dispatch(clearMessages());
dispatch(clearTerminal());
dispatch(clearJupyter());
dispatch(clearInitialQuery()); // Clear initial query when navigating to /app
});

const {
Expand Down
26 changes: 26 additions & 0 deletions frontend/tests/redirect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,29 @@ test("should redirect to /app after selecting a repo", async ({ page }) => {
await page.waitForURL("/app");
expect(page.url()).toBe("http://127.0.0.1:3000/app");
});

// FIXME: This fails because the MSW WS mocks change state too quickly,
// missing the OPENING status where the initial query is rendered.
test.fail(
"should redirect the user to /app with their initial query after selecting a project",
async ({ page }) => {
await page.goto("/");
await confirmSettings(page);

// enter query
const testQuery = "this is my test query";
const textbox = page.getByPlaceholder(/what do you want to build/i);
expect(textbox).not.toBeNull();
await textbox.fill(testQuery);

const fileInput = page.getByLabel("Upload a .zip");
const filePath = path.join(dirname, "fixtures/project.zip");
await fileInput.setInputFiles(filePath);

await page.waitForURL("/app");

// get user message
const userMessage = page.getByTestId("user-message");
expect(await userMessage.textContent()).toBe(testQuery);
},
);
Loading