diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index af5f2f44..4cf8c8ca 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -169,6 +169,10 @@ export const ChatImpl = memo( supabase: { isConnected: supabaseConn.isConnected, hasSelectedProject: !!selectedProject, + credentials: { + supabaseUrl: supabaseConn?.credentials?.supabaseUrl, + anonKey: supabaseConn?.credentials?.anonKey, + }, }, }, sendExtraMessageFields: true, diff --git a/app/components/chat/SupabaseConnection.tsx b/app/components/chat/SupabaseConnection.tsx index 2b03f442..6b2e34a0 100644 --- a/app/components/chat/SupabaseConnection.tsx +++ b/app/components/chat/SupabaseConnection.tsx @@ -21,11 +21,11 @@ export function SupabaseConnection() { handleCreateProject, updateToken, isConnected, + fetchProjectApiKeys, } = useSupabaseConnection(); const currentChatId = useStore(chatId); - // Add event listener for opening the connection dialog useEffect(() => { const handleOpenConnectionDialog = () => { setIsDialogOpen(true); @@ -38,7 +38,6 @@ export function SupabaseConnection() { }; }, [setIsDialogOpen]); - // Load the selected project from localStorage when connected or chat changes useEffect(() => { if (isConnected && currentChatId) { const savedProjectId = localStorage.getItem(`supabase-project-${currentChatId}`); @@ -70,6 +69,12 @@ export function SupabaseConnection() { } }, [isConnected, supabaseConn.token]); + useEffect(() => { + if (isConnected && supabaseConn.selectedProjectId && supabaseConn.token) { + fetchProjectApiKeys(supabaseConn.selectedProjectId).catch(console.error); + } + }, [isConnected, supabaseConn.selectedProjectId, supabaseConn.token]); + return (
diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 1958dfad..7f5b4857 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -16,6 +16,10 @@ export interface StreamingOptions extends Omit[0] supabaseConnection?: { isConnected: boolean; hasSelectedProject: boolean; + credentials?: { + anonKey?: string; + supabaseUrl?: string; + }; }; } @@ -105,6 +109,7 @@ export async function streamText(props: { supabase: { isConnected: options?.supabaseConnection?.isConnected || false, hasSelectedProject: options?.supabaseConnection?.hasSelectedProject || false, + credentials: options?.supabaseConnection?.credentials || undefined, }, }) ?? getSystemPrompt(); diff --git a/app/lib/common/prompt-library.ts b/app/lib/common/prompt-library.ts index 084925b7..f4747d71 100644 --- a/app/lib/common/prompt-library.ts +++ b/app/lib/common/prompt-library.ts @@ -8,6 +8,10 @@ export interface PromptOptions { supabase?: { isConnected: boolean; hasSelectedProject: boolean; + credentials?: { + anonKey?: string; + supabaseUrl?: string; + }; }; } diff --git a/app/lib/common/prompts/prompts.ts b/app/lib/common/prompts/prompts.ts index 149f1b5f..eb7ec1a1 100644 --- a/app/lib/common/prompts/prompts.ts +++ b/app/lib/common/prompts/prompts.ts @@ -4,7 +4,11 @@ import { stripIndents } from '~/utils/stripIndent'; export const getSystemPrompt = ( cwd: string = WORK_DIR, - supabase?: { isConnected: boolean; hasSelectedProject: boolean }, + supabase?: { + isConnected: boolean; + hasSelectedProject: boolean; + credentials?: { anonKey?: string; supabaseUrl?: string }; + }, ) => ` You are Bolt, an expert AI assistant and exceptional senior software developer with vast knowledge across multiple programming languages, frameworks, and best practices. @@ -76,8 +80,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w : '' : '' } - The environment variables for Supabase connection will be available in the project's \`.env\` file. - IMPORTANT: Create a .env file if it doesnt exist. + IMPORTANT: Create a .env file if it doesnt exist and include the following variables: + ${ + supabase?.isConnected && + supabase?.hasSelectedProject && + supabase?.credentials?.supabaseUrl && + supabase?.credentials?.anonKey + ? `VITE_SUPABASE_URL=${supabase.credentials.supabaseUrl} + VITE_SUPABASE_ANON_KEY=${supabase.credentials.anonKey}` + : 'SUPABASE_URL=your_supabase_url\nSUPABASE_ANON_KEY=your_supabase_anon_key' + } NEVER modify any Supabase configuration or \`.env\` files. CRITICAL DATA PRESERVATION AND SAFETY REQUIREMENTS: diff --git a/app/lib/hooks/useSupabaseConnection.ts b/app/lib/hooks/useSupabaseConnection.ts index 5bbe12ea..8a2f8118 100644 --- a/app/lib/hooks/useSupabaseConnection.ts +++ b/app/lib/hooks/useSupabaseConnection.ts @@ -2,21 +2,39 @@ import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { useStore } from '@nanostores/react'; import { logStore } from '~/lib/stores/logs'; -import { supabaseConnection, isConnecting, isFetchingStats, updateSupabaseConnection } from '~/lib/stores/supabase'; +import { + supabaseConnection, + isConnecting, + isFetchingStats, + isFetchingApiKeys, + updateSupabaseConnection, + fetchProjectApiKeys, +} from '~/lib/stores/supabase'; export function useSupabaseConnection() { const connection = useStore(supabaseConnection); const connecting = useStore(isConnecting); const fetchingStats = useStore(isFetchingStats); + const fetchingApiKeys = useStore(isFetchingApiKeys); const [isProjectsExpanded, setIsProjectsExpanded] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); useEffect(() => { const savedConnection = localStorage.getItem('supabase_connection'); + const savedCredentials = localStorage.getItem('supabaseCredentials'); if (savedConnection) { const parsed = JSON.parse(savedConnection); + + if (savedCredentials && !parsed.credentials) { + parsed.credentials = JSON.parse(savedCredentials); + } + updateSupabaseConnection(parsed); + + if (parsed.token && parsed.selectedProjectId && !parsed.credentials) { + fetchProjectApiKeys(parsed.selectedProjectId, parsed.token).catch(console.error); + } } }, []); @@ -50,7 +68,6 @@ export function useSupabaseConnection() { toast.success('Successfully connected to Supabase'); - // Keep the dialog open and expand the projects section setIsProjectsExpanded(true); return true; @@ -72,7 +89,7 @@ export function useSupabaseConnection() { setIsDropdownOpen(false); }; - const selectProject = (projectId: string) => { + const selectProject = async (projectId: string) => { const currentState = supabaseConnection.get(); let projectData = undefined; @@ -85,7 +102,18 @@ export function useSupabaseConnection() { project: projectData, }); - toast.success('Project selected successfully'); + if (projectId && currentState.token) { + try { + await fetchProjectApiKeys(projectId, currentState.token); + toast.success('Project selected successfully'); + } catch (error) { + console.error('Failed to fetch API keys:', error); + toast.error('Selected project but failed to fetch API keys'); + } + } else { + toast.success('Project selected successfully'); + } + setIsDropdownOpen(false); }; @@ -97,6 +125,7 @@ export function useSupabaseConnection() { connection, connecting, fetchingStats, + fetchingApiKeys, isProjectsExpanded, setIsProjectsExpanded, isDropdownOpen, @@ -107,5 +136,12 @@ export function useSupabaseConnection() { handleCreateProject, updateToken: (token: string) => updateSupabaseConnection({ ...connection, token }), isConnected: !!(connection.user && connection.token), + fetchProjectApiKeys: (projectId: string) => { + if (connection.token) { + return fetchProjectApiKeys(projectId, connection.token); + } + + return Promise.reject(new Error('No token available')); + }, }; } diff --git a/app/lib/stores/supabase.ts b/app/lib/stores/supabase.ts index 9f624e5b..3b8a5813 100644 --- a/app/lib/stores/supabase.ts +++ b/app/lib/stores/supabase.ts @@ -1,5 +1,5 @@ import { atom } from 'nanostores'; -import type { SupabaseUser, SupabaseStats } from '~/types/supabase'; +import type { SupabaseUser, SupabaseStats, SupabaseApiKey, SupabaseCredentials } from '~/types/supabase'; export interface SupabaseProject { id: string; @@ -22,10 +22,10 @@ export interface SupabaseConnectionState { stats?: SupabaseStats; selectedProjectId?: string; isConnected?: boolean; - project?: SupabaseProject; // Add the selected project data + project?: SupabaseProject; + credentials?: SupabaseCredentials; } -// Init from localStorage if available const savedConnection = typeof localStorage !== 'undefined' ? localStorage.getItem('supabase_connection') : null; const initialState: SupabaseConnectionState = savedConnection @@ -36,30 +36,28 @@ const initialState: SupabaseConnectionState = savedConnection stats: undefined, selectedProjectId: undefined, isConnected: false, - project: undefined, // Initialize as undefined + project: undefined, }; export const supabaseConnection = atom(initialState); -// After init, fetch stats if we have a token if (initialState.token && !initialState.stats) { fetchSupabaseStats(initialState.token).catch(console.error); } export const isConnecting = atom(false); export const isFetchingStats = atom(false); +export const isFetchingApiKeys = atom(false); export function updateSupabaseConnection(connection: Partial) { const currentState = supabaseConnection.get(); - // Set isConnected based on user presence AND token if (connection.user !== undefined || connection.token !== undefined) { const newUser = connection.user !== undefined ? connection.user : currentState.user; const newToken = connection.token !== undefined ? connection.token : currentState.token; connection.isConnected = !!(newUser && newToken); } - // Update the project data when selectedProjectId changes if (connection.selectedProjectId !== undefined) { if (connection.selectedProjectId && currentState.stats?.projects) { const selectedProject = currentState.stats.projects.find( @@ -69,7 +67,6 @@ export function updateSupabaseConnection(connection: Partial key.name === 'anon' || key.name === 'public'); + + if (anonKey) { + const supabaseUrl = `https://${projectId}.supabase.co`; + + updateSupabaseConnection({ + credentials: { + anonKey: anonKey.api_key, + supabaseUrl, + }, + }); + + return { anonKey: anonKey.api_key, supabaseUrl }; + } + + return null; + } catch (error) { + console.error('Failed to fetch project API keys:', error); + throw error; + } finally { + isFetchingApiKeys.set(false); + } +} diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index 6861d15d..5917dfc4 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -45,6 +45,10 @@ async function chatAction({ context, request }: ActionFunctionArgs) { supabase?: { isConnected: boolean; hasSelectedProject: boolean; + credentials?: { + anonKey?: string; + supabaseUrl?: string; + }; }; }>(); @@ -183,7 +187,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) { // logger.debug('Code Files Selected'); } - // Stream the text const options: StreamingOptions = { supabaseConnection: supabase, toolChoice: 'none', diff --git a/app/routes/api.supabase.ts b/app/routes/api.supabase.ts index 56d9f810..955ae44e 100644 --- a/app/routes/api.supabase.ts +++ b/app/routes/api.supabase.ts @@ -7,7 +7,6 @@ export const action: ActionFunction = async ({ request }) => { return json({ error: 'Method not allowed' }, { status: 405 }); } - // Inside the action function try { const { token } = (await request.json()) as any; @@ -27,17 +26,14 @@ export const action: ActionFunction = async ({ request }) => { const projects = (await projectsResponse.json()) as SupabaseProject[]; - // Create a Map to store unique projects by ID const uniqueProjectsMap = new Map(); - // Only keep the latest version of each project for (const project of projects) { if (!uniqueProjectsMap.has(project.id)) { uniqueProjectsMap.set(project.id, project); } } - // Debug log to see unique projects console.log( 'Unique projects:', Array.from(uniqueProjectsMap.values()).map((p) => ({ id: p.id, name: p.name })), @@ -45,7 +41,6 @@ export const action: ActionFunction = async ({ request }) => { const uniqueProjects = Array.from(uniqueProjectsMap.values()); - // Sort projects by creation date (newest first) uniqueProjects.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); return json({ diff --git a/app/routes/api.supabase.variables.ts b/app/routes/api.supabase.variables.ts new file mode 100644 index 00000000..fd2d028f --- /dev/null +++ b/app/routes/api.supabase.variables.ts @@ -0,0 +1,33 @@ +import { json } from '@remix-run/node'; +import type { ActionFunctionArgs } from '@remix-run/node'; + +export async function action({ request }: ActionFunctionArgs) { + try { + // Add proper type assertion for the request body + const body = (await request.json()) as { projectId?: string; token?: string }; + const { projectId, token } = body; + + if (!projectId || !token) { + return json({ error: 'Project ID and token are required' }, { status: 400 }); + } + + const response = await fetch(`https://api.supabase.com/v1/projects/${projectId}/api-keys`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + return json({ error: `Failed to fetch API keys: ${response.statusText}` }, { status: response.status }); + } + + const apiKeys = await response.json(); + + return json({ apiKeys }); + } catch (error) { + console.error('Error fetching project API keys:', error); + return json({ error: error instanceof Error ? error.message : 'Unknown error occurred' }, { status: 500 }); + } +} diff --git a/app/types/supabase.ts b/app/types/supabase.ts index d16d50c2..f99bbaf2 100644 --- a/app/types/supabase.ts +++ b/app/types/supabase.ts @@ -19,3 +19,13 @@ export interface SupabaseStats { projects: SupabaseProject[]; totalProjects: number; } + +export interface SupabaseApiKey { + name: string; + api_key: string; +} + +export interface SupabaseCredentials { + anonKey?: string; + supabaseUrl?: string; +}