diff --git a/frontend/src/ide/chat-ai/Body.jsx b/frontend/src/ide/chat-ai/Body.jsx index 9b08de47..9ce04bdf 100644 --- a/frontend/src/ide/chat-ai/Body.jsx +++ b/frontend/src/ide/chat-ai/Body.jsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useRef } from "react"; +import { useState, useCallback, useEffect } from "react"; import PropTypes from "prop-types"; import { useSocketService } from "../../service/socket-service"; @@ -6,8 +6,8 @@ import { ExistingChat } from "./ExistingChat"; import { NewChat } from "./NewChat"; import { useChatAIService } from "./services"; import { useProjectStore } from "../../store/project-store"; +import { useExplorerStore } from "../../store/explorer-store"; import { useNotificationService } from "../../service/notification-service"; -import { explorerService } from "../explorer/explorer-service"; // Cloud-only: fetch token balance (unavailable in OSS — import fails gracefully) let getTokenBalance = null; @@ -71,8 +71,9 @@ const Body = function Body({ seedsData: {}, dbData: {}, }); - const explorerSvc = useRef(explorerService()).current; const { projectId } = useProjectStore(); + const explorerData = useExplorerStore((state) => state.explorerData); + const dbExplorerData = useExplorerStore((state) => state.dbExplorerData); const { postChatPrompt, getChatIntents, getChatLlmModels } = useChatAIService(); @@ -202,37 +203,28 @@ const Body = function Body({ }); }, [selectedChatId, chatMessages.length, realTokenBalance, notify]); + // Autocomplete data is mirrored passively from useExplorerStore, which is + // populated exclusively by explorer-component.jsx. This component deliberately + // does NOT fetch on its own — the IDE layout mounts the explorer before the + // chat drawer opens, so the store is populated by the time autocomplete is + // triggered. If that invariant ever breaks (lazy-loaded explorer, standalone + // chat route, explorer fetch failure), add a fetch fallback here or a + // loading/unavailable state in the InputPrompt autocomplete UI. useEffect(() => { - if (!projectId || !isChatDrawerOpen) return; - - // fetch database schemas -> update immediately when ready - explorerSvc - .getDbExplorer(projectId) - .then((res) => { - setPromptAutoComplete((prev) => ({ - ...prev, - dbData: res?.data || {}, - })); - }) - .catch(() => { - console.error("Failed to fetch database schemas"); - }); + const children = explorerData || []; + setPromptAutoComplete((prev) => ({ + ...prev, + modelsData: children[0] || {}, + seedsData: children[1] || {}, + })); + }, [explorerData]); - // fetch models & seeds -> update as soon as ready - explorerSvc - .getExplorer(projectId) - .then((res) => { - const children = res?.data?.children || []; - setPromptAutoComplete((prev) => ({ - ...prev, - modelsData: children[0] || {}, - seedsData: children[1] || {}, - })); - }) - .catch(() => { - console.error("Failed to fetch models and seeds"); - }); - }, [projectId, isChatDrawerOpen, explorerSvc]); + useEffect(() => { + setPromptAutoComplete((prev) => ({ + ...prev, + dbData: dbExplorerData || {}, + })); + }, [dbExplorerData]); const triggerGetChatMessagesApi = useCallback(() => { setIsGetChatMessages(true); diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 2a0471ae..21414009 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -63,6 +63,7 @@ import "../ide-layout.css"; import { useNotificationService } from "../../service/notification-service.js"; import { SpinnerLoader } from "../../widgets/spinner_loader/index.js"; import { useRefreshModelsStore } from "../../store/refresh-models-store.js"; +import { useExplorerStore } from "../../store/explorer-store.js"; import { LinearScale } from "../../base/icons"; // Static sort options for model explorer @@ -176,6 +177,13 @@ const IdeExplorer = ({ const currentSchema = useProjectStore((state) => state.currentSchema); const setCurrentSchema = useProjectStore((state) => state.setCurrentSchema); const setSchemaList = useProjectStore((state) => state.setSchemaList); + const setExplorerData = useExplorerStore((state) => state.setExplorerData); + const setDbExplorerData = useExplorerStore( + (state) => state.setDbExplorerData + ); + const clearExplorerData = useExplorerStore( + (state) => state.clearExplorerData + ); // Reset currentSchema on unmount to prevent stale data useEffect(() => { @@ -194,7 +202,7 @@ const IdeExplorer = ({ const [openNameModal, setOpenNameModal] = useState(false); const [newSchemaName, setNewSchemaName] = useState(""); const [isSchemaModalOpen, setIsSchemaModalOpen] = useState(false); - const [schemaMenu, setSchemaMenu] = useState([]); + const [schemaMenu, setSchemaMenu] = useState(null); const [dbExplorer, setDBExplorer] = useState([]); const [activeMenu, setActiveMenu] = useState(""); const [dbLoading, setDbLoading] = useState(false); @@ -976,7 +984,7 @@ const IdeExplorer = ({ <> ({ + items: (schemaMenu || []).map((el) => ({ ...el, label: el.key === "add-new-schema" ? ( @@ -1014,7 +1022,7 @@ const IdeExplorer = ({ ? "Run Seed Disabled - Please select a schema" : previewTimeTravel ? "Run Seed Disabled - Time travel mode active" - : schemaMenu.length <= 1 + : (schemaMenu || []).length <= 1 ? "Run Seed Disabled - No schemas available" : "Run Seed" } @@ -1035,7 +1043,7 @@ const IdeExplorer = ({ disabled={ previewTimeTravel || !currentSchema || - schemaMenu.length <= 1 + (schemaMenu || []).length <= 1 } > @@ -1118,7 +1126,7 @@ const IdeExplorer = ({ if ( !previewTimeTravel && currentSchema && - schemaMenu.length > 1 + (schemaMenu || []).length > 1 ) { handleSeedIconClick(event, child.title); } @@ -1127,7 +1135,7 @@ const IdeExplorer = ({ previewTimeTravel || seedRunningRef.current || !currentSchema || - schemaMenu.length <= 1 + (schemaMenu || []).length <= 1 ? "seed-icon-disabled" : "" }`} @@ -1244,12 +1252,28 @@ const IdeExplorer = ({ [modelSortBy, handleModelSort] ); + // schemaMenu starts as null; becomes an array (possibly empty) after + // getSchemas resolves. Gating on truthiness skips the redundant mount-time + // fetch while still firing for projects whose schema list is legitimately + // empty. useEffect(() => { if (schemaMenu) { getExplorer(projectId); } }, [schemaMenu, currentSchema]); + // Clear shared explorer data on project switch so other consumers + // (e.g. chat autocomplete) don't momentarily read the previous project's tree. + // Ref-gated so the clear does NOT fire on initial mount / remount within the + // same project — only when projectId actually changes. + const prevProjectIdRef = useRef(projectId); + useEffect(() => { + if (prevProjectIdRef.current !== projectId) { + clearExplorerData(); + prevProjectIdRef.current = projectId; + } + }, [projectId, clearExplorerData]); + function getExplorer(projectId) { if (!projectId) return; setLoading(true); @@ -1258,6 +1282,9 @@ const IdeExplorer = ({ .then((res) => { const treeData = res.data.children; rawTreeDataRef.current = JSON.parse(JSON.stringify(treeData)); + // Publish the raw (pre-mutation) shape to the shared store so + // consumers like chat-ai/Body.jsx get the untransformed children. + setExplorerData(rawTreeDataRef.current); // Apply sort and decorations to no_code models BEFORE transformTree // so that _isChild flag is set when className is assigned treeData.forEach((node) => { @@ -1299,6 +1326,7 @@ const IdeExplorer = ({ const treeData = res.data; const mappedData = mapIconsToTreeData([treeData]); setDBExplorer(mappedData); + setDbExplorerData(treeData); setCachedLists((prev) => ({ ...prev, 2: generateList([treeData]), // Correct key diff --git a/frontend/src/store/explorer-store.js b/frontend/src/store/explorer-store.js new file mode 100644 index 00000000..41f945a8 --- /dev/null +++ b/frontend/src/store/explorer-store.js @@ -0,0 +1,20 @@ +import { create } from "zustand"; + +/** + * Explorer Store + * Holds the shared responses of explorerSvc.getExplorer(projectId) and + * explorerSvc.getDbExplorer(projectId) so multiple consumers (explorer + * tree, chat autocomplete) don't refetch. + * Owner of writes: frontend/src/ide/explorer/explorer-component.jsx + */ +const useExplorerStore = create((set) => ({ + // res.data.children from /explorer API — array where [0]=models, [1]=seeds + explorerData: null, + // res.data from /db_explorer API — single DB tree object + dbExplorerData: null, + setExplorerData: (data) => set({ explorerData: data }), + setDbExplorerData: (data) => set({ dbExplorerData: data }), + clearExplorerData: () => set({ explorerData: null, dbExplorerData: null }), +})); + +export { useExplorerStore };