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

This commit is contained in:
sp.wack 2024-11-15 16:56:47 +02:00 committed by GitHub
parent 9cd248d475
commit ffc4d32440
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 65 additions and 59 deletions

View File

@ -20,6 +20,7 @@ import {
} from "#/services/terminalService";
import {
clearFiles,
clearInitialQuery,
clearSelectedRepository,
setImportedProjectZip,
} from "#/state/initial-query-slice";
@ -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();
@ -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 = "";
@ -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
}
}

View File

@ -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;
@ -44,6 +20,7 @@ interface GitHubRepositoriesSuggestionBoxProps {
}
export function GitHubRepositoriesSuggestionBox({
handleSubmit,
repositories,
gitHubAuthUrl,
user,
@ -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 && (

View File

@ -1,16 +1,16 @@
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) => {
@ -18,7 +18,7 @@ export function GitHubRepositorySelector({
if (repo) {
// set query param
dispatch(setSelectedRepository(repo.full_name));
navigate("/app");
onSelect();
}
};

View File

@ -5,7 +5,6 @@ import {
defer,
redirect,
useLoaderData,
useNavigate,
useRouteLoaderData,
} from "@remix-run/react";
import React from "react";
@ -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
@ -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
@ -100,6 +99,7 @@ function Home() {
<Await resolve={repositories}>
{(resolvedRepositories) => (
<GitHubRepositoriesSuggestionBox
handleSubmit={() => formRef.current?.requestSubmit()}
repositories={resolvedRepositories}
gitHubAuthUrl={githubAuthUrl}
user={rootData?.user || null}
@ -129,7 +129,7 @@ function Home() {
dispatch(
setImportedProjectZip(await convertZipToBase64(zip)),
);
navigate("/app");
formRef.current?.requestSubmit();
} else {
// TODO: handle error
}

View File

@ -13,7 +13,7 @@ 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();
@ -21,7 +21,6 @@ export function TaskForm() {
(state: RootState) => state.initalQuery,
);
const formRef = React.useRef<HTMLFormElement>(null);
const [text, setText] = React.useState("");
const [suggestion, setSuggestion] = React.useState(
getRandomKey(SUGGESTIONS["non-repo"]),
@ -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
@ -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)}
@ -116,4 +115,6 @@ export function TaskForm() {
)}
</div>
);
}
});
TaskForm.displayName = "TaskForm";

View File

@ -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({
function CodeEditorComponent({
onMount,
isReadOnly,
}: CodeEditorCompoonentProps) {
}: CodeEditorComponentProps) {
const { t } = useTranslation();
const {
files,
@ -107,4 +107,4 @@ function CodeEditorCompoonent({
);
}
export default React.memo(CodeEditorCompoonent);
export default React.memo(CodeEditorComponent);

View File

@ -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";
@ -138,7 +138,7 @@ function CodeEditor() {
/>
</div>
)}
<CodeEditorCompoonent
<CodeEditorComponent
onMount={handleEditorDidMount}
isReadOnly={!isEditingAllowed}
/>

View File

@ -18,7 +18,6 @@ import { useEffectOnce } from "#/utils/use-effect-once";
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";
@ -28,8 +27,6 @@ import { EventHandler } from "#/components/event-handler";
export const clientLoader = async () => {
const ghToken = localStorage.getItem("ghToken");
const q = store.getState().initalQuery.initialQuery;
const repo =
store.getState().initalQuery.selectedRepository ||
localStorage.getItem("repo");
@ -55,7 +52,6 @@ export const clientLoader = async () => {
token,
ghToken,
repo,
q,
lastCommit,
});
};
@ -91,7 +87,6 @@ function App() {
dispatch(clearMessages());
dispatch(clearTerminal());
dispatch(clearJupyter());
dispatch(clearInitialQuery()); // Clear initial query when navigating to /app
});
const {

View File

@ -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);
},
);