510 lines
14 KiB
TypeScript
510 lines
14 KiB
TypeScript
import { ApiRequestOptions } from '../hooks/useApi';
|
|
|
|
// ============================================================================
|
|
// TYPES & INTERFACES
|
|
// ============================================================================
|
|
|
|
export interface KnowledgePreferences {
|
|
schemaVersion?: number;
|
|
mailContentDepth?: 'metadata' | 'snippet' | 'full';
|
|
mailIndexAttachments?: boolean;
|
|
filesIndexBinaries?: boolean;
|
|
clickupScope?: 'titles' | 'title_description' | 'with_comments';
|
|
clickupIndexAttachments?: boolean;
|
|
maxAgeDays?: number;
|
|
}
|
|
|
|
export interface Connection {
|
|
id: string;
|
|
userId: string;
|
|
authority: 'local' | 'google' | 'msft' | 'clickup' | 'infomaniak';
|
|
externalId: string;
|
|
externalUsername: string;
|
|
externalEmail?: string;
|
|
status: 'active' | 'expired' | 'revoked' | 'pending';
|
|
connectedAt: number; // Backend uses float for UTC timestamp in seconds
|
|
lastChecked: number; // Backend uses float for UTC timestamp in seconds
|
|
expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds
|
|
knowledgeIngestionEnabled?: boolean;
|
|
knowledgePreferences?: KnowledgePreferences | null;
|
|
[key: string]: any; // Allow additional properties
|
|
}
|
|
|
|
export interface AttributeDefinition {
|
|
name: string;
|
|
label: string;
|
|
type: 'string' | 'number' | 'date' | 'boolean' | 'enum';
|
|
sortable?: boolean;
|
|
filterable?: boolean;
|
|
searchable?: boolean;
|
|
width?: number;
|
|
minWidth?: number;
|
|
maxWidth?: number;
|
|
filterOptions?: string[];
|
|
}
|
|
|
|
export interface PaginationParams {
|
|
page?: number;
|
|
pageSize?: number;
|
|
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
|
filters?: Record<string, any>;
|
|
search?: string;
|
|
/** Key of a saved view to apply (server loads groupByLevels, filters, sort from DB). */
|
|
viewKey?: string;
|
|
/** Explicit grouping levels; when sent (incl. []), overrides the view for this request. */
|
|
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
|
|
}
|
|
|
|
export interface GroupBand {
|
|
path: string[];
|
|
label: string;
|
|
startRowIndex: number;
|
|
rowCount: number;
|
|
}
|
|
|
|
export interface GroupLayout {
|
|
levels: string[];
|
|
bands: GroupBand[];
|
|
}
|
|
|
|
export interface PaginatedResponse<T> {
|
|
items: T[];
|
|
pagination?: {
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
};
|
|
groupLayout?: GroupLayout;
|
|
appliedView?: { viewKey?: string; displayName?: string };
|
|
}
|
|
|
|
export interface CreateConnectionData {
|
|
id?: string;
|
|
userId?: string;
|
|
authority?: 'msft' | 'google' | 'clickup' | 'infomaniak';
|
|
type?: 'msft' | 'google' | 'clickup' | 'infomaniak'; // Backend maps type → authority
|
|
externalId?: string;
|
|
externalUsername?: string;
|
|
externalEmail?: string;
|
|
status?: 'active' | 'expired' | 'revoked' | 'pending';
|
|
knowledgeIngestionEnabled?: boolean;
|
|
knowledgePreferences?: KnowledgePreferences | null;
|
|
connectedAt?: number;
|
|
lastChecked?: number;
|
|
expiresAt?: number;
|
|
}
|
|
|
|
export interface ConnectResponse {
|
|
authUrl: string;
|
|
}
|
|
|
|
// Type for the request function passed to API functions
|
|
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
|
|
|
|
// ============================================================================
|
|
// API REQUEST FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Fetch connection attributes from backend
|
|
* Endpoint: GET /api/attributes/UserConnection
|
|
*/
|
|
export async function fetchConnectionAttributes(_request: ApiRequestFunction): Promise<AttributeDefinition[]> {
|
|
// Note: This uses api.get directly due to response format handling
|
|
// For now, we'll use api.get directly in the hook as well
|
|
throw new Error('fetchConnectionAttributes should use api instance directly for response format handling');
|
|
}
|
|
|
|
/**
|
|
* Fetch list of connections with optional pagination
|
|
* Endpoint: GET /api/connections/
|
|
*/
|
|
export async function fetchConnections(
|
|
request: ApiRequestFunction,
|
|
params?: PaginationParams
|
|
): Promise<PaginatedResponse<Connection> | Connection[]> {
|
|
const requestParams: any = {};
|
|
|
|
// Build pagination object if provided
|
|
if (params) {
|
|
const paginationObj: any = {};
|
|
|
|
if (params.page !== undefined) paginationObj.page = params.page;
|
|
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
|
if (params.sort) paginationObj.sort = params.sort;
|
|
if (params.filters) paginationObj.filters = params.filters;
|
|
if (params.search) paginationObj.search = params.search;
|
|
if (params.viewKey) paginationObj.viewKey = params.viewKey;
|
|
if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
|
|
|
|
if (Object.keys(paginationObj).length > 0) {
|
|
requestParams.pagination = JSON.stringify(paginationObj);
|
|
}
|
|
}
|
|
|
|
const data = await request({
|
|
url: '/api/connections/',
|
|
method: 'get',
|
|
params: requestParams
|
|
});
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Create a new connection
|
|
* Endpoint: POST /api/connections/
|
|
*/
|
|
export async function createConnection(
|
|
request: ApiRequestFunction,
|
|
connectionData: CreateConnectionData
|
|
): Promise<Connection> {
|
|
return await request({
|
|
url: '/api/connections/',
|
|
method: 'post',
|
|
data: connectionData
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Connect to a service (initiate OAuth)
|
|
* Endpoint: POST /api/connections/{connectionId}/connect
|
|
*
|
|
* @param reauth If true, forces the OAuth provider to re-show the consent screen.
|
|
* Required when newly added scopes (e.g. Calendar/Contacts after a
|
|
* feature rollout) need to be granted on top of the existing token.
|
|
*/
|
|
export async function connectService(
|
|
request: ApiRequestFunction,
|
|
connectionId: string,
|
|
reauth: boolean = false
|
|
): Promise<ConnectResponse> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}/connect`,
|
|
method: 'post',
|
|
data: reauth ? { reauth: true } : undefined,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Disconnect from a service
|
|
* Endpoint: POST /api/connections/{connectionId}/disconnect
|
|
*/
|
|
export async function disconnectService(
|
|
request: ApiRequestFunction,
|
|
connectionId: string
|
|
): Promise<{ message: string }> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}/disconnect`,
|
|
method: 'post'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a connection
|
|
* Endpoint: DELETE /api/connections/{connectionId}
|
|
*/
|
|
export async function deleteConnection(
|
|
request: ApiRequestFunction,
|
|
connectionId: string
|
|
): Promise<{ message: string }> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}`,
|
|
method: 'delete'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update a connection
|
|
* Endpoint: PUT /api/connections/{connectionId}
|
|
*/
|
|
export async function updateConnection(
|
|
request: ApiRequestFunction,
|
|
connectionId: string,
|
|
updateData: Partial<Connection>
|
|
): Promise<Connection> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}`,
|
|
method: 'put',
|
|
data: updateData
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refresh Microsoft token
|
|
* Endpoint: POST /api/msft/refresh
|
|
*/
|
|
export async function refreshMicrosoftToken(
|
|
request: ApiRequestFunction,
|
|
connectionId: string
|
|
): Promise<Connection> {
|
|
return await request({
|
|
url: '/api/msft/refresh',
|
|
method: 'post',
|
|
data: { connectionId }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refresh Google token
|
|
* Endpoint: POST /api/google/refresh
|
|
*/
|
|
export async function refreshGoogleToken(
|
|
request: ApiRequestFunction,
|
|
connectionId: string
|
|
): Promise<Connection> {
|
|
return await request({
|
|
url: '/api/google/refresh',
|
|
method: 'post',
|
|
data: { connectionId }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Submit an Infomaniak Personal Access Token (kdrive + mail) for an existing
|
|
* UserConnection. The backend validates the token via /1/profile and stores it
|
|
* as the connection's data-access bearer token.
|
|
* Endpoint: POST /api/infomaniak/connections/{connectionId}/token
|
|
*/
|
|
export async function submitInfomaniakToken(
|
|
request: ApiRequestFunction,
|
|
connectionId: string,
|
|
token: string
|
|
): Promise<{
|
|
id: string;
|
|
status: string;
|
|
type: string;
|
|
externalUsername: string;
|
|
externalEmail?: string | null;
|
|
lastChecked: number;
|
|
}> {
|
|
return await request({
|
|
url: `/api/infomaniak/connections/${connectionId}/token`,
|
|
method: 'post',
|
|
data: { token }
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// RAG KNOWLEDGE CONSENT & CONTROL
|
|
// ============================================================================
|
|
|
|
export async function patchKnowledgeConsent(
|
|
request: ApiRequestFunction,
|
|
connectionId: string,
|
|
enabled: boolean
|
|
): Promise<{ connectionId: string; knowledgeIngestionEnabled: boolean; purged?: any; cancelledJobs?: number; bootstrapEnqueued?: boolean }> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}/knowledge-consent`,
|
|
method: 'patch',
|
|
data: { enabled }
|
|
});
|
|
}
|
|
|
|
export async function patchKnowledgePreferences(
|
|
request: ApiRequestFunction,
|
|
connectionId: string,
|
|
preferences: KnowledgePreferences
|
|
): Promise<{ connectionId: string; knowledgePreferences: KnowledgePreferences; updated: boolean }> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}/knowledge-preferences`,
|
|
method: 'patch',
|
|
data: { preferences }
|
|
});
|
|
}
|
|
|
|
export async function postKnowledgeStop(
|
|
request: ApiRequestFunction,
|
|
connectionId: string
|
|
): Promise<{ connectionId: string; cancelled: number }> {
|
|
return await request({
|
|
url: `/api/connections/${connectionId}/knowledge-stop`,
|
|
method: 'post'
|
|
});
|
|
}
|
|
|
|
export interface RagLimits {
|
|
maxItems?: number;
|
|
maxBytes?: number;
|
|
maxFileSize?: number;
|
|
maxDepth?: number;
|
|
// ClickUp variant
|
|
maxTasks?: number;
|
|
maxWorkspaces?: number;
|
|
maxListsPerWorkspace?: number;
|
|
}
|
|
|
|
export interface DataSourceSettings {
|
|
ragLimits?: RagLimits;
|
|
}
|
|
|
|
export interface CostEstimate {
|
|
estimatedTokens: number;
|
|
estimatedUsd: number;
|
|
basis: {
|
|
kind: string;
|
|
limits: Record<string, number>;
|
|
assumptions: Record<string, any>;
|
|
notes: string;
|
|
};
|
|
sourceId?: string;
|
|
}
|
|
|
|
export async function patchDataSourceSettings(
|
|
request: ApiRequestFunction,
|
|
dataSourceId: string,
|
|
settings: DataSourceSettings
|
|
): Promise<{ sourceId: string; settings: DataSourceSettings; updated: boolean }> {
|
|
return await request({
|
|
url: `/api/datasources/${dataSourceId}/settings`,
|
|
method: 'patch',
|
|
data: { settings }
|
|
});
|
|
}
|
|
|
|
export async function getDataSourceCostEstimate(
|
|
request: ApiRequestFunction,
|
|
dataSourceId: string
|
|
): Promise<CostEstimate> {
|
|
return await request({
|
|
url: `/api/datasources/${dataSourceId}/cost-estimate`,
|
|
method: 'get'
|
|
});
|
|
}
|
|
|
|
export interface PatchFlagResponse {
|
|
sourceId: string;
|
|
resetDescendantIds: string[];
|
|
updatedAncestors: { id: string; [key: string]: any }[];
|
|
[key: string]: any;
|
|
}
|
|
|
|
export async function patchDataSourceRagIndex(
|
|
request: ApiRequestFunction,
|
|
dataSourceId: string,
|
|
ragIndexEnabled: boolean | null
|
|
): Promise<PatchFlagResponse> {
|
|
return await request({
|
|
url: `/api/datasources/${dataSourceId}/rag-index`,
|
|
method: 'patch',
|
|
data: { ragIndexEnabled }
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// RAG INVENTORY
|
|
// ============================================================================
|
|
|
|
export interface RagDataSourceDto {
|
|
id: string;
|
|
label: string;
|
|
path: string;
|
|
sourceType: string;
|
|
/** Three-state inherit semantics on backend; UI reads as effective boolean from RAG inventory aggregator. */
|
|
ragIndexEnabled: boolean | null;
|
|
neutralize: boolean | null;
|
|
lastIndexed: number | null;
|
|
/** Distinct files indexed for this DataSource (one row per source document). */
|
|
fileCount: number;
|
|
/** Embedding-sized text fragments (one per ContentChunk row, ~400 tokens each). */
|
|
chunkCount: number;
|
|
}
|
|
|
|
export interface RagConnectionDto {
|
|
id: string;
|
|
authority: string;
|
|
externalEmail: string;
|
|
knowledgeIngestionEnabled: boolean;
|
|
preferences: KnowledgePreferences;
|
|
dataSources: RagDataSourceDto[];
|
|
totalFiles: number;
|
|
totalChunks: number;
|
|
runningJobs: {
|
|
jobId: string;
|
|
progress: number;
|
|
/** Already translated server-side. */
|
|
progressMessage: string;
|
|
}[];
|
|
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
|
|
lastSuccess?: {
|
|
jobId: string;
|
|
finishedAt: number | null;
|
|
indexed: number;
|
|
skippedDuplicate: number;
|
|
skippedPolicy: number;
|
|
failed: number;
|
|
durationMs: number;
|
|
/** Name of the first budget that bit (e.g. "maxBytes", "maxItems", "maxTasks"); null if walk completed naturally. */
|
|
stoppedAtLimit?: string | null;
|
|
/** Effective limits used by the walker, for showing the value next to the limit name. */
|
|
limits?: Record<string, number>;
|
|
bytesProcessed?: number;
|
|
} | null;
|
|
}
|
|
|
|
export interface RagFeatureDataSourceDto {
|
|
id: string;
|
|
label: string;
|
|
tableName: string;
|
|
featureCode: string;
|
|
ragIndexEnabled: boolean;
|
|
}
|
|
|
|
export interface RagFeatureInstanceDto {
|
|
featureInstanceId: string;
|
|
featureCode: string;
|
|
label: string;
|
|
mandateId: string;
|
|
fileCount: number;
|
|
chunkCount: number;
|
|
statusCounts: Record<string, number>;
|
|
dataSources: RagFeatureDataSourceDto[];
|
|
ragEnabled: boolean;
|
|
runningJobs?: {
|
|
jobId: string;
|
|
progress: number;
|
|
progressMessage: string;
|
|
}[];
|
|
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
|
|
lastSuccess?: {
|
|
jobId: string;
|
|
finishedAt: number | null;
|
|
indexed: number;
|
|
skippedDuplicate: number;
|
|
failed: number;
|
|
} | null;
|
|
}
|
|
|
|
export interface RagInventoryDto {
|
|
connections: RagConnectionDto[];
|
|
featureInstances?: RagFeatureInstanceDto[];
|
|
totals: { files: number; chunks: number; bytes?: number };
|
|
}
|
|
|
|
export interface RagActiveJobDto {
|
|
jobId: string;
|
|
connectionId: string;
|
|
connectionLabel?: string;
|
|
jobType: string;
|
|
progress: number | null;
|
|
/** Already translated server-side. */
|
|
progressMessage: string;
|
|
}
|
|
|
|
export async function getRagInventoryMe(request: ApiRequestFunction): Promise<RagInventoryDto> {
|
|
return await request({ url: '/api/rag/inventory/me', method: 'get' });
|
|
}
|
|
|
|
export async function getRagInventoryMandate(request: ApiRequestFunction): Promise<RagInventoryDto> {
|
|
return await request({ url: '/api/rag/inventory/mandate', method: 'get' });
|
|
}
|
|
|
|
export async function getRagInventoryPlatform(request: ApiRequestFunction): Promise<any> {
|
|
return await request({ url: '/api/rag/inventory/platform', method: 'get' });
|
|
}
|
|
|
|
export async function getRagActiveJobs(request: ApiRequestFunction): Promise<RagActiveJobDto[]> {
|
|
return await request({ url: '/api/rag/inventory/jobs', method: 'get' });
|
|
}
|
|
|