From befcc634934db364d8d700ee3c273328bb705bd1 Mon Sep 17 00:00:00 2001 From: Monica Khoury Date: Sat, 22 Aug 2026 22:23:08 +0300 Subject: [PATCH] address review comments --- .../SupportAssistantSuccessCard.test.tsx | 2 - .../ui/AIAssistantPanel/AIAssistant.utils.ts | 15 ++++++- .../ui/AIAssistantPanel/AssistantChat.tsx | 10 +---- .../SupportAssistantSuccessCardContent.tsx | 42 +++++++++---------- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.test.tsx b/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.test.tsx index 6d00e06e134..4a2777cb7e1 100644 --- a/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.test.tsx +++ b/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.test.tsx @@ -92,8 +92,6 @@ const supportRequest: SubmittedSupportRequest = { frontConversationId: 'front-conversation-1', } -// A minimal but contract-accurate ProjectDetailResponse, so a mock drifting -// from the OpenAPI shape (missing fields, stale enum values) fails to compile. const readyProjectDetail: ProjectDetailResponse = { id: 1, ref: 'project-1', diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts index 92d4b1d694e..c812f25e9b6 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts @@ -13,12 +13,25 @@ import { databasePoliciesKeys } from '@/data/database-policies/keys' import { databaseTriggerKeys } from '@/data/database-triggers/keys' import { databaseKeys } from '@/data/database/keys' import { enumeratedTypesKeys } from '@/data/enumerated-types/keys' -import { handleError } from '@/data/fetchers' +import { handleError, isValidConnString } from '@/data/fetchers' +import type { ProjectDetail } from '@/data/projects/project-detail-query' import { tableKeys } from '@/data/tables/keys' import { tryParseJson } from '@/lib/helpers' import type { SqlSnippet } from '@/state/ai-assistant-state' import { ResponseError } from '@/types' +// [Monica] Whether a project has finished provisioning and has a usable connection string +export function isProjectReadyForAssistant( + projectDetail: Pick | undefined +): boolean { + return ( + !!projectDetail && + projectDetail.status !== 'COMING_UP' && + projectDetail.status !== 'UNKNOWN' && + isValidConnString(projectDetail.connectionString) + ) +} + export type MutationCategory = 'functions' | 'rls-policies' // [Joshen] This is just very basic identification, but possible can extend perhaps diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx index 7a04cf6dc62..2bfb94a9785 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx @@ -17,6 +17,7 @@ import { ASSISTANT_ERRORS } from './AiAssistant.constants' import { containsLogsSnippets, hasPendingToolApproval, + isProjectReadyForAssistant, onErrorChat, resolvePendingToolApprovalsAsDenied, } from './AIAssistant.utils' @@ -31,7 +32,6 @@ import { Message } from './Message' import { Markdown } from '@/components/interfaces/Markdown' import { useCheckOpenAIKeyQuery } from '@/data/ai/check-api-key-query' import { useRateMessageMutation } from '@/data/ai/rate-message-mutation' -import { isValidConnString } from '@/data/fetchers' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { useTablesQuery } from '@/data/tables/tables-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' @@ -179,13 +179,7 @@ export const AssistantChat = ({ isError: isSupportChatProjectError, refetch: refetchSupportChatProjectDetail, } = useProjectDetailQuery({ ref: supportMetadata?.projectRef }, { enabled: isOrgViewSupportChat }) - // Mirrors the readiness check in SupportAssistantSuccessCardContent — a project still - // coming up, or with a connection string that isn't valid yet, isn't ready to send to. - const isSupportChatProjectReady = - !!supportChatProjectDetail && - supportChatProjectDetail.status !== 'COMING_UP' && - supportChatProjectDetail.status !== 'UNKNOWN' && - isValidConnString(supportChatProjectDetail.connectionString) + const isSupportChatProjectReady = isProjectReadyForAssistant(supportChatProjectDetail) const isResolvingSupportChatConnectionString = isOrgViewSupportChat && !isSupportChatProjectReady // Update context in state. On org-level pages there's no project in the URL, so diff --git a/apps/studio/components/ui/AIAssistantPanel/SupportAssistantSuccessCardContent.tsx b/apps/studio/components/ui/AIAssistantPanel/SupportAssistantSuccessCardContent.tsx index ba2970b8584..d4343210e03 100644 --- a/apps/studio/components/ui/AIAssistantPanel/SupportAssistantSuccessCardContent.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/SupportAssistantSuccessCardContent.tsx @@ -1,7 +1,15 @@ import type { UIMessage as MessageType } from '@ai-sdk/react' import { ArrowUpRight } from 'lucide-react' import dynamic from 'next/dynamic' -import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react' +import { + useCallback, + useEffect, + useEffectEvent, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from 'react' import type { JSX, MouseEvent } from 'react' import type { StreamdownProps } from 'streamdown' import { @@ -16,11 +24,11 @@ import { Skeleton, } from 'ui' +import { isProjectReadyForAssistant } from './AIAssistant.utils' import { buildSupportAssistantPrompt } from '@/components/interfaces/Support/SupportAssistant.utils' import type { SubmittedSupportRequest } from '@/components/interfaces/Support/SupportForm.state' import { NO_PROJECT_MARKER } from '@/components/interfaces/Support/SupportForm.utils' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { isValidConnString } from '@/data/fetchers' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { useTrack } from '@/lib/telemetry/track' import { @@ -72,21 +80,10 @@ export function SupportAssistantSuccessCardContent({ refetch: refetchProjectDetail, } = useProjectDetailQuery({ ref: request.projectRef }, { enabled: hasAssistantContext }) - // Mirrors the readiness check useProjectDetailQuery itself polls on: a project - // still coming up, or one without a usable connection string yet, isn't ready - // to receive requests, even though the query already returned some data. - const isProjectReady = - !!projectDetail && - projectDetail.status !== 'COMING_UP' && - projectDetail.status !== 'UNKNOWN' && - isValidConnString(projectDetail.connectionString) + const isProjectReady = isProjectReadyForAssistant(projectDetail) const connectionString = projectDetail?.connectionString ?? undefined - useEffect(() => { - if (!hasAssistantContext) return - if (!isProjectReady) return - if (createdChatIdRef.current) return - + const createSupportChat = useEffectEvent(() => { aiAssistantState.setContext({ projectRef: request.projectRef, orgSlug: request.organizationSlug, @@ -100,9 +97,15 @@ export function SupportAssistantSuccessCardContent({ createdChatIdRef.current = newChatId setChatId(newChatId) - // aiAssistantState is a stable context value (same identity across renders) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [aiAssistant, assistantPrompt, connectionString, hasAssistantContext, isProjectReady, request]) + }) + + useEffect(() => { + if (!hasAssistantContext) return + if (!isProjectReady) return + if (createdChatIdRef.current) return + + createSupportChat() + }, [hasAssistantContext, isProjectReady]) const handleOpenAssistant = () => { track( @@ -153,9 +156,6 @@ export function SupportAssistantSuccessCardContent({ if (!hasAssistantContext) return null - // Before the chat exists (still loading, or failed) there's nothing for the card to - // open — disable its click/keyboard handlers so only "Try again" (while erroring) is - // interactive, and clicking mid-load can't open the sidebar with no chat prepared. const isInteractive = !!chat return (