diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx index 922bd07e57736..a4aa58474a60f 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/CopyEnvButton.tsx @@ -2,7 +2,7 @@ import { Copy } from 'lucide-react' import { useCallback, useState } from 'react' import { toast } from 'sonner' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { Button } from 'ui' import { getDecryptedValue } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { copyToClipboard } from 'ui' @@ -34,29 +34,14 @@ export const CopyEnvButton = ({ ).then((values) => values.join('\n')) copyToClipboard(envFile, () => { - toast.success('Copied to clipboard') + toast.success('Copied to clipboard as .env file') setIsLoading(false) }) }, [serverOptions, values]) return ( - } - onClick={onCopy} - tooltip={{ - content: { - text: ( - - Copies an .env file with the configuration details - to your clipboard. - - ), - }, - }} - > - Copy env values - + ) } diff --git a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx index 7b9bae83c096e..ad4d675eaa4ef 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticBucketDetails/DecryptedReadOnlyInput.tsx @@ -4,7 +4,9 @@ import { useState } from 'react' import { useParams } from 'common' import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Button, CardContent, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' export const DecryptedReadOnlyInput = ({ value, @@ -41,52 +43,64 @@ export const DecryptedReadOnlyInput = ({ : value return ( - - {label} - - - - - - - View parameter in Vault - - - } - value={renderedValue} - type={secureEntry ? (isLoading ? 'text' : showHidden ? 'text' : 'password') : 'text'} - descriptionText={descriptionText} - layout="horizontal" - actions={ - secureEntry ? ( - isLoading ? ( -
-
- ) : ( -
-
- ) - ) : null - } - /> + + + {label} + + + + + + + Open in Vault + + + } + description={descriptionText} + isReactForm={false} + > + + + + + + + + + Name + Schema + Created at + + + + + +

No tables yet

+

+ Create an analytics table to get started +

+
+
+
+
+
+ ) : ( - - - - - Namespace - Schema - Tables - - - - - {namespaces.map(({ namespace, schema, tables }) => ( - - ))} - -
-
+ + + Namespaces + + Connected namespaces and tables. + + + + {isLoadingNamespaces || isFDWsLoading ? ( + + ) : namespaces.length === 0 ? ( + + + + + Namespace + Schema + Tables + + + + + + +

+ No namespaces in this bucket +

+

+ Create a namespace and add some data +

+
+
+
+
+
+ ) : ( + + + + + Namespace + Schema + Tables + + + + + {namespaces.map(({ namespace, schema, tables }) => ( + + ))} + +
+
+ )} +
)} - - -
-
-
- Connection Details - - You can use the following parameters to connect to the bucket from an Iceberg - client. - -
-
+ + + +
+ Configuration + + Connect to this bucket from an Iceberg client.{' '} + + Learn more + + +
!option.hidden && wrapperValues[option.name] )} values={wrapperValues} /> - +
+ + + {wrapperMeta.server.options + .filter((option) => !option.hidden && wrapperValues[option.name]) + .sort((a, b) => OPTION_ORDER.indexOf(a.name) - OPTION_ORDER.indexOf(b.name)) + .map((option) => { + return ( + + ) + })} + +
+ + )} + {state === 'missing' && } + + +
+ Manage +
+ + +
+

Delete bucket

+

+ This will also delete any data in your bucket. Make sure you have a backup if + you want to keep your data. +

