Skip to content

Commit fc00c46

Browse files
FIX: Deduplicate getExplorer and getDbExplorer API calls via Zustand store (#71)
* fix: dedupe getExplorer API call via Zustand store Both explorer-component.jsx and chat-ai/Body.jsx were independently calling explorerSvc.getExplorer(projectId), causing a duplicate GET /explorer request every time the chat drawer opened. Introduce a minimal Zustand store (explorer-store.js) that holds the shared response. explorer-component.jsx becomes the single writer (updates on every successful fetch, clears on project switch), and Body.jsx reads explorerData from the store to populate prompt autocomplete instead of issuing its own fetch. getDbExplorer in Body.jsx is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: dedupe getDbExplorer API call via the same Zustand store Extend useExplorerStore with a dbExplorerData slice so chat-ai/Body.jsx stops fetching /db_explorer independently. Previously, opening the chat drawer triggered a second identical GET /db_explorer alongside the one already issued by explorer-component.jsx on mount. explorer-component.jsx becomes the single writer for both slices (getExplorer and getDbExplorer wrappers), and the existing project-switch clear effect now resets both slices via an expanded clearExplorerData. Body.jsx subscribes to the store and mirrors dbExplorerData into the existing promptAutoComplete.dbData shape — no direct explorerService usage remains in chat-ai/Body.jsx (explorerService import, useRef import, and the explorerSvc ref were dropped). Also updated the PR description to cover both refactors in one PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: address review feedback on explorer dedup refactor Three review-driven tweaks from the first round of review on this branch: - explorer-component.jsx: publish the raw /explorer response to useExplorerStore (via rawTreeDataRef.current) before the in-place mutations by sortModels, applyModelDecorations, and transformTree begin. The store now matches its documented contract and Body.jsx's autocomplete receives the untransformed children shape it did before the dedup refactor. - explorer-component.jsx: add clearExplorerData to the dependency array of the project-switch clear effect. Zustand action refs are stable in practice, so effect cadence is unchanged, but the deps array is now exhaustive and future-proof against a non-stable refactor. - Body.jsx: expand the short comment above the mirror effects into a block that documents why this component is read-only and lists the escape hatches (fetch fallback here or a loading state in InputPrompt) to reach for if the "explorer mounts before chat drawer" invariant ever breaks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: use null-sentinel for schemaMenu to gate explorer fetch correctly The prior `schemaMenu?.length` guard on the schema/schema-change useEffect conflated "schemas not loaded yet" (schemaMenu === []) with "loaded but empty" (same []). A reviewer correctly flagged that a project with no DB connection would have the fetch silently suppressed. Fix: - Initialise schemaMenu as null (sentinel for "not yet loaded") instead of []. - Revert the effect guard to `if (schemaMenu)` so it fires the moment schemaMenu transitions from null to an array, regardless of whether that array has items. - Add null-safe guards — `(schemaMenu || [])` — at the JSX consumers (`.map`, `.length` comparisons) that now need to handle the pre-load null state. During loading, seed-related actions remain disabled and the schema dropdown is empty, both the correct UX. - Add an inline comment above the effect explaining the sentinel so this design choice is visible at the call site. Net result: the original optimisation (skip the redundant mount-time fetch while schemaMenu is still []) is preserved, and the reviewer's no-DB regression is fixed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: only clear explorer store on actual project change The project-switch clear effect was also firing on initial mount (and on any remount of explorer-component within the same session), which briefly wiped valid store data and could leave chat autocomplete empty until the next fetch resolved. Gate the clear with a prevProjectIdRef so clearExplorerData() only fires when projectId actually changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent bd06a62 commit fc00c46

3 files changed

Lines changed: 78 additions & 38 deletions

File tree

frontend/src/ide/chat-ai/Body.jsx

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { useState, useCallback, useEffect, useRef } from "react";
1+
import { useState, useCallback, useEffect } from "react";
22
import PropTypes from "prop-types";
33

44
import { useSocketService } from "../../service/socket-service";
55
import { ExistingChat } from "./ExistingChat";
66
import { NewChat } from "./NewChat";
77
import { useChatAIService } from "./services";
88
import { useProjectStore } from "../../store/project-store";
9+
import { useExplorerStore } from "../../store/explorer-store";
910
import { useNotificationService } from "../../service/notification-service";
10-
import { explorerService } from "../explorer/explorer-service";
1111

1212
// Cloud-only: fetch token balance (unavailable in OSS — import fails gracefully)
1313
let getTokenBalance = null;
@@ -71,8 +71,9 @@ const Body = function Body({
7171
seedsData: {},
7272
dbData: {},
7373
});
74-
const explorerSvc = useRef(explorerService()).current;
7574
const { projectId } = useProjectStore();
75+
const explorerData = useExplorerStore((state) => state.explorerData);
76+
const dbExplorerData = useExplorerStore((state) => state.dbExplorerData);
7677

7778
const { postChatPrompt, getChatIntents, getChatLlmModels } =
7879
useChatAIService();
@@ -202,37 +203,28 @@ const Body = function Body({
202203
});
203204
}, [selectedChatId, chatMessages.length, realTokenBalance, notify]);
204205

