Skip to content

Commit 42b3e12

Browse files
Reafctor MACAE-V4 UI
1 parent fba0395 commit 42b3e12

39 files changed

Lines changed: 1979 additions & 1021 deletions

src/frontend/src/api/apiClient.tsx

Lines changed: 43 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,52 @@
1-
import { headerBuilder, getApiUrl } from './config';
2-
3-
// Helper function to build URL with query parameters
4-
const buildUrl = (url: string, params?: Record<string, any>): string => {
5-
if (!params) return url;
6-
7-
const searchParams = new URLSearchParams();
8-
Object.entries(params).forEach(([key, value]) => {
9-
if (value !== undefined && value !== null) {
10-
searchParams.append(key, String(value));
11-
}
12-
});
13-
14-
const queryString = searchParams.toString();
15-
return queryString ? `${url}?${queryString}` : url;
16-
};
17-
18-
// Fetch with Authentication Headers
19-
const fetchWithAuth = async (url: string, method: string = "GET", body: BodyInit | null = null) => {
20-
const token = localStorage.getItem('token'); // Get the token from localStorage
21-
const authHeaders = headerBuilder(); // Get authentication headers
22-
23-
const headers: Record<string, string> = {
24-
...authHeaders, // Include auth headers from headerBuilder
25-
};
26-
27-
if (token) {
28-
headers['Authorization'] = `Bearer ${token}`; // Add the token to the Authorization header
29-
}
30-
31-
// If body is FormData, do not set Content-Type header
32-
if (body && body instanceof FormData) {
33-
delete headers['Content-Type'];
34-
} else {
35-
headers['Content-Type'] = 'application/json';
36-
body = body ? JSON.stringify(body) : null;
1+
/**
2+
* API Client — thin adapter over the centralized httpClient.
3+
*
4+
* Auth headers (x-ms-client-principal-id, Authorization) are now injected
5+
* automatically by httpClient's request interceptor, eliminating all manual
6+
* headerBuilder() / localStorage.getItem('token') calls.
7+
*/
8+
import httpClient from './httpClient';
9+
import { getApiUrl } from './config';
10+
11+
/**
12+
* Ensure httpClient's base URL stays in sync with the runtime config.
13+
* Called lazily on every request so it picks up late-initialized API_URL.
14+
*/
15+
function syncBaseUrl(): void {
16+
const apiUrl = getApiUrl();
17+
if (apiUrl && httpClient.getBaseUrl() !== apiUrl) {
18+
httpClient.setBaseUrl(apiUrl);
3719
}
20+
}
3821

39-
const options: RequestInit = {
40-
method,
41-
headers,
42-
body: body || undefined,
43-
};
44-
45-
try {
46-
const apiUrl = getApiUrl();
47-
const finalUrl = `${apiUrl}${url}`;
48-
// Log the request details
49-
const response = await fetch(finalUrl, options);
50-
51-
if (!response.ok) {
52-
const errorText = await response.text();
53-
throw new Error(errorText || 'Something went wrong');
54-
}
55-
56-
const isJson = response.headers.get('content-type')?.includes('application/json');
57-
const responseData = isJson ? await response.json() : null;
58-
return responseData;
59-
} catch (error) {
60-
console.info('API Error:', (error as Error).message);
61-
throw error;
62-
}
63-
};
22+
export const apiClient = {
23+
get: <T = any>(url: string, config?: { params?: Record<string, unknown> }): Promise<T> => {
24+
syncBaseUrl();
25+
return httpClient.get<T>(url, { params: config?.params });
26+
},
6427

65-
// Vanilla Fetch without Auth for Login
66-
const fetchWithoutAuth = async (url: string, method: string = "POST", body: BodyInit | null = null) => {
67-
const headers: Record<string, string> = {
68-
'Content-Type': 'application/json',
69-
};
28+
post: <T = any>(url: string, body?: unknown): Promise<T> => {
29+
syncBaseUrl();
30+
return httpClient.post<T>(url, body);
31+
},
7032

71-
const options: RequestInit = {
72-
method,
73-
headers,
74-
body: body ? JSON.stringify(body) : undefined,
75-
};
33+
put: <T = any>(url: string, body?: unknown): Promise<T> => {
34+
syncBaseUrl();
35+
return httpClient.put<T>(url, body);
36+
},
7637

77-
try {
78-
const apiUrl = getApiUrl();
79-
const response = await fetch(`${apiUrl}${url}`, options);
38+
delete: <T = any>(url: string): Promise<T> => {
39+
syncBaseUrl();
40+
return httpClient.del<T>(url);
41+
},
8042

81-
if (!response.ok) {
82-
const errorText = await response.text();
83-
throw new Error(errorText || 'Login failed');
84-
}
85-
const isJson = response.headers.get('content-type')?.includes('application/json');
86-
return isJson ? await response.json() : null;
87-
} catch (error) {
88-
console.log('Login Error:', (error as Error).message);
89-
throw error;
90-
}
91-
};
43+
upload: <T = any>(url: string, formData: FormData): Promise<T> => {
44+
syncBaseUrl();
45+
return httpClient.upload<T>(url, formData);
46+
},
9247

93-
// Authenticated requests (with token) and login (without token)
94-
export const apiClient = {
95-
get: (url: string, config?: { params?: Record<string, any> }) => {
96-
const finalUrl = buildUrl(url, config?.params);
97-
return fetchWithAuth(finalUrl, 'GET');
48+
login: <T = any>(url: string, body?: unknown): Promise<T> => {
49+
syncBaseUrl();
50+
return httpClient.postWithoutAuth<T>(url, body);
9851
},
99-
post: (url: string, body?: any) => fetchWithAuth(url, 'POST', body),
100-
put: (url: string, body?: any) => fetchWithAuth(url, 'PUT', body),
101-
delete: (url: string) => fetchWithAuth(url, 'DELETE'),
102-
upload: (url: string, formData: FormData) => fetchWithAuth(url, 'POST', formData),
103-
login: (url: string, body?: any) => fetchWithoutAuth(url, 'POST', body), // For login without auth
10452
};

src/frontend/src/api/apiService.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ export class APIService {
156156
if (!data) {
157157
throw new Error(`Plan with ID ${planId} not found`);
158158
}
159-
console.log('Fetched plan by ID:', data);
160159
const results = {
161160
plan: data.plan as Plan,
162161
messages: data.messages as AgentMessageBE[],
@@ -190,8 +189,6 @@ export class APIService {
190189
const requestKey = `approve-plan-${planApprovalData.m_plan_id}`;
191190

192191
return this._requestTracker.trackRequest(requestKey, async () => {
193-
console.log('📤 Approving plan via v4 API:', planApprovalData);
194-
195192
const response = await apiClient.post(API_ENDPOINTS.PLAN_APPROVAL, planApprovalData);
196193

197194
// Invalidate cache since plan execution will start
@@ -200,7 +197,6 @@ export class APIService {
200197
this._cache.invalidate(new RegExp(`^plan.*_${planApprovalData.plan_id}`));
201198
}
202199

203-
console.log('✅ Plan approval successful:', response);
204200
return response;
205201
});
206202
}
@@ -260,13 +256,7 @@ export class APIService {
260256
return response;
261257
}
262258
async sendAgentMessage(data: AgentMessageResponse): Promise<AgentMessage> {
263-
const t0 = performance.now();
264259
const result = await apiClient.post(API_ENDPOINTS.AGENT_MESSAGE, data);
265-
console.log('[agent_message] sent', {
266-
ms: +(performance.now() - t0).toFixed(1),
267-
agent: data.agent,
268-
type: data.agent_type
269-
});
270260
return result;
271261
}
272262
}

src/frontend/src/api/apiUtils.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* API Utility Functions
3+
*
4+
* Centralized helpers for error response construction, retry logic,
5+
* and request deduplication. Single source of truth — eliminates
6+
* duplicated error patterns across API functions.
7+
*/
8+
9+
/**
10+
* Create a standardized error response object.
11+
* Replaces repeated `{ ...new Response(), ok: false, status: 500 }` patterns.
12+
*/
13+
export function createErrorResponse(status: number, message: string): Response {
14+
return new Response(JSON.stringify({ error: message }), {
15+
status,
16+
statusText: message,
17+
headers: { 'Content-Type': 'application/json' },
18+
});
19+
}
20+
21+
/**
22+
* Retry a request with exponential backoff.
23+
* @param fn - The async function to retry
24+
* @param maxRetries - Maximum number of retry attempts (default: 3)
25+
* @param baseDelay - Base delay in ms before exponential increase (default: 1000)
26+
*/
27+
export async function retryRequest<T>(
28+
fn: () => Promise<T>,
29+
maxRetries = 3,
30+
baseDelay = 1000
31+
): Promise<T> {
32+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
33+
try {
34+
return await fn();
35+
} catch (error) {
36+
if (attempt === maxRetries) throw error;
37+
const delay = baseDelay * Math.pow(2, attempt);
38+
await new Promise((resolve) => setTimeout(resolve, delay));
39+
}
40+
}
41+
throw new Error('Max retries exceeded');
42+
}
43+
44+
/**
45+
* Request cache with TTL and deduplication of in-flight requests.
46+
* Prevents duplicate API calls for the same data.
47+
*/
48+
interface CacheEntry<T> {
49+
data: T;
50+
timestamp: number;
51+
expiresAt: number;
52+
}
53+
54+
export class RequestCache {
55+
private cache = new Map<string, CacheEntry<unknown>>();
56+
private pendingRequests = new Map<string, Promise<unknown>>();
57+
58+
/** Get cached data or fetch it, deduplicating concurrent identical requests */
59+
async get<T>(
60+
key: string,
61+
fetcher: () => Promise<T>,
62+
ttlMs = 30000
63+
): Promise<T> {
64+
// Return cached data if still fresh
65+
const cached = this.cache.get(key);
66+
if (cached && Date.now() < cached.expiresAt) {
67+
return cached.data as T;
68+
}
69+
70+
// Deduplicate concurrent identical requests
71+
const pending = this.pendingRequests.get(key);
72+
if (pending) {
73+
return pending as Promise<T>;
74+
}
75+
76+
const request = fetcher()
77+
.then((data) => {
78+
this.cache.set(key, {
79+
data,
80+
timestamp: Date.now(),
81+
expiresAt: Date.now() + ttlMs,
82+
});
83+
this.pendingRequests.delete(key);
84+
return data;
85+
})
86+
.catch((error) => {
87+
this.pendingRequests.delete(key);
88+
throw error;
89+
});
90+
91+
this.pendingRequests.set(key, request);
92+
return request;
93+
}
94+
95+
/** Invalidate cached entries matching a key pattern */
96+
invalidate(pattern?: string | RegExp): void {
97+
if (!pattern) {
98+
this.cache.clear();
99+
return;
100+
}
101+
for (const key of this.cache.keys()) {
102+
const matches = typeof pattern === 'string'
103+
? key.includes(pattern)
104+
: pattern.test(key);
105+
if (matches) this.cache.delete(key);
106+
}
107+
}
108+
109+
/** Clear all cached data */
110+
clear(): void {
111+
this.cache.clear();
112+
this.pendingRequests.clear();
113+
}
114+
}
115+
116+
/** Shared request cache singleton */
117+
export const requestCache = new RequestCache();
118+
119+
/**
120+
* Debounce utility — delays calling `fn` until `delayMs` has elapsed
121+
* since the last invocation.
122+
*/
123+
export function debounce<T extends (...args: unknown[]) => void>(
124+
fn: T,
125+
delayMs: number
126+
): (...args: Parameters<T>) => void {
127+
let timer: ReturnType<typeof setTimeout>;
128+
return (...args: Parameters<T>) => {
129+
clearTimeout(timer);
130+
timer = setTimeout(() => fn(...args), delayMs);
131+
};
132+
}
133+
134+
/**
135+
* Throttle utility — ensures `fn` is called at most once per `limitMs`.
136+
*/
137+
export function throttle<T extends (...args: unknown[]) => void>(
138+
fn: T,
139+
limitMs: number
140+
): (...args: Parameters<T>) => void {
141+
let lastCall = 0;
142+
return (...args: Parameters<T>) => {
143+
const now = Date.now();
144+
if (now - lastCall >= limitMs) {
145+
lastCall = now;
146+
fn(...args);
147+
}
148+
};
149+
}

src/frontend/src/api/config.tsx

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ export async function getUserInfo(): Promise<UserInfo> {
5252
try {
5353
const response = await fetch("/.auth/me");
5454
if (!response.ok) {
55-
console.log(
56-
"No identity provider found. Access to chat will be blocked."
57-
);
5855
return {} as UserInfo;
5956
}
6057
const payload = await response.json();
@@ -97,40 +94,20 @@ export function getUserInfoGlobal() {
9794
}
9895

9996
if (!USER_INFO) {
100-
// console.info('User info not yet configured');
10197
return null;
10298
}
10399

104100
return USER_INFO;
105101
}
106102

107103
export function getUserId(): string {
108-
// USER_ID = getUserInfoGlobal()?.user_id || null;
109104
if (!USER_ID) {
110105
USER_ID = getUserInfoGlobal()?.user_id || null;
111106
}
112107
const userId = USER_ID ?? "00000000-0000-0000-0000-000000000000";
113108
return userId;
114109
}
115110

116-
/**
117-
* Build headers with authentication information
118-
* @param headers Optional additional headers to merge
119-
* @returns Combined headers object with authentication
120-
*/
121-
export function headerBuilder(headers?: Record<string, string>): Record<string, string> {
122-
let userId = getUserId();
123-
//console.log('headerBuilder: Using user ID:', userId);
124-
let defaultHeaders = {
125-
"x-ms-client-principal-id": String(userId) || "", // Custom header
126-
};
127-
//console.log('headerBuilder: Created headers:', defaultHeaders);
128-
return {
129-
...defaultHeaders,
130-
...(headers ? headers : {})
131-
};
132-
}
133-
134111
export const toBoolean = (value: any): boolean => {
135112
if (typeof value !== 'string') {
136113
return false;

0 commit comments

Comments
 (0)