Backend: - token_version in JWT (bump_token_version, get_token_version on User model); jwt_required checks tv claim → 401 on mismatch; login routes embed version - Quota pre-flight in all 3 LLM public methods (QuotaExceededError bubbles up) - AI runner catches QuotaExceededError → sets status paused_quota + emits WS event - Admin routes: POST /users (create), POST /users/<id>/reset-password, POST /pricing, GET /focus-groups with aggregated cost; PUT /users/<id> now bumps token_version on disable or role change - backfill_usage.py: idempotent estimated-event generator for historical data, tiktoken for GPT models, char/3.8 for Gemini, --dry-run flag Frontend: - 402 interceptor dispatches quota_exceeded CustomEvent - adminApi: createUser, resetPassword, createPricing, listFocusGroups - UsersTab: New User dialog + Reset Password in edit dialog - PricingTab: New Price dialog (model, provider, input/output/cached prices) - FocusGroupsTab: focus groups table sorted by total cost - Admin.tsx: 4th tab (Focus Groups) - FocusGroupSession: admin-only cost badge + dismissable quota exceeded banner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
758 lines
No EOL
25 KiB
TypeScript
Executable file
758 lines
No EOL
25 KiB
TypeScript
Executable file
import axios from 'axios';
|
|
|
|
// Base URL for API requests
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/semblance_back/api';
|
|
|
|
|
|
// Create axios instance with baseURL
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
// Default timeout for regular endpoints; AI endpoints override below
|
|
timeout: 120000 // 2 minutes
|
|
});
|
|
|
|
// Helper function to check if JWT token is expired
|
|
const isTokenExpired = (token: string): boolean => {
|
|
// Offline mode token is never expired
|
|
if (localStorage.getItem('offline_mode') === 'true') return false;
|
|
try {
|
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
const currentTime = Date.now() / 1000;
|
|
return payload.exp < currentTime;
|
|
} catch (error) {
|
|
console.error('Error parsing JWT token:', error);
|
|
return true; // Treat malformed tokens as expired
|
|
}
|
|
};
|
|
|
|
// Request interceptor to add JWT token
|
|
api.interceptors.request.use(
|
|
(config) => {
|
|
const token = localStorage.getItem('auth_token');
|
|
if (token) {
|
|
// Check if token is expired before making request
|
|
if (isTokenExpired(token)) {
|
|
localStorage.removeItem('auth_token');
|
|
localStorage.removeItem('user');
|
|
localStorage.removeItem('auth_type');
|
|
|
|
// Dispatch auth error to trigger redirect
|
|
dispatchAuthError({ source: config.url });
|
|
|
|
// Reject the request
|
|
return Promise.reject(new Error('Token expired'));
|
|
}
|
|
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
return config;
|
|
},
|
|
(error) => Promise.reject(error)
|
|
);
|
|
|
|
// Create a custom event for auth errors
|
|
export const AUTH_ERROR_EVENT = 'auth_error';
|
|
|
|
// Custom event with details
|
|
export interface AuthErrorDetail {
|
|
source?: string;
|
|
isPersonaCreation?: boolean;
|
|
}
|
|
|
|
// Function to dispatch auth error event
|
|
export const dispatchAuthError = (details?: AuthErrorDetail) => {
|
|
// Only remove auth data if not a persona creation request
|
|
if (!details?.isPersonaCreation) {
|
|
// Clean up local storage auth data
|
|
localStorage.removeItem('auth_token');
|
|
localStorage.removeItem('user');
|
|
}
|
|
|
|
// Dispatch event for handling in auth context
|
|
const event = new CustomEvent(AUTH_ERROR_EVENT, {
|
|
detail: details || {}
|
|
});
|
|
window.dispatchEvent(event);
|
|
};
|
|
|
|
// Response interceptor to handle common errors
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
// Handle 402 (Quota Exceeded)
|
|
if (error.response && error.response.status === 402) {
|
|
const data = error.response.data;
|
|
window.dispatchEvent(new CustomEvent('quota_exceeded', {
|
|
detail: { scope: data.scope, limit_usd: data.limit_usd, used_usd: data.used_usd }
|
|
}));
|
|
}
|
|
|
|
// Handle 401 (Unauthorized) - dispatch event instead of redirect
|
|
if (error.response && error.response.status === 401) {
|
|
// Check if this is a persona-related request - these are handled separately
|
|
const isPersonaRequest =
|
|
error.config &&
|
|
(error.config.url?.includes('/personas') ||
|
|
error.config.url?.includes('/personas/batch') ||
|
|
(error.config.method && error.config.url?.startsWith('/personas')));
|
|
|
|
|
|
// For persona-related requests, don't automatically log out
|
|
// Let the component handle the auth error
|
|
if (!isPersonaRequest) {
|
|
dispatchAuthError({
|
|
source: error.config?.url,
|
|
isPersonaCreation: false
|
|
});
|
|
} else {
|
|
console.warn('Authentication error in persona request, letting component handle it');
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Auth endpoints
|
|
export const authApi = {
|
|
login: (username: string, password: string) =>
|
|
api.post('/auth/login', { username, password }),
|
|
|
|
loginWithMicrosoft: (idToken: string) =>
|
|
api.post('/auth/microsoft', { id_token: idToken }),
|
|
|
|
register: (username: string, email: string, password: string) =>
|
|
api.post('/auth/register', { username, email, password }),
|
|
|
|
getProfile: () =>
|
|
api.get('/auth/me')
|
|
};
|
|
|
|
// Personas endpoints
|
|
export const personasApi = {
|
|
getAll: () =>
|
|
api.get('/personas/all'),
|
|
|
|
getById: (id: string) =>
|
|
api.get(`/personas/${id}`),
|
|
|
|
create: (personaData: any) =>
|
|
api.post('/personas', personaData),
|
|
|
|
update: (id: string, personaData: any) => {
|
|
// Don't try to update with local-prefixed IDs - they won't work
|
|
if (id && id.startsWith('local-')) {
|
|
return api.post('/personas', personaData);
|
|
}
|
|
return api.put(`/personas/${id}`, personaData);
|
|
},
|
|
|
|
modifyWithAI: (id: string, modificationData: any) => {
|
|
const personaId = typeof id === 'object' && id !== null ? (id as any)._id || id : id;
|
|
return api.post(`/personas/${personaId}/modify-with-ai`, modificationData);
|
|
},
|
|
|
|
delete: (id: string) => {
|
|
// Handle different ID formats
|
|
// If the ID is an object with an _id property, use that
|
|
// Otherwise, use the provided ID string
|
|
const personaId = typeof id === 'object' && id !== null ? (id as any)._id || id : id;
|
|
return api.delete(`/personas/${personaId}`);
|
|
},
|
|
|
|
createBatch: (personasData: any[]) =>
|
|
api.post('/personas/batch', personasData),
|
|
|
|
// Export individual persona profile as markdown
|
|
exportProfile: (id: string, options?: { llm_model?: string; temperature?: number }) =>
|
|
api.post(`/personas/${id}/export-profile`, options || {}, {
|
|
timeout: 180000 // 3 minutes for profile export
|
|
}),
|
|
|
|
// Bulk export personas to specified format
|
|
bulkExportPersonas: (data: {
|
|
persona_ids: string[];
|
|
export_format: 'markdown' | 'json' | 'csv';
|
|
}) =>
|
|
api.post('/personas/bulk-export', data, {
|
|
timeout: 180000 // 3 minutes for bulk export
|
|
})
|
|
};
|
|
|
|
// AI Persona Generation endpoints
|
|
export const aiPersonasApi = {
|
|
// Legacy endpoints
|
|
// Generate a persona without saving
|
|
generate: (options?: any) =>
|
|
api.post('/ai-personas/generate', options || {}, {
|
|
timeout: 180000 // 3 minutes for single persona generation
|
|
}),
|
|
|
|
// Generate and immediately save to database
|
|
generateAndSave: (options?: any) =>
|
|
api.post('/ai-personas/generate-and-save', options || {}, {
|
|
timeout: 180000 // 3 minutes for single persona generation
|
|
}),
|
|
|
|
// Generate multiple personas without saving
|
|
batchGenerate: (options: { count: number, customizations?: any[], temperature?: number }) =>
|
|
api.post('/ai-personas/batch-generate', options, {
|
|
timeout: 180000 // 3 minutes for batch generation
|
|
}),
|
|
|
|
// Generate multiple personas and save to database
|
|
batchGenerateAndSave: (options: { count: number, customizations?: any[], temperature?: number }) =>
|
|
api.post('/ai-personas/batch-generate-and-save', options, {
|
|
timeout: 180000 // 3 minutes for batch generation and save
|
|
}),
|
|
|
|
// Two-stage generation endpoints
|
|
// First stage: Generate basic profiles
|
|
generateBasicProfiles: (
|
|
audienceBrief: string,
|
|
count: number = 5,
|
|
temperature: number = 0.8
|
|
) =>
|
|
api.post('/ai-personas/generate-basic-profiles', {
|
|
audience_brief: audienceBrief,
|
|
count,
|
|
temperature
|
|
}, {
|
|
timeout: 180000 // 3 minutes for basic profile generation
|
|
}),
|
|
|
|
// Second stage: Complete a single persona without saving
|
|
completePersona: (basicProfile: any, temperature: number = 0.7) =>
|
|
api.post('/ai-personas/complete-persona', {
|
|
basic_profile: basicProfile,
|
|
temperature
|
|
}, {
|
|
timeout: 180000 // 3 minutes for detailed persona generation
|
|
}),
|
|
|
|
// Second stage: Complete a single persona and save to database
|
|
completeAndSavePersona: (
|
|
basicProfile: any,
|
|
temperature: number = 0.7,
|
|
audienceBrief?: string,
|
|
researchObjective?: string,
|
|
customerDataSessionId?: string,
|
|
llmModel?: string
|
|
) =>
|
|
api.post('/ai-personas/complete-and-save-persona', {
|
|
basic_profile: basicProfile,
|
|
temperature,
|
|
audience_brief: audienceBrief,
|
|
research_objective: researchObjective,
|
|
customer_data_session_id: customerDataSessionId,
|
|
llm_model: llmModel
|
|
}, {
|
|
timeout: 180000 // 3 minutes for detailed persona generation and saving
|
|
}),
|
|
|
|
// Generate summary for an existing persona
|
|
generatePersonaSummary: (personaData: any, temperature: number = 0.7) =>
|
|
api.post('/ai-personas/generate-persona-summary', {
|
|
persona_data: personaData,
|
|
temperature
|
|
}, {
|
|
timeout: 180000 // 3 minutes for summary generation
|
|
}),
|
|
|
|
// Helper method for two-stage batch generation and save
|
|
batchGenerateWithStages: async (
|
|
audienceBrief: string,
|
|
researchObjective?: string,
|
|
count: number = 5,
|
|
temperature: number = 0.7,
|
|
customerDataSessionId?: string,
|
|
llmModel?: string,
|
|
onTaskIdReceived?: (taskId: string) => void
|
|
) => {
|
|
try {
|
|
// First stage: Generate basic profiles
|
|
const basicProfilesResponse = await api.post('/ai-personas/generate-basic-profiles', {
|
|
audience_brief: audienceBrief,
|
|
research_objective: researchObjective,
|
|
count,
|
|
temperature: 0.7, // Use 0.7 temperature for basic profiles
|
|
customer_data_session_id: customerDataSessionId,
|
|
llm_model: llmModel || 'gemini-2.5-pro'
|
|
}, {
|
|
timeout: 180000 // 3 minutes for basic profile generation
|
|
});
|
|
|
|
const basicProfiles = basicProfilesResponse.data.profiles;
|
|
const taskId = basicProfilesResponse.data.task_id; // Extract task_id from first call
|
|
|
|
// Call the callback immediately with the task ID
|
|
if (taskId && onTaskIdReceived) {
|
|
onTaskIdReceived(taskId);
|
|
}
|
|
|
|
const personas = [];
|
|
const personaIds = [];
|
|
const errors = [];
|
|
|
|
// Second stage: Complete each profile in parallel
|
|
const completeRequests = basicProfiles.map(profile =>
|
|
api.post('/ai-personas/complete-and-save-persona', {
|
|
basic_profile: profile,
|
|
temperature,
|
|
audience_brief: audienceBrief,
|
|
research_objective: researchObjective,
|
|
customer_data_session_id: customerDataSessionId,
|
|
llm_model: llmModel || 'gemini-2.5-pro'
|
|
}, {
|
|
timeout: 180000 // 3 minutes for each persona completion
|
|
})
|
|
);
|
|
|
|
// Wait for all requests to complete
|
|
const results = await Promise.allSettled(completeRequests);
|
|
|
|
// Process results
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
personas.push(result.value.data.persona);
|
|
personaIds.push(result.value.data.persona_id);
|
|
} else {
|
|
const basicProfile = basicProfiles[index];
|
|
const errorInfo = {
|
|
index,
|
|
name: basicProfile.name || `Persona ${index + 1}`,
|
|
error: result.reason
|
|
};
|
|
errors.push(errorInfo);
|
|
console.error(`Failed to complete persona ${index + 1} (${basicProfile.name || 'unnamed'}):`, result.reason);
|
|
}
|
|
});
|
|
|
|
// Only fail completely if ALL personas failed
|
|
if (personas.length === 0 && errors.length > 0) {
|
|
throw new Error(`Failed to generate any personas. ${errors.length} profile(s) failed.`);
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
message: `Generated and saved ${personas.length} personas${errors.length > 0 ? ` (${errors.length} failed)` : ''}`,
|
|
personas,
|
|
persona_ids: personaIds,
|
|
task_id: taskId,
|
|
errors: errors.length > 0 ? errors : undefined,
|
|
partial_success: errors.length > 0 && personas.length > 0
|
|
}
|
|
};
|
|
} catch (error) {
|
|
// For errors in the first stage
|
|
if (error.response?.status === 504 || error.code === "ECONNABORTED") {
|
|
throw new Error(`Timeout error: The server took too long to generate personas. Please try with fewer personas or try again later.`);
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Enhance audience brief endpoint
|
|
enhanceAudienceBrief: (audienceBrief: string, researchObjective: string, temperature: number = 0.7) =>
|
|
api.post('/ai-personas/enhance-audience-brief', {
|
|
audience_brief: audienceBrief,
|
|
research_objective: researchObjective,
|
|
temperature
|
|
}, {
|
|
timeout: 180000 // 3 minutes timeout for AI enhancement
|
|
}),
|
|
|
|
// Batch generate summaries for download
|
|
batchGenerateSummaries: (personaIds: string[], temperature: number = 0.7, llmModel?: string) => {
|
|
return api.post('/ai-personas/batch-generate-summaries', {
|
|
persona_ids: personaIds,
|
|
temperature,
|
|
llm_model: llmModel || 'gemini-2.5-pro'
|
|
}, {
|
|
timeout: 180000 // 3 minutes timeout for batch processing
|
|
});
|
|
},
|
|
|
|
// Upload customer data files for parsing
|
|
uploadCustomerData: (files: FileList) => {
|
|
const formData = new FormData();
|
|
for (let i = 0; i < files.length; i++) {
|
|
formData.append('files', files[i]);
|
|
}
|
|
|
|
return api.post('/ai-personas/upload-customer-data', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
timeout: 180000 // 3 minutes for file upload and parsing
|
|
});
|
|
},
|
|
|
|
// Clean up customer data files for a session
|
|
cleanupCustomerData: (sessionId: string) =>
|
|
api.delete(`/ai-personas/cleanup-customer-data/${sessionId}`),
|
|
|
|
// Unified persona generation endpoint with cancellation support
|
|
generatePersonasFull: async (
|
|
audienceBrief: string,
|
|
researchObjective?: string,
|
|
count: number = 5,
|
|
temperature: number = 0.8,
|
|
customerDataSessionId?: string,
|
|
llmModel?: string,
|
|
targetFolderId?: string
|
|
) => {
|
|
return api.post('/ai-personas/generate-personas-full', {
|
|
audience_brief: audienceBrief,
|
|
research_objective: researchObjective,
|
|
count,
|
|
temperature,
|
|
customer_data_session_id: customerDataSessionId,
|
|
llm_model: llmModel || 'gemini-2.5-pro',
|
|
target_folder_id: targetFolderId
|
|
}, {
|
|
timeout: 10000 // 10 seconds — endpoint returns immediately with task_id
|
|
});
|
|
}
|
|
};
|
|
|
|
// Focus Groups endpoints
|
|
export const focusGroupsApi = {
|
|
getAll: () =>
|
|
api.get('/focus-groups'),
|
|
|
|
getById: (id: string) =>
|
|
api.get(`/focus-groups/${id}`),
|
|
|
|
create: (focusGroupData: any) =>
|
|
api.post('/focus-groups', focusGroupData),
|
|
|
|
update: (id: string, focusGroupData: any) =>
|
|
api.put(`/focus-groups/${id}`, focusGroupData),
|
|
|
|
delete: (id: string) =>
|
|
api.delete(`/focus-groups/${id}`),
|
|
|
|
addParticipant: (focusGroupId: string, personaId: string) =>
|
|
api.post(`/focus-groups/${focusGroupId}/participants`, { persona_id: personaId }),
|
|
|
|
removeParticipant: (focusGroupId: string, personaId: string) =>
|
|
api.delete(`/focus-groups/${focusGroupId}/participants/${personaId}`),
|
|
|
|
sendMessage: (focusGroupId: string, messageData: any) =>
|
|
api.post(`/focus-groups/${focusGroupId}/messages`, messageData),
|
|
|
|
getMessages: (focusGroupId: string) =>
|
|
api.get(`/focus-groups/${focusGroupId}/messages`),
|
|
|
|
updateMessageHighlight: (focusGroupId: string, messageId: string, highlighted: boolean) =>
|
|
api.patch(`/focus-groups/${focusGroupId}/messages/${messageId}`, { highlighted }),
|
|
|
|
describeAsset: (focusGroupId: string, assetFilename: string) =>
|
|
api.post(`/focus-groups/${focusGroupId}/describe-asset`, { asset_filename: assetFilename }, {
|
|
timeout: 120000 // 2 minutes timeout for AI description generation
|
|
}),
|
|
|
|
generateDiscussionGuide: (data: any) =>
|
|
api.post('/focus-groups/generate-discussion-guide', data, {
|
|
timeout: 180000 // 3 minutes timeout for AI generation
|
|
}),
|
|
|
|
generateDiscussionGuideForGroup: (focusGroupId: string, data: any) =>
|
|
api.post(`/focus-groups/${focusGroupId}/generate-discussion-guide`, data, {
|
|
timeout: 180000 // 3 minutes timeout for AI generation
|
|
}),
|
|
|
|
downloadDiscussionGuide: async (focusGroupId: string) => {
|
|
try {
|
|
const response = await api.get(`/focus-groups/${focusGroupId}/discussion-guide/download`, {
|
|
responseType: 'blob', // Important for file download
|
|
timeout: 30000 // 30 seconds timeout for download
|
|
});
|
|
|
|
// Extract filename from Content-Disposition header
|
|
const contentDisposition = response.headers['content-disposition'];
|
|
let filename = 'discussion-guide.md';
|
|
|
|
if (contentDisposition) {
|
|
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
|
|
if (filenameMatch) {
|
|
filename = filenameMatch[1];
|
|
}
|
|
}
|
|
|
|
// Create blob URL and trigger download
|
|
const blob = new Blob([response.data], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Create temporary anchor element for download
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = filename;
|
|
anchor.style.display = 'none';
|
|
|
|
// Trigger download
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
document.body.removeChild(anchor);
|
|
|
|
// Clean up
|
|
URL.revokeObjectURL(url);
|
|
|
|
return { success: true, filename };
|
|
} catch (error) {
|
|
console.error('Error downloading discussion guide:', error);
|
|
throw new Error('Failed to download discussion guide');
|
|
}
|
|
},
|
|
|
|
// Notes endpoints
|
|
createNote: (focusGroupId: string, noteData: any) =>
|
|
api.post(`/focus-groups/${focusGroupId}/notes`, noteData),
|
|
|
|
getNotes: (focusGroupId: string) =>
|
|
api.get(`/focus-groups/${focusGroupId}/notes`),
|
|
|
|
deleteNote: (focusGroupId: string, noteId: string) =>
|
|
api.delete(`/focus-groups/${focusGroupId}/notes/${noteId}`),
|
|
|
|
// Asset management endpoints
|
|
uploadAssets: (focusGroupId: string, formData: FormData, replace?: boolean) => {
|
|
// Add replace flag to form data if specified
|
|
if (replace === true) {
|
|
formData.append('replace', 'true');
|
|
}
|
|
|
|
return api.post(`/focus-groups/${focusGroupId}/assets`, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data'
|
|
},
|
|
timeout: 120000 // 2 minutes for file upload
|
|
});
|
|
},
|
|
|
|
getAssets: (focusGroupId: string) =>
|
|
api.get(`/focus-groups/${focusGroupId}/assets`),
|
|
|
|
getAssetUrl: (focusGroupId: string, filename: string) =>
|
|
`${API_BASE_URL}/focus-groups/${focusGroupId}/assets/${filename}`,
|
|
|
|
updateAssetName: (focusGroupId: string, filename: string, userAssignedName: string) =>
|
|
api.patch(`/focus-groups/${focusGroupId}/assets/${filename}`, {
|
|
user_assigned_name: userAssignedName
|
|
}),
|
|
|
|
deleteAsset: (focusGroupId: string, filename: string) =>
|
|
api.delete(`/focus-groups/${focusGroupId}/assets/${filename}`)
|
|
};
|
|
|
|
// Focus Group AI endpoints
|
|
export const focusGroupAiApi = {
|
|
generateResponse: (
|
|
focusGroupId: string,
|
|
personaId: string,
|
|
currentTopic: string,
|
|
temperature: number = 0.7
|
|
) =>
|
|
api.post('/focus-group-ai/generate-response', {
|
|
focus_group_id: focusGroupId,
|
|
persona_id: personaId,
|
|
current_topic: currentTopic,
|
|
temperature: temperature
|
|
}, {
|
|
timeout: 180000 // 3 minutes for AI response generation
|
|
}),
|
|
|
|
generateKeyThemes: (
|
|
focusGroupId: string,
|
|
temperature: number = 0.7
|
|
) =>
|
|
api.post('/focus-group-ai/generate-key-themes', {
|
|
focus_group_id: focusGroupId,
|
|
temperature: temperature
|
|
}, {
|
|
timeout: 180000 // 3 minutes for key theme generation
|
|
}),
|
|
|
|
getKeyThemes: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/key-themes/${focusGroupId}`),
|
|
|
|
deleteKeyTheme: (focusGroupId: string, themeId: string) =>
|
|
api.delete(`/focus-group-ai/key-themes/${focusGroupId}/${themeId}`),
|
|
|
|
// AI Moderator endpoints
|
|
getModeratorStatus: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/moderator/status/${focusGroupId}`),
|
|
|
|
advanceModeratorDiscussion: (focusGroupId: string) =>
|
|
api.post(`/focus-group-ai/moderator/advance/${focusGroupId}`, {}, {
|
|
timeout: 180000 // 3 minutes for AI moderator response
|
|
}),
|
|
|
|
setModeratorPosition: (focusGroupId: string, sectionId: string, itemId?: string) =>
|
|
api.put(`/focus-group-ai/moderator/position/${focusGroupId}`, {
|
|
section_id: sectionId,
|
|
item_id: itemId
|
|
}),
|
|
|
|
// Autonomous Conversation endpoints
|
|
startAutonomousConversation: (focusGroupId: string, initialPrompt?: string) =>
|
|
api.post(`/focus-group-ai/autonomous/start/${focusGroupId}`, {
|
|
initial_prompt: initialPrompt
|
|
}, {
|
|
timeout: 180000 // 3 minutes for autonomous conversation startup
|
|
}),
|
|
|
|
stopAutonomousConversation: (focusGroupId: string, reason?: string) =>
|
|
api.post(`/focus-group-ai/autonomous/stop/${focusGroupId}`, {
|
|
reason: reason
|
|
}),
|
|
|
|
getAutonomousConversationStatus: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/autonomous/status/${focusGroupId}`),
|
|
|
|
// Conversation State and Analytics endpoints
|
|
getConversationState: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/conversation/state/${focusGroupId}`),
|
|
|
|
getConversationAnalytics: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/conversation/analytics/${focusGroupId}`),
|
|
|
|
makeConversationDecision: (focusGroupId: string, temperature: number = 0.7, mode: string = 'ai') =>
|
|
api.post(`/focus-group-ai/conversation/decision/${focusGroupId}`, {
|
|
temperature: temperature,
|
|
mode: mode
|
|
}, {
|
|
timeout: 10000 // Returns 202 immediately
|
|
}),
|
|
|
|
getConversationInsights: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/conversation/insights/${focusGroupId}`, {
|
|
timeout: 180000 // LLM call, keep long timeout
|
|
}),
|
|
|
|
manualIntervention: (focusGroupId: string, action: string, message?: string, participantId?: string) =>
|
|
api.post(`/focus-group-ai/conversation/intervene/${focusGroupId}`, {
|
|
action: action,
|
|
message: message,
|
|
participant_id: participantId
|
|
}),
|
|
|
|
getReasoningHistory: (focusGroupId: string) =>
|
|
api.get(`/focus-group-ai/conversation/reasoning-history/${focusGroupId}`),
|
|
|
|
// Session ending endpoint
|
|
endSession: (focusGroupId: string, reason?: string) =>
|
|
api.post(`/focus-group-ai/moderator/end-session/${focusGroupId}`, {
|
|
reason: reason || 'session_ended'
|
|
})
|
|
};
|
|
|
|
// Folders endpoints
|
|
export const foldersApi = {
|
|
getAll: () =>
|
|
api.get('/folders'),
|
|
|
|
getById: (id: string) =>
|
|
api.get(`/folders/${id}`),
|
|
|
|
create: (folderData: any) =>
|
|
api.post('/folders', folderData),
|
|
|
|
update: (id: string, folderData: any) =>
|
|
api.put(`/folders/${id}`, folderData),
|
|
|
|
delete: (id: string) =>
|
|
api.delete(`/folders/${id}`),
|
|
|
|
addPersona: (folderId: string, personaId: string) =>
|
|
api.post(`/folders/${folderId}/personas`, { persona_id: personaId }),
|
|
|
|
removePersona: (folderId: string, personaId: string) =>
|
|
api.delete(`/folders/${folderId}/personas/${personaId}`),
|
|
|
|
addPersonasBatch: (folderId: string, personaIds: string[]) =>
|
|
api.post(`/folders/${folderId}/personas/batch`, { persona_ids: personaIds }),
|
|
|
|
removePersonasBatch: (folderId: string, personaIds: string[]) => {
|
|
return api.post(`/folders/${folderId}/personas/remove-batch`, {
|
|
persona_ids: personaIds
|
|
});
|
|
},
|
|
|
|
// Hierarchical operations
|
|
moveFolder: (folderId: string, parentFolderId: string | null) =>
|
|
api.put(`/folders/${folderId}/move`, { parent_folder_id: parentFolderId }),
|
|
|
|
getDescendants: (folderId: string) =>
|
|
api.get(`/folders/${folderId}/descendants`),
|
|
|
|
// New endpoints for multiple folder management
|
|
addPersonaToMultipleFolders: (personaId: string, folderIds: string[]) => {
|
|
// Add persona to multiple folders
|
|
const promises = folderIds.map(folderId =>
|
|
api.post(`/folders/${folderId}/personas`, { persona_id: personaId })
|
|
);
|
|
return Promise.all(promises);
|
|
},
|
|
|
|
removePersonaFromAllFolders: (personaId: string) => {
|
|
// This would require getting all folders first, then removing from each
|
|
// For now, we'll handle this on a per-folder basis in the UI
|
|
throw new Error("Use removePersona for specific folders");
|
|
}
|
|
};
|
|
|
|
// ─── Admin API ────────────────────────────────────────────────────────────────
|
|
|
|
export const adminApi = {
|
|
// Users
|
|
listUsers: (params?: { q?: string; role?: string; skip?: number; limit?: number }) =>
|
|
api.get('/admin/users', { params }),
|
|
|
|
getUser: (id: string) =>
|
|
api.get(`/admin/users/${id}`),
|
|
|
|
updateUser: (id: string, data: { role?: string; is_active?: boolean; quota?: { monthly_usd?: number }; override_quota?: boolean }) =>
|
|
api.put(`/admin/users/${id}`, data),
|
|
|
|
disableUser: (id: string) =>
|
|
api.post(`/admin/users/${id}/disable`),
|
|
|
|
enableUser: (id: string) =>
|
|
api.post(`/admin/users/${id}/enable`),
|
|
|
|
// Usage
|
|
usageSummary: (params?: { from?: string; to?: string; group_by?: string; user_id?: string; focus_group_id?: string }) =>
|
|
api.get('/admin/usage/summary', { params }),
|
|
|
|
usageEvents: (params?: { user_id?: string; focus_group_id?: string; feature?: string; skip?: number; limit?: number }) =>
|
|
api.get('/admin/usage/events', { params }),
|
|
|
|
// Pricing
|
|
listPricing: () =>
|
|
api.get('/admin/pricing'),
|
|
|
|
createPricing: (data: any) => api.post('/admin/pricing', data),
|
|
|
|
// Users — create + reset password
|
|
createUser: (data: { username: string; email: string; password: string; role?: string }) =>
|
|
api.post('/admin/users', data),
|
|
|
|
resetPassword: (id: string, password: string) =>
|
|
api.post(`/admin/users/${id}/reset-password`, { password }),
|
|
|
|
// Focus Groups (admin view)
|
|
listFocusGroups: (params?: { skip?: number; limit?: number }) =>
|
|
api.get('/admin/focus-groups', { params }),
|
|
};
|
|
|
|
export const usageApi = {
|
|
me: () => api.get('/auth/me'),
|
|
};
|
|
|
|
export default api; |