549 lines
19 KiB
TypeScript
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 || {};
|
|
}
|