Backend changes: - Add admin analytics endpoints for daily usage per user - Add GET /tokens/daily-users endpoint with date/user breakdown - Update OpenAI SDK from 1.58.1 to 2.6.1 - Switch from Assistants API to Responses API with file_search tool - Implement strict RAG-only system instructions - Add citation validation to prevent hallucinations - Add get_daily_usage_by_user repository method - Add DailyUserUsage schema for admin analytics Frontend changes: - Implement comprehensive admin usage dashboard - Add overall system statistics (users, conversations, messages, tokens, cost) - Add daily usage table with per-user breakdown - Add chat state clearing on logout and user change for isolation - Center welcome message and input field in chat interface - Add admin-specific styling for usage analytics tables - Fix useCallback dependencies to prevent infinite loops Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
159 lines
4.5 KiB
TypeScript
159 lines
4.5 KiB
TypeScript
/**
|
|
* API Service
|
|
*
|
|
* Axios configuration and API client for backend communication
|
|
*/
|
|
|
|
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
|
|
|
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/v1';
|
|
|
|
// Create axios instance
|
|
const apiClient: AxiosInstance = axios.create({
|
|
baseURL: API_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Request interceptor to add auth token
|
|
apiClient.interceptors.request.use(
|
|
(config: InternalAxiosRequestConfig) => {
|
|
const token = localStorage.getItem('access_token');
|
|
|
|
if (token && config.headers) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
return config;
|
|
},
|
|
(error) => {
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Response interceptor for error handling
|
|
apiClient.interceptors.response.use(
|
|
(response) => response,
|
|
async (error) => {
|
|
const originalRequest = error.config;
|
|
|
|
// Handle 401 errors (token expired)
|
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
originalRequest._retry = true;
|
|
|
|
try {
|
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
|
|
if (refreshToken) {
|
|
const response = await axios.post(`${API_URL}/auth/refresh`, {
|
|
refresh_token: refreshToken,
|
|
});
|
|
|
|
const { access_token } = response.data;
|
|
localStorage.setItem('access_token', access_token);
|
|
|
|
// Retry original request with new token
|
|
originalRequest.headers.Authorization = `Bearer ${access_token}`;
|
|
return apiClient(originalRequest);
|
|
}
|
|
} catch (refreshError) {
|
|
// Refresh failed, clear tokens and redirect to login
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
window.location.href = '/';
|
|
return Promise.reject(refreshError);
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
export default apiClient;
|
|
|
|
// API methods
|
|
export const authAPI = {
|
|
login: (idToken: string) =>
|
|
apiClient.post('/auth/login', { id_token: idToken }),
|
|
|
|
loginSimple: (email: string, password: string) =>
|
|
apiClient.post('/auth/login/simple', { email, password }),
|
|
|
|
logout: () =>
|
|
apiClient.post('/auth/logout'),
|
|
|
|
getCurrentUser: () =>
|
|
apiClient.get('/auth/me'),
|
|
|
|
refreshToken: (refreshToken: string) =>
|
|
apiClient.post('/auth/refresh', { refresh_token: refreshToken }),
|
|
};
|
|
|
|
export const conversationAPI = {
|
|
list: (includeArchived: boolean = false, skip: number = 0, limit: number = 50) =>
|
|
apiClient.get('/conversations', {
|
|
params: { include_archived: includeArchived, skip, limit },
|
|
}),
|
|
|
|
create: (title?: string) =>
|
|
apiClient.post('/conversations', { title }),
|
|
|
|
get: (id: string) =>
|
|
apiClient.get(`/conversations/${id}`),
|
|
|
|
update: (id: string, title: string) =>
|
|
apiClient.put(`/conversations/${id}`, { title }),
|
|
|
|
archive: (id: string) =>
|
|
apiClient.post(`/conversations/${id}/archive`),
|
|
|
|
delete: (id: string) =>
|
|
apiClient.delete(`/conversations/${id}`),
|
|
};
|
|
|
|
export const messageAPI = {
|
|
list: (conversationId: string, skip: number = 0, limit: number = 100) =>
|
|
apiClient.get(`/conversations/${conversationId}/messages`, {
|
|
params: { skip, limit },
|
|
}),
|
|
|
|
send: (conversationId: string, content: string) =>
|
|
apiClient.post(`/conversations/${conversationId}/messages`, { content }),
|
|
};
|
|
|
|
export const tokenAPI = {
|
|
getUsage: (days: number = 30) =>
|
|
apiClient.get('/tokens/usage', { params: { days } }),
|
|
|
|
getUsersUsage: (days: number = 30) =>
|
|
apiClient.get('/tokens/users', { params: { days } }),
|
|
|
|
getDailyUsageByUser: (days: number = 30) =>
|
|
apiClient.get('/tokens/daily-users', { params: { days } }),
|
|
};
|
|
|
|
export const adminAPI = {
|
|
// User management
|
|
listUsers: (skip: number = 0, limit: number = 100) =>
|
|
apiClient.get('/admin/users', { params: { skip, limit } }),
|
|
|
|
updateUserRole: (userId: string, role: string) =>
|
|
apiClient.put(`/admin/users/${userId}/role`, { role }),
|
|
|
|
activateUser: (userId: string) =>
|
|
apiClient.put(`/admin/users/${userId}/activate`),
|
|
|
|
deactivateUser: (userId: string) =>
|
|
apiClient.put(`/admin/users/${userId}/deactivate`),
|
|
|
|
// Analytics
|
|
getSystemAnalytics: () =>
|
|
apiClient.get('/admin/analytics/system'),
|
|
|
|
getUsersAnalytics: (days: number = 30, limit: number = 50) =>
|
|
apiClient.get('/admin/analytics/users', { params: { days, limit } }),
|
|
|
|
getAllConversations: (skip: number = 0, limit: number = 50) =>
|
|
apiClient.get('/admin/conversations/all', { params: { skip, limit } }),
|
|
};
|