frontend_nyla/src/api/commcoachApi.ts

549 lines
19 KiB
TypeScript

import api from '../api';
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils';
import { ApiRequestOptions } from '../hooks/useApi';
// ============================================================================
// Types
// ============================================================================
export interface CoachingContext {
id: string;
userId: string;
mandateId: string;
instanceId: string;
title: string;
description?: string;
category: string;
status: string;
goals?: string;
insights?: string;
sessionCount: number;
taskCount: number;
lastSessionAt?: string;
createdAt?: string;
updatedAt?: string;
}
export interface CoachingSession {
id: string;
contextId: string;
userId: string;
status: string;
personaId?: string;
summary?: string;
durationSeconds: number;
messageCount: number;
competenceScore?: number;
emailSent: boolean;
startedAt?: string;
endedAt?: string;
}
export interface CoachingPersona {
id: string;
userId: string;
key: string;
label: string;
description: string;
gender?: string;
category: string;
isActive: boolean;
}
export interface CoachingBadge {
id: string;
userId: string;
badgeKey: string;
label?: string;
description?: string;
icon?: string;
awardedAt?: string;
}
export interface CoachingMessage {
id: string;
sessionId: string;
contextId: string;
role: 'user' | 'assistant' | 'system';
content: string;
contentType: string;
audioRef?: string;
createdAt?: string;
}
export interface CoachingTask {
id: string;
contextId: string;
sessionId?: string;
title: string;
description?: string;
status: string;
priority: string;
dueDate?: string;
completedAt?: string;
createdAt?: string;
}
export interface CoachingScore {
id: string;
contextId: string;
sessionId: string;
dimension: string;
score: number;
trend: string;
evidence?: string;
createdAt?: string;
}
export interface CoachingUserProfile {
id: string;
userId: string;
dailyReminderTime?: string;
dailyReminderEnabled: boolean;
emailSummaryEnabled: boolean;
streakDays: number;
longestStreak: number;
totalSessions: number;
totalMinutes: number;
lastSessionAt?: string;
}
export interface DashboardData {
totalContexts: number;
activeContexts: number;
totalSessions: number;
totalMinutes: number;
streakDays: number;
longestStreak: number;
averageScore?: number;
recentScores: CoachingScore[];
openTasks: number;
completedTasks: number;
goalProgress?: number;
badges?: CoachingBadge[];
level?: { number: number; label: string; totalSessions: number };
contexts: Array<{ id: string; title: string; category: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>;
}
export interface SSEEvent {
type: string;
data?: any;
timestamp?: string;
}
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
// ============================================================================
// Context API
// ============================================================================
export async function getContextsApi(request: ApiRequestFunction, instanceId: string): Promise<CoachingContext[]> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts`, method: 'get' });
return data.contexts || [];
}
export async function createContextApi(request: ApiRequestFunction, instanceId: string, body: {
title: string; description?: string; category?: string; goals?: string[];
}): Promise<CoachingContext> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts`, method: 'post', data: body });
return data.context;
}
export async function getContextDetailApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<{
context: CoachingContext; tasks: CoachingTask[]; scores: CoachingScore[]; sessions: CoachingSession[];
}> {
const data = await request({
url: `/api/commcoach/${instanceId}/contexts/${contextId}`,
method: 'get',
params: { _t: Date.now() },
});
const ctx = data?.context ?? data;
return {
context: ctx,
tasks: data?.tasks ?? [],
scores: data?.scores ?? [],
sessions: data?.sessions ?? [],
};
}
export async function updateContextApi(request: ApiRequestFunction, instanceId: string, contextId: string, body: any): Promise<CoachingContext> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}`, method: 'put', data: body });
return data.context;
}
export async function deleteContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<void> {
await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}`, method: 'delete' });
}
export async function archiveContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<CoachingContext> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/archive`, method: 'post' });
return data.context;
}
export async function activateContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<CoachingContext> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/activate`, method: 'post' });
return data.context;
}
// ============================================================================
// Session API
// ============================================================================
export async function startSessionApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<{
session: CoachingSession; messages: CoachingMessage[]; resumed: boolean;
}> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start`, method: 'post' });
return data;
}
export async function startSessionStreamApi(
instanceId: string,
contextId: string,
onEvent: (event: SSEEvent) => void,
onError?: (error: Error) => void,
onComplete?: () => void,
personaId?: string,
): Promise<void> {
try {
const baseURL = api.defaults.baseURL || '';
const personaParam = personaId ? `?personaId=${encodeURIComponent(personaId)}` : '';
const url = `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start${personaParam}`;
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
const authToken = localStorage.getItem('authToken');
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
if (!getCSRFToken()) generateAndStoreCSRFToken();
addCSRFTokenToHeaders(headers);
const response = await fetch(url, {
method: 'POST',
headers,
credentials: 'include',
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
if (!response.body) throw new Error('Response body is null');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.slice(6);
if (jsonStr.trim()) {
const event: SSEEvent = JSON.parse(jsonStr);
onEvent(event);
}
} catch {
// skip malformed lines
}
}
}
}
onComplete?.();
} finally {
reader.releaseLock();
}
} catch (error: any) {
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
else throw error;
}
}
export async function getSessionApi(request: ApiRequestFunction, instanceId: string, sessionId: string): Promise<{
session: CoachingSession; messages: CoachingMessage[];
}> {
const data = await request({ url: `/api/commcoach/${instanceId}/sessions/${sessionId}`, method: 'get' });
return data;
}
export async function completeSessionApi(request: ApiRequestFunction, instanceId: string, sessionId: string): Promise<CoachingSession> {
const data = await request({ url: `/api/commcoach/${instanceId}/sessions/${sessionId}/complete`, method: 'post' });
return data.session;
}
export async function cancelSessionApi(request: ApiRequestFunction, instanceId: string, sessionId: string): Promise<void> {
await request({ url: `/api/commcoach/${instanceId}/sessions/${sessionId}/cancel`, method: 'post' });
}
// ============================================================================
// Streaming Chat API
// ============================================================================
export interface SendMessageOptions {
fileIds?: string[];
dataSourceIds?: string[];
featureDataSourceIds?: string[];
allowedProviders?: string[];
}
export async function sendMessageStreamApi(
instanceId: string,
sessionId: string,
content: string,
onEvent: (event: SSEEvent) => void,
onError?: (error: Error) => void,
onComplete?: () => void,
signal?: AbortSignal,
options?: SendMessageOptions,
): Promise<void> {
try {
const baseURL = api.defaults.baseURL || '';
const url = `${baseURL}/api/commcoach/${instanceId}/sessions/${sessionId}/message/stream`;
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
const authToken = localStorage.getItem('authToken');
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
if (!getCSRFToken()) generateAndStoreCSRFToken();
addCSRFTokenToHeaders(headers);
const body: Record<string, unknown> = { content };
if (options?.fileIds?.length) body.fileIds = options.fileIds;
if (options?.dataSourceIds?.length) body.dataSourceIds = options.dataSourceIds;
if (options?.featureDataSourceIds?.length) body.featureDataSourceIds = options.featureDataSourceIds;
if (options?.allowedProviders?.length) body.allowedProviders = options.allowedProviders;
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(body),
credentials: 'include',
signal,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
if (!response.body) throw new Error('Response body is null');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.slice(6);
if (jsonStr.trim()) {
const event: SSEEvent = JSON.parse(jsonStr);
onEvent(event);
}
} catch {
// skip malformed lines
}
}
}
}
onComplete?.();
} finally {
reader.releaseLock();
}
} catch (error: any) {
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
else throw error;
}
}
export async function sendAudioStreamApi(
instanceId: string,
sessionId: string,
audioBlob: Blob,
onEvent: (event: SSEEvent) => void,
onError?: (error: Error) => void,
onComplete?: () => void,
): Promise<void> {
try {
const baseURL = api.defaults.baseURL || '';
const url = `${baseURL}/api/commcoach/${instanceId}/sessions/${sessionId}/audio/stream`;
const headers: Record<string, string> = { 'Content-Type': 'application/octet-stream' };
const authToken = localStorage.getItem('authToken');
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
const pathMatch = window.location.pathname.match(/^\/mandates\/([^/]+)\/([^/]+)\/([^/]+)/);
if (pathMatch) {
headers['X-Mandate-Id'] = pathMatch[1];
headers['X-Instance-Id'] = pathMatch[3];
}
if (!getCSRFToken()) generateAndStoreCSRFToken();
addCSRFTokenToHeaders(headers);
const audioBuffer = await audioBlob.arrayBuffer();
const response = await fetch(url, {
method: 'POST',
headers,
body: audioBuffer,
credentials: 'include',
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
if (!response.body) throw new Error('Response body is null');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const jsonStr = line.slice(6);
if (jsonStr.trim()) onEvent(JSON.parse(jsonStr));
} catch { /* skip */ }
}
}
}
onComplete?.();
} finally {
reader.releaseLock();
}
} catch (error: any) {
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
else throw error;
}
}
// ============================================================================
// Task API
// ============================================================================
export async function getTasksApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<CoachingTask[]> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/tasks`, method: 'get' });
return data.tasks || [];
}
export async function createTaskApi(request: ApiRequestFunction, instanceId: string, contextId: string, body: {
title: string; description?: string; priority?: string; dueDate?: string;
}): Promise<CoachingTask> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/tasks`, method: 'post', data: body });
return data.task;
}
export async function updateTaskApi(request: ApiRequestFunction, instanceId: string, taskId: string, body: any): Promise<CoachingTask> {
const data = await request({ url: `/api/commcoach/${instanceId}/tasks/${taskId}`, method: 'put', data: body });
return data.task;
}
export async function updateTaskStatusApi(request: ApiRequestFunction, instanceId: string, taskId: string, status: string): Promise<CoachingTask> {
const data = await request({ url: `/api/commcoach/${instanceId}/tasks/${taskId}/status`, method: 'put', data: { status } });
return data.task;
}
export async function deleteTaskApi(request: ApiRequestFunction, instanceId: string, taskId: string): Promise<void> {
await request({ url: `/api/commcoach/${instanceId}/tasks/${taskId}`, method: 'delete' });
}
// ============================================================================
// Dashboard API
// ============================================================================
export async function getDashboardApi(request: ApiRequestFunction, instanceId: string): Promise<DashboardData> {
const data = await request({ url: `/api/commcoach/${instanceId}/dashboard`, method: 'get' });
return data.dashboard;
}
// ============================================================================
// Profile API
// ============================================================================
export async function getProfileApi(request: ApiRequestFunction, instanceId: string): Promise<CoachingUserProfile> {
const data = await request({ url: `/api/commcoach/${instanceId}/profile`, method: 'get' });
return data.profile;
}
export async function updateProfileApi(request: ApiRequestFunction, instanceId: string, body: any): Promise<CoachingUserProfile> {
const data = await request({ url: `/api/commcoach/${instanceId}/profile`, method: 'put', data: body });
return data.profile;
}
// ============================================================================
// Persona API (Iteration 2)
// ============================================================================
export async function getPersonasApi(request: ApiRequestFunction, instanceId: string): Promise<CoachingPersona[]> {
const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'get' });
return data.personas || [];
}
export async function createPersonaApi(request: ApiRequestFunction, instanceId: string, body: {
label: string; description: string; gender?: string; systemPromptOverride?: string;
}): Promise<CoachingPersona> {
const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'post', data: body });
return data.persona;
}
export async function deletePersonaApi(request: ApiRequestFunction, instanceId: string, personaId: string): Promise<void> {
await request({ url: `/api/commcoach/${instanceId}/personas/${personaId}`, method: 'delete' });
}
// ============================================================================
// Badge API (Iteration 2)
// ============================================================================
export async function getBadgesApi(request: ApiRequestFunction, instanceId: string): Promise<CoachingBadge[]> {
const data = await request({ url: `/api/commcoach/${instanceId}/badges`, method: 'get' });
return data.badges || [];
}
// ============================================================================
// Export API (Iteration 2)
// ============================================================================
export function getDossierExportUrl(instanceId: string, contextId: string, format: string = 'md'): string {
const baseURL = api.defaults.baseURL || '';
return `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/export?format=${format}`;
}
export function getSessionExportUrl(instanceId: string, sessionId: string, format: string = 'md'): string {
const baseURL = api.defaults.baseURL || '';
return `${baseURL}/api/commcoach/${instanceId}/sessions/${sessionId}/export?format=${format}`;
}
// ============================================================================
// Score History API (Iteration 2)
// ============================================================================
export async function getScoreHistoryApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<Record<string, Array<{
score: number; trend: string; evidence?: string; createdAt?: string; sessionId?: string;
}>>> {
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/scores/history`, method: 'get' });
return data.history || {};
}