mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:01:08 +02:00
633 lines
17 KiB
TypeScript
633 lines
17 KiB
TypeScript
/**
|
|
* 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<string | null> {
|
|
try {
|
|
return await SecureStore.getItemAsync(APP_TOKEN_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function apiRequest<T>(
|
|
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<Conversation[]> {
|
|
const params = spaceId ? `?spaceId=${spaceId}` : '';
|
|
const { data, error } = await apiRequest<Conversation[]>(`/api/conversations${params}`);
|
|
if (error) {
|
|
console.error('Failed to fetch conversations:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getArchivedConversations(): Promise<Conversation[]> {
|
|
const { data, error } = await apiRequest<Conversation[]>('/api/conversations/archived');
|
|
if (error) {
|
|
console.error('Failed to fetch archived conversations:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getConversation(id: string): Promise<Conversation | null> {
|
|
const { data, error } = await apiRequest<Conversation>(`/api/conversations/${id}`);
|
|
if (error) {
|
|
console.error('Failed to fetch conversation:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async getMessages(conversationId: string): Promise<Message[]> {
|
|
const { data, error } = await apiRequest<Message[]>(`/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<Conversation | null> {
|
|
const { data, error } = await apiRequest<Conversation>('/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<Message | null> {
|
|
const { data, error } = await apiRequest<Message>(`/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<boolean> {
|
|
const { error } = await apiRequest(`/api/conversations/${conversationId}/title`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async archiveConversation(conversationId: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/conversations/${conversationId}/archive`, {
|
|
method: 'POST',
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async unarchiveConversation(conversationId: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/conversations/${conversationId}/unarchive`, {
|
|
method: 'POST',
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async deleteConversation(conversationId: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/conversations/${conversationId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return !error;
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Template API
|
|
// ============================================================================
|
|
|
|
export const templateApi = {
|
|
async getTemplates(): Promise<Template[]> {
|
|
const { data, error } = await apiRequest<Template[]>('/api/templates');
|
|
if (error) {
|
|
console.error('Failed to fetch templates:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getTemplate(id: string): Promise<Template | null> {
|
|
const { data, error } = await apiRequest<Template>(`/api/templates/${id}`);
|
|
if (error) {
|
|
console.error('Failed to fetch template:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async getDefaultTemplate(): Promise<Template | null> {
|
|
const { data, error } = await apiRequest<Template>('/api/templates/default');
|
|
if (error) {
|
|
// Not finding a default template is not an error
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async createTemplate(template: {
|
|
name: string;
|
|
description?: string;
|
|
systemPrompt: string;
|
|
initialQuestion?: string;
|
|
modelId?: string;
|
|
color?: string;
|
|
documentMode?: boolean;
|
|
}): Promise<Template | null> {
|
|
const { data, error } = await apiRequest<Template>('/api/templates', {
|
|
method: 'POST',
|
|
body: JSON.stringify(template),
|
|
});
|
|
if (error) {
|
|
console.error('Failed to create template:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async updateTemplate(
|
|
id: string,
|
|
updates: Partial<{
|
|
name: string;
|
|
description: string;
|
|
systemPrompt: string;
|
|
initialQuestion: string;
|
|
modelId: string;
|
|
color: string;
|
|
documentMode: boolean;
|
|
}>
|
|
): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/templates/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(updates),
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async setDefaultTemplate(id: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/templates/${id}/default`, {
|
|
method: 'POST',
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async deleteTemplate(id: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/templates/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return !error;
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Space API
|
|
// ============================================================================
|
|
|
|
export const spaceApi = {
|
|
async getUserSpaces(): Promise<Space[]> {
|
|
const { data, error } = await apiRequest<Space[]>('/api/spaces');
|
|
if (error) {
|
|
console.error('Failed to fetch spaces:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getOwnedSpaces(): Promise<Space[]> {
|
|
const { data, error } = await apiRequest<Space[]>('/api/spaces/owned');
|
|
if (error) {
|
|
console.error('Failed to fetch owned spaces:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getSpace(id: string): Promise<Space | null> {
|
|
const { data, error } = await apiRequest<Space>(`/api/spaces/${id}`);
|
|
if (error) {
|
|
console.error('Failed to fetch space:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async getSpaceMembers(spaceId: string): Promise<SpaceMember[]> {
|
|
const { data, error } = await apiRequest<SpaceMember[]>(`/api/spaces/${spaceId}/members`);
|
|
if (error) {
|
|
console.error('Failed to fetch space members:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getUserRoleInSpace(spaceId: string): Promise<'owner' | 'admin' | 'member' | 'viewer' | null> {
|
|
const { data, error } = await apiRequest<{ role: 'owner' | 'admin' | 'member' | 'viewer' }>(`/api/spaces/${spaceId}/role`);
|
|
if (error) {
|
|
return null;
|
|
}
|
|
return data?.role || null;
|
|
},
|
|
|
|
async getPendingInvitations(): Promise<Array<{ invitation: SpaceMember; space: Space }>> {
|
|
const { data, error } = await apiRequest<Array<{ invitation: SpaceMember; space: Space }>>('/api/spaces/invitations/pending');
|
|
if (error) {
|
|
console.error('Failed to fetch pending invitations:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async createSpace(name: string, description?: string): Promise<Space | null> {
|
|
const { data, error } = await apiRequest<Space>('/api/spaces', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name, description }),
|
|
});
|
|
if (error) {
|
|
console.error('Failed to create space:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async updateSpace(
|
|
id: string,
|
|
updates: { name?: string; description?: string; isArchived?: boolean }
|
|
): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/spaces/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(updates),
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async deleteSpace(id: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/spaces/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async inviteUser(
|
|
spaceId: string,
|
|
userId: string,
|
|
role: 'admin' | 'member' | 'viewer' = 'member'
|
|
): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/members`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ userId, role }),
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async respondToInvitation(
|
|
spaceId: string,
|
|
status: 'accepted' | 'declined'
|
|
): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/invitation`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ status }),
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async removeMember(spaceId: string, userId: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/members/${userId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return !error;
|
|
},
|
|
|
|
async changeMemberRole(
|
|
spaceId: string,
|
|
userId: string,
|
|
newRole: 'admin' | 'member' | 'viewer'
|
|
): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/spaces/${spaceId}/members/${userId}/role`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ role: newRole }),
|
|
});
|
|
return !error;
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Document API
|
|
// ============================================================================
|
|
|
|
export const documentApi = {
|
|
async getLatestDocument(conversationId: string): Promise<Document | null> {
|
|
const { data, error } = await apiRequest<Document>(`/api/documents/conversation/${conversationId}/latest`);
|
|
if (error) {
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async getAllDocumentVersions(conversationId: string): Promise<Document[]> {
|
|
const { data, error } = await apiRequest<Document[]>(`/api/documents/conversation/${conversationId}`);
|
|
if (error) {
|
|
console.error('Failed to fetch document versions:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async hasDocument(conversationId: string): Promise<boolean> {
|
|
const { data, error } = await apiRequest<{ exists: boolean }>(`/api/documents/conversation/${conversationId}/exists`);
|
|
if (error) {
|
|
return false;
|
|
}
|
|
return data?.exists || false;
|
|
},
|
|
|
|
async createDocument(conversationId: string, content: string): Promise<Document | null> {
|
|
const { data, error } = await apiRequest<Document>('/api/documents', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ conversationId, content }),
|
|
});
|
|
if (error) {
|
|
console.error('Failed to create document:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async createDocumentVersion(conversationId: string, content: string): Promise<Document | null> {
|
|
const { data, error } = await apiRequest<Document>(`/api/documents/conversation/${conversationId}/version`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
if (error) {
|
|
console.error('Failed to create document version:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
|
|
async deleteDocumentVersion(documentId: string): Promise<boolean> {
|
|
const { error } = await apiRequest(`/api/documents/${documentId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
return !error;
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Model API
|
|
// ============================================================================
|
|
|
|
export const modelApi = {
|
|
async getModels(): Promise<AIModel[]> {
|
|
const { data, error } = await apiRequest<AIModel[]>('/api/chat/models');
|
|
if (error) {
|
|
console.error('Failed to fetch models:', error);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
},
|
|
|
|
async getModel(id: string): Promise<AIModel | null> {
|
|
const { data, error } = await apiRequest<AIModel>(`/api/models/${id}`);
|
|
if (error) {
|
|
console.error('Failed to fetch model:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Chat API
|
|
// ============================================================================
|
|
|
|
export const chatApi = {
|
|
async createCompletion(params: {
|
|
messages: ChatMessage[];
|
|
modelId: string;
|
|
temperature?: number;
|
|
maxTokens?: number;
|
|
}): Promise<ChatCompletionResponse | null> {
|
|
const { data, error } = await apiRequest<ChatCompletionResponse>('/api/chat/completions', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
messages: params.messages,
|
|
modelId: params.modelId,
|
|
temperature: params.temperature ?? 0.7,
|
|
maxTokens: params.maxTokens ?? 1000,
|
|
}),
|
|
});
|
|
if (error) {
|
|
console.error('Failed to create completion:', error);
|
|
return null;
|
|
}
|
|
return data;
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// Usage Log API
|
|
// ============================================================================
|
|
|
|
export const usageApi = {
|
|
async logTokenUsage(params: {
|
|
conversationId: string;
|
|
messageId: string;
|
|
modelId: string;
|
|
promptTokens: number;
|
|
completionTokens: number;
|
|
totalTokens: number;
|
|
estimatedCost: number;
|
|
}): Promise<boolean> {
|
|
const { error } = await apiRequest('/api/usage-logs', {
|
|
method: 'POST',
|
|
body: JSON.stringify(params),
|
|
});
|
|
return !error;
|
|
},
|
|
};
|