206+
// Autocomplete data is mirrored passively from useExplorerStore, which is
207+
// populated exclusively by explorer-component.jsx. This component deliberately
208+
// does NOT fetch on its own — the IDE layout mounts the explorer before the
209+
// chat drawer opens, so the store is populated by the time autocomplete is
210+
// triggered. If that invariant ever breaks (lazy-loaded explorer, standalone
211+
// chat route, explorer fetch failure), add a fetch fallback here or a
212+
// loading/unavailable state in the InputPrompt autocomplete UI.
205213
useEffect(() => {
206-
if (!projectId || !isChatDrawerOpen) return;
207-
208-
// fetch database schemas -> update immediately when ready
209-
explorerSvc
210-
.getDbExplorer(projectId)
211-
.then((res) => {
212-
setPromptAutoComplete((prev) => ({
213-
...prev,
214-
dbData: res?.data || {},
215-
}));
216-
})
217-
.catch(() => {
218-
console.error("Failed to fetch database schemas");
219-
});
214+
const children = explorerData || [];
215+
setPromptAutoComplete((prev) => ({
216+
...prev,
217+
modelsData: children[0] || {},
218+
seedsData: children[1] || {},
219+
}));
220+
}, [explorerData]);
220221

221-
// fetch models & seeds -> update as soon as ready
222-
explorerSvc
223-
.getExplorer(projectId)
224-
.then((res) => {
225-
const children = res?.data?.children || [];
226-
setPromptAutoComplete((prev) => ({
227-
...prev,
228-
modelsData: children[0] || {},
229-
seedsData: children[1] || {},
230-
}));
231-
})
232-
.catch(() => {
233-
console.error("Failed to fetch models and seeds");
234-
});
235-
}, [projectId, isChatDrawerOpen, explorerSvc]);
222+
useEffect(() => {
223+
setPromptAutoComplete((prev) => ({
224+
...prev,
225+
dbData: dbExplorerData || {},
226+
}));
227+
}, [dbExplorerData]);
236228

