mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(frontend): Keep prompt after project upload or repo selection (#4925)
This commit is contained in:
parent
9cd248d475
commit
ffc4d32440
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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…
x
Reference in New Issue
Block a user