-
- - {wrapperMeta.server.options - .filter((option) => !option.hidden && wrapperValues[option.name]) - .sort((a, b) => OPTION_ORDER.indexOf(a.name) - OPTION_ORDER.indexOf(b.name)) - .map((option) => { - return ( - - ) - })} - -
- - )} - {state === 'missing' && } - -
+ +
+ + + + + + setModal(null)} + /> + ) } @@ -284,24 +386,22 @@ const ExtensionNotInstalled = ({ return ( <> - - - - You need to install the wrappers extension to connect this Analytics bucket to the - database. - - + +

- The {wrapperMeta.label} wrapper requires the Wrappers extension to be installed. You can - install version {wrappersExtension?.installed_version} + The Wrappers extension is required in order to query analytics tables.{' '} {databaseNeedsUpgrading && - ' which is below the minimum version that supports Iceberg wrapper'} - . Please {databaseNeedsUpgrading && 'upgrade your database then '}install the{' '} - wrappers extension to create this wrapper. + 'Please first upgrade your database and then install the extension.'}{' '} + + Learn more +

-
- - - -
+ + ) @@ -337,26 +437,19 @@ const ExtensionNeedsUpgrade = ({ return ( <> - - - - Your extension version is outdated for this wrapper. - - + +

The {wrapperMeta.label} wrapper requires a minimum extension version of{' '} {wrapperMeta.minimumExtensionVersion}. You have version{' '} {wrappersExtension?.installed_version} installed. Please{' '} - {databaseNeedsUpgrading && 'upgrade your database then '}update the extension by - disabling and enabling the wrappers extension to create - this wrapper. + {databaseNeedsUpgrading && 'first upgrade your database, and then '}update the extension + by disabling and enabling the Wrappers extension.

-

- Warning: Before reinstalling the wrapper extension, you must first remove all existing - wrappers. Afterward, you can recreate the wrappers. +

+ Before reinstalling the wrapper extension, you must first remove all existing wrappers. + Afterward, you can recreate the wrappers.

-
- - -
+ + ) @@ -385,20 +478,14 @@ const WrapperMissing = ({ bucketName }: { bucketName: string }) => { return ( <> - - - - This Analytics bucket does not have a foreign data wrapper setup. - - -

You need to setup a wrapper to connect this bucket to the database.

-
- + + +

The Iceberg Wrapper integration is required in order to query analytics tables.

-
-
+ + ) diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 994707ac6c727..6f5171d26beb7 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -550,59 +550,61 @@ export const CreateBucketModal = ({ ) : ( <> {icebergWrapperExtensionState === 'installed' ? ( - -

- Supabase will setup a - - foreign data wrapper - {bucketName && {`${bucketName}_fdw`}} - - - {' '} - for easier access to the data. This action will also create{' '} - - S3 Access Keys - {bucketName && ( - <> - {' '} - named {`${bucketName}_keys`} - - )} - - and + + +

+ Supabase will setup a - four Vault Secrets - {bucketName && ( - <> - {' '} - prefixed with{' '} - {`${bucketName}_vault_`} - - )} + foreign data wrapper + {bucketName && {`${bucketName}_fdw`}} - . - -

-

- As a final step, you'll need to create an{' '} - Iceberg namespace before you - connect the Iceberg data to your database. -

-
+ + {' '} + for easier access to the data. This action will also create{' '} + + S3 Access Keys + {bucketName && ( + <> + {' '} + named {`${bucketName}_keys`} + + )} + + and + + four Vault Secrets + {bucketName && ( + <> + {' '} + prefixed with{' '} + {`${bucketName}_vault_`} + + )} + + . + +

+

+ As a final step, you'll need to create an{' '} + Iceberg namespace before you + connect the Iceberg data to your database. +

+ + ) : ( - + You need to install the Iceberg wrapper extension to connect your Analytic diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts index 2023b7c044665..ad340ad84d234 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/useSelectedBucket.ts @@ -1,11 +1,19 @@ import { useParams } from 'common' import { useBucketsQuery } from 'data/storage/buckets-query' +import { useStorageV2Page } from '../Storage.utils' export const useSelectedBucket = () => { + const page = useStorageV2Page() const { ref, bucketId } = useParams() const { data: buckets = [], isSuccess, isError, error } = useBucketsQuery({ projectRef: ref }) - const bucket = buckets.find((b) => b.id === bucketId) + const bucketsByType = + page === 'files' + ? buckets.filter((b) => b.type === 'STANDARD') + : page === 'analytics' + ? buckets.filter((b) => b.type === 'ANALYTICS') + : buckets + const bucket = bucketsByType.find((b) => b.id === bucketId) return { bucket, isSuccess, isError, error } } diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx index aebbc62f7ec5d..17c85bcc69a25 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -26,6 +26,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { const suffix = !!featurePreviewModal ? `?featurePreviewModal=${featurePreviewModal}` : '' if (isStorageV2) { + // From old UI to new UI if (pathname.endsWith('/storage/settings')) { router.push(`/project/${ref}/storage/files/settings${suffix}`) } else if (pathname.endsWith('/storage/policies')) { @@ -38,6 +39,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { } } } else { + // From new UI to old UI if (pathname.endsWith('/files/settings')) { router.push(`/project/${ref}/storage/settings${suffix}`) } else if (pathname.endsWith('/files/policies')) { @@ -47,6 +49,12 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => { pathname.endsWith('/analytics/buckets/[bucketId]') ) { router.push(`/project/${ref}/storage/buckets/${bucketId}${suffix}`) + } else if ( + pathname.endsWith('/storage/files') || + pathname.endsWith('/storage/analytics') || + pathname.endsWith('/storage/vectors') + ) { + router.push(`/project/${ref}/storage/buckets`) } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index ea619b5d3caf5..9e5083fd63326 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -22,6 +22,7 @@ import { useHotKey } from 'hooks/ui/useHotKey' import { prepareMessagesForAPI } from 'lib/ai/message-utils' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' import uuidv4 from 'lib/uuid' +import type { AssistantModel } from 'state/ai-assistant-state' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { Button, cn, KeyboardShortcut } from 'ui' @@ -59,6 +60,19 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { const { snippets } = useSqlEditorV2StateSnapshot() const snap = useAiAssistantStateSnapshot() + const isPaidPlan = selectedOrganization?.plan?.id !== 'free' + + const selectedModel = useMemo(() => { + const defaultModel: AssistantModel = isPaidPlan ? 'gpt-5' : 'gpt-5-mini' + const model = snap.model ?? defaultModel + + if (!isPaidPlan && model === 'gpt-5') { + return 'gpt-5-mini' + } + + return model + }, [isPaidPlan, snap.model]) + const [updatedOptInSinceMCP] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN, false @@ -201,6 +215,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { table: currentTable?.name, chatName: currentChat, orgSlug: selectedOrganizationRef.current?.slug, + model: selectedModel, }, headers: { Authorization: authorizationHeader ?? '' }, } @@ -640,6 +655,8 @@ export const AIAssistant = ({ className }: AIAssistantProps) => { snap.setSqlSnippets(newSnippets) }} includeSnippetsInMessage={aiOptInLevel !== 'disabled'} + selectedModel={selectedModel} + onSelectModel={(model) => snap.setModel(model)} /> diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx index edf949772a2b4..2839ee44f4fb5 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx @@ -6,6 +6,7 @@ import { ExpandingTextArea } from 'ui' import { cn } from 'ui/src/lib/utils' import { ButtonTooltip } from '../ButtonTooltip' import { type SqlSnippet } from './AIAssistant.types' +import { ModelSelector } from './ModelSelector' import { getSnippetContent, SnippetRow } from './SnippetRow' export interface FormProps { @@ -43,6 +44,10 @@ export interface FormProps { className?: string /* If currently editing an existing message */ isEditing?: boolean + /* The currently selected AI model */ + selectedModel: 'gpt-5' | 'gpt-5-mini' + /* Callback when a model is chosen */ + onSelectModel: (model: 'gpt-5' | 'gpt-5-mini') => void } const AssistantChatFormComponent = forwardRef( @@ -62,6 +67,8 @@ const AssistantChatFormComponent = forwardRef( includeSnippetsInMessage = false, className, isEditing = false, + selectedModel, + onSelectModel, ...props }, ref @@ -114,7 +121,7 @@ const AssistantChatFormComponent = forwardRef( ref={textAreaRef} disabled={disabled} className={cn( - 'text-sm pr-10 max-h-64', + 'text-sm pr-10 pb-9 max-h-64', sqlSnippets && sqlSnippets.length > 0 && 'pt-10' )} placeholder={placeholder} @@ -124,33 +131,39 @@ const AssistantChatFormComponent = forwardRef( onChange={(event) => onValueChange(event)} onKeyDown={handleKeyDown} /> -
- {loading ? ( - onStop ? ( +
+
+ +
+ +
+ {loading ? ( + onStop ? ( + } + onClick={onStop} + className="w-7 h-7 rounded-full p-0 text-center flex items-center justify-center" + tooltip={{ content: { side: 'top', text: 'Stop response' } }} + /> + ) : ( + + ) + ) : ( } - onClick={onStop} - className="w-7 h-7 rounded-full p-0 text-center flex items-center justify-center" - tooltip={{ content: { side: 'top', text: 'Stop response' } }} + htmlType="submit" + aria-label="Send message" + icon={} + disabled={!canSubmit} + className={cn( + 'w-7 h-7 rounded-full p-0 text-center flex items-center justify-center', + !canSubmit ? 'opacity-50' : 'opacity-100' + )} + tooltip={{ content: { side: 'top', text: 'Send message' } }} /> - ) : ( - - ) - ) : ( - } - disabled={!canSubmit} - className={cn( - 'w-7 h-7 rounded-full p-0 text-center flex items-center justify-center', - !canSubmit ? 'opacity-50' : 'opacity-100' - )} - tooltip={{ content: { side: 'top', text: 'Send message' } }} - /> - )} + )} +
diff --git a/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx b/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx new file mode 100644 index 0000000000000..1a2a73a3608d5 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/ModelSelector.tsx @@ -0,0 +1,103 @@ +import { Check, ChevronsUpDown } from 'lucide-react' +import { useState } from 'react' + +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useRouter } from 'next/router' +import { + Badge, + Button, + CommandGroup_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Command_Shadcn_, + PopoverContent_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, + TooltipContent, + TooltipTrigger, + Tooltip, +} from 'ui' + +interface ModelSelectorProps { + selectedModel: 'gpt-5' | 'gpt-5-mini' + onSelectModel: (model: 'gpt-5' | 'gpt-5-mini') => void +} + +export const ModelSelector = ({ selectedModel, onSelectModel }: ModelSelectorProps) => { + const router = useRouter() + const { data: organization } = useSelectedOrganizationQuery() + + const [open, setOpen] = useState(false) + + const canAccessProModels = organization?.plan?.id !== 'free' + const slug = organization?.slug ?? '_' + + const upgradeHref = `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=ai-assistant-model` + + const handleSelectModel = (model: 'gpt-5' | 'gpt-5-mini') => { + if (model === 'gpt-5' && !canAccessProModels) { + setOpen(false) + void router.push(upgradeHref) + return + } + + onSelectModel(model) + setOpen(false) + } + + return ( + + + + + + + + + handleSelectModel('gpt-5-mini')} + className="flex justify-between" + > + gpt-5-mini + {selectedModel === 'gpt-5-mini' && } + + handleSelectModel('gpt-5')} + className="flex justify-between" + > + gpt-5 + {canAccessProModels ? ( + selectedModel === 'gpt-5' ? ( + + ) : null + ) : ( + + +
+ + Upgrade + +
+
+ + gpt-5 is available on Pro plans and above + +
+ )} +
+
+
+
+
+
+ ) +} diff --git a/apps/studio/components/ui/InlineLink.tsx b/apps/studio/components/ui/InlineLink.tsx index 1d69d11e68c49..d82ba4eefa0b2 100644 --- a/apps/studio/components/ui/InlineLink.tsx +++ b/apps/studio/components/ui/InlineLink.tsx @@ -11,7 +11,7 @@ interface InlineLinkProps { } export const InlineLinkClassName = - 'underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground text-foreground' + 'underline transition underline-offset-2 decoration-foreground-lighter hover:decoration-foreground text-inherit hover:text-foreground' export const InlineLink = ({ href, diff --git a/apps/studio/lib/github.ts b/apps/studio/lib/github.ts index d2493e2ddcfd1..454fa222ee710 100644 --- a/apps/studio/lib/github.ts +++ b/apps/studio/lib/github.ts @@ -2,18 +2,22 @@ import { LOCAL_STORAGE_KEYS } from 'common' import { makeRandomString } from './helpers' const GITHUB_INTEGRATION_APP_NAME = - process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' - ? `supabase` - : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' - ? `supabase-staging` - : `supabase-local-testing` + process.env.NIMBUS_PROD_PROJECTS_URL !== undefined + ? 'supabase-snap' + : process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' + ? `supabase` + : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' + ? `supabase-staging` + : `supabase-local-testing` const GITHUB_INTEGRATION_CLIENT_ID = - process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' - ? `Iv1.b91a6d8eaa272168` - : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' - ? `Iv1.2681ab9a0360d8ad` - : `Iv1.5022a3b44d150fbf` + process.env.NIMBUS_PROD_PROJECTS_URL !== undefined + ? 'Iv23li2pAiqDGgaSrP8q' + : process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' + ? `Iv1.b91a6d8eaa272168` + : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' + ? `Iv1.2681ab9a0360d8ad` + : `Iv1.5022a3b44d150fbf` const GITHUB_INTEGRATION_AUTHORIZATION_URL = `https://github.com/login/oauth/authorize?client_id=${GITHUB_INTEGRATION_CLIENT_ID}` export const GITHUB_INTEGRATION_INSTALLATION_URL = `https://github.com/apps/${GITHUB_INTEGRATION_APP_NAME}/installations/new` diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts index f57fc2d6c75c9..ecd441f503085 100644 --- a/apps/studio/pages/api/ai/sql/generate-v4.ts +++ b/apps/studio/pages/api/ai/sql/generate-v4.ts @@ -62,6 +62,7 @@ const requestBodySchema = z.object({ table: z.string().optional(), chatName: z.string().optional(), orgSlug: z.string().optional(), + model: z.enum(['gpt-5', 'gpt-5-mini']).optional(), }) async function handlePost(req: NextApiRequest, res: NextApiResponse) { @@ -79,7 +80,14 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { return res.status(400).json({ error: 'Invalid request body', issues: parseError.issues }) } - const { messages: rawMessages, projectRef, connectionString, orgSlug, chatName } = data + const { + messages: rawMessages, + projectRef, + connectionString, + orgSlug, + chatName, + model: requestedModel, + } = data let aiOptInLevel: AiOptInLevel = 'disabled' let isLimited = false @@ -139,7 +147,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { providerOptions, } = await getModel({ provider: 'openai', - model: 'gpt-5', + model: requestedModel ?? 'gpt-5', routingKey: projectRef, isLimited, }) diff --git a/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx new file mode 100644 index 0000000000000..19e8e6768178c --- /dev/null +++ b/apps/studio/pages/project/[ref]/storage/analytics/buckets/[bucketId].tsx @@ -0,0 +1,48 @@ +import { useParams } from 'common' + +import { AnalyticBucketDetails } from 'components/interfaces/Storage/AnalyticBucketDetails' +import StorageBucketsError from 'components/interfaces/Storage/StorageBucketsError' +import { useSelectedBucket } from 'components/interfaces/Storage/StorageExplorer/useSelectedBucket' +import DefaultLayout from 'components/layouts/DefaultLayout' +import StorageLayout from 'components/layouts/StorageLayout/StorageLayout' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' +import type { NextPageWithLayout } from 'types' + +const AnalyticsBucketPage: NextPageWithLayout = () => { + const { bucketId } = useParams() + const { data: project } = useSelectedProjectQuery() + const { projectRef } = useStorageExplorerStateSnapshot() + const { bucket, error, isSuccess, isError } = useSelectedBucket() + + // [Joshen] Checking against projectRef from storage explorer to check if the store has initialized + if (!project || !projectRef) return null + + return ( +
+ {isError && } + + {isSuccess ? ( + !bucket ? ( +
+

Bucket {bucketId} cannot be found

+
+ ) : bucket.type === 'ANALYTICS' ? ( + + ) : ( +
+

This bucket is not an analytics bucket

+
+ ) + ) : null} +
+ ) +} + +AnalyticsBucketPage.getLayout = (page) => ( + + {page} + +) + +export default AnalyticsBucketPage diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index cb138eebb8272..70b855b3a1d1d 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -17,6 +17,8 @@ export type AssistantMessageType = MessageType & { results?: { [id: string]: any export type SqlSnippet = string | { label: string; content: string } +export type AssistantModel = 'gpt-5' | 'gpt-5-mini' + type ChatSession = { id: string name: string @@ -33,6 +35,7 @@ type AiAssistantData = { tables: { schema: string; name: string }[] chats: Record activeChatId?: string + model: AssistantModel } // Data structure stored in IndexedDB @@ -41,6 +44,7 @@ type StoredAiAssistantState = { open: boolean activeChatId?: string chats: Record + model?: AssistantModel } const INITIAL_AI_ASSISTANT: AiAssistantData = { @@ -51,6 +55,7 @@ const INITIAL_AI_ASSISTANT: AiAssistantData = { tables: [], chats: {}, activeChatId: undefined, + model: 'gpt-5', } const DB_NAME = 'ai-assistant-db' @@ -165,6 +170,7 @@ async function tryMigrateFromLocalStorage( open: parsedFromLocalStorage.open ?? false, activeChatId: parsedFromLocalStorage.activeChatId, chats: parsedFromLocalStorage.chats, + model: parsedFromLocalStorage.model ?? INITIAL_AI_ASSISTANT.model, } } else { console.warn('Data in localStorage is not in the expected format, ignoring.') @@ -251,6 +257,10 @@ export const createAiAssistantState = (): AiAssistantState => { state.open = !state.open }, + setModel: (model: AssistantModel) => { + state.model = model + }, + // Chat management get activeChat(): ChatSession | undefined { return state.activeChatId ? state.chats[state.activeChatId] : undefined @@ -392,6 +402,7 @@ export const createAiAssistantState = (): AiAssistantState => { state.open = persistedState.open state.chats = persistedState.chats 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') { @@ -433,6 +444,7 @@ export type AiAssistantState = AiAssistantData & { closeAssistant: () => void toggleAssistant: () => void activeChat: ChatSession | undefined + setModel: (model: AssistantModel) => void newChat: ( options?: { name?: string } & Partial< Pick @@ -512,6 +524,7 @@ export const AiAssistantStateContextProvider = ({ children }: PropsWithChildren) projectRef: project?.ref, open: snap.open, activeChatId: snap.activeChatId, + model: snap.model, chats: snap.chats ? Object.entries(snap.chats).reduce((acc, [chatId, chat]) => { // Limit messages before saving