237229
const triggerGetChatMessagesApi = useCallback(() => {
238230
setIsGetChatMessages(true);

frontend/src/ide/explorer/explorer-component.jsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import "../ide-layout.css";
6363
import { useNotificationService } from "../../service/notification-service.js";
6464
import { SpinnerLoader } from "../../widgets/spinner_loader/index.js";
6565
import { useRefreshModelsStore } from "../../store/refresh-models-store.js";
66+
import { useExplorerStore } from "../../store/explorer-store.js";
6667
import { LinearScale } from "../../base/icons";
6768

6869
// Static sort options for model explorer
@@ -176,6 +177,13 @@ const IdeExplorer = ({
176177
const currentSchema = useProjectStore((state) => state.currentSchema);
177178
const setCurrentSchema = useProjectStore((state) => state.setCurrentSchema);
178179
const setSchemaList = useProjectStore((state) => state.setSchemaList);
180+
const setExplorerData = useExplorerStore((state) => state.setExplorerData);
181+
const setDbExplorerData = useExplorerStore(
182+
(state) => state.setDbExplorerData
183+
);
184+
const clearExplorerData = useExplorerStore(
185+
(state) => state.clearExplorerData
186+
);
179187

180188
// Reset currentSchema on unmount to prevent stale data
181189
useEffect(() => {
@@ -194,7 +202,7 @@ const IdeExplorer = ({
194202
const [openNameModal, setOpenNameModal] = useState(false);
195203
const [newSchemaName, setNewSchemaName] = useState("");
196204
const [isSchemaModalOpen, setIsSchemaModalOpen] = useState(false);
197-
const [schemaMenu, setSchemaMenu] = useState([]);
205+
const [schemaMenu, setSchemaMenu] = useState(null);
198206
const [dbExplorer, setDBExplorer] = useState([]);
199207
const [activeMenu, setActiveMenu] = useState("");
200208
const [dbLoading, setDbLoading] = useState(false);
@@ -976,7 +984,7 @@ const IdeExplorer = ({
976984
<>
977985
<Dropdown
978986
menu={{
979-
items: schemaMenu.map((el) => ({
987+
items: (schemaMenu || []).map((el) => ({
980988
...el,
981989
label:
982990
el.key === "add-new-schema" ? (
@@ -1014,7 +1022,7 @@ const IdeExplorer = ({
10141022
? "Run Seed Disabled - Please select a schema"
10151023
: previewTimeTravel
10161024
? "Run Seed Disabled - Time travel mode active"
1017-
: schemaMenu.length <= 1
1025+
: (schemaMenu || []).length <= 1
10181026
? "Run Seed Disabled - No schemas available"
10191027
: "Run Seed"
10201028
}
@@ -1035,7 +1043,7 @@ const IdeExplorer = ({
10351043
disabled={
10361044
previewTimeTravel ||
10371045
!currentSchema ||
1038-
schemaMenu.length <= 1
1046+
(schemaMenu || []).length <= 1
10391047
}
10401048
>
10411049
<PlayCircleOutlined />
@@ -1118,7 +1126,7 @@ const IdeExplorer = ({
11181126
if (
11191127
!previewTimeTravel &&
11201128
currentSchema &&
1121-
schemaMenu.length > 1
1129+
(schemaMenu || []).length > 1
11221130
) {
11231131
handleSeedIconClick(event, child.title);
11241132
}
@@ -1127,7 +1135,7 @@ const IdeExplorer = ({
11271135
previewTimeTravel ||
11281136
seedRunningRef.current ||
11291137
!currentSchema ||
1130-
schemaMenu.length <= 1
1138+
(schemaMenu || []).length <= 1
11311139
? "seed-icon-disabled"
11321140
: ""
11331141
}`}
@@ -1244,12 +1252,28 @@ const IdeExplorer = ({
12441252
[modelSortBy, handleModelSort]
12451253
);
12461254

1255+
// schemaMenu starts as null; becomes an array (possibly empty) after
1256+
// getSchemas resolves. Gating on truthiness skips the redundant mount-time
1257+
// fetch while still firing for projects whose schema list is legitimately
1258+
// empty.
12471259
useEffect(() => {
12481260
if (schemaMenu) {
12491261
getExplorer(projectId);
12501262
}
12511263
}, [schemaMenu, currentSchema]);
12521264

1265+
// Clear shared explorer data on project switch so other consumers
1266+
// (e.g. chat autocomplete) don't momentarily read the previous project's tree.
1267+
// Ref-gated so the clear does NOT fire on initial mount / remount within the
1268+
// same project — only when projectId actually changes.
1269+
const prevProjectIdRef = useRef(projectId);
1270+
useEffect(() => {
1271+
if (prevProjectIdRef.current !== projectId) {
1272+
clearExplorerData();
1273+
prevProjectIdRef.current = projectId;
1274+
}
1275+
}, [projectId, clearExplorerData]);
1276+
12531277
function getExplorer(projectId) {
12541278
if (!projectId) return;
12551279
setLoading(true);
@@ -1258,6 +1282,9 @@ const IdeExplorer = ({
12581282
.then((res) => {
12591283
const treeData = res.data.children;
12601284
rawTreeDataRef.current = JSON.parse(JSON.stringify(treeData));
1285+
// Publish the raw (pre-mutation) shape to the shared store so
1286+
// consumers like chat-ai/Body.jsx get the untransformed children.
1287+
setExplorerData(rawTreeDataRef.current);
12611288
// Apply sort and decorations to no_code models BEFORE transformTree
12621289
// so that _isChild flag is set when className is assigned
12631290
treeData.forEach((node) => {
@@ -1299,6 +1326,7 @@ const IdeExplorer = ({
12991326
const treeData = res.data;
13001327
const mappedData = mapIconsToTreeData([treeData]);
13011328
setDBExplorer(mappedData);
1329+
setDbExplorerData(treeData);
13021330
setCachedLists((prev) => ({
13031331
...prev,
13041332
2: generateList([treeData]), // Correct key
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { create } from "zustand";
2+
3+
/**
4+
* Explorer Store
5+
* Holds the shared responses of explorerSvc.getExplorer(projectId) and
6+
* explorerSvc.getDbExplorer(projectId) so multiple consumers (explorer
7+
* tree, chat autocomplete) don't refetch.
8+
* Owner of writes: frontend/src/ide/explorer/explorer-component.jsx
9+
*/
10+
const useExplorerStore = create((set) => ({
11+
// res.data.children from /explorer API — array where [0]=models, [1]=seeds
12+
explorerData: null,
13+
// res.data from /db_explorer API — single DB tree object
14+
dbExplorerData: null,
15+
setExplorerData: (data) => set({ explorerData: data }),
16+
setDbExplorerData: (data) => set({ dbExplorerData: data }),
17+
clearExplorerData: () => set({ explorerData: null, dbExplorerData: null }),
18+
}));
19+
20+
export { useExplorerStore };

0 commit comments

Comments
 (0)