fixes and refactoring

This commit is contained in:
Saxon Fletcher committed 2025-10-21 14:35:49 +10:00
1 parent 7914c4adf7
commit cfd89fc984
11 files changed
+79 -148

No files matched your search

@@ -94,7 +94,7 @@ export const AdvisorSidebar = () => {
const advisorSnap = useAdvisorCenterStateSnapshot()
const { data: project } = useSelectedProjectQuery()
const isSidebarOpen = Boolean(advisorSnap.open)
const isSidebarOpen = sidebarManagerState.isSidebarOpen(SIDEBAR_KEYS.ADVISOR_CENTER)
const {
data: lintData,
@@ -228,7 +228,7 @@ export const AdvisorSidebar = () => {
const hiddenItemsCount = itemsFilteredByTabOnly.length - filteredItems.length
useEffect(() => {
if (!advisorSnap.open) return
if (!isSidebarOpen) return
if (advisorSnap.selectedItemId) {
const selectedExists = filteredItems.some((item) => item.id === advisorSnap.selectedItemId)
@@ -238,7 +238,7 @@ export const AdvisorSidebar = () => {
} else if (filteredItems.length === 0) {
advisorCenterState.selectItem(undefined)
}
}, [advisorSnap.open, filteredItems, advisorSnap.selectedItemId])
}, [isSidebarOpen, filteredItems, advisorSnap.selectedItemId])
const selectedItem = filteredItems.find((item) => item.id === advisorSnap.selectedItemId)
const isDetailView = !!selectedItem
@@ -1,19 +1,19 @@
import { PropsWithChildren } from 'react'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { SIDEBAR_KEYS, sidebarManagerState } from 'state/sidebar-manager-state'
import { cn } from 'ui'
/**
* Standardized padding and width layout for non-custom reports
*/
const ReportPadding = ({ children }: PropsWithChildren<{}>) => {
const snap = useAiAssistantStateSnapshot()
const isAiAssistantOpen = sidebarManagerState.isSidebarOpen(SIDEBAR_KEYS.AI_ASSISTANT)
return (
<div
className={cn(
'flex flex-col flex-grow gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 2xl:px-32 w-full',
snap.open ? 'xl:px-6' : 'xl:px-22'
isAiAssistantOpen ? 'xl:px-6' : 'xl:px-22'
)}
>
{children}
@@ -41,7 +41,7 @@ export const AIAssistantOption = ({
}, [projectRef, organizationSlug, sendEvent])
// If no specific project selected, use the wildcard route
const aiLink = `/project/${projectRef !== NO_PROJECT_MARKER ? projectRef : '_'}?aiAssistantPanelOpen=true&slug=${organizationSlug}`
const aiLink = `/project/${projectRef !== NO_PROJECT_MARKER ? projectRef : '_'}?sidebar=ai-assistant&slug=${organizationSlug}`
if (!organizationSlug || organizationSlug === NO_ORG_MARKER) return null
@@ -3,7 +3,6 @@ import { ShieldAlert } from 'lucide-react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useProjectLintsQuery } from 'data/lint/lint-query'
import { useAdvisorCenterStateSnapshot } from 'state/advisor-center-state'
import {
SIDEBAR_KEYS,
sidebarManagerState,
@@ -13,22 +12,17 @@ import { cn } from 'ui'
export const AdvisorButton = () => {
const { ref: projectRef } = useParams()
const advisorSnap = useAdvisorCenterStateSnapshot()
const sidebarSnap = useSidebarManagerSnapshot()
const { data: lints } = useProjectLintsQuery({ projectRef })
const hasCriticalIssues = Array.isArray(lints) && lints.some((lint) => lint.level === 'ERROR')
const handleClick = () => {
if (advisorSnap.open) {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.ADVISOR_CENTER)
} else {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.ADVISOR_CENTER)
}
}
const isOpen = sidebarSnap.panels[SIDEBAR_KEYS.ADVISOR_CENTER]?.open
const handleClick = () => {
sidebarManagerState.toggleSidebar(SIDEBAR_KEYS.ADVISOR_CENTER)
}
return (
<div className="relative">
<ButtonTooltip
@@ -1,24 +1,55 @@
import { Fragment, ReactNode } from 'react'
import { useRouter } from 'next/router'
import { Fragment, ReactNode, useEffect } from 'react'
import { AdvisorSidebar } from 'components/interfaces/AdvisorCenter/AdvisorSidebar'
import { AIAssistant } from 'components/ui/AIAssistantPanel/AIAssistant'
import EditorPanel from 'components/ui/EditorPanel/EditorPanel'
import { advisorCenterState } from 'state/advisor-center-state'
import { editorPanelState } from 'state/editor-panel-state'
import {
SIDEBAR_KEYS,
SidebarKey,
sidebarManagerState,
useSidebarManagerSnapshot,
} from 'state/sidebar-manager-state'
import { ResizableHandle, ResizablePanel, cn } from 'ui'
type LayoutSidebarProps = {
isClient: boolean
}
// Register all sidebars at module level (runs once when module loads)
sidebarManagerState.registerSidebar(SIDEBAR_KEYS.AI_ASSISTANT, {})
export const LayoutSidebar = ({ isClient }: LayoutSidebarProps) => {
sidebarManagerState.registerSidebar(SIDEBAR_KEYS.ADVISOR_CENTER, {
onClose: () => {
advisorCenterState.selectedItemId = undefined
},
})
sidebarManagerState.registerSidebar(SIDEBAR_KEYS.EDITOR_PANEL, {
onClose: () => {
editorPanelState.reset()
},
})
export const LayoutSidebar = () => {
const router = useRouter()
const sidebarSnap = useSidebarManagerSnapshot()
const activeSidebar = isClient ? sidebarSnap.activeSidebar : undefined
const activeSidebar = sidebarSnap.activeSidebar
if (!isClient || !activeSidebar) return null
// Handle URL params for deep linking
useEffect(() => {
if (!router.isReady) return
const sidebarParamRaw = router.query.sidebar
const validSidebarKeys = Object.values(SIDEBAR_KEYS) as string[]
const sidebarParam = typeof sidebarParamRaw === 'string' ? sidebarParamRaw : null
if (!sidebarParam || !validSidebarKeys.includes(sidebarParam)) return
if (sidebarSnap.activeSidebar !== sidebarParam) {
sidebarManagerState.openSidebar(sidebarParam as SidebarKey)
}
}, [router.isReady, router.query.sidebar, sidebarSnap.activeSidebar])
if (!activeSidebar) return null
let sidebarContent: ReactNode = null
@@ -1,7 +1,7 @@
import { AnimatePresence, motion } from 'framer-motion'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react'
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect } from 'react'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { CreateBranchModal } from 'components/interfaces/BranchManagement/CreateBranchModal'
@@ -91,7 +91,6 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
ref
) => {
const router = useRouter()
const [isClient, setIsClient] = useState(false)
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: selectedProject } = useSelectedProjectQuery()
const { mobileMenuOpen, showSidebar, setMobileMenuOpen } = useAppStateSnapshot()
@@ -136,10 +135,6 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
const showPausedState = isPaused && !ignorePausedState
useEffect(() => {
setIsClient(true)
}, [])
return (
<>
<Head>
@@ -238,7 +233,7 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
)}
</main>
</ResizablePanel>
<LayoutSidebar isClient={isClient} />
<LayoutSidebar />
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
@@ -432,12 +432,14 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
}
}, [snap.initialInput])
const isSidebarOpen = sidebarManagerState.isSidebarOpen(SIDEBAR_KEYS.AI_ASSISTANT)
useEffect(() => {
if (snap.open && isInSQLEditor && !!snippetContent) {
if (isSidebarOpen && isInSQLEditor && !!snippetContent) {
snap.setSqlSnippets([{ label: 'Current Query', content: snippetContent }])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.open, isInSQLEditor, snippetContent])
}, [isSidebarOpen, isInSQLEditor, snippetContent])
return (
<ErrorBoundary
-11
View File
@@ -6,7 +6,6 @@ export type AdvisorCenterTab = 'all' | 'security' | 'performance' | 'messages'
export type AdvisorSeverity = 'critical' | 'warning' | 'info'
const initialState = {
open: false,
activeTab: 'all' as AdvisorCenterTab,
severityFilters: ['critical'] as AdvisorSeverity[],
selectedItemId: undefined as string | undefined,
@@ -44,16 +43,6 @@ export const advisorCenterState = proxy({
},
})
sidebarManagerState.registerSidebar(SIDEBAR_KEYS.ADVISOR_CENTER, {
onOpen: () => {
advisorCenterState.open = true
},
onClose: () => {
advisorCenterState.open = false
advisorCenterState.selectedItemId = undefined
},
})
export const getAdvisorCenterStateSnapshot = () => snapshot(advisorCenterState)
export const useAdvisorCenterStateSnapshot = (
+20 -91
View File
@@ -1,13 +1,12 @@
import type { UIMessage as MessageType } from '@ai-sdk/react'
import { DBSchema, IDBPDatabase, openDB } from 'idb'
import { debounce } from 'lodash'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { createContext, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { SIDEBAR_KEYS, sidebarManagerState } from './sidebar-manager-state'
type SuggestionsType = {
title: string
@@ -29,7 +28,6 @@ type ChatSession = {
}
type AiAssistantData = {
open: boolean
initialInput: string
sqlSnippets?: SqlSnippet[]
suggestions?: SuggestionsType
@@ -42,14 +40,12 @@ type AiAssistantData = {
// Data structure stored in IndexedDB
type StoredAiAssistantState = {
projectRef: string
open: boolean
activeChatId?: string
chats: Record<string, ChatSession>
model?: AssistantModel
}
const INITIAL_AI_ASSISTANT: AiAssistantData = {
open: false,
initialInput: '',
sqlSnippets: undefined,
suggestions: undefined,
@@ -168,7 +164,6 @@ async function tryMigrateFromLocalStorage(
if (parsedFromLocalStorage && typeof parsedFromLocalStorage.chats === 'object') {
migratedState = {
projectRef: projectRef,
open: parsedFromLocalStorage.open ?? false,
activeChatId: parsedFromLocalStorage.activeChatId,
chats: parsedFromLocalStorage.chats,
model: parsedFromLocalStorage.model ?? INITIAL_AI_ASSISTANT.model,
@@ -200,20 +195,6 @@ async function tryMigrateFromLocalStorage(
// Helper function to ensure an active chat exists or initialize a new one
function ensureActiveChatOrInitialize(state: AiAssistantState) {
// Check URL param again to override loaded 'open' state if present
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search)
const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen')
if (aiAssistantPanelOpenParam !== null) {
const shouldOpen = aiAssistantPanelOpenParam === 'true'
if (shouldOpen) {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
} else {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
}
}
// Ensure an active chat exists after loading/migration
if (!state.activeChatId || !state.chats[state.activeChatId]) {
const chatIds = Object.keys(state.chats)
@@ -234,20 +215,10 @@ export const createAiAssistantState = (): AiAssistantState => {
// Initialize with defaults, loading happens asynchronously in the provider
const initialState = { ...INITIAL_AI_ASSISTANT }
// Check URL params for initial 'open' state, overriding any loaded state later if present
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search)
const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen')
if (aiAssistantPanelOpenParam !== null) {
initialState.open = aiAssistantPanelOpenParam === 'true'
}
}
const state: AiAssistantState = proxy({
...initialState, // Spread initial values directly
resetAiAssistantPanel: () => {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
Object.assign(state, INITIAL_AI_ASSISTANT)
},
@@ -262,14 +233,9 @@ export const createAiAssistantState = (): AiAssistantState => {
newChat: (
options?: { name?: string } & Partial<
Pick<AiAssistantData, 'open' | 'initialInput' | 'sqlSnippets' | 'suggestions' | 'tables'>
Pick<AiAssistantData, 'initialInput' | 'sqlSnippets' | 'suggestions' | 'tables'>
>
) => {
if (options?.open === true) {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
} else if (options?.open === false) {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
const chatId = uuidv4()
const newChat: ChatSession = {
id: chatId,
@@ -401,33 +367,11 @@ export const createAiAssistantState = (): AiAssistantState => {
state.activeChatId = persistedState.activeChatId
state.model = persistedState.model ?? INITIAL_AI_ASSISTANT.model
// Check URL param again to override loaded 'open' state if present
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search)
const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen')
if (aiAssistantPanelOpenParam !== null) {
const shouldOpen = aiAssistantPanelOpenParam === 'true'
if (shouldOpen) {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
} else {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
} else if (persistedState.open) {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
} else {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
} else if (persistedState.open) {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
} else {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
// Ensure an active chat exists after loading
if (!state.activeChat) {
const chatIds = Object.keys(state.chats)
if (chatIds.length > 0) {
// Maybe select the most recently updated? For now, first.
// Select the most recently updated chat
state.activeChatId = chatIds.sort(
(a, b) =>
(state.chats[b].updatedAt?.getTime() || 0) -
@@ -454,7 +398,7 @@ export type AiAssistantState = AiAssistantData & {
setModel: (model: AssistantModel) => void
newChat: (
options?: { name?: string } & Partial<
Pick<AiAssistantData, 'open' | 'initialInput' | 'sqlSnippets' | 'suggestions' | 'tables'>
Pick<AiAssistantData, 'initialInput' | 'sqlSnippets' | 'suggestions' | 'tables'>
>
) => string
selectChat: (id: string) => void
@@ -477,42 +421,28 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren)
const { data: project } = useSelectedProjectQuery()
// Initialize state. createAiAssistantState now just sets defaults.
const [state] = useState(() => createAiAssistantState())
useEffect(() => {
const shouldRestoreOpen = state.open
sidebarManagerState.registerSidebar(SIDEBAR_KEYS.AI_ASSISTANT, {
onOpen: () => {
state.open = true
},
onClose: () => {
state.open = false
},
})
if (shouldRestoreOpen) {
sidebarManagerState.openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
} else {
sidebarManagerState.closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
return () => {
sidebarManagerState.unregisterSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
}
}, [state])
const previousProjectRef = useRef<string | undefined>(project?.ref)
// Effect to load state from IndexedDB on mount or projectRef change
useEffect(() => {
let isMounted = true
async function loadAndInitializeState() {
if (!project?.ref || typeof window === 'undefined') {
if (project?.ref === undefined) {
state.resetAiAssistantPanel()
}
return // Don't load if no projectRef or not in browser
}
if (typeof window === 'undefined') {
return
}
if (!project?.ref) {
// Only reset if we previously had a project ref (e.g., navigating away)
if (previousProjectRef.current) {
state.resetAiAssistantPanel()
}
previousProjectRef.current = project?.ref
return
}
previousProjectRef.current = project.ref
async function loadAndInitializeState() {
let loadedState: StoredAiAssistantState | null = null
// 1. Try loading from IndexedDB
@@ -552,7 +482,6 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren)
// Prepare state for IndexedDB
const stateToSave: StoredAiAssistantState = {
projectRef: project?.ref,
open: snap.open,
activeChatId: snap.activeChatId,
model: snap.model,
chats: snap.chats
-8
View File
@@ -79,14 +79,6 @@ export const editorPanelState = proxy({
},
})
// Register to reset state when sidebar closes (but don't control sidebar open/close)
sidebarManagerState.registerSidebar(SIDEBAR_KEYS.EDITOR_PANEL, {
onClose: () => {
// Auto-reset state when sidebar closes
editorPanelState.reset()
},
})
export const getEditorPanelStateSnapshot = () => snapshot(editorPanelState)
export const useEditorPanelStateSnapshot = (
+4 -5
View File
@@ -56,12 +56,11 @@ const createSidebarManagerState = () => {
},
openSidebar(id: SidebarKey) {
if (!state.panels[id]) {
state.registerSidebar(id)
}
const panel = state.panels[id]
if (!panel) return
if (!panel) {
console.warn(`Sidebar "${id}" is not registered. Register it before opening.`)
return
}
if (panel.open) {
state.activeSidebar = id