refactor(frontend): migration of initial-query-slice.ts to zustand (#11020)

This commit is contained in:
Hiep Le
2025-09-19 22:27:20 +07:00
committed by GitHub
parent a8a3e9e604
commit 88cd16ae21
7 changed files with 135 additions and 80 deletions

View File

@@ -1,20 +1,24 @@
import { describe, it, expect } from "vitest";
import store from "../src/store";
import {
setInitialPrompt,
clearInitialPrompt,
} from "../src/state/initial-query-slice";
import { describe, it, expect, beforeEach } from "vitest";
import { useInitialQueryStore } from "../src/stores/initial-query-store";
describe("Initial Query Behavior", () => {
it("should clear initial query when clearInitialPrompt is dispatched", () => {
beforeEach(() => {
// Reset the store before each test
useInitialQueryStore.getState().reset();
});
it("should clear initial query when clearInitialPrompt is called", () => {
const { setInitialPrompt, clearInitialPrompt, initialPrompt } =
useInitialQueryStore.getState();
// Set up initial query in the store
store.dispatch(setInitialPrompt("test query"));
expect(store.getState().initialQuery.initialPrompt).toBe("test query");
setInitialPrompt("test query");
expect(useInitialQueryStore.getState().initialPrompt).toBe("test query");
// Clear the initial query
store.dispatch(clearInitialPrompt());
clearInitialPrompt();
// Verify initial query is cleared
expect(store.getState().initialQuery.initialPrompt).toBeNull();
expect(useInitialQueryStore.getState().initialPrompt).toBeNull();
});
});

View File

@@ -59,7 +59,8 @@
"tailwind-scrollbar": "^4.0.2",
"vite": "^7.1.4",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
"ws": "^8.18.2",
"zustand": "^5.0.8"
},
"devDependencies": {
"@babel/parser": "^7.28.3",
@@ -18326,6 +18327,35 @@
"dev": true,
"license": "MIT"
},
"node_modules/zustand": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -58,7 +58,8 @@
"tailwind-scrollbar": "^4.0.2",
"vite": "^7.1.4",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
"ws": "^8.18.2",
"zustand": "^5.0.8"
},
"scripts": {
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",

View File

@@ -18,6 +18,7 @@ import { useWsClient } from "#/context/ws-client-provider";
import { Messages } from "./messages";
import { ChatSuggestions } from "./chat-suggestions";
import { ScrollProvider } from "#/context/scroll-context";
import { useInitialQueryStore } from "#/stores/initial-query-store";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
@@ -67,9 +68,7 @@ export function ChatInterface() {
"positive" | "negative"
>("positive");
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
const { selectedRepository, replayJson } = useSelector(
(state: RootState) => state.initialQuery,
);
const { selectedRepository, replayJson } = useInitialQueryStore();
const params = useParams();
const { mutateAsync: uploadFiles } = useUploadFiles();

View File

@@ -1,62 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Provider } from "#/types/settings";
import { GitRepository } from "#/types/git";
type SliceState = {
files: string[]; // base64 encoded images
initialPrompt: string | null;
selectedRepository: GitRepository | null;
selectedRepositoryProvider: Provider | null;
replayJson: string | null;
};
const initialState: SliceState = {
files: [],
initialPrompt: null,
selectedRepository: null,
selectedRepositoryProvider: null,
replayJson: null,
};
export const selectedFilesSlice = createSlice({
name: "initialQuery",
initialState,
reducers: {
addFile(state, action: PayloadAction<string>) {
state.files.push(action.payload);
},
removeFile(state, action: PayloadAction<number>) {
state.files.splice(action.payload, 1);
},
clearFiles(state) {
state.files = [];
},
setInitialPrompt(state, action: PayloadAction<string>) {
state.initialPrompt = action.payload;
},
clearInitialPrompt(state) {
state.initialPrompt = null;
},
setSelectedRepository(state, action: PayloadAction<GitRepository | null>) {
state.selectedRepository = action.payload;
},
clearSelectedRepository(state) {
state.selectedRepository = null;
},
setReplayJson(state, action: PayloadAction<string | null>) {
state.replayJson = action.payload;
},
},
});
export const {
addFile,
removeFile,
clearFiles,
setInitialPrompt,
clearInitialPrompt,
setSelectedRepository,
clearSelectedRepository,
setReplayJson,
} = selectedFilesSlice.actions;
export default selectedFilesSlice.reducer;

View File

@@ -2,7 +2,6 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
import agentReducer from "./state/agent-slice";
import browserReducer from "./state/browser-slice";
import fileStateReducer from "./state/file-state-slice";
import initialQueryReducer from "./state/initial-query-slice";
import commandReducer from "./state/command-slice";
import { jupyterReducer } from "./state/jupyter-slice";
import securityAnalyzerReducer from "./state/security-analyzer-slice";
@@ -14,7 +13,6 @@ import eventMessageReducer from "./state/event-message-slice";
export const rootReducer = combineReducers({
fileState: fileStateReducer,
initialQuery: initialQueryReducer,
browser: browserReducer,
cmd: commandReducer,
agent: agentReducer,

View File

@@ -0,0 +1,85 @@
import { create } from "zustand";
import { Provider } from "#/types/settings";
import { GitRepository } from "#/types/git";
interface InitialQueryState {
files: string[]; // base64 encoded images
initialPrompt: string | null;
selectedRepository: GitRepository | null;
selectedRepositoryProvider: Provider | null;
replayJson: string | null;
}
interface InitialQueryActions {
addFile: (file: string) => void;
removeFile: (index: number) => void;
clearFiles: () => void;
setInitialPrompt: (prompt: string) => void;
clearInitialPrompt: () => void;
setSelectedRepository: (repository: GitRepository | null) => void;
clearSelectedRepository: () => void;
setSelectedRepositoryProvider: (provider: Provider | null) => void;
setReplayJson: (replayJson: string | null) => void;
reset: () => void;
}
type InitialQueryStore = InitialQueryState & InitialQueryActions;
const initialState: InitialQueryState = {
files: [],
initialPrompt: null,
selectedRepository: null,
selectedRepositoryProvider: null,
replayJson: null,
};
export const useInitialQueryStore = create<InitialQueryStore>((set) => ({
...initialState,
addFile: (file: string) =>
set((state) => ({
files: [...state.files, file],
})),
removeFile: (index: number) =>
set((state) => ({
files: state.files.filter((_, i) => i !== index),
})),
clearFiles: () =>
set(() => ({
files: [],
})),
setInitialPrompt: (prompt: string) =>
set(() => ({
initialPrompt: prompt,
})),
clearInitialPrompt: () =>
set(() => ({
initialPrompt: null,
})),
setSelectedRepository: (repository: GitRepository | null) =>
set(() => ({
selectedRepository: repository,
})),
clearSelectedRepository: () =>
set(() => ({
selectedRepository: null,
})),
setSelectedRepositoryProvider: (provider: Provider | null) =>
set(() => ({
selectedRepositoryProvider: provider,
})),
setReplayJson: (replayJson: string | null) =>
set(() => ({
replayJson,
})),
reset: () => set(() => initialState),
}));