/** * API Client for Chat Mobile App * Handles all communication with the NestJS backend */ import * as SecureStore from 'expo-secure-store'; const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3001'; // Token storage key (must match what @manacore/shared-auth uses) const APP_TOKEN_KEY = '@manacore/app_token'; // ============================================================================ // Types // ============================================================================ export type Conversation = { id: string; userId: string; modelId: string; templateId?: string; spaceId?: string; conversationMode: 'free' | 'guided' | 'template'; documentMode: boolean; title?: string; isArchived: boolean; createdAt: string; updatedAt: string; }; export type Message = { id: string; conversationId: string; sender: 'user' | 'assistant' | 'system'; messageText: string; createdAt: string; updatedAt: string; }; export type Template = { id: string; userId: string; name: string; description?: string; systemPrompt: string; initialQuestion?: string; modelId?: string; color: string; isDefault: boolean; documentMode: boolean; createdAt: string; updatedAt: string; }; export type Space = { id: string; name: string; description?: string; ownerId: string; isArchived: boolean; createdAt: string; updatedAt: string; }; export type SpaceMember = { id: string; spaceId: string; userId: string; role: 'owner' | 'admin' | 'member' | 'viewer'; invitationStatus: 'pending' | 'accepted' | 'declined'; invitedBy?: string; invitedAt: string; joinedAt?: string; createdAt: string; updatedAt: string; }; export type Document = { id: string; conversationId: string; version: number; content: string; createdAt: string; updatedAt: string; }; export type AIModel = { id: string; name: string; description?: string; parameters: { temperature?: number; max_tokens?: number; provider?: string; deployment?: string; endpoint?: string; api_version?: string; }; costSettings?: { prompt_per_1k_tokens?: number; completion_per_1k_tokens?: number; }; isActive: boolean; createdAt: string; updatedAt: string; }; export type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string; }; export type TokenUsage = { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; export type ChatCompletionResponse = { content: string; usage: TokenUsage; }; // ============================================================================ // Base API Functions // ============================================================================ async function getAuthToken(): Promise { try { return await SecureStore.getItemAsync(APP_TOKEN_KEY); } catch { return null; } } async function apiRequest( endpoint: string, options: RequestInit = {} ): Promise<{ data: T | null; error: string | null }> { try { const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json', ...(options.headers || {}), }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${BACKEND_URL}${endpoint}`, { ...options, headers, }); if (!response.ok) { const errorText = await response.text(); console.error(`API Error [${response.status}]: ${errorText}`); return { data: null, error: `API Error: ${response.status}` }; } // Handle empty responses const text = await response.text(); if (!text) { return { data: null, error: null }; } const data = JSON.parse(text); return { data, error: null }; } catch (error) { console.error('API Request failed:', error); return { data: null, error: error instanceof Error ? error.message : 'Unknown error' }; } } // ============================================================================ // Conversation API // ============================================================================ export const conversationApi = { async getConversations(spaceId?: string): Promise { const params = spaceId ? `?spaceId=${spaceId}` : ''; const { data, error } = await apiRequest(`/api/conversations${params}`); if (error) { console.error('Failed to fetch conversations:', error); return []; } return data || []; }, async getArchivedConversations(): Promise { const { data, error } = await apiRequest('/api/conversations/archived'); if (error) { console.error('Failed to fetch archived conversations:', error); return []; } return data || []; }, async getConversation(id: string): Promise { const { data, error } = await apiRequest(`/api/conversations/${id}`); if (error) { console.error('Failed to fetch conversation:', error); return null; } return data; }, async getMessages(conversationId: string): Promise { const { data, error } = await apiRequest(`/api/conversations/${conversationId}/messages`); if (error) { console.error('Failed to fetch messages:', error); return []; } return data || []; }, async createConversation(params: { modelId: string; conversationMode?: 'free' | 'guided' | 'template'; templateId?: string; documentMode?: boolean; spaceId?: string; }): Promise { const { data, error } = await apiRequest('/api/conversations', { method: 'POST', body: JSON.stringify(params), }); if (error) { console.error('Failed to create conversation:', error); return null; } return data; }, async addMessage( conversationId: string, sender: 'user' | 'assistant' | 'system', messageText: string ): Promise { const { data, error } = await apiRequest(`/api/conversations/${conversationId}/messages`, { method: 'POST', body: JSON.stringify({ sender, messageText }), }); if (error) { console.error('Failed to add message:', error); return null; } return data; }, async updateTitle(conversationId: string, title: string): Promise { const { error } = await apiRequest(`/api/conversations/${conversationId}/title`, { method: 'PATCH', body: JSON.stringify({ title }), }); return !error; }, async archiveConversation(conversationId: string): Promise { const { error } = await apiRequest(`/api/conversations/${conversationId}/archive`, { method: 'POST', }); return !error; }, async unarchiveConversation(conversationId: string): Promise { const { error } = await apiRequest(`/api/conversations/${conversationId}/unarchive`, { method: 'POST', }); return !error; }, async deleteConversation(conversationId: string): Promise { const { error } = await apiRequest(`/api/conversations/${conversationId}`, { method: 'DELETE', }); return !error; }, }; // ============================================================================ // Template API // ============================================================================ export const templateApi = { async getTemplates(): Promise { const { data, error } = await apiRequest('/api/templates'); if (error) { console.error('Failed to fetch templates:', error); return []; } return data || []; }, async getTemplate(id: string): Promise