import { createClient } from '@/lib/supabase/client'; import { handleApiError } from './error-handler'; // Get backend URL from environment variables const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || ''; // Set to keep track of agent runs that are known to be non-running const nonRunningAgentRuns = new Set(); // Map to keep track of active EventSource streams const activeStreams = new Map(); // Custom error for billing issues export class BillingError extends Error { status: number; detail: { message: string; [key: string]: any }; // Allow other properties in detail constructor( status: number, detail: { message: string; [key: string]: any }, message?: string, ) { super(message || detail.message || `Billing Error: ${status}`); this.name = 'BillingError'; this.status = status; this.detail = detail; // Set the prototype explicitly. Object.setPrototypeOf(this, BillingError.prototype); } } export class NoAccessTokenAvailableError extends Error { constructor(message?: string, options?: { cause?: Error }) { super(message || 'No access token available', options); } name = 'NoAccessTokenAvailableError'; } // Type Definitions (moved from potential separate file for clarity) export type Project = { id: string; name: string; description: string; account_id: string; created_at: string; updated_at?: string; sandbox: { vnc_preview?: string; sandbox_url?: string; id?: string; pass?: string; }; is_public?: boolean; // Flag to indicate if the project is public [key: string]: any; // Allow additional properties to handle database fields }; export type Thread = { thread_id: string; account_id: string | null; project_id?: string | null; is_public?: boolean; created_at: string; updated_at: string; [key: string]: any; // Allow additional properties to handle database fields }; export type Message = { role: string; content: string; type: string; agent_id?: string; agents?: { name: string; avatar?: string; avatar_color?: string; }; }; export type AgentRun = { id: string; thread_id: string; status: 'running' | 'completed' | 'stopped' | 'error'; started_at: string; completed_at: string | null; responses: Message[]; error: string | null; }; export type ToolCall = { name: string; arguments: Record; }; export interface InitiateAgentResponse { thread_id: string; agent_run_id: string; } export interface HealthCheckResponse { status: string; timestamp: string; instance_id: string; } export interface FileInfo { name: string; path: string; is_dir: boolean; size: number; mod_time: string; permissions?: string; } export type WorkflowExecution = { id: string; workflow_id: string; workflow_name: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; started_at: string | null; completed_at: string | null; result: any; error: string | null; }; export type WorkflowExecutionLog = { id: string; execution_id: string; node_id: string; node_name: string; node_type: string; started_at: string; completed_at: string | null; status: 'running' | 'completed' | 'failed'; input_data: any; output_data: any; error: string | null; }; // Workflow Types export type Workflow = { id: string; name: string; description: string; status: 'draft' | 'active' | 'paused' | 'disabled' | 'archived'; project_id: string; account_id: string; definition: { name: string; description: string; nodes: any[]; edges: any[]; variables?: Record; }; created_at: string; updated_at: string; }; export type WorkflowNode = { id: string; type: string; position: { x: number; y: number }; data: any; }; export type WorkflowEdge = { id: string; source: string; target: string; sourceHandle?: string; targetHandle?: string; }; // Project APIs export const getProjects = async (): Promise => { try { const supabase = createClient(); // Get the current user's ID to filter projects const { data: userData, error: userError } = await supabase.auth.getUser(); if (userError) { console.error('Error getting current user:', userError); return []; } // If no user is logged in, return an empty array if (!userData.user) { console.log('[API] No user logged in, returning empty projects array'); return []; } // Query only projects where account_id matches the current user's ID const { data, error } = await supabase .from('projects') .select('*') .eq('account_id', userData.user.id); if (error) { // Handle permission errors specifically if ( error.code === '42501' && error.message.includes('has_role_on_account') ) { console.error( 'Permission error: User does not have proper account access', ); return []; // Return empty array instead of throwing } throw error; } console.log('[API] Raw projects from DB:', data?.length, data); // Map database fields to our Project type const mappedProjects: Project[] = (data || []).map((project) => ({ id: project.project_id, name: project.name || '', description: project.description || '', account_id: project.account_id, created_at: project.created_at, updated_at: project.updated_at, sandbox: project.sandbox || { id: '', pass: '', vnc_preview: '', sandbox_url: '', }, })); console.log('[API] Mapped projects for frontend:', mappedProjects.length); return mappedProjects; } catch (err) { console.error('Error fetching projects:', err); handleApiError(err, { operation: 'load projects', resource: 'projects' }); // Return empty array for permission errors to avoid crashing the UI return []; } }; export const getProject = async (projectId: string): Promise => { const supabase = createClient(); try { const { data, error } = await supabase .from('projects') .select('*') .eq('project_id', projectId) .single(); if (error) { // Handle the specific "no rows returned" error from Supabase if (error.code === 'PGRST116') { throw new Error(`Project not found or not accessible: ${projectId}`); } throw error; } console.log('Raw project data from database:', data); // If project has a sandbox, ensure it's started if (data.sandbox?.id) { // Fire off sandbox activation without blocking const ensureSandboxActive = async () => { try { const { data: { session }, } = await supabase.auth.getSession(); // For public projects, we don't need authentication const headers: Record = { 'Content-Type': 'application/json', }; if (session?.access_token) { headers['Authorization'] = `Bearer ${session.access_token}`; } console.log(`Ensuring sandbox is active for project ${projectId}...`); const response = await fetch( `${API_URL}/project/${projectId}/sandbox/ensure-active`, { method: 'POST', headers, }, ); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.warn( `Failed to ensure sandbox is active: ${response.status} ${response.statusText}`, errorText, ); } else { console.log('Sandbox activation successful'); } } catch (sandboxError) { console.warn('Failed to ensure sandbox is active:', sandboxError); } }; // Start the sandbox activation without awaiting ensureSandboxActive(); } // Map database fields to our Project type const mappedProject: Project = { id: data.project_id, name: data.name || '', description: data.description || '', account_id: data.account_id, is_public: data.is_public || false, created_at: data.created_at, sandbox: data.sandbox || { id: '', pass: '', vnc_preview: '', sandbox_url: '', }, }; // console.log('Mapped project data for frontend:', mappedProject); return mappedProject; } catch (error) { console.error(`Error fetching project ${projectId}:`, error); handleApiError(error, { operation: 'load project', resource: `project ${projectId}` }); throw error; } }; export const createProject = async ( projectData: { name: string; description: string }, accountId?: string, ): Promise => { const supabase = createClient(); // If accountId is not provided, we'll need to get the user's ID if (!accountId) { const { data: userData, error: userError } = await supabase.auth.getUser(); if (userError) throw userError; if (!userData.user) throw new Error('You must be logged in to create a project'); // In Basejump, the personal account ID is the same as the user ID accountId = userData.user.id; } const { data, error } = await supabase .from('projects') .insert({ name: projectData.name, description: projectData.description || null, account_id: accountId, }) .select() .single(); if (error) { handleApiError(error, { operation: 'create project', resource: 'project' }); throw error; } const project = { id: data.project_id, name: data.name, description: data.description || '', account_id: data.account_id, created_at: data.created_at, sandbox: { id: '', pass: '', vnc_preview: '' }, }; return project; }; export const updateProject = async ( projectId: string, data: Partial, ): Promise => { const supabase = createClient(); console.log('Updating project with ID:', projectId); console.log('Update data:', data); // Sanity check to avoid update errors if (!projectId || projectId === '') { console.error('Attempted to update project with invalid ID:', projectId); throw new Error('Cannot update project: Invalid project ID'); } const { data: updatedData, error } = await supabase .from('projects') .update(data) .eq('project_id', projectId) .select() .single(); if (error) { console.error('Error updating project:', error); handleApiError(error, { operation: 'update project', resource: `project ${projectId}` }); throw error; } if (!updatedData) { const noDataError = new Error('No data returned from update'); handleApiError(noDataError, { operation: 'update project', resource: `project ${projectId}` }); throw noDataError; } // Dispatch a custom event to notify components about the project change if (typeof window !== 'undefined') { window.dispatchEvent( new CustomEvent('project-updated', { detail: { projectId, updatedData: { id: updatedData.project_id, name: updatedData.name, description: updatedData.description, }, }, }), ); } // Return formatted project data - use same mapping as getProject const project = { id: updatedData.project_id, name: updatedData.name, description: updatedData.description || '', account_id: updatedData.account_id, created_at: updatedData.created_at, sandbox: updatedData.sandbox || { id: '', pass: '', vnc_preview: '', sandbox_url: '', }, }; return project; }; export const deleteProject = async (projectId: string): Promise => { const supabase = createClient(); const { error } = await supabase .from('projects') .delete() .eq('project_id', projectId); if (error) { handleApiError(error, { operation: 'delete project', resource: `project ${projectId}` }); throw error; } }; // Thread APIs export const getThreads = async (projectId?: string): Promise => { const supabase = createClient(); // Get the current user's ID to filter threads const { data: userData, error: userError } = await supabase.auth.getUser(); if (userError) { console.error('Error getting current user:', userError); return []; } // If no user is logged in, return an empty array if (!userData.user) { console.log('[API] No user logged in, returning empty threads array'); return []; } let query = supabase.from('threads').select('*'); // Always filter by the current user's account ID query = query.eq('account_id', userData.user.id); if (projectId) { console.log('[API] Filtering threads by project_id:', projectId); query = query.eq('project_id', projectId); } const { data, error } = await query; if (error) { handleApiError(error, { operation: 'load threads', resource: projectId ? `threads for project ${projectId}` : 'threads' }); throw error; } const mappedThreads: Thread[] = (data || []) .filter((thread) => { const metadata = thread.metadata || {}; return !metadata.is_agent_builder; }) .map((thread) => ({ thread_id: thread.thread_id, account_id: thread.account_id, project_id: thread.project_id, created_at: thread.created_at, updated_at: thread.updated_at, metadata: thread.metadata, })); return mappedThreads; }; export const getThread = async (threadId: string): Promise => { const supabase = createClient(); const { data, error } = await supabase .from('threads') .select('*') .eq('thread_id', threadId) .single(); if (error) { handleApiError(error, { operation: 'load thread', resource: `thread ${threadId}` }); throw error; } return data; }; export const createThread = async (projectId: string): Promise => { const supabase = createClient(); // If user is not logged in, redirect to login const { data: { user }, } = await supabase.auth.getUser(); if (!user) { throw new Error('You must be logged in to create a thread'); } const { data, error } = await supabase .from('threads') .insert({ project_id: projectId, account_id: user.id, // Use the current user's ID as the account ID }) .select() .single(); if (error) { handleApiError(error, { operation: 'create thread', resource: 'thread' }); throw error; } return data; }; export const addUserMessage = async ( threadId: string, content: string, ): Promise => { const supabase = createClient(); // Format the message in the format the LLM expects - keep it simple with only required fields const message = { role: 'user', content: content, }; // Insert the message into the messages table const { error } = await supabase.from('messages').insert({ thread_id: threadId, type: 'user', is_llm_message: true, content: JSON.stringify(message), }); if (error) { console.error('Error adding user message:', error); handleApiError(error, { operation: 'add message', resource: 'message' }); throw new Error(`Error adding message: ${error.message}`); } }; export const getMessages = async (threadId: string): Promise => { const supabase = createClient(); let allMessages: Message[] = []; let from = 0; const batchSize = 1000; let hasMore = true; while (hasMore) { const { data, error } = await supabase .from('messages') .select(` *, agents:agent_id ( name, avatar, avatar_color ) `) .eq('thread_id', threadId) .neq('type', 'cost') .neq('type', 'summary') .order('created_at', { ascending: true }) .range(from, from + batchSize - 1); if (error) { console.error('Error fetching messages:', error); handleApiError(error, { operation: 'load messages', resource: `messages for thread ${threadId}` }); throw new Error(`Error getting messages: ${error.message}`); } if (data && data.length > 0) { allMessages = allMessages.concat(data); from += batchSize; hasMore = data.length === batchSize; } else { hasMore = false; } } console.log('[API] Messages fetched count:', allMessages.length); return allMessages; }; // Agent APIs export const startAgent = async ( threadId: string, options?: { model_name?: string; enable_thinking?: boolean; reasoning_effort?: string; stream?: boolean; agent_id?: string; }, ): Promise<{ agent_run_id: string }> => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } // Check if backend URL is configured if (!API_URL) { throw new Error( 'Backend URL is not configured. Set NEXT_PUBLIC_BACKEND_URL in your environment.', ); } console.log( `[API] Starting agent for thread ${threadId} using ${API_URL}/thread/${threadId}/agent/start`, ); const defaultOptions = { model_name: 'claude-3-7-sonnet-latest', enable_thinking: false, reasoning_effort: 'low', stream: true, agent_id: undefined, }; const finalOptions = { ...defaultOptions, ...options }; const body: any = { model_name: finalOptions.model_name, enable_thinking: finalOptions.enable_thinking, reasoning_effort: finalOptions.reasoning_effort, stream: finalOptions.stream, }; // Only include agent_id if it's provided if (finalOptions.agent_id) { body.agent_id = finalOptions.agent_id; } const response = await fetch(`${API_URL}/thread/${threadId}/agent/start`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}`, }, body: JSON.stringify(body), }); if (!response.ok) { // Check for 402 Payment Required first if (response.status === 402) { try { const errorData = await response.json(); console.error(`[API] Billing error starting agent (402):`, errorData); // Ensure detail exists and has a message property const detail = errorData?.detail || { message: 'Payment Required' }; if (typeof detail.message !== 'string') { detail.message = 'Payment Required'; // Default message if missing } throw new BillingError(response.status, detail); } catch (parseError) { // Handle cases where parsing fails or the structure isn't as expected console.error( '[API] Could not parse 402 error response body:', parseError, ); throw new BillingError( response.status, { message: 'Payment Required' }, `Error starting agent: ${response.statusText} (402)`, ); } } // Handle other errors const errorText = await response .text() .catch(() => 'No error details available'); console.error( `[API] Error starting agent: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error starting agent: ${response.statusText} (${response.status})`, ); } const result = await response.json(); return result; } catch (error) { // Rethrow BillingError instances directly if (error instanceof BillingError) { throw error; } if (error instanceof NoAccessTokenAvailableError) { throw error; } console.error('[API] Failed to start agent:', error); // Handle different error types with appropriate user messages if ( error instanceof TypeError && error.message.includes('Failed to fetch') ) { const networkError = new Error( `Cannot connect to backend server. Please check your internet connection and make sure the backend is running.`, ); handleApiError(networkError, { operation: 'start agent', resource: 'AI assistant' }); throw networkError; } // For other errors, add context and rethrow handleApiError(error, { operation: 'start agent', resource: 'AI assistant' }); throw error; } }; export const stopAgent = async (agentRunId: string): Promise => { // Add to non-running set immediately to prevent reconnection attempts nonRunningAgentRuns.add(agentRunId); // Close any existing stream const existingStream = activeStreams.get(agentRunId); if (existingStream) { console.log( `[API] Closing existing stream for ${agentRunId} before stopping agent`, ); existingStream.close(); activeStreams.delete(agentRunId); } const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { const authError = new NoAccessTokenAvailableError(); handleApiError(authError, { operation: 'stop agent', resource: 'AI assistant' }); throw authError; } const response = await fetch(`${API_URL}/agent-run/${agentRunId}/stop`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}`, }, // Add cache: 'no-store' to prevent caching cache: 'no-store', }); if (!response.ok) { const stopError = new Error(`Error stopping agent: ${response.statusText}`); handleApiError(stopError, { operation: 'stop agent', resource: 'AI assistant' }); throw stopError; } }; export const getAgentStatus = async (agentRunId: string): Promise => { console.log(`[API] Requesting agent status for ${agentRunId}`); // If we already know this agent is not running, throw an error if (nonRunningAgentRuns.has(agentRunId)) { console.log( `[API] Agent run ${agentRunId} is known to be non-running, returning error`, ); throw new Error(`Agent run ${agentRunId} is not running`); } try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { console.error('[API] No access token available for getAgentStatus'); throw new NoAccessTokenAvailableError(); } const url = `${API_URL}/agent-run/${agentRunId}`; console.log(`[API] Fetching from: ${url}`); const response = await fetch(url, { headers: { Authorization: `Bearer ${session.access_token}`, }, // Add cache: 'no-store' to prevent caching cache: 'no-store', }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `[API] Error getting agent status: ${response.status} ${response.statusText}`, errorText, ); // If we get a 404, add to non-running set if (response.status === 404) { nonRunningAgentRuns.add(agentRunId); } throw new Error( `Error getting agent status: ${response.statusText} (${response.status})`, ); } const data = await response.json(); console.log(`[API] Successfully got agent status:`, data); // If agent is not running, add to non-running set if (data.status !== 'running') { nonRunningAgentRuns.add(agentRunId); } return data; } catch (error) { console.error('[API] Failed to get agent status:', error); handleApiError(error, { operation: 'get agent status', resource: 'AI assistant status', silent: true }); throw error; } }; export const getAgentRuns = async (threadId: string): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const response = await fetch(`${API_URL}/thread/${threadId}/agent-runs`, { headers: { Authorization: `Bearer ${session.access_token}`, }, // Add cache: 'no-store' to prevent caching cache: 'no-store', }); if (!response.ok) { throw new Error(`Error getting agent runs: ${response.statusText}`); } const data = await response.json(); return data.agent_runs || []; } catch (error) { if (error instanceof NoAccessTokenAvailableError) { throw error; } console.error('Failed to get agent runs:', error); handleApiError(error, { operation: 'load agent runs', resource: 'conversation history' }); throw error; } }; export const streamAgent = ( agentRunId: string, callbacks: { onMessage: (content: string) => void; onError: (error: Error | string) => void; onClose: () => void; }, ): (() => void) => { console.log(`[STREAM] streamAgent called for ${agentRunId}`); // Check if this agent run is known to be non-running if (nonRunningAgentRuns.has(agentRunId)) { console.log( `[STREAM] Agent run ${agentRunId} is known to be non-running, not creating stream`, ); // Notify the caller immediately setTimeout(() => { callbacks.onError(`Agent run ${agentRunId} is not running`); callbacks.onClose(); }, 0); // Return a no-op cleanup function return () => {}; } // Check if there's already an active stream for this agent run const existingStream = activeStreams.get(agentRunId); if (existingStream) { console.log( `[STREAM] Stream already exists for ${agentRunId}, closing it first`, ); existingStream.close(); activeStreams.delete(agentRunId); } // Set up a new stream try { const setupStream = async () => { // First verify the agent is actually running try { const status = await getAgentStatus(agentRunId); if (status.status !== 'running') { console.log( `[STREAM] Agent run ${agentRunId} is not running (status: ${status.status}), not creating stream`, ); nonRunningAgentRuns.add(agentRunId); callbacks.onError( `Agent run ${agentRunId} is not running (status: ${status.status})`, ); callbacks.onClose(); return; } } catch (err) { console.error(`[STREAM] Error verifying agent run ${agentRunId}:`, err); // Check if this is a "not found" error const errorMessage = err instanceof Error ? err.message : String(err); const isNotFoundError = errorMessage.includes('not found') || errorMessage.includes('404') || errorMessage.includes('does not exist'); if (isNotFoundError) { console.log( `[STREAM] Agent run ${agentRunId} not found, not creating stream`, ); nonRunningAgentRuns.add(agentRunId); } callbacks.onError(errorMessage); callbacks.onClose(); return; } const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { const authError = new NoAccessTokenAvailableError(); console.error('[STREAM] No auth token available'); callbacks.onError(authError); callbacks.onClose(); return; } const url = new URL(`${API_URL}/agent-run/${agentRunId}/stream`); url.searchParams.append('token', session.access_token); console.log(`[STREAM] Creating EventSource for ${agentRunId}`); const eventSource = new EventSource(url.toString()); // Store the EventSource in the active streams map activeStreams.set(agentRunId, eventSource); eventSource.onopen = () => { console.log(`[STREAM] Connection opened for ${agentRunId}`); }; eventSource.onmessage = (event) => { try { const rawData = event.data; if (rawData.includes('"type":"ping"')) return; // Log raw data for debugging (truncated for readability) console.log( `[STREAM] Received data for ${agentRunId}: ${rawData.substring(0, 100)}${rawData.length > 100 ? '...' : ''}`, ); // Skip empty messages if (!rawData || rawData.trim() === '') { console.debug('[STREAM] Received empty message, skipping'); return; } // Check for error status messages try { const jsonData = JSON.parse(rawData); if (jsonData.status === 'error') { console.error(`[STREAM] Error status received for ${agentRunId}:`, jsonData); // Pass the error message to the callback callbacks.onError(jsonData.message || 'Unknown error occurred'); // Don't close the stream for error status messages as they may continue return; } } catch (jsonError) { // Not JSON or invalid JSON, continue with normal processing } // Check for "Agent run not found" error if ( rawData.includes('Agent run') && rawData.includes('not found in active runs') ) { console.log( `[STREAM] Agent run ${agentRunId} not found in active runs, closing stream`, ); // Add to non-running set to prevent future reconnection attempts nonRunningAgentRuns.add(agentRunId); // Notify about the error callbacks.onError('Agent run not found in active runs'); // Clean up eventSource.close(); activeStreams.delete(agentRunId); callbacks.onClose(); return; } // Check for completion messages if ( rawData.includes('"type":"status"') && rawData.includes('"status":"completed"') ) { console.log( `[STREAM] Detected completion status message for ${agentRunId}`, ); // Check for specific completion messages that indicate we should stop checking if ( rawData.includes('Run data not available for streaming') || rawData.includes('Stream ended with status: completed') ) { console.log( `[STREAM] Detected final completion message for ${agentRunId}, adding to non-running set`, ); // Add to non-running set to prevent future reconnection attempts nonRunningAgentRuns.add(agentRunId); } // Notify about the message callbacks.onMessage(rawData); // Clean up eventSource.close(); activeStreams.delete(agentRunId); callbacks.onClose(); return; } // Check for thread run end message if ( rawData.includes('"type":"status"') && rawData.includes('"status_type":"thread_run_end"') ) { console.log( `[STREAM] Detected thread run end message for ${agentRunId}`, ); // Add to non-running set nonRunningAgentRuns.add(agentRunId); // Notify about the message callbacks.onMessage(rawData); // Clean up eventSource.close(); activeStreams.delete(agentRunId); callbacks.onClose(); return; } // For all other messages, just pass them through callbacks.onMessage(rawData); } catch (error) { console.error(`[STREAM] Error handling message:`, error); callbacks.onError(error instanceof Error ? error : String(error)); } }; eventSource.onerror = (event) => { console.log(`[STREAM] EventSource error for ${agentRunId}:`, event); // Check if the agent is still running getAgentStatus(agentRunId) .then((status) => { if (status.status !== 'running') { console.log( `[STREAM] Agent run ${agentRunId} is not running after error, closing stream`, ); nonRunningAgentRuns.add(agentRunId); eventSource.close(); activeStreams.delete(agentRunId); callbacks.onClose(); } else { console.log( `[STREAM] Agent run ${agentRunId} is still running after error, keeping stream open`, ); // Let the browser handle reconnection for non-fatal errors } }) .catch((err) => { console.error( `[STREAM] Error checking agent status after stream error:`, err, ); // Check if this is a "not found" error const errMsg = err instanceof Error ? err.message : String(err); const isNotFoundErr = errMsg.includes('not found') || errMsg.includes('404') || errMsg.includes('does not exist'); if (isNotFoundErr) { console.log( `[STREAM] Agent run ${agentRunId} not found after error, closing stream`, ); nonRunningAgentRuns.add(agentRunId); eventSource.close(); activeStreams.delete(agentRunId); callbacks.onClose(); } // For other errors, notify but don't close the stream callbacks.onError(errMsg); }); }; }; // Start the stream setup setupStream(); // Return a cleanup function return () => { console.log(`[STREAM] Cleanup called for ${agentRunId}`); const stream = activeStreams.get(agentRunId); if (stream) { console.log(`[STREAM] Closing stream for ${agentRunId}`); stream.close(); activeStreams.delete(agentRunId); } }; } catch (error) { console.error(`[STREAM] Error setting up stream for ${agentRunId}:`, error); callbacks.onError(error instanceof Error ? error : String(error)); callbacks.onClose(); return () => {}; } }; // Sandbox API Functions export const createSandboxFile = async ( sandboxId: string, filePath: string, content: string, ): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); // Use FormData to handle both text and binary content more reliably const formData = new FormData(); formData.append('path', filePath); // Create a Blob from the content string and append as a file const blob = new Blob([content], { type: 'application/octet-stream' }); formData.append('file', blob, filePath.split('/').pop() || 'file'); const headers: Record = {}; if (session?.access_token) { headers['Authorization'] = `Bearer ${session.access_token}`; } const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, { method: 'POST', headers, body: formData, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error creating sandbox file: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error creating sandbox file: ${response.statusText} (${response.status})`, ); } const result = await response.json(); return result; } catch (error) { console.error('Failed to create sandbox file:', error); handleApiError(error, { operation: 'create file', resource: `file ${filePath}` }); throw error; } }; // Fallback method for legacy support using JSON export const createSandboxFileJson = async ( sandboxId: string, filePath: string, content: string, ): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); const headers: Record = { 'Content-Type': 'application/json', }; if (session?.access_token) { headers['Authorization'] = `Bearer ${session.access_token}`; } const response = await fetch( `${API_URL}/sandboxes/${sandboxId}/files/json`, { method: 'POST', headers, body: JSON.stringify({ path: filePath, content: content, }), }, ); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error creating sandbox file (JSON): ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error creating sandbox file: ${response.statusText} (${response.status})`, ); } const result = await response.json(); return result; } catch (error) { console.error('Failed to create sandbox file with JSON:', error); handleApiError(error, { operation: 'create file', resource: `file ${filePath}` }); throw error; } }; // Helper function to normalize file paths with Unicode characters function normalizePathWithUnicode(path: string): string { try { // Replace escaped Unicode sequences with actual characters return path.replace(/\\u([0-9a-fA-F]{4})/g, (_, hexCode) => { return String.fromCharCode(parseInt(hexCode, 16)); }); } catch (e) { console.error('Error processing Unicode escapes in path:', e); return path; } } export const listSandboxFiles = async ( sandboxId: string, path: string, ): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); const url = new URL(`${API_URL}/sandboxes/${sandboxId}/files`); // Normalize the path to handle Unicode escape sequences const normalizedPath = normalizePathWithUnicode(path); // Properly encode the path parameter for UTF-8 support url.searchParams.append('path', normalizedPath); const headers: Record = {}; if (session?.access_token) { headers['Authorization'] = `Bearer ${session.access_token}`; } const response = await fetch(url.toString(), { headers, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error listing sandbox files: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error listing sandbox files: ${response.statusText} (${response.status})`, ); } const data = await response.json(); return data.files || []; } catch (error) { console.error('Failed to list sandbox files:', error); // handleApiError(error, { operation: 'list files', resource: `directory ${path}` }); throw error; } }; export const getSandboxFileContent = async ( sandboxId: string, path: string, ): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); const url = new URL(`${API_URL}/sandboxes/${sandboxId}/files/content`); // Normalize the path to handle Unicode escape sequences const normalizedPath = normalizePathWithUnicode(path); // Properly encode the path parameter for UTF-8 support url.searchParams.append('path', normalizedPath); const headers: Record = {}; if (session?.access_token) { headers['Authorization'] = `Bearer ${session.access_token}`; } const response = await fetch(url.toString(), { headers, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error getting sandbox file content: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error getting sandbox file content: ${response.statusText} (${response.status})`, ); } // Check if it's a text file or binary file based on content-type const contentType = response.headers.get('content-type'); if ( (contentType && contentType.includes('text')) || contentType?.includes('application/json') ) { return await response.text(); } else { return await response.blob(); } } catch (error) { console.error('Failed to get sandbox file content:', error); handleApiError(error, { operation: 'load file content', resource: `file ${path}` }); throw error; } }; // Function to get public projects export const getPublicProjects = async (): Promise => { try { const supabase = createClient(); // Query for threads that are marked as public const { data: publicThreads, error: threadsError } = await supabase .from('threads') .select('project_id') .eq('is_public', true); if (threadsError) { console.error('Error fetching public threads:', threadsError); return []; } // If no public threads found, return empty array if (!publicThreads?.length) { return []; } // Extract unique project IDs from public threads const publicProjectIds = [ ...new Set(publicThreads.map((thread) => thread.project_id)), ].filter(Boolean); // If no valid project IDs, return empty array if (!publicProjectIds.length) { return []; } // Get the projects that have public threads const { data: projects, error: projectsError } = await supabase .from('projects') .select('*') .in('project_id', publicProjectIds); if (projectsError) { console.error('Error fetching public projects:', projectsError); return []; } console.log( '[API] Raw public projects from DB:', projects?.length, projects, ); // Map database fields to our Project type const mappedProjects: Project[] = (projects || []).map((project) => ({ id: project.project_id, name: project.name || '', description: project.description || '', account_id: project.account_id, created_at: project.created_at, updated_at: project.updated_at, sandbox: project.sandbox || { id: '', pass: '', vnc_preview: '', sandbox_url: '', }, is_public: true, // Mark these as public projects })); console.log( '[API] Mapped public projects for frontend:', mappedProjects.length, ); return mappedProjects; } catch (err) { console.error('Error fetching public projects:', err); handleApiError(err, { operation: 'load public projects', resource: 'public projects' }); return []; } }; export const initiateAgent = async ( formData: FormData, ): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } if (!API_URL) { throw new Error( 'Backend URL is not configured. Set NEXT_PUBLIC_BACKEND_URL in your environment.', ); } console.log( `[API] Initiating agent with files using ${API_URL}/agent/initiate`, ); const response = await fetch(`${API_URL}/agent/initiate`, { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}`, }, body: formData, cache: 'no-store', }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `[API] Error initiating agent: ${response.status} ${response.statusText}`, errorText, ); if (response.status === 402) { throw new Error('Payment Required'); } else if (response.status === 401) { throw new Error('Authentication error: Please sign in again'); } else if (response.status >= 500) { throw new Error('Server error: Please try again later'); } throw new Error( `Error initiating agent: ${response.statusText} (${response.status})`, ); } const result = await response.json(); return result; } catch (error) { console.error('[API] Failed to initiate agent:', error); if ( error instanceof TypeError && error.message.includes('Failed to fetch') ) { const networkError = new Error( `Cannot connect to backend server. Please check your internet connection and make sure the backend is running.`, ); handleApiError(networkError, { operation: 'initiate agent', resource: 'AI assistant' }); throw networkError; } handleApiError(error, { operation: 'initiate agent' }); throw error; } }; export const checkApiHealth = async (): Promise => { try { const response = await fetch(`${API_URL}/health`, { cache: 'no-store', }); if (!response.ok) { throw new Error(`API health check failed: ${response.statusText}`); } return response.json(); } catch (error) { throw error; } }; // Billing API Types export interface CreateCheckoutSessionRequest { price_id: string; success_url: string; cancel_url: string; referral_id?: string; } export interface CreatePortalSessionRequest { return_url: string; } export interface SubscriptionStatus { status: string; // Includes 'active', 'trialing', 'past_due', 'scheduled_downgrade', 'no_subscription' plan_name?: string; price_id?: string; // Added current_period_end?: string; // ISO Date string cancel_at_period_end: boolean; trial_end?: string; // ISO Date string minutes_limit?: number; cost_limit?: number; current_usage?: number; // Fields for scheduled changes has_schedule: boolean; scheduled_plan_name?: string; scheduled_price_id?: string; // Added scheduled_change_date?: string; // ISO Date string - Deprecate? Check backend usage schedule_effective_date?: string; // ISO Date string - Added for consistency } export interface BillingStatusResponse { can_run: boolean; message: string; subscription: { price_id: string; plan_name: string; minutes_limit?: number; }; } export interface Model { id: string; display_name: string; short_name?: string; requires_subscription?: boolean; is_available?: boolean; input_cost_per_million_tokens?: number | null; output_cost_per_million_tokens?: number | null; max_tokens?: number | null; } export interface AvailableModelsResponse { models: Model[]; subscription_tier: string; total_models: number; } export interface UsageLogEntry { message_id: string; thread_id: string; created_at: string; content: { usage: { prompt_tokens: number; completion_tokens: number; }; model: string; }; total_tokens: number; estimated_cost: number; project_id: string; } export interface UsageLogsResponse { logs: UsageLogEntry[]; has_more: boolean; message?: string; } export interface CreateCheckoutSessionResponse { status: | 'upgraded' | 'downgrade_scheduled' | 'checkout_created' | 'no_change' | 'new' | 'updated' | 'scheduled'; subscription_id?: string; schedule_id?: string; session_id?: string; url?: string; effective_date?: string; message?: string; details?: { is_upgrade?: boolean; effective_date?: string; current_price?: number; new_price?: number; invoice?: { id: string; status: string; amount_due: number; amount_paid: number; }; }; } // Billing API Functions export const createCheckoutSession = async ( request: CreateCheckoutSessionRequest, ): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const requestBody = { ...request, tolt_referral: window.tolt_referral }; console.log('Tolt Referral ID:', requestBody.tolt_referral); const response = await fetch(`${API_URL}/billing/create-checkout-session`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}`, }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error creating checkout session: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error creating checkout session: ${response.statusText} (${response.status})`, ); } const data = await response.json(); console.log('Checkout session response:', data); // Handle all possible statuses switch (data.status) { case 'upgraded': case 'updated': case 'downgrade_scheduled': case 'scheduled': case 'no_change': return data; case 'new': case 'checkout_created': if (!data.url) { throw new Error('No checkout URL provided'); } return data; default: console.warn( 'Unexpected status from createCheckoutSession:', data.status, ); return data; } } catch (error) { console.error('Failed to create checkout session:', error); handleApiError(error, { operation: 'create checkout session', resource: 'billing' }); throw error; } }; export const createPortalSession = async ( request: CreatePortalSessionRequest, ): Promise<{ url: string }> => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const response = await fetch(`${API_URL}/billing/create-portal-session`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}`, }, body: JSON.stringify(request), }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error creating portal session: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error creating portal session: ${response.statusText} (${response.status})`, ); } return response.json(); } catch (error) { console.error('Failed to create portal session:', error); handleApiError(error, { operation: 'create portal session', resource: 'billing portal' }); throw error; } }; export const getSubscription = async (): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const response = await fetch(`${API_URL}/billing/subscription`, { headers: { Authorization: `Bearer ${session.access_token}`, }, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error getting subscription: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error getting subscription: ${response.statusText} (${response.status})`, ); } return response.json(); } catch (error) { if (error instanceof NoAccessTokenAvailableError) { throw error; } console.error('Failed to get subscription:', error); handleApiError(error, { operation: 'load subscription', resource: 'billing information' }); throw error; } }; export const getAvailableModels = async (): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const response = await fetch(`${API_URL}/billing/available-models`, { headers: { Authorization: `Bearer ${session.access_token}`, }, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error getting available models: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error getting available models: ${response.statusText} (${response.status})`, ); } return response.json(); } catch (error) { if (error instanceof NoAccessTokenAvailableError) { throw error; } console.error('Failed to get available models:', error); handleApiError(error, { operation: 'load available models', resource: 'AI models' }); throw error; } }; export const checkBillingStatus = async (): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const response = await fetch(`${API_URL}/billing/check-status`, { headers: { Authorization: `Bearer ${session.access_token}`, }, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error checking billing status: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error checking billing status: ${response.statusText} (${response.status})`, ); } return response.json(); } catch (error) { if (error instanceof NoAccessTokenAvailableError) { throw error; } console.error('Failed to check billing status:', error); throw error; } }; // Transcription API Types export interface TranscriptionResponse { text: string; } // Transcription API Functions export const transcribeAudio = async (audioFile: File): Promise => { try { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const formData = new FormData(); formData.append('audio_file', audioFile); const response = await fetch(`${API_URL}/transcription`, { method: 'POST', headers: { Authorization: `Bearer ${session.access_token}`, }, body: formData, }); if (!response.ok) { const errorText = await response .text() .catch(() => 'No error details available'); console.error( `Error transcribing audio: ${response.status} ${response.statusText}`, errorText, ); throw new Error( `Error transcribing audio: ${response.statusText} (${response.status})`, ); } return response.json(); } catch (error) { if (error instanceof NoAccessTokenAvailableError) { throw error; } console.error('Failed to transcribe audio:', error); handleApiError(error, { operation: 'transcribe audio', resource: 'speech-to-text' }); throw error; } }; export const getAgentBuilderChatHistory = async (agentId: string): Promise<{messages: Message[], thread_id: string | null}> => { const supabase = createClient(); const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) { throw new NoAccessTokenAvailableError(); } const response = await fetch(`${API_URL}/agents/${agentId}/builder-chat-history`, { headers: { Authorization: `Bearer ${session.access_token}`, }, }); if (!response.ok) { const errorText = await response.text().catch(() => 'No error details available'); console.error(`Error getting agent builder chat history: ${response.status} ${response.statusText}`, errorText); throw new Error(`Error getting agent builder chat history: ${response.statusText}`); } const data = await response.json(); console.log('[API] Agent builder chat history fetched:', data); return data; };