diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 965e5c87059..9a9d8196565 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -58,6 +58,7 @@ const nextConfig = { experimental: { outputFileTracingIncludes: { '/api/crawlers': ['./features/docs/generated/**/*', './docs/ref/**/*'], + '/reference/**/*': ['./features/docs/generated/**/*', './docs/ref/**/*'], }, }, webpack: (config, options) => { diff --git a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx index 51b183dc3fc..eb49a069446 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrainDestinationSheetForm.tsx @@ -68,6 +68,8 @@ const formUnion = z.discriminatedUnion('type', [ type: z.literal('loki'), url: z.string().min(1, { message: 'Loki URL is required' }), headers: z.record(z.string(), z.string()), + username: z.string().optional(), + password: z.string().optional(), }), ]) @@ -162,6 +164,8 @@ export function LogDrainDestinationSheetForm({ url: defaultValues?.config?.url || '', api_key: defaultValues?.config?.api_key || '', region: defaultValues?.config?.region || '', + username: defaultValues?.config?.username || '', + password: defaultValues?.config?.password || '', }, }) @@ -422,10 +426,24 @@ export function LogDrainDestinationSheetForm({ + + )} diff --git a/apps/studio/components/interfaces/LogDrains/LogDrains.tsx b/apps/studio/components/interfaces/LogDrains/LogDrains.tsx index 894832547f7..acd9d90ec13 100644 --- a/apps/studio/components/interfaces/LogDrains/LogDrains.tsx +++ b/apps/studio/components/interfaces/LogDrains/LogDrains.tsx @@ -131,8 +131,15 @@ export function LogDrains({ {logDrains?.map((drain) => ( - {drain.name} - {drain.description} + + {drain.name} + + + {drain.description} + {drain.type} @@ -180,7 +187,7 @@ export function LogDrains({ }} onCancel={() => setIsDeleteModalOpen(false)} > -
+

Are you sure you want to delete{' '} {selectedLogDrain?.name}? diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx index 9a55270116d..994de7cb037 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx @@ -4,10 +4,14 @@ import { Info } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' +import { Alert, AlertDescription, AlertTitle } from '@ui/components/shadcn/ui/alert' import { Message as MessageType } from 'ai' import { useChat } from 'ai/react' import { useParams } from 'common' +import { IS_PLATFORM } from 'common/constants/environment' +import { Markdown } from 'components/interfaces/Markdown' import { SchemaComboBox } from 'components/ui/SchemaComboBox' +import { useCheckOpenAIKeyQuery } from 'data/ai/check-api-key-query' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' @@ -73,6 +77,9 @@ export const AiAssistantPanel = ({ const shouldShowNotOptimizedAlert = selectedOrganization && !includeSchemaMetadata && showAiNotOptimizedWarningSetting + const { data: check } = useCheckOpenAIKeyQuery() + const isApiKeySet = IS_PLATFORM || !!check?.hasKey + const { data } = useEntityDefinitionsQuery( { schemas: selectedSchemas, @@ -289,11 +296,21 @@ export const AiAssistantPanel = ({

-
+
+ {!isApiKeySet && ( + + OpenAI API key not set + + + + + )} - ) : isAWSProvider ? ( + ) : !isAWSProvider ? ( diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx index 368489cebad..6105135bba8 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx @@ -9,9 +9,11 @@ import { toast } from 'sonner' import type { Message as MessageType } from 'ai/react' import { useChat } from 'ai/react' import { useParams } from 'common' +import { Markdown } from 'components/interfaces/Markdown' import OptInToOpenAIToggle from 'components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle' import { MessageWithDebug } from 'components/interfaces/SQLEditor/AiAssistantPanel' import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types' +import { useCheckOpenAIKeyQuery } from 'data/ai/check-api-key-query' import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' import { useEntityDefinitionQuery } from 'data/database/entity-definition-query' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' @@ -116,6 +118,9 @@ export const AIAssistant = ({ selectedSchemas.length === 0 && selectedTables.length === 0 + const { data: check } = useCheckOpenAIKeyQuery() + const isApiKeySet = IS_PLATFORM || !!check?.hasKey + const { data: existingDefinition } = useEntityDefinitionQuery({ id: entity?.id, type: editor, @@ -457,6 +462,19 @@ export const AIAssistant = ({ description="Give us a moment while we work on bringing the Assistant back online" /> )} + {!isApiKeySet && ( + + } + /> + )}
@@ -619,7 +637,7 @@ export const AIAssistant = ({ '[&>textarea]:rounded-none [&>textarea]:border-0 [&>textarea]:!outline-none [&>textarea]:!ring-offset-0 [&>textarea]:!ring-0' )} loading={isLoading} - disabled={disablePrompts || isLoading} + disabled={!isApiKeySet || disablePrompts || isLoading} placeholder={ hasMessages ? 'Reply to the assistant...' : 'How can we help you today?' } @@ -630,7 +648,7 @@ export const AIAssistant = ({ sendMessageToAssistant(value) }} /> - {!hasMessages && ( + {!hasMessages && IS_PLATFORM && (
The Assistant is in Alpha and your prompts might be rate limited
diff --git a/apps/studio/data/ai/check-api-key-query.ts b/apps/studio/data/ai/check-api-key-query.ts new file mode 100644 index 00000000000..8e21abd1df2 --- /dev/null +++ b/apps/studio/data/ai/check-api-key-query.ts @@ -0,0 +1,31 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { get, isResponseOk } from 'lib/common/fetch' +import { BASE_PATH, IS_PLATFORM } from 'lib/constants' +import { resourceKeys } from './keys' + +// check to see if the OPENAI_API_KEY env var is set in self-hosted +// so we can disable the chat editor and add a warning about manually adding the key + +export async function checkOpenAIKey(signal?: AbortSignal) { + const response = await get(`${BASE_PATH}/api/ai/sql/check-api-key`, { signal }) + + if (!isResponseOk(response)) { + throw (response as any).error + } + + return response as { hasKey: boolean } +} + +export type ResourceData = Awaited> +export type ResourceError = { errorEventId: string; message: string } + +export const useCheckOpenAIKeyQuery = ({ + enabled = true, + ...options +}: UseQueryOptions = {}) => + useQuery( + resourceKeys.apiKey(), + ({ signal }) => checkOpenAIKey(signal), + { enabled: !IS_PLATFORM && enabled, ...options } + ) diff --git a/apps/studio/data/ai/keys.ts b/apps/studio/data/ai/keys.ts new file mode 100644 index 00000000000..051620ea5e1 --- /dev/null +++ b/apps/studio/data/ai/keys.ts @@ -0,0 +1,3 @@ +export const resourceKeys = { + apiKey: () => ['api-key'] as const, +} diff --git a/apps/studio/pages/api/ai/sql/check-api-key.ts b/apps/studio/pages/api/ai/sql/check-api-key.ts new file mode 100644 index 00000000000..5c95b625b26 --- /dev/null +++ b/apps/studio/pages/api/ai/sql/check-api-key.ts @@ -0,0 +1,24 @@ +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { + if (process.env.OPENAI_API_KEY) { + return res.status(200).json({ hasKey: true }) + } else { + return res.status(404).json({ hasKey: false }) + } +}