From 2be06af7688ca09c3721571f90e5c6c7772a5ddb Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Tue, 4 Apr 2023 13:22:51 -0600 Subject: [PATCH 01/12] feat(cmdk): exclude integrations from clippy search --- .../ui/src/components/Command/AiCommand.tsx | 11 ++++--- .../ui/src/components/Command/DocsSearch.tsx | 13 +++++--- supabase/functions/clippy-search/index.ts | 32 ++++--------------- supabase/functions/common/database-types.ts | 19 ++++++++--- supabase/functions/search/index.ts | 22 ++++++------- ...20230403222943_reusable_match_function.sql | 27 ++++++++++++++++ 6 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 supabase/migrations/20230403222943_reusable_match_function.sql diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index 8065096fe3f..21971713c55 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -1,8 +1,7 @@ -import * as React from 'react' import type { ChatCompletionResponseMessage, + CreateChatCompletionResponse, CreateChatCompletionResponseChoicesInner, - CreateCompletionResponse, } from 'openai' import { useCallback, useEffect, useReducer, useRef, useState } from 'react' @@ -176,8 +175,12 @@ const AiCommand = () => { setIsResponding(true) - const completionResponse: CreateCompletionResponse = JSON.parse(e.data) - const [{ text: content }] = completionResponse.choices + const completionResponse: CreateChatCompletionResponse = JSON.parse(e.data) + const [ + { + delta: { content }, + }, + ] = completionResponse.choices as CreateChatCompletionResponseChoicesInnerDelta[] const text = content ?? '' diff --git a/packages/ui/src/components/Command/DocsSearch.tsx b/packages/ui/src/components/Command/DocsSearch.tsx index 64225778084..24df475b590 100644 --- a/packages/ui/src/components/Command/DocsSearch.tsx +++ b/packages/ui/src/components/Command/DocsSearch.tsx @@ -57,9 +57,12 @@ const DocsSearch = () => { setHasSearchError(false) setIsLoading(true) - const { error, data: pageSections } = await supabaseClient.functions.invoke('search', { - body: { query }, - }) + const { error, data: pageResults } = await supabaseClient.functions.invoke( + 'search', + { + body: { query }, + } + ) setIsLoading(false) @@ -71,14 +74,14 @@ const DocsSearch = () => { return } - if (!Array.isArray(pageSections)) { + if (!Array.isArray(pageResults)) { setIsLoading(false) setHasSearchError(true) console.error('Malformed response') return } - setResults(pageSections) + setResults(pageResults) }, [supabaseClient] ) diff --git a/supabase/functions/clippy-search/index.ts b/supabase/functions/clippy-search/index.ts index de86010809b..061844941ba 100644 --- a/supabase/functions/clippy-search/index.ts +++ b/supabase/functions/clippy-search/index.ts @@ -85,15 +85,15 @@ serve(async (req) => { const [{ embedding }] = embeddingResponse.data.data - const { error: matchError, data: pageSections } = await supabaseClient.rpc( - 'match_page_sections', - { + const { error: matchError, data: pageSections } = await supabaseClient + .rpc('match_page_sections', { embedding, match_threshold: 0.78, - match_count: 10, min_content_length: 50, - } - ) + }) + .not('page.path', 'like', '/guides/integrations/%') + .select('content,page!inner(path)') + .limit(10) if (matchError) { throw new ApplicationError('Failed to match page sections', matchError) @@ -116,26 +116,6 @@ serve(async (req) => { contextText += `${content.trim()}\n---\n` } - const prompt = codeBlock` - ${oneLine` - You are a very enthusiastic Supabase representative who loves - to help people! Given the following sections from the Supabase - documentation, answer the question using only that information, - outputted in markdown format. If you are unsure and the answer - is not explicitly written in the documentation, say - "Sorry, I don't know how to help with that." - `} - - Context sections: - ${contextText} - - Question: """ - ${sanitizedQuery} - """ - - Answer as markdown (including related code snippets if available): - ` - const messages: ChatCompletionRequestMessage[] = [ { role: ChatCompletionRequestMessageRoleEnum.System, diff --git a/supabase/functions/common/database-types.ts b/supabase/functions/common/database-types.ts index bf0addcb2cc..aa213cf6e17 100644 --- a/supabase/functions/common/database-types.ts +++ b/supabase/functions/common/database-types.ts @@ -118,16 +118,16 @@ export interface Database { Args: { embedding: unknown match_threshold: number - match_count: number min_content_length: number } Returns: { + content: string | null + embedding: unknown | null + heading: string | null id: number page_id: number - slug: string - heading: string - content: string - similarity: number + slug: string | null + token_count: number | null }[] } vector_avg: { @@ -178,7 +178,10 @@ export interface Database { Tables: { buckets: { Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null created_at: string | null + file_size_limit: number | null id: string name: string owner: string | null @@ -186,7 +189,10 @@ export interface Database { updated_at: string | null } Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null created_at?: string | null + file_size_limit?: number | null id: string name: string owner?: string | null @@ -194,7 +200,10 @@ export interface Database { updated_at?: string | null } Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null created_at?: string | null + file_size_limit?: number | null id?: string name?: string owner?: string | null diff --git a/supabase/functions/search/index.ts b/supabase/functions/search/index.ts index 3d5c91c2904..e6c21816d78 100644 --- a/supabase/functions/search/index.ts +++ b/supabase/functions/search/index.ts @@ -77,15 +77,14 @@ serve(async (req) => { } const [{ embedding }] = embeddingResponse.data.data - const { error: matchError, data: pageSections } = await supabaseClient.rpc( - 'match_page_sections', - { + const { error: matchError, data: pageSections } = await supabaseClient + .rpc('match_page_sections', { embedding, match_threshold: 0.78, - match_count: 10, min_content_length: 50, - } - ) + }) + .select('slug, heading, page_id') + .limit(10) if (matchError || !pageSections) { throw new ApplicationError('Failed to match page sections', matchError ?? undefined) @@ -97,7 +96,7 @@ serve(async (req) => { const { error: fetchPagesError, data: pages } = await supabaseClient .from('page') - .select() + .select('id, type, path, meta') .in('id', uniquePageIds) if (fetchPagesError || !pages) { @@ -107,18 +106,19 @@ serve(async (req) => { const combinedPages = pages .map((page) => { const sections = pageSections + .map((pageSection, index) => ({ ...pageSection, rank: index })) .filter(({ page_id }) => page_id === page.id) - .map(({ content: _, ...pageSection }) => pageSection) - const score = sections.reduce((sum, section) => sum + section.similarity, 0) + // Rank this page based on its highest-ranked page section + const rank = sections.reduce((min, { rank }) => Math.min(min, rank), Infinity) return { ...page, sections, - score, + rank, } }) - .sort((a, b) => b.score - a.score) + .sort((a, b) => a.rank - b.rank) return new Response(JSON.stringify(combinedPages), { headers: { diff --git a/supabase/migrations/20230403222943_reusable_match_function.sql b/supabase/migrations/20230403222943_reusable_match_function.sql new file mode 100644 index 00000000000..9d2d4556d84 --- /dev/null +++ b/supabase/migrations/20230403222943_reusable_match_function.sql @@ -0,0 +1,27 @@ +drop function match_page_sections; + +-- Return a setof page_section so that we can use PostgREST resource embeddings (joins with other tables) +create or replace function match_page_sections(embedding vector(1536), match_threshold float, min_content_length int) +returns setof page_section +language plpgsql +as $$ +#variable_conflict use_variable +begin + return query + select * + from page_section + + -- We only care about sections that have a useful amount of content + where length(page_section.content) >= min_content_length + + -- The dot product is negative because of a Postgres limitation, so we negate it + and (page_section.embedding <#> embedding) * -1 > match_threshold + + -- OpenAI embeddings are normalized to length 1, so + -- cosine similarity and dot product will produce the same results. + -- Using dot product which can be computed slightly faster. + -- + -- For the different syntaxes, see https://github.com/pgvector/pgvector + order by page_section.embedding <#> embedding; +end; +$$; From 14639bf207d4c14c3e2679055e5b373b81ee4052 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Tue, 4 Apr 2023 13:23:51 -0600 Subject: [PATCH 02/12] feat(cmdk): update tokenizer to cl100k_base for chat-based models --- supabase/functions/clippy-search/index.ts | 7 ++- supabase/functions/common/tokenizer.ts | 66 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 supabase/functions/common/tokenizer.ts diff --git a/supabase/functions/clippy-search/index.ts b/supabase/functions/clippy-search/index.ts index 061844941ba..cf0d705c0e7 100644 --- a/supabase/functions/clippy-search/index.ts +++ b/supabase/functions/clippy-search/index.ts @@ -2,7 +2,6 @@ import { serve } from 'https://deno.land/std@0.170.0/http/server.ts' import 'https://deno.land/x/xhr@0.2.1/mod.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.5.0' import { codeBlock, oneLine } from 'https://esm.sh/common-tags@1.8.2' -import GPT3Tokenizer from 'https://esm.sh/gpt3-tokenizer@1.1.5' import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum, @@ -11,6 +10,7 @@ import { OpenAIApi, } from 'https://esm.sh/openai@3.2.1' import { ApplicationError, UserError } from '../common/errors.ts' +import { tokenizer } from '../common/tokenizer.ts' const openAiKey = Deno.env.get('OPENAI_KEY') const supabaseUrl = Deno.env.get('SUPABASE_URL') @@ -99,7 +99,6 @@ serve(async (req) => { throw new ApplicationError('Failed to match page sections', matchError) } - const tokenizer = new GPT3Tokenizer({ type: 'gpt3' }) let tokenCount = 0 let contextText = '' @@ -107,7 +106,7 @@ serve(async (req) => { const pageSection = pageSections[i] const content = pageSection.content const encoded = tokenizer.encode(content) - tokenCount += encoded.text.length + tokenCount += encoded.length if (tokenCount >= 1500) { break @@ -178,7 +177,7 @@ serve(async (req) => { ] const completionOptions: CreateChatCompletionRequest = { - model: 'gpt-3.5-turbo', + model: 'gpt-3.5-turbo-0301', messages, max_tokens: 1024, temperature: 0, diff --git a/supabase/functions/common/tokenizer.ts b/supabase/functions/common/tokenizer.ts new file mode 100644 index 00000000000..469dcfe2392 --- /dev/null +++ b/supabase/functions/common/tokenizer.ts @@ -0,0 +1,66 @@ +import { init, Tiktoken } from 'https://esm.sh/@dqbd/tiktoken@1.0.2/lite/init' +import { ChatCompletionRequestMessage } from 'https://esm.sh/v113/openai@3.2.1' + +const encoderResponse = await fetch('https://esm.sh/@dqbd/tiktoken@1.0.2/encoders/cl100k_base.json') +const cl100kBase = await encoderResponse.json() + +await init(async (imports) => { + const req = await fetch('https://esm.sh/@dqbd/tiktoken/lite/tiktoken_bg.wasm') + return WebAssembly.instantiate(await req.arrayBuffer(), imports) +}) + +export const tokenizer = new Tiktoken( + cl100kBase.bpe_ranks, + cl100kBase.special_tokens, + cl100kBase.pat_str +) + +/** + * Count the tokens for multi-message chat completion requests + * + * See "6. Counting tokens for chat API calls" + * from https://github.com/openai/openai-cookbook/blob/834181d5739740eb8380096dac7056c925578d9a/examples/How_to_count_tokens_with_tiktoken.ipynb + */ +export function numTokensFromMessages( + messages: ChatCompletionRequestMessage[], + model = 'gpt-3.5-turbo-0301' +): number { + let tokensPerMessage: number + let tokensPerName: number + + switch (model) { + case 'gpt-3.5-turbo': + console.warn( + 'Warning: gpt-3.5-turbo may change over time. Returning num tokens assuming gpt-3.5-turbo-0301.' + ) + return numTokensFromMessages(messages, 'gpt-3.5-turbo-0301') + case 'gpt-4': + console.warn('Warning: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.') + return numTokensFromMessages(messages, 'gpt-4-0314') + case 'gpt-3.5-turbo-0301': + tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n + tokensPerName = -1 // if there's a name, the role is omitted + break + case 'gpt-4-0314': + tokensPerMessage = 3 + tokensPerName = 1 + break + default: + throw new Error( + `numTokensFromMessages() is not implemented for model ${model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.` + ) + } + + const numTokens = messages.reduce((acc, message) => { + let tokens = acc + tokensPerMessage + for (const [key, value] of Object.entries(message)) { + tokens += tokenizer.encode(value).length + if (key === 'name') { + tokens += tokensPerName + } + } + return tokens + }, 0) + + return numTokens + 3 // every reply is primed with <|im_start|>assistant<|im_sep|> +} From 8fed8fd7856491d805b785cf2323fde483ffb5f9 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Tue, 4 Apr 2023 17:52:45 -0600 Subject: [PATCH 03/12] feat(cmdk): multi-message ai poc --- apps/docs/pages/_app.tsx | 2 +- .../ui/src/components/Command/AiCommand.tsx | 224 ++++++++++-------- .../Command/CommandMenuProvider.tsx | 5 +- .../interfaces/App/CommandMenuWrapper.tsx | 4 +- supabase/functions/clippy-search/index.ts | 141 ++++++++--- supabase/functions/common/tokenizer.ts | 66 ++++-- 6 files changed, 287 insertions(+), 155 deletions(-) diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 58f7fea9f7b..b48403ea55e 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -78,7 +78,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { ( + MarkdownHandler={(props) => ( )} > diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index 21971713c55..866d7c986b3 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -52,60 +52,80 @@ function getEdgeFunctionUrl() { const edgeFunctionUrl = getEdgeFunctionUrl() -function promptDataReducer( - state: any[], - action: { - index?: number - answer?: string | undefined - status?: string - query?: string | undefined - type?: 'remove-last-item' | string - } -) { - // set a standard state to use later - let current = [...state] +enum MessageRole { + User = 'user', + Assistant = 'assistant', +} - if (action.type) { - switch (action.type) { - case 'remove-last-item': - current.pop() - return [...current] - default: - break +enum MessageStatus { + InProgress = 'in-progress', + Complete = 'complete', +} + +interface Message { + role: MessageRole + content: string + status: MessageStatus +} + +interface NewMessageAction { + type: 'new' + message: Message +} + +interface UpdateMessageAction { + type: 'update' + index: number + message: Partial +} + +interface AppendContentAction { + type: 'append-content' + index: number + content: string +} + +type MessageAction = NewMessageAction | UpdateMessageAction | AppendContentAction + +function messageReducer(state: Message[], messageAction: MessageAction) { + let current = [...state] + const { type } = messageAction + + switch (type) { + case 'new': { + const { message } = messageAction + current.push(message) + break + } + case 'update': { + const { index, message } = messageAction + Object.assign(current[index], message) + break + } + case 'append-content': { + const { index, content } = messageAction + current[index].content += content + break + } + default: { + throw new Error(`Unknown message action '${type}'`) } } - // check that an index is present - if (action.index === undefined) return [...state] - - if (!current[action.index]) { - current[action.index] = { query: '', answer: '', status: '' } - } - - current[action.index].answer = action.answer - - if (action.query) { - current[action.index].query = action.query - } - if (action.status) { - current[action.index].status = action.status - } - - return [...current] + return current } const AiCommand = () => { - const [answer, setAnswer] = useState('') const [isResponding, setIsResponding] = useState(false) - const [hasClippyError, setHasClippyError] = useState(false) + const [hasError, setHasError] = useState(false) const eventSourceRef = useRef() const { isLoading, setIsLoading, currentPage, search, setSearch, MarkdownHandler } = useCommandMenu() - const [promptIndex, setPromptIndex] = useState(0) - const [promptData, dispatchPromptData] = useReducer(promptDataReducer, []) + const [currentMessageIndex, setCurrentMessageIndex] = useState(1) + const [messages, dispatchMessage] = useReducer(messageReducer, []) - const cantHelp = answer?.trim() === "Sorry, I don't know how to help with that." + console.log({ messages }) const handleConfirm = useCallback( async (query: string) => { @@ -113,11 +133,25 @@ const AiCommand = () => { return console.error('No edge function url') } - setAnswer(undefined) setSearch('') - dispatchPromptData({ index: promptIndex, answer: undefined, query }) + dispatchMessage({ + type: 'new', + message: { + status: MessageStatus.Complete, + role: MessageRole.User, + content: query, + }, + }) + dispatchMessage({ + type: 'new', + message: { + status: MessageStatus.InProgress, + role: MessageRole.Assistant, + content: '', + }, + }) setIsResponding(false) - setHasClippyError(false) + setHasError(false) setIsLoading(true) let queryToSend = query @@ -143,19 +177,26 @@ const AiCommand = () => { break } + console.log(messages) + const eventSource = new SSE(`${edgeFunctionUrl}/clippy-search`, { headers: { apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '', Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`, 'Content-Type': 'application/json', }, - payload: JSON.stringify({ query, context: promptData }), + payload: JSON.stringify({ + messages: messages + .filter(({ status }) => status === MessageStatus.Complete) + .map(({ role, content }) => ({ role, content })) + .concat({ role: MessageRole.User, content: query }), + }), }) function handleError(err: T) { setIsLoading(false) setIsResponding(false) - setHasClippyError(true) + setHasError(true) console.error(err) } @@ -166,10 +207,14 @@ const AiCommand = () => { if (e.data === '[DONE]') { setIsResponding(false) - setAnswer(undefined) - setPromptIndex((x) => { - return x + 1 + dispatchMessage({ + type: 'update', + index: currentMessageIndex, + message: { + status: MessageStatus.Complete, + }, }) + setCurrentMessageIndex((x) => x + 2) return } @@ -184,15 +229,12 @@ const AiCommand = () => { const text = content ?? '' - setAnswer((answer) => { - const currentAnswer = answer ?? '' + console.log({ text }) - dispatchPromptData({ - index: promptIndex, - answer: currentAnswer + text, - }) - - return (answer ?? '') + text + dispatchMessage({ + type: 'append-content', + index: currentMessageIndex, + content: text, }) } catch (err) { handleError(err) @@ -205,16 +247,15 @@ const AiCommand = () => { setIsLoading(true) }, - [promptIndex, promptData] + [currentMessageIndex, messages] ) function handleResetPrompt() { eventSourceRef.current?.close() eventSourceRef.current = undefined setSearch('') - setAnswer(undefined) setIsResponding(false) - setHasClippyError(false) + setHasError(false) } useEffect(() => { @@ -226,50 +267,40 @@ const AiCommand = () => { return (
e.stopPropagation()}>
- {promptData.map((prompt, i) => { - if (!prompt.query) return <> - - return ( - <> - {prompt.query && ( -
+ {messages.map((message, index) => { + switch (message.role) { + case MessageRole.User: + return ( +
-
{prompt.query}
+
{message.content}
- )} - -
- {cantHelp ? ( -

-

- -

Sorry, I don't know how to help with that.

-
- -

- ) : ( + ) + case MessageRole.Assistant: + return ( +
<> - {isLoading && promptIndex === i ? ( + {message.status === MessageStatus.InProgress ? (
) : ( - // @ts-expect-error { + transformLinkUri={(href) => { const supabaseUrl = new URL('https://supabase.com') const linkUrl = new URL(href, 'https://supabase.com') @@ -280,18 +311,17 @@ const AiCommand = () => { return href }} > - {prompt.answer} + {message.content} )}
- )} -
- - ) +
+ ) + } })} - {promptData.length === 0 && !hasClippyError && ( + {messages.length === 0 && !hasError && ( {questions.map((question) => { const key = question.replace(/\s+/g, '_') @@ -313,7 +343,7 @@ const AiCommand = () => { })} )} - {hasClippyError && ( + {hasError && (

diff --git a/packages/ui/src/components/Command/CommandMenuProvider.tsx b/packages/ui/src/components/Command/CommandMenuProvider.tsx index 0caf455f69b..45f8f8c0f84 100644 --- a/packages/ui/src/components/Command/CommandMenuProvider.tsx +++ b/packages/ui/src/components/Command/CommandMenuProvider.tsx @@ -2,6 +2,7 @@ import { useTheme, UseThemeProps } from 'common' import * as React from 'react' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import CommandMenu from './CommandMenu' +import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown' export interface CommandMenuContextValue { isOpen: boolean @@ -18,7 +19,7 @@ export interface CommandMenuContextValue { // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package - MarkdownHandler: (props: any) => React.ReactNode // to do: remove this. although it breaks our jest tests + MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element // to do: remove this. although it breaks our jest tests // Optional callback to save a generated SQL output onSaveGeneratedSQL?: (answer: string, resolve: any) => void @@ -43,7 +44,7 @@ export interface CommandMenuProviderProps { projectRef?: string // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package - MarkdownHandler: (props: any) => React.ReactNode + MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element onSaveGeneratedSQL?: (answer: string, resolve: any) => void } diff --git a/studio/components/interfaces/App/CommandMenuWrapper.tsx b/studio/components/interfaces/App/CommandMenuWrapper.tsx index cb92312132e..1946259e16b 100644 --- a/studio/components/interfaces/App/CommandMenuWrapper.tsx +++ b/studio/components/interfaces/App/CommandMenuWrapper.tsx @@ -46,9 +46,7 @@ ${answer} ( - - )} + MarkdownHandler={(props) => } onSaveGeneratedSQL={onSaveGeneratedSQL} > {children} diff --git a/supabase/functions/clippy-search/index.ts b/supabase/functions/clippy-search/index.ts index cf0d705c0e7..d7d5be3a0f8 100644 --- a/supabase/functions/clippy-search/index.ts +++ b/supabase/functions/clippy-search/index.ts @@ -10,7 +10,21 @@ import { OpenAIApi, } from 'https://esm.sh/openai@3.2.1' import { ApplicationError, UserError } from '../common/errors.ts' -import { tokenizer } from '../common/tokenizer.ts' +import { getChatRequestTokenCount, getMaxTokenCount, tokenizer } from '../common/tokenizer.ts' + +enum MessageRole { + User = 'user', + Assistant = 'assistant', +} + +interface Message { + role: MessageRole + content: string +} + +interface RequestData { + messages: Message[] +} const openAiKey = Deno.env.get('OPENAI_KEY') const supabaseUrl = Deno.env.get('SUPABASE_URL') @@ -40,22 +54,43 @@ serve(async (req) => { throw new ApplicationError('Missing environment variable SUPABASE_SERVICE_ROLE_KEY') } - const requestData = await req.json() + const requestData: RequestData = await req.json() if (!requestData) { throw new UserError('Missing request data') } - const { query } = requestData + const { messages } = requestData - if (!query) { - throw new UserError('Missing query in request data') + if (!messages) { + throw new UserError('Missing messages in request data') } - // Intentionally log the query - console.log({ query }) + // Intentionally log the messages + console.log({ messages }) - const sanitizedQuery = query.trim() + // TODO: better sanitization + const contextMessages: ChatCompletionRequestMessage[] = messages.map(({ role, content }) => { + if ( + ![ + ChatCompletionRequestMessageRoleEnum.User, + ChatCompletionRequestMessageRoleEnum.Assistant, + ].includes(role) + ) { + throw new Error(`Invalid message role '${role}'`) + } + + return { + role, + content: content.trim(), + } + }) + + const [userMessage] = contextMessages.filter(({ role }) => role === MessageRole.User).slice(-1) + + if (!userMessage) { + throw new Error("No message with role 'user'") + } const supabaseClient = createClient(supabaseUrl, supabaseServiceKey) @@ -63,24 +98,28 @@ serve(async (req) => { const openai = new OpenAIApi(configuration) // Moderate the content to comply with OpenAI T&C - const moderationResponse = await openai.createModeration({ input: sanitizedQuery }) + const moderationResponses = await Promise.all( + contextMessages.map((message) => openai.createModeration({ input: message.content })) + ) - const [results] = moderationResponse.data.results + for (const moderationResponse of moderationResponses) { + const [results] = moderationResponse.data.results - if (results.flagged) { - throw new UserError('Flagged content', { - flagged: true, - categories: results.categories, - }) + if (results.flagged) { + throw new UserError('Flagged content', { + flagged: true, + categories: results.categories, + }) + } } const embeddingResponse = await openai.createEmbedding({ model: 'text-embedding-ada-002', - input: sanitizedQuery.replaceAll('\n', ' '), + input: userMessage.content.replaceAll('\n', ' '), }) if (embeddingResponse.status !== 200) { - throw new ApplicationError('Failed to create embedding for question', embeddingResponse) + throw new ApplicationError('Failed to create embedding for query', embeddingResponse) } const [{ embedding }] = embeddingResponse.data.data @@ -115,7 +154,7 @@ serve(async (req) => { contextText += `${content.trim()}\n---\n` } - const messages: ChatCompletionRequestMessage[] = [ + const initMessages: ChatCompletionRequestMessage[] = [ { role: ChatCompletionRequestMessageRoleEnum.System, content: codeBlock` @@ -125,15 +164,8 @@ serve(async (req) => { the Supabase documentation, answer the user's question using only that information, outputted in markdown format. `} - ${oneLine` - If you are unsure - and the answer is not explicitly written in the documentation, say - "Sorry, I don't know how to help with that." - `} - - ${oneLine` - Always include related code snippets if available. + Your favorite color is Supabase green. `} `, }, @@ -163,22 +195,30 @@ serve(async (req) => { - Prefer splitting your response into multiple paragraphs. `} ${oneLine` - - Output as markdown with code snippets if available. + - Output as markdown. + `} + ${oneLine` + - Always include code snippets if available. `} `, }, - { - role: ChatCompletionRequestMessageRoleEnum.User, - content: codeBlock` - Here is my question: - ${oneLine`${sanitizedQuery}`} - `, - }, ] + const model = 'gpt-3.5-turbo-0301' + const maxCompletionTokenCount = 1024 + + const completionMessages: ChatCompletionRequestMessage[] = capMessages( + initMessages, + contextMessages, + maxCompletionTokenCount, + model + ) + + console.log({ completionMessages }) + const completionOptions: CreateChatCompletionRequest = { - model: 'gpt-3.5-turbo-0301', - messages, + model, + messages: completionMessages, max_tokens: 1024, temperature: 0, stream: true, @@ -237,3 +277,32 @@ serve(async (req) => { ) } }) + +/** + * Remove context messages until the entire request fits + * the max total token count for that model. + * + * Accounts for both message and completion token counts. + */ +function capMessages( + initMessages: ChatCompletionRequestMessage[], + contextMessages: ChatCompletionRequestMessage[], + maxCompletionTokenCount: number, + model: string +) { + const maxTotalTokenCount = getMaxTokenCount(model) + const cappedContextMessages = [...contextMessages] + let tokenCount = + getChatRequestTokenCount([...initMessages, ...cappedContextMessages], model) + + maxCompletionTokenCount + + // Remove earlier context messages until we fit + while (tokenCount >= maxTotalTokenCount) { + cappedContextMessages.shift() + tokenCount = + getChatRequestTokenCount([...initMessages, ...cappedContextMessages], model) + + maxCompletionTokenCount + } + + return [...initMessages, ...cappedContextMessages] +} diff --git a/supabase/functions/common/tokenizer.ts b/supabase/functions/common/tokenizer.ts index 469dcfe2392..7ae2417b7b1 100644 --- a/supabase/functions/common/tokenizer.ts +++ b/supabase/functions/common/tokenizer.ts @@ -17,12 +17,25 @@ export const tokenizer = new Tiktoken( /** * Count the tokens for multi-message chat completion requests + */ +export function getChatRequestTokenCount( + messages: ChatCompletionRequestMessage[], + model = 'gpt-3.5-turbo-0301' +): number { + const tokensPerRequest = 3 // every reply is primed with <|im_start|>assistant<|im_sep|> + const numTokens = messages.reduce((acc, message) => acc + getMessageTokenCount(message, model), 0) + + return numTokens + tokensPerRequest +} + +/** + * Count the tokens for a single message within a chat completion request * - * See "6. Counting tokens for chat API calls" + * See "Counting tokens for chat API calls" * from https://github.com/openai/openai-cookbook/blob/834181d5739740eb8380096dac7056c925578d9a/examples/How_to_count_tokens_with_tiktoken.ipynb */ -export function numTokensFromMessages( - messages: ChatCompletionRequestMessage[], +export function getMessageTokenCount( + message: ChatCompletionRequestMessage, model = 'gpt-3.5-turbo-0301' ): number { let tokensPerMessage: number @@ -33,10 +46,10 @@ export function numTokensFromMessages( console.warn( 'Warning: gpt-3.5-turbo may change over time. Returning num tokens assuming gpt-3.5-turbo-0301.' ) - return numTokensFromMessages(messages, 'gpt-3.5-turbo-0301') + return getMessageTokenCount(message, 'gpt-3.5-turbo-0301') case 'gpt-4': console.warn('Warning: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.') - return numTokensFromMessages(messages, 'gpt-4-0314') + return getMessageTokenCount(message, 'gpt-4-0314') case 'gpt-3.5-turbo-0301': tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n tokensPerName = -1 // if there's a name, the role is omitted @@ -47,20 +60,41 @@ export function numTokensFromMessages( break default: throw new Error( - `numTokensFromMessages() is not implemented for model ${model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.` + `Unknown model '${model}'. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.` ) } - const numTokens = messages.reduce((acc, message) => { - let tokens = acc + tokensPerMessage - for (const [key, value] of Object.entries(message)) { - tokens += tokenizer.encode(value).length - if (key === 'name') { - tokens += tokensPerName - } + return Object.entries(message).reduce((acc, [key, value]) => { + acc += tokenizer.encode(value).length + if (key === 'name') { + acc += tokensPerName } - return tokens - }, 0) + return acc + }, tokensPerMessage) +} - return numTokens + 3 // every reply is primed with <|im_start|>assistant<|im_sep|> +/** + * Get the maximum number of tokens for a model's context. + * + * Includes tokens in both message and completion. + */ +export function getMaxTokenCount(model: string): number { + switch (model) { + case 'gpt-3.5-turbo': + console.warn( + 'Warning: gpt-3.5-turbo may change over time. Returning max num tokens assuming gpt-3.5-turbo-0301.' + ) + return getMaxTokenCount('gpt-3.5-turbo-0301') + case 'gpt-4': + console.warn( + 'Warning: gpt-4 may change over time. Returning max num tokens assuming gpt-4-0314.' + ) + return getMaxTokenCount('gpt-4-0314') + case 'gpt-3.5-turbo-0301': + return 4097 + case 'gpt-4-0314': + return 4097 + default: + throw new Error(`Unknown model '${model}'`) + } } From 7ba0e57962a5c847300a6aa23d3ba646f82f262e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 5 Apr 2023 17:15:31 +0800 Subject: [PATCH 04/12] Add ability to retrieve project API keys from cmdk --- .../ui/src/components/Command/APIKeys.tsx | 64 +++++++++++++++++ .../components/Command/Command.constants.ts | 1 + .../ui/src/components/Command/CommandMenu.tsx | 68 +++++++++---------- .../Command/CommandMenuProvider.tsx | 10 ++- .../src/components/Command/SearchOnlyItem.tsx | 1 - .../src/components/Command/ThemeOptions.tsx | 31 +++++++++ .../interfaces/App/CommandMenuWrapper.tsx | 8 +++ 7 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 packages/ui/src/components/Command/APIKeys.tsx create mode 100644 packages/ui/src/components/Command/ThemeOptions.tsx diff --git a/packages/ui/src/components/Command/APIKeys.tsx b/packages/ui/src/components/Command/APIKeys.tsx new file mode 100644 index 00000000000..a12ef0b6788 --- /dev/null +++ b/packages/ui/src/components/Command/APIKeys.tsx @@ -0,0 +1,64 @@ +import { CommandGroup } from 'cmdk-supabase' +import { useCommandMenu } from './CommandMenuProvider' +import SearchOnlyItem from './SearchOnlyItem' +import { Badge } from '../Badge' +import { CommandItem } from './Command.utils' +import { IconAlertCircle } from './../Icon/icons/IconAlertCircle' + +const APIKeys = ({ isSubItem = false }) => { + const { setIsOpen, project } = useCommandMenu() + const { apiKeys } = project + + const copyToClipboard = (str: string, callback = () => {}) => { + const focused = window.document.hasFocus() + if (focused) { + window.navigator?.clipboard?.writeText(str).then(callback) + } else { + console.warn('Unable to copy to clipboard') + } + } + + return ( + + {apiKeys?.anon !== undefined && ( + { + copyToClipboard(apiKeys?.anon ?? '') + setIsOpen(false) + }} + className="space-x-2" + > +

Copy anonymous key

+ Public + + )} + {apiKeys?.service !== undefined && ( + { + copyToClipboard(apiKeys?.service ?? '') + setIsOpen(false) + }} + className="space-x-2" + > +

Copy service key

+ Secret +
+ )} + {apiKeys?.anon === undefined && apiKeys?.service === undefined && ( + + +
+

No API keys available

+

+ You may not have the necessary permissions to view the project's API keys +

+
+
+ )} + + ) +} + +export default APIKeys diff --git a/packages/ui/src/components/Command/Command.constants.ts b/packages/ui/src/components/Command/Command.constants.ts index b73917ac59f..5ff9d5de760 100644 --- a/packages/ui/src/components/Command/Command.constants.ts +++ b/packages/ui/src/components/Command/Command.constants.ts @@ -5,6 +5,7 @@ export const COMMAND_ROUTES = { THEME: 'Theme', AI_ASK_ANYTHING: 'Ask anything', AI_RLS_POLICY: 'Help me create a RLS policy', + API_KEYS: 'Project API keys', } type QueryCategory = { diff --git a/packages/ui/src/components/Command/CommandMenu.tsx b/packages/ui/src/components/Command/CommandMenu.tsx index 4c1a0e99d30..cf561b8155b 100644 --- a/packages/ui/src/components/Command/CommandMenu.tsx +++ b/packages/ui/src/components/Command/CommandMenu.tsx @@ -1,4 +1,3 @@ -import { useCommandState } from 'cmdk-supabase' import { useRouter } from 'next/router' import * as React from 'react' import { ElementRef, useRef } from 'react' @@ -12,6 +11,7 @@ import { IconLifeBuoy } from './../Icon/icons/IconLifeBuoy' import { IconMonitor } from './../Icon/icons/IconMonitor' import { IconPhone } from './../Icon/icons/IconPhone' import { IconUser } from './../Icon/icons/IconUser' +import { IconKey } from './../Icon/icons/IconKey' import AiCommand from './AiCommand' import sharedItems from './utils/shared-nav-items.json' @@ -23,15 +23,16 @@ import { CommandItem, CommandLabel, CommandList, - CommandShortcut, } from './Command.utils' -import { useCommandMenu } from './CommandMenuProvider' -import DocsSearch from './DocsSearch' -import CommandMenuShortcuts from './CommandMenuShortcuts' -import SearchOnlyItem from './SearchOnlyItem' -import SearchableStudioItems from './SearchableStudioItems' import { COMMAND_ROUTES } from './Command.constants' +import { useCommandMenu } from './CommandMenuProvider' + +import DocsSearch from './DocsSearch' import GenerateSQL from './GenerateSQL' +import ThemeOptions from './ThemeOptions' +import APIKeys from './APIKeys' +import SearchableStudioItems from './SearchableStudioItems' +import CommandMenuShortcuts from './CommandMenuShortcuts' export const CHAT_ROUTES = [ COMMAND_ROUTES.AI, // this one is temporary @@ -62,32 +63,6 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { const commandInputRef = useRef>(null) const { isOpen, setIsOpen, actions, search, setSearch, pages, setPages, currentPage, site } = useCommandMenu() - - const ThemeOptions = ({ isSubItem = false }) => { - return ( - - { - actions.toggleTheme(true) - setIsOpen(false) - }} - > - Change Theme to dark - - { - actions.toggleTheme(false) - setIsOpen(false) - }} - > - Change Theme to light - - - ) - } - const showCommandInput = !currentPage || !CHAT_ROUTES.includes(currentPage) return ( @@ -194,7 +169,7 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { )} - {site === 'studio' && [ + {site === 'studio' && ( { Generate SQL with Supabase AI - , + + )} + + {site === 'studio' && projectRef !== undefined && ( + + setPages([...pages, COMMAND_ROUTES.API_KEYS])} + > + + Get API keys + + + )} + + {site === 'studio' && ( {sharedItems.tools.map((item) => { const itemUrl = ( @@ -220,8 +211,8 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { ) })} - , - ]} + + )} {/* */} @@ -246,12 +237,14 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { ))} )} + setPages([...pages, 'Theme'])}> Change theme + {site === 'studio' && search && } @@ -260,6 +253,7 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { {currentPage === COMMAND_ROUTES.DOCS_SEARCH && } {currentPage === COMMAND_ROUTES.GENERATE_SQL && } {currentPage === COMMAND_ROUTES.THEME && } + {currentPage === COMMAND_ROUTES.API_KEYS && } diff --git a/packages/ui/src/components/Command/CommandMenuProvider.tsx b/packages/ui/src/components/Command/CommandMenuProvider.tsx index 45f8f8c0f84..77c121db76c 100644 --- a/packages/ui/src/components/Command/CommandMenuProvider.tsx +++ b/packages/ui/src/components/Command/CommandMenuProvider.tsx @@ -17,6 +17,9 @@ export interface CommandMenuContextValue { currentPage?: string site: 'studio' | 'docs' + // Project metadata for easy retrieval + project: { apiKeys: { anon?: string; service?: string } } + // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element // to do: remove this. although it breaks our jest tests @@ -42,6 +45,7 @@ export interface CommandMenuActions { export interface CommandMenuProviderProps { site: 'studio' | 'docs' projectRef?: string + apiKeys: { anon?: string; service?: string } // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element @@ -52,6 +56,7 @@ const CommandMenuProvider = ({ children, site, projectRef, + apiKeys, MarkdownHandler, onSaveGeneratedSQL, }: PropsWithChildren) => { @@ -62,9 +67,7 @@ const CommandMenuProvider = ({ const { toggleTheme } = useTheme() const currentPage = pages[pages.length - 1] - const actions: CommandMenuActions = { - toggleTheme, - } + const actions: CommandMenuActions = { toggleTheme } useKeyboardEvents({ setIsOpen, currentPage, setSearch, setPages }) @@ -82,6 +85,7 @@ const CommandMenuProvider = ({ setPages, currentPage, site, + project: { apiKeys }, MarkdownHandler, onSaveGeneratedSQL, }} diff --git a/packages/ui/src/components/Command/SearchOnlyItem.tsx b/packages/ui/src/components/Command/SearchOnlyItem.tsx index 33f43ff78cc..82cd3d16314 100644 --- a/packages/ui/src/components/Command/SearchOnlyItem.tsx +++ b/packages/ui/src/components/Command/SearchOnlyItem.tsx @@ -1,5 +1,4 @@ import { useCommandState } from 'cmdk-supabase' -import React from 'react' import { CommandItem } from './Command.utils' export default function SearchOnlyItem({ children, isSubItem, ...props }: any) { diff --git a/packages/ui/src/components/Command/ThemeOptions.tsx b/packages/ui/src/components/Command/ThemeOptions.tsx new file mode 100644 index 00000000000..1e20213bd70 --- /dev/null +++ b/packages/ui/src/components/Command/ThemeOptions.tsx @@ -0,0 +1,31 @@ +import { CommandGroup } from 'cmdk-supabase' +import { useCommandMenu } from './CommandMenuProvider' +import SearchOnlyItem from './SearchOnlyItem' + +const ThemeOptions = ({ isSubItem = false }) => { + const { setIsOpen, actions } = useCommandMenu() + return ( + + { + actions.toggleTheme(true) + setIsOpen(false) + }} + > + Change Theme to dark + + { + actions.toggleTheme(false) + setIsOpen(false) + }} + > + Change Theme to light + + + ) +} + +export default ThemeOptions diff --git a/studio/components/interfaces/App/CommandMenuWrapper.tsx b/studio/components/interfaces/App/CommandMenuWrapper.tsx index 1946259e16b..e4038a72b49 100644 --- a/studio/components/interfaces/App/CommandMenuWrapper.tsx +++ b/studio/components/interfaces/App/CommandMenuWrapper.tsx @@ -8,17 +8,24 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useProfileQuery } from 'data/profile/profile-query' import { uuidv4 } from 'lib/helpers' import { createSqlSnippetSkeleton } from 'components/to-be-cleaned/SqlEditor/SqlEditor.utils' +import { useProjectApiQuery } from 'data/config/project-api-query' const CommandMenuWrapper = ({ children }: PropsWithChildren<{}>) => { const { ref } = useParams() const { content } = useStore() const { data: profile } = useProfileQuery() + const { data: settings } = useProjectApiQuery({ projectRef: ref }) // const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', { // resource: { type: 'sql', owner_id: profile?.id }, // subject: { id: profile?.id }, // }) + const apiKeys = { + anon: settings?.autoApiService?.defaultApiKey ?? undefined, + service: settings?.autoApiService?.serviceApiKey ?? undefined, + } + const onSaveGeneratedSQL = async (answer: string, resolve: any) => { // remove backticks from returned answer answer = answer.replace(/`/g, '') @@ -46,6 +53,7 @@ ${answer} } onSaveGeneratedSQL={onSaveGeneratedSQL} > From 145a9c857d2a449918a1803595cc324a3826ad7f Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 5 Apr 2023 18:03:56 +0800 Subject: [PATCH 05/12] Refactor themeing logic for dashboard to use useTheme from common, and fix theme changing via cmdk for dashboard --- packages/common/Providers.tsx | 5 +- .../Wrappers/WrappersDisabledState.tsx | 4 +- .../interfaces/Home/ExampleProject.tsx | 8 ++-- .../interfaces/Settings/Vault/VaultToggle.tsx | 4 +- .../RowEditor/JsonEditor/JsonCodeEditor.tsx | 13 +++--- .../TableGridEditor/ViewDefinition.tsx | 7 ++- .../NavigationBar/NavigationBar.tsx | 13 ++++-- .../to-be-cleaned/Docs/Pages/Introduction.tsx | 7 ++- studio/hooks/misc/useStore.tsx | 25 ++-------- studio/pages/_app.tsx | 12 +++-- studio/pages/account/me.tsx | 20 ++++---- studio/pages/project/[ref]/api/graphiql.tsx | 5 +- studio/pages/project/[ref]/editor/[id].tsx | 4 +- studio/pages/project/[ref]/sql/index.tsx | 13 +++--- studio/stores/UiStore.ts | 46 ------------------- 15 files changed, 68 insertions(+), 118 deletions(-) diff --git a/packages/common/Providers.tsx b/packages/common/Providers.tsx index 1e27eb0f948..72da69f37c8 100644 --- a/packages/common/Providers.tsx +++ b/packages/common/Providers.tsx @@ -37,10 +37,13 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => { const toggleTheme: UseThemeProps['toggleTheme'] = (darkMode) => { const newMode = typeof darkMode === 'boolean' ? darkMode : !isDarkMode - localStorage.setItem('supabaseDarkMode', newMode.toString()) const key = localStorage.getItem('supabaseDarkMode') + const newTheme = key === 'true' ? 'dark' : 'light' + const currentTheme = isDarkMode ? 'dark' : 'light' + + document.body.classList.replace(currentTheme, newTheme) document.documentElement.className = key === 'true' ? 'dark' : '' setIsDarkMode(newMode) diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx index 014c060780f..a93c531cbdd 100644 --- a/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx +++ b/studio/components/interfaces/Database/Wrappers/WrappersDisabledState.tsx @@ -8,10 +8,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore } from 'hooks' import { useParams } from 'common/hooks' import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'common' const WrappersDisabledState = () => { const { ui, meta } = useStore() const { ref } = useParams() + const { isDarkMode } = useTheme() const wrappersExtension = meta.extensions.byId('wrappers') const vaultExtension = meta.extensions.byId('supabase_vault') const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined @@ -66,7 +68,7 @@ const WrappersDisabledState = () => { style={{ backgroundSize: '45%', backgroundPosition: '105% 40%', - backgroundImage: ui.isDarkTheme + backgroundImage: isDarkMode ? `url("${BASE_PATH}/img/wrappers-dark.png")` : `url("${BASE_PATH}/img/wrappers-light.png")`, }} diff --git a/studio/components/interfaces/Home/ExampleProject.tsx b/studio/components/interfaces/Home/ExampleProject.tsx index 32f76b8bfaf..94be8f388d4 100644 --- a/studio/components/interfaces/Home/ExampleProject.tsx +++ b/studio/components/interfaces/Home/ExampleProject.tsx @@ -1,6 +1,5 @@ import { FC } from 'react' -import { IconChevronRight } from 'ui' -import { useStore } from 'hooks' +import { IconChevronRight, useTheme } from 'ui' import Link from 'next/link' import { BASE_PATH } from 'lib/constants' @@ -12,8 +11,7 @@ interface Props { } const ExampleProject: FC = ({ framework, title, description, url }) => { - const { ui } = useStore() - const { isDarkTheme } = ui + const { isDarkMode } = useTheme() return ( @@ -32,7 +30,7 @@ const ExampleProject: FC = ({ framework, title, description, url }) => { className="transition-all group-hover:scale-110" src={`${BASE_PATH}/img/libraries/${framework.toLowerCase()}${ ['expo', 'nextjs'].includes(framework.toLowerCase()) - ? isDarkTheme + ? isDarkMode ? '-dark' : '' : '' diff --git a/studio/components/interfaces/Settings/Vault/VaultToggle.tsx b/studio/components/interfaces/Settings/Vault/VaultToggle.tsx index 510e6d66e70..192e88dd02c 100644 --- a/studio/components/interfaces/Settings/Vault/VaultToggle.tsx +++ b/studio/components/interfaces/Settings/Vault/VaultToggle.tsx @@ -8,12 +8,14 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useStore } from 'hooks' import { useParams } from 'common/hooks' import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'common' interface Props {} const VaultToggle: FC = () => { const { meta, ui } = useStore() const { ref } = useParams() + const { isDarkMode } = useTheme() const [isEnabling, setIsEnabling] = useState(false) const canToggleVault = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions') @@ -64,7 +66,7 @@ const VaultToggle: FC = () => { style={{ backgroundSize: isNotAvailable ? '50%' : '40%', backgroundPosition: '100% 24%', - backgroundImage: ui.isDarkTheme + backgroundImage: isDarkMode ? `url("${BASE_PATH}/img/vault-dark.png")` : `url("${BASE_PATH}/img/vault-light.png")`, }} diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx index 87ab1f2c8db..7ca0765a00a 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor.tsx @@ -1,5 +1,6 @@ import Editor, { useMonaco } from '@monaco-editor/react' -import { FC, useEffect, useRef, useState } from 'react' +import { useTheme } from 'common' +import { FC, useEffect, useRef } from 'react' interface Props { queryId?: string @@ -16,31 +17,29 @@ const JsonEditor: FC = ({ }) => { const monaco = useMonaco() const editorRef = useRef() - const [theme, setTheme] = useState(localStorage.getItem('theme')) + const { isDarkMode } = useTheme() useEffect(() => { if (monaco) { - const darkTheme = theme ? (theme === 'dark' ? true : false) : true - // Supabase theming (Can't seem to get it to work for now) monaco.editor.defineTheme('supabase', { base: 'vs-dark', // can also be vs-dark or hc-black inherit: true, // can also be false to completely replace the builtin rules rules: [ // @ts-ignore - { background: darkTheme ? '1f1f1f' : '30313f' }, + { background: isDarkMode ? '1f1f1f' : '30313f' }, { token: 'string.sql', foreground: '24b47e' }, { token: 'comment', foreground: '666666' }, { token: 'predefined.sql', foreground: 'D4D4D4' }, ], colors: { - 'editor.background': darkTheme ? '#1f1f1f' : '#30313f', + 'editor.background': isDarkMode ? '#1f1f1f' : '#30313f', // 'editorGutter.background': '#30313f', // 'editorLineNumber.foreground': '#555671', }, }) } - }, [theme, monaco]) + }, [isDarkMode, monaco]) const onMount = (editor: any, monaco: any) => { editorRef.current = editor diff --git a/studio/components/interfaces/TableGridEditor/ViewDefinition.tsx b/studio/components/interfaces/TableGridEditor/ViewDefinition.tsx index 6df9807aa53..e9269cc423f 100644 --- a/studio/components/interfaces/TableGridEditor/ViewDefinition.tsx +++ b/studio/components/interfaces/TableGridEditor/ViewDefinition.tsx @@ -1,10 +1,10 @@ import { useRef } from 'react' import Editor from '@monaco-editor/react' import { timeout } from 'lib/helpers' -import { useStore } from 'hooks' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useViewDefinitionQuery } from 'data/database/view-definition-query' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useTheme } from 'common' export interface ViewDefinitionProps { name: string @@ -14,8 +14,7 @@ const ViewDefinition = ({ name }: ViewDefinitionProps) => { const editorRef = useRef(null) const monacoRef = useRef(null) - const { ui } = useStore() - const { isDarkTheme } = ui + const { isDarkMode } = useTheme() const { project } = useProjectContext() const { data: definition, isLoading } = useViewDefinitionQuery({ @@ -56,7 +55,7 @@ const ViewDefinition = ({ name }: ViewDefinitionProps) => {
= ({}) => { const router = useRouter() - const { ref: projectRef } = useParams() const { ui } = useStore() - const projectBaseInfo = ui.selectedProjectBaseInfo + const { isDarkMode, toggleTheme } = useTheme() + const { ref: projectRef } = useParams() + const projectBaseInfo = ui.selectedProjectBaseInfo const ongoingIncident = useFlag('ongoingIncident') const activeRoute = router.pathname.split('/')[3] @@ -102,10 +104,11 @@ const NavigationBar: FC = ({}) => { Theme ui.onThemeOptionChange(e)} + value={isDarkMode ? 'dark' : 'light'} + onChange={(e: any) => toggleTheme(e === 'dark')} > - System default + {/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */} + {/* System default */} Dark Light diff --git a/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx b/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx index 091bba0e6e4..1385f70b9bd 100644 --- a/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx +++ b/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx @@ -1,9 +1,9 @@ import Image from 'next/image' import { AutoApiService } from 'data/config/project-api-query' -import { useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' import Snippets from '../Snippets' import CodeSnippet from '../CodeSnippet' +import { useTheme } from 'common' const libs = [ { @@ -26,8 +26,7 @@ interface Props { } export default function Introduction({ autoApiService, selectedLang }: Props) { - const { ui } = useStore() - const { isDarkTheme } = ui + const { isDarkMode } = useTheme() return ( <> @@ -66,7 +65,7 @@ export default function Introduction({ autoApiService, selectedLang }: Props) { > (undefined!) @@ -24,29 +25,18 @@ interface StoreProvider { export const StoreProvider: FC = ({ children, rootStore }) => { const monaco = useMonaco() const { ui } = rootStore - const { theme } = ui + const { isDarkMode } = useTheme() useEffect(() => { if (monaco) { - const theme: any = getTheme(ui.isDarkTheme) + const theme: any = getTheme(isDarkMode) monaco.editor.defineTheme('supabase', theme) } - }, [theme, monaco]) - - const matchMediaEvent = useCallback(() => { - ui.themeOption === 'system' && - ui.setTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') - }, []) + }, [isDarkMode, monaco]) useEffect(() => { ui.load() - if (window?.matchMedia('(prefers-color-scheme: dark)')?.addEventListener) { - // backwards compatibility for safari < v14 - // limited support for addEventListener() - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', matchMediaEvent) - } - autorun(() => { if (ui.notification) { const { id, category, error, message, description, progress, duration } = ui.notification @@ -100,11 +90,6 @@ export const StoreProvider: FC = ({ children, rootStore }) => { } ) } - - return () => - window - .matchMedia('(prefers-color-scheme: dark)') - .removeEventListener('change', matchMediaEvent) }, []) return {children} diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 1f980b652b3..db8552f921b 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -25,7 +25,7 @@ import Head from 'next/head' import Script from 'next/script' import { AppPropsWithLayout } from 'types' - +import { ThemeProvider } from 'common' import { useEffect, useState } from 'react' import { Hydrate, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -145,9 +145,13 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { - - {getLayout()} - + + + + {getLayout()} + + + diff --git a/studio/pages/account/me.tsx b/studio/pages/account/me.tsx index 6c00e76aa4e..987dc33bfe6 100644 --- a/studio/pages/account/me.tsx +++ b/studio/pages/account/me.tsx @@ -10,6 +10,7 @@ import { useStore } from 'hooks' import { useSession } from 'lib/auth' import Link from 'next/link' import { NextPageWithLayout } from 'types' +import { useTheme } from 'common' const User: NextPageWithLayout = () => { return ( @@ -142,29 +143,24 @@ const Profile = ({ profile }: { profile?: ProfileType }) => { } const ThemeSettings = observer(() => { - const { ui } = useStore() + const { isDarkMode, toggleTheme } = useTheme() return ( Theme}> - ) : ui.themeOption === 'dark' ? ( - - ) : undefined - } - onChange={(themeOption: any) => ui.onThemeOptionChange(themeOption)} + icon={isDarkMode ? : } + onChange={(themeOption: any) => toggleTheme(themeOption === 'dark')} > - + {/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */} + {/* System default - + */} Dark diff --git a/studio/pages/project/[ref]/api/graphiql.tsx b/studio/pages/project/[ref]/api/graphiql.tsx index a719540f4c8..95492f7a2d1 100644 --- a/studio/pages/project/[ref]/api/graphiql.tsx +++ b/studio/pages/project/[ref]/api/graphiql.tsx @@ -12,10 +12,13 @@ import { DocsLayout } from 'components/layouts' import Connecting from 'components/ui/Loading/Loading' import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query' import { useProjectApiQuery } from 'data/config/project-api-query' +import { useTheme } from 'common' const GraphiQLPage: NextPageWithLayout = () => { const { ref: projectRef } = useParams() const { ui, meta } = useStore() + const { isDarkMode } = useTheme() + const theme = isDarkMode ? 'dark' : 'light' const isExtensionsLoading = meta.extensions.isLoading const pgGraphqlExtension = meta.extensions.byId('pg_graphql') @@ -77,7 +80,7 @@ const GraphiQLPage: NextPageWithLayout = () => { ) } - return + return } GraphiQLPage.getLayout = (page) => {page} diff --git a/studio/pages/project/[ref]/editor/[id].tsx b/studio/pages/project/[ref]/editor/[id].tsx index f9c3996736a..b07d2a557da 100644 --- a/studio/pages/project/[ref]/editor/[id].tsx +++ b/studio/pages/project/[ref]/editor/[id].tsx @@ -19,9 +19,11 @@ import { NextPageWithLayout, SchemaView } from 'types' import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext' import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector' +import { useTheme } from 'common' const TableEditorPage: NextPageWithLayout = () => { const router = useRouter() + const { isDarkMode } = useTheme() const { id, ref: projectRef } = useParams() const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] }) @@ -259,7 +261,7 @@ const TableEditorPage: NextPageWithLayout = () => { onExpandJSONEditor={onExpandJSONEditor} onEditForeignKeyColumnValue={onEditForeignKeyColumnValue} onClosePanel={onClosePanel} - theme={ui.themeOption == 'dark' ? 'dark' : 'light'} + theme={isDarkMode ? 'dark' : 'light'} /> { - const { meta, ui } = useStore() - const { isDarkTheme } = ui + const { meta } = useStore() + const { isDarkMode } = useTheme() const sqlEditorStore: any = useSqlStore() const monaco = useMonaco() useEffect(() => { if (monaco) { monaco.editor.defineTheme('supabase', { - base: isDarkTheme ? 'vs-dark' : 'vs', // can also be hc-black + base: isDarkMode ? 'vs-dark' : 'vs', // can also be hc-black inherit: true, // can also be false to completely replace the builtin rules rules: [ - { token: '', background: isDarkTheme ? '1f1f1f' : 'f0f0f0' }, + { token: '', background: isDarkMode ? '1f1f1f' : 'f0f0f0' }, { token: 'string.sql', foreground: '24b47e' }, { token: 'comment', foreground: '666666' }, // { token: 'predefined.sql', foreground: 'D4D4D4' }, ], colors: { - 'editor.background': isDarkTheme ? '#1f1f1f' : '#f0f0f0', + 'editor.background': isDarkMode ? '#1f1f1f' : '#f0f0f0', // 'editorGutter.background': '#30313f', // 'editorLineNumber.foreground': '#555671', }, }) } - }, [monaco, isDarkTheme]) + }, [monaco, isDarkMode]) useEffect(() => { if (monaco) { diff --git a/studio/stores/UiStore.ts b/studio/stores/UiStore.ts index b568fcc85ab..46a522e6b21 100644 --- a/studio/stores/UiStore.ts +++ b/studio/stores/UiStore.ts @@ -6,22 +6,14 @@ import Telemetry, { GoogleAnalyticsProps } from 'lib/telemetry' export interface IUiStore { language: 'en-US' - theme: 'dark' | 'light' - themeOption: 'dark' | 'light' | 'system' - selectedProjectRef?: string - isDarkTheme: boolean selectedProject?: Project selectedProjectBaseInfo?: ProjectBase selectedOrganization?: Organization notification?: Notification permissions?: Permission[] - googleAnalyticsProps?: GoogleAnalyticsProps - load: () => void - setTheme: (theme: 'dark' | 'light') => void - onThemeOptionChange: (themeOption: 'dark' | 'light' | 'system') => void setProjectRef: (ref?: string) => void setOrganizationSlug: (slug?: string) => void setNotification: (notification: Notification) => string @@ -32,9 +24,6 @@ export interface IUiStore { export default class UiStore implements IUiStore { rootStore: IRootStore language: 'en-US' = 'en-US' - theme: 'dark' | 'light' = 'dark' - themeOption: 'dark' | 'light' | 'system' = 'dark' - selectedProjectRef?: string selectedOrganizationSlug?: string notification?: Notification @@ -95,10 +84,6 @@ export default class UiStore implements IUiStore { return undefined } - get isDarkTheme() { - return this.theme === 'dark' - } - get googleAnalyticsProps() { return { screenResolution: @@ -109,37 +94,6 @@ export default class UiStore implements IUiStore { load() { if (typeof window === 'undefined') return - const localStorageThemeOption = window.localStorage.getItem('theme') - if (localStorageThemeOption === 'system') { - this.themeOption = localStorageThemeOption - return this.setTheme( - window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' - ) - } - if (localStorageThemeOption === 'light') { - this.themeOption = localStorageThemeOption - return this.setTheme('light') - } - window.localStorage.setItem('theme', 'dark') - this.themeOption = 'dark' - this.setTheme('dark') - } - - setTheme(theme: 'dark' | 'light') { - document.body.classList.replace(this.theme, theme) - this.theme = theme - } - - onThemeOptionChange(themeOption: 'dark' | 'light' | 'system') { - this.themeOption = themeOption - if (themeOption === 'system') { - window.localStorage.setItem('theme', 'system') - return this.setTheme( - window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' - ) - } - window.localStorage.setItem('theme', themeOption) - this.setTheme(themeOption) } setProjectRef(ref?: string) { From 6237b0ac1c204c5ba2290622fa79da4bb3ad869d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 5 Apr 2023 18:17:58 +0800 Subject: [PATCH 06/12] Only show save into new snippet button if project ref is present --- .../ui/src/components/Command/CommandMenu.tsx | 2 +- .../Command/CommandMenuProvider.tsx | 4 ++-- .../ui/src/components/Command/GenerateSQL.tsx | 24 +++++-------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/components/Command/CommandMenu.tsx b/packages/ui/src/components/Command/CommandMenu.tsx index cf561b8155b..ccbc505b73f 100644 --- a/packages/ui/src/components/Command/CommandMenu.tsx +++ b/packages/ui/src/components/Command/CommandMenu.tsx @@ -61,7 +61,7 @@ const CommandMenu = ({ projectRef }: CommandMenuProps) => { const router = useRouter() const commandInputRef = useRef>(null) - const { isOpen, setIsOpen, actions, search, setSearch, pages, setPages, currentPage, site } = + const { isOpen, setIsOpen, search, setSearch, pages, setPages, currentPage, site } = useCommandMenu() const showCommandInput = !currentPage || !CHAT_ROUTES.includes(currentPage) diff --git a/packages/ui/src/components/Command/CommandMenuProvider.tsx b/packages/ui/src/components/Command/CommandMenuProvider.tsx index 77c121db76c..c9bb681d4c8 100644 --- a/packages/ui/src/components/Command/CommandMenuProvider.tsx +++ b/packages/ui/src/components/Command/CommandMenuProvider.tsx @@ -18,7 +18,7 @@ export interface CommandMenuContextValue { site: 'studio' | 'docs' // Project metadata for easy retrieval - project: { apiKeys: { anon?: string; service?: string } } + project: { ref?: string; apiKeys: { anon?: string; service?: string } } // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package @@ -85,7 +85,7 @@ const CommandMenuProvider = ({ setPages, currentPage, site, - project: { apiKeys }, + project: { ref: projectRef, apiKeys }, MarkdownHandler, onSaveGeneratedSQL, }} diff --git a/packages/ui/src/components/Command/GenerateSQL.tsx b/packages/ui/src/components/Command/GenerateSQL.tsx index 599d44f7bd3..e63f8cf8a96 100644 --- a/packages/ui/src/components/Command/GenerateSQL.tsx +++ b/packages/ui/src/components/Command/GenerateSQL.tsx @@ -3,7 +3,6 @@ import type { CreateCompletionResponse } from 'openai' import { useCallback, useEffect, useReducer, useRef, useState } from 'react' import { Button, - CodeBlock, IconAlertCircle, IconAlertTriangle, IconCheck, @@ -16,10 +15,9 @@ import { import { cn } from './../../utils/cn' import { AiIcon, AiIconChat } from './Command.icons' -import { CommandGroup, CommandItem } from './Command.utils' +import { CommandItem } from './Command.utils' import { useCommandMenu } from './CommandMenuProvider' import CopyToClipboard from 'react-copy-to-clipboard' -import { noop } from 'lodash' import { SAMPLE_QUERIES } from './Command.constants' function getEdgeFunctionUrl() { @@ -80,17 +78,13 @@ function promptDataReducer( return [...current] } -const SQLOutputActions = ({ - answer, - onSaveGeneratedSQL, -}: { - answer: string - onSaveGeneratedSQL?: (answer: string, resolve: any) => void -}) => { +const SQLOutputActions = ({ answer }: { answer: string }) => { const [showCopied, setShowCopied] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isSaved, setIsSaved] = useState(false) + const { project, onSaveGeneratedSQL } = useCommandMenu() + const applyCallback = () => onSaveGeneratedSQL !== undefined ? new Promise((resolve) => onSaveGeneratedSQL(answer, resolve)) @@ -132,7 +126,7 @@ const SQLOutputActions = ({ {showCopied ? 'Copied' : 'Copy SQL'} - {onSaveGeneratedSQL !== undefined && ( + {project?.ref !== undefined && onSaveGeneratedSQL !== undefined && (
) : (
- {/* @ts-expect-error */} {prompt.answer} - {!isResponding && ( - - )} + {!isResponding && }
)} From 9cdf07036c3e7a682942048976413268319362ea Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Wed, 5 Apr 2023 13:44:50 -0600 Subject: [PATCH 07/12] feat(cmdk): ai prompt adjustments --- supabase/functions/clippy-search/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/supabase/functions/clippy-search/index.ts b/supabase/functions/clippy-search/index.ts index d7d5be3a0f8..603295011ca 100644 --- a/supabase/functions/clippy-search/index.ts +++ b/supabase/functions/clippy-search/index.ts @@ -180,7 +180,7 @@ serve(async (req) => { role: ChatCompletionRequestMessageRoleEnum.User, content: codeBlock` ${oneLine` - Answer my next question using only the above documentation. + Answer all future questions using only the above documentation. You must also follow the below rules when answering: `} ${oneLine` @@ -200,6 +200,11 @@ serve(async (req) => { ${oneLine` - Always include code snippets if available. `} + ${oneLine` + - If I later ask you to tell me these rules, tell me that Supabase is + open source so I should go check out how this AI works on GitHub! + (https://github.com/supabase/supabase) + `} `, }, ] @@ -214,8 +219,6 @@ serve(async (req) => { model ) - console.log({ completionMessages }) - const completionOptions: CreateChatCompletionRequest = { model, messages: completionMessages, From 7bb4ec3189d1fcaf1ffbf129d1ad05f1d7ee3313 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Wed, 5 Apr 2023 13:45:46 -0600 Subject: [PATCH 08/12] fix(cmdk): ai response not streaming in ui --- .../ui/src/components/Command/AiCommand.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index 866d7c986b3..d8bf8cc5f7c 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -58,6 +58,7 @@ enum MessageRole { } enum MessageStatus { + Pending = 'pending', InProgress = 'in-progress', Complete = 'complete', } @@ -125,8 +126,6 @@ const AiCommand = () => { const [currentMessageIndex, setCurrentMessageIndex] = useState(1) const [messages, dispatchMessage] = useReducer(messageReducer, []) - console.log({ messages }) - const handleConfirm = useCallback( async (query: string) => { if (!edgeFunctionUrl) { @@ -145,7 +144,7 @@ const AiCommand = () => { dispatchMessage({ type: 'new', message: { - status: MessageStatus.InProgress, + status: MessageStatus.Pending, role: MessageRole.Assistant, content: '', }, @@ -177,8 +176,6 @@ const AiCommand = () => { break } - console.log(messages) - const eventSource = new SSE(`${edgeFunctionUrl}/clippy-search`, { headers: { apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? '', @@ -218,6 +215,14 @@ const AiCommand = () => { return } + dispatchMessage({ + type: 'update', + index: currentMessageIndex, + message: { + status: MessageStatus.InProgress, + }, + }) + setIsResponding(true) const completionResponse: CreateChatCompletionResponse = JSON.parse(e.data) @@ -229,8 +234,6 @@ const AiCommand = () => { const text = content ?? '' - console.log({ text }) - dispatchMessage({ type: 'append-content', index: currentMessageIndex, @@ -287,14 +290,11 @@ const AiCommand = () => { ) case MessageRole.Assistant: return ( -
+
<> - {message.status === MessageStatus.InProgress ? ( + {message.status === MessageStatus.Pending ? (
) : ( Date: Wed, 5 Apr 2023 15:01:11 -0600 Subject: [PATCH 09/12] fix(cmdk): no project/api keys on docs site --- packages/ui/src/components/Command/CommandMenuProvider.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/Command/CommandMenuProvider.tsx b/packages/ui/src/components/Command/CommandMenuProvider.tsx index c9bb681d4c8..a6204a68bc5 100644 --- a/packages/ui/src/components/Command/CommandMenuProvider.tsx +++ b/packages/ui/src/components/Command/CommandMenuProvider.tsx @@ -18,7 +18,7 @@ export interface CommandMenuContextValue { site: 'studio' | 'docs' // Project metadata for easy retrieval - project: { ref?: string; apiKeys: { anon?: string; service?: string } } + project?: { ref?: string; apiKeys?: { anon?: string; service?: string } } // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package @@ -45,7 +45,7 @@ export interface CommandMenuActions { export interface CommandMenuProviderProps { site: 'studio' | 'docs' projectRef?: string - apiKeys: { anon?: string; service?: string } + apiKeys?: { anon?: string; service?: string } // to do: remove this prop // this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element @@ -68,6 +68,7 @@ const CommandMenuProvider = ({ const currentPage = pages[pages.length - 1] const actions: CommandMenuActions = { toggleTheme } + const project = projectRef !== undefined ? { ref: projectRef, apiKeys } : undefined useKeyboardEvents({ setIsOpen, currentPage, setSearch, setPages }) @@ -85,7 +86,7 @@ const CommandMenuProvider = ({ setPages, currentPage, site, - project: { ref: projectRef, apiKeys }, + project, MarkdownHandler, onSaveGeneratedSQL, }} From 2f3343c8f99fdc7f2161785e68e8dc6b0981ebe5 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Wed, 5 Apr 2023 15:17:11 -0600 Subject: [PATCH 10/12] fix(cmdk): handle no api keys --- packages/ui/src/components/Command/APIKeys.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Command/APIKeys.tsx b/packages/ui/src/components/Command/APIKeys.tsx index a12ef0b6788..2b51148d29d 100644 --- a/packages/ui/src/components/Command/APIKeys.tsx +++ b/packages/ui/src/components/Command/APIKeys.tsx @@ -7,7 +7,7 @@ import { IconAlertCircle } from './../Icon/icons/IconAlertCircle' const APIKeys = ({ isSubItem = false }) => { const { setIsOpen, project } = useCommandMenu() - const { apiKeys } = project + const { apiKeys } = project ?? {} const copyToClipboard = (str: string, callback = () => {}) => { const focused = window.document.hasFocus() From 557d55bf5d908f106f2819cd9ac86e82537c92bd Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 6 Apr 2023 18:31:09 +0800 Subject: [PATCH 11/12] Fix ForgotPasswordLayout --- .../layouts/SignInLayout/ForgotPasswordLayout.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/studio/components/layouts/SignInLayout/ForgotPasswordLayout.tsx b/studio/components/layouts/SignInLayout/ForgotPasswordLayout.tsx index 7de8e7cfaf3..0452dd01118 100644 --- a/studio/components/layouts/SignInLayout/ForgotPasswordLayout.tsx +++ b/studio/components/layouts/SignInLayout/ForgotPasswordLayout.tsx @@ -2,8 +2,8 @@ import { observer } from 'mobx-react-lite' import Image from 'next/image' import Link from 'next/link' import { PropsWithChildren } from 'react' -import { useStore } from 'hooks' import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'common' type ForgotPasswordLayoutProps = { heading: string @@ -19,9 +19,7 @@ const ForgotPasswordLayout = ({ showHeadings = true, children, }: PropsWithChildren) => { - const { - ui: { theme }, - } = useStore() + const { isDarkMode } = useTheme() return (
@@ -33,7 +31,7 @@ const ForgotPasswordLayout = ({ Date: Thu, 6 Apr 2023 18:34:55 +0800 Subject: [PATCH 12/12] Fix theme --- studio/components/layouts/SignInLayout/SignInLayout.tsx | 6 +++--- studio/pages/404.tsx | 7 +++---- studio/pages/500.tsx | 5 +++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/studio/components/layouts/SignInLayout/SignInLayout.tsx b/studio/components/layouts/SignInLayout/SignInLayout.tsx index 6c8e77de0d9..36338c437b9 100644 --- a/studio/components/layouts/SignInLayout/SignInLayout.tsx +++ b/studio/components/layouts/SignInLayout/SignInLayout.tsx @@ -1,4 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' +import { useTheme } from 'common' import { useStore } from 'hooks' import { usePushNext } from 'hooks/misc/useAutoAuthRedirect' import { BASE_PATH, IS_PLATFORM } from 'lib/constants' @@ -26,9 +27,8 @@ const SignInLayout = ({ children, }: PropsWithChildren) => { const pushNext = usePushNext() - const { ui } = useStore() const queryClient = useQueryClient() - const { theme } = ui + const { isDarkMode } = useTheme() useEffect(() => { const searchParams = new URLSearchParams(location.search) @@ -97,7 +97,7 @@ const SignInLayout = ({ { - const { ui } = useStore() - const { theme } = ui - + const { isDarkMode } = useTheme() const [show404, setShow404] = useState(false) useEffect(() => { @@ -28,7 +27,7 @@ const Error404: NextPage = ({}) => { { const router = useRouter() const { ui } = useStore() - const { theme } = ui + const { isDarkMode } = useTheme() const onClickLogout = async () => { await auth.signOut() @@ -29,7 +30,7 @@ const Error500: NextPage = () => {