From 42b3e12ba6d5ad205f2ea071116249455d870c12 Mon Sep 17 00:00:00 2001 From: "Niraj Chaudhari (Persistent Systems Inc)" Date: Wed, 4 Mar 2026 14:21:24 +0530 Subject: [PATCH 01/10] Reafctor MACAE-V4 UI --- src/frontend/src/api/apiClient.tsx | 138 +-- src/frontend/src/api/apiService.tsx | 10 - src/frontend/src/api/apiUtils.ts | 149 +++ src/frontend/src/api/config.tsx | 23 - src/frontend/src/api/httpClient.ts | 246 +++++ src/frontend/src/api/index.tsx | 6 + .../src/components/common/TeamSelector.tsx | 10 +- .../src/components/content/HomeInput.tsx | 10 +- .../src/components/content/PlanChat.tsx | 4 +- .../src/components/content/PlanChatBody.tsx | 5 +- .../src/components/content/PlanPanelLeft.tsx | 8 +- .../src/components/content/PlanPanelRight.tsx | 4 +- .../src/components/content/TaskList.tsx | 4 +- .../streaming/StreamingBufferMessage.tsx | 4 +- src/frontend/src/coral/modules/Chat.tsx | 17 +- src/frontend/src/hooks/index.tsx | 5 +- src/frontend/src/hooks/useAutoScroll.tsx | 22 + src/frontend/src/hooks/usePlanActions.tsx | 91 ++ .../src/hooks/usePlanCancellationAlert.tsx | 3 +- src/frontend/src/hooks/usePlanWebSocket.tsx | 313 ++++++ src/frontend/src/hooks/useTeamSelection.tsx | 5 - src/frontend/src/hooks/useWebSocket.tsx | 6 +- src/frontend/src/index.tsx | 19 +- src/frontend/src/models/taskDetails.tsx | 2 +- src/frontend/src/pages/HomePage.tsx | 237 ++--- src/frontend/src/pages/PlanPage.tsx | 890 +++++------------- src/frontend/src/services/PlanDataService.tsx | 7 +- src/frontend/src/services/TeamService.tsx | 13 +- .../src/services/WebSocketService.tsx | 63 +- src/frontend/src/state/hooks.ts | 14 + src/frontend/src/state/index.ts | 13 + src/frontend/src/state/slices/appSlice.ts | 49 + src/frontend/src/state/slices/chatSlice.ts | 82 ++ src/frontend/src/state/slices/planSlice.ts | 282 ++++++ .../src/state/slices/streamingSlice.ts | 76 ++ src/frontend/src/state/slices/teamSlice.ts | 60 ++ src/frontend/src/state/store.ts | 42 + src/frontend/src/utils/index.ts | 20 + src/frontend/src/utils/messageUtils.ts | 48 + 39 files changed, 1979 insertions(+), 1021 deletions(-) create mode 100644 src/frontend/src/api/apiUtils.ts create mode 100644 src/frontend/src/api/httpClient.ts create mode 100644 src/frontend/src/hooks/useAutoScroll.tsx create mode 100644 src/frontend/src/hooks/usePlanActions.tsx create mode 100644 src/frontend/src/hooks/usePlanWebSocket.tsx create mode 100644 src/frontend/src/state/hooks.ts create mode 100644 src/frontend/src/state/index.ts create mode 100644 src/frontend/src/state/slices/appSlice.ts create mode 100644 src/frontend/src/state/slices/chatSlice.ts create mode 100644 src/frontend/src/state/slices/planSlice.ts create mode 100644 src/frontend/src/state/slices/streamingSlice.ts create mode 100644 src/frontend/src/state/slices/teamSlice.ts create mode 100644 src/frontend/src/state/store.ts create mode 100644 src/frontend/src/utils/index.ts create mode 100644 src/frontend/src/utils/messageUtils.ts diff --git a/src/frontend/src/api/apiClient.tsx b/src/frontend/src/api/apiClient.tsx index 88bc4d60..7eaab10f 100644 --- a/src/frontend/src/api/apiClient.tsx +++ b/src/frontend/src/api/apiClient.tsx @@ -1,104 +1,52 @@ -import { headerBuilder, getApiUrl } from './config'; - -// Helper function to build URL with query parameters -const buildUrl = (url: string, params?: Record): string => { - if (!params) return url; - - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - searchParams.append(key, String(value)); - } - }); - - const queryString = searchParams.toString(); - return queryString ? `${url}?${queryString}` : url; -}; - -// Fetch with Authentication Headers -const fetchWithAuth = async (url: string, method: string = "GET", body: BodyInit | null = null) => { - const token = localStorage.getItem('token'); // Get the token from localStorage - const authHeaders = headerBuilder(); // Get authentication headers - - const headers: Record = { - ...authHeaders, // Include auth headers from headerBuilder - }; - - if (token) { - headers['Authorization'] = `Bearer ${token}`; // Add the token to the Authorization header - } - - // If body is FormData, do not set Content-Type header - if (body && body instanceof FormData) { - delete headers['Content-Type']; - } else { - headers['Content-Type'] = 'application/json'; - body = body ? JSON.stringify(body) : null; +/** + * API Client β€” thin adapter over the centralized httpClient. + * + * Auth headers (x-ms-client-principal-id, Authorization) are now injected + * automatically by httpClient's request interceptor, eliminating all manual + * headerBuilder() / localStorage.getItem('token') calls. + */ +import httpClient from './httpClient'; +import { getApiUrl } from './config'; + +/** + * Ensure httpClient's base URL stays in sync with the runtime config. + * Called lazily on every request so it picks up late-initialized API_URL. + */ +function syncBaseUrl(): void { + const apiUrl = getApiUrl(); + if (apiUrl && httpClient.getBaseUrl() !== apiUrl) { + httpClient.setBaseUrl(apiUrl); } +} - const options: RequestInit = { - method, - headers, - body: body || undefined, - }; - - try { - const apiUrl = getApiUrl(); - const finalUrl = `${apiUrl}${url}`; - // Log the request details - const response = await fetch(finalUrl, options); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Something went wrong'); - } - - const isJson = response.headers.get('content-type')?.includes('application/json'); - const responseData = isJson ? await response.json() : null; - return responseData; - } catch (error) { - console.info('API Error:', (error as Error).message); - throw error; - } -}; +export const apiClient = { + get: (url: string, config?: { params?: Record }): Promise => { + syncBaseUrl(); + return httpClient.get(url, { params: config?.params }); + }, -// Vanilla Fetch without Auth for Login -const fetchWithoutAuth = async (url: string, method: string = "POST", body: BodyInit | null = null) => { - const headers: Record = { - 'Content-Type': 'application/json', - }; + post: (url: string, body?: unknown): Promise => { + syncBaseUrl(); + return httpClient.post(url, body); + }, - const options: RequestInit = { - method, - headers, - body: body ? JSON.stringify(body) : undefined, - }; + put: (url: string, body?: unknown): Promise => { + syncBaseUrl(); + return httpClient.put(url, body); + }, - try { - const apiUrl = getApiUrl(); - const response = await fetch(`${apiUrl}${url}`, options); + delete: (url: string): Promise => { + syncBaseUrl(); + return httpClient.del(url); + }, - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Login failed'); - } - const isJson = response.headers.get('content-type')?.includes('application/json'); - return isJson ? await response.json() : null; - } catch (error) { - console.log('Login Error:', (error as Error).message); - throw error; - } -}; + upload: (url: string, formData: FormData): Promise => { + syncBaseUrl(); + return httpClient.upload(url, formData); + }, -// Authenticated requests (with token) and login (without token) -export const apiClient = { - get: (url: string, config?: { params?: Record }) => { - const finalUrl = buildUrl(url, config?.params); - return fetchWithAuth(finalUrl, 'GET'); + login: (url: string, body?: unknown): Promise => { + syncBaseUrl(); + return httpClient.postWithoutAuth(url, body); }, - post: (url: string, body?: any) => fetchWithAuth(url, 'POST', body), - put: (url: string, body?: any) => fetchWithAuth(url, 'PUT', body), - delete: (url: string) => fetchWithAuth(url, 'DELETE'), - upload: (url: string, formData: FormData) => fetchWithAuth(url, 'POST', formData), - login: (url: string, body?: any) => fetchWithoutAuth(url, 'POST', body), // For login without auth }; diff --git a/src/frontend/src/api/apiService.tsx b/src/frontend/src/api/apiService.tsx index 06415442..f6f6ba3d 100644 --- a/src/frontend/src/api/apiService.tsx +++ b/src/frontend/src/api/apiService.tsx @@ -156,7 +156,6 @@ export class APIService { if (!data) { throw new Error(`Plan with ID ${planId} not found`); } - console.log('Fetched plan by ID:', data); const results = { plan: data.plan as Plan, messages: data.messages as AgentMessageBE[], @@ -190,8 +189,6 @@ export class APIService { const requestKey = `approve-plan-${planApprovalData.m_plan_id}`; return this._requestTracker.trackRequest(requestKey, async () => { - console.log('πŸ“€ Approving plan via v4 API:', planApprovalData); - const response = await apiClient.post(API_ENDPOINTS.PLAN_APPROVAL, planApprovalData); // Invalidate cache since plan execution will start @@ -200,7 +197,6 @@ export class APIService { this._cache.invalidate(new RegExp(`^plan.*_${planApprovalData.plan_id}`)); } - console.log('βœ… Plan approval successful:', response); return response; }); } @@ -260,13 +256,7 @@ export class APIService { return response; } async sendAgentMessage(data: AgentMessageResponse): Promise { - const t0 = performance.now(); const result = await apiClient.post(API_ENDPOINTS.AGENT_MESSAGE, data); - console.log('[agent_message] sent', { - ms: +(performance.now() - t0).toFixed(1), - agent: data.agent, - type: data.agent_type - }); return result; } } diff --git a/src/frontend/src/api/apiUtils.ts b/src/frontend/src/api/apiUtils.ts new file mode 100644 index 00000000..f3872025 --- /dev/null +++ b/src/frontend/src/api/apiUtils.ts @@ -0,0 +1,149 @@ +/** + * API Utility Functions + * + * Centralized helpers for error response construction, retry logic, + * and request deduplication. Single source of truth β€” eliminates + * duplicated error patterns across API functions. + */ + +/** + * Create a standardized error response object. + * Replaces repeated `{ ...new Response(), ok: false, status: 500 }` patterns. + */ +export function createErrorResponse(status: number, message: string): Response { + return new Response(JSON.stringify({ error: message }), { + status, + statusText: message, + headers: { 'Content-Type': 'application/json' }, + }); +} + +/** + * Retry a request with exponential backoff. + * @param fn - The async function to retry + * @param maxRetries - Maximum number of retry attempts (default: 3) + * @param baseDelay - Base delay in ms before exponential increase (default: 1000) + */ +export async function retryRequest( + fn: () => Promise, + maxRetries = 3, + baseDelay = 1000 +): Promise { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + if (attempt === maxRetries) throw error; + const delay = baseDelay * Math.pow(2, attempt); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + throw new Error('Max retries exceeded'); +} + +/** + * Request cache with TTL and deduplication of in-flight requests. + * Prevents duplicate API calls for the same data. + */ +interface CacheEntry { + data: T; + timestamp: number; + expiresAt: number; +} + +export class RequestCache { + private cache = new Map>(); + private pendingRequests = new Map>(); + + /** Get cached data or fetch it, deduplicating concurrent identical requests */ + async get( + key: string, + fetcher: () => Promise, + ttlMs = 30000 + ): Promise { + // Return cached data if still fresh + const cached = this.cache.get(key); + if (cached && Date.now() < cached.expiresAt) { + return cached.data as T; + } + + // Deduplicate concurrent identical requests + const pending = this.pendingRequests.get(key); + if (pending) { + return pending as Promise; + } + + const request = fetcher() + .then((data) => { + this.cache.set(key, { + data, + timestamp: Date.now(), + expiresAt: Date.now() + ttlMs, + }); + this.pendingRequests.delete(key); + return data; + }) + .catch((error) => { + this.pendingRequests.delete(key); + throw error; + }); + + this.pendingRequests.set(key, request); + return request; + } + + /** Invalidate cached entries matching a key pattern */ + invalidate(pattern?: string | RegExp): void { + if (!pattern) { + this.cache.clear(); + return; + } + for (const key of this.cache.keys()) { + const matches = typeof pattern === 'string' + ? key.includes(pattern) + : pattern.test(key); + if (matches) this.cache.delete(key); + } + } + + /** Clear all cached data */ + clear(): void { + this.cache.clear(); + this.pendingRequests.clear(); + } +} + +/** Shared request cache singleton */ +export const requestCache = new RequestCache(); + +/** + * Debounce utility β€” delays calling `fn` until `delayMs` has elapsed + * since the last invocation. + */ +export function debounce void>( + fn: T, + delayMs: number +): (...args: Parameters) => void { + let timer: ReturnType; + return (...args: Parameters) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delayMs); + }; +} + +/** + * Throttle utility β€” ensures `fn` is called at most once per `limitMs`. + */ +export function throttle void>( + fn: T, + limitMs: number +): (...args: Parameters) => void { + let lastCall = 0; + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastCall >= limitMs) { + lastCall = now; + fn(...args); + } + }; +} diff --git a/src/frontend/src/api/config.tsx b/src/frontend/src/api/config.tsx index b7609e7e..d3b216ee 100644 --- a/src/frontend/src/api/config.tsx +++ b/src/frontend/src/api/config.tsx @@ -52,9 +52,6 @@ export async function getUserInfo(): Promise { try { const response = await fetch("/.auth/me"); if (!response.ok) { - console.log( - "No identity provider found. Access to chat will be blocked." - ); return {} as UserInfo; } const payload = await response.json(); @@ -97,7 +94,6 @@ export function getUserInfoGlobal() { } if (!USER_INFO) { - // console.info('User info not yet configured'); return null; } @@ -105,7 +101,6 @@ export function getUserInfoGlobal() { } export function getUserId(): string { - // USER_ID = getUserInfoGlobal()?.user_id || null; if (!USER_ID) { USER_ID = getUserInfoGlobal()?.user_id || null; } @@ -113,24 +108,6 @@ export function getUserId(): string { return userId; } -/** - * Build headers with authentication information - * @param headers Optional additional headers to merge - * @returns Combined headers object with authentication - */ -export function headerBuilder(headers?: Record): Record { - let userId = getUserId(); - //console.log('headerBuilder: Using user ID:', userId); - let defaultHeaders = { - "x-ms-client-principal-id": String(userId) || "", // Custom header - }; - //console.log('headerBuilder: Created headers:', defaultHeaders); - return { - ...defaultHeaders, - ...(headers ? headers : {}) - }; -} - export const toBoolean = (value: any): boolean => { if (typeof value !== 'string') { return false; diff --git a/src/frontend/src/api/httpClient.ts b/src/frontend/src/api/httpClient.ts new file mode 100644 index 00000000..866709c3 --- /dev/null +++ b/src/frontend/src/api/httpClient.ts @@ -0,0 +1,246 @@ +/** + * Centralized HTTP Client with Interceptors + * + * Singleton class that wraps all API calls with: + * - Automatic auth header injection via request interceptors + * - Uniform error handling via response interceptors + * - Built-in timeout, configurable base URL, and params serialization + * + * Eliminates duplicated localStorage/header logic across API functions. + */ +import { getUserId } from './config'; + +type RequestConfig = RequestInit & { url: string }; +type RequestInterceptor = (config: RequestConfig) => RequestConfig; +type ResponseInterceptor = (response: Response) => Response | Promise; + +class HttpClient { + private baseUrl: string; + private requestInterceptors: RequestInterceptor[] = []; + private responseInterceptors: ResponseInterceptor[] = []; + private timeout: number; + + constructor(baseUrl = '', timeout = 30000) { + this.baseUrl = baseUrl; + this.timeout = timeout; + } + + /** Set or update the base URL at runtime (after config is loaded) */ + setBaseUrl(url: string): void { + this.baseUrl = url; + } + + getBaseUrl(): string { + return this.baseUrl; + } + + /** Register a request interceptor (runs before every request) */ + addRequestInterceptor(interceptor: RequestInterceptor): void { + this.requestInterceptors.push(interceptor); + } + + /** Register a response interceptor (runs after every response) */ + addResponseInterceptor(interceptor: ResponseInterceptor): void { + this.responseInterceptors.push(interceptor); + } + + /** Build URL with query parameters */ + private buildUrl(path: string, params?: Record): string { + const base = this.baseUrl ? `${this.baseUrl}${path}` : path; + if (!params) return base; + + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + + const queryString = searchParams.toString(); + return queryString ? `${base}?${queryString}` : base; + } + + /** Core request method β€” applies interceptors, timeout, and error handling */ + private async request( + path: string, + options: RequestInit & { params?: Record } = {} + ): Promise { + const { params, ...fetchOptions } = options; + const url = this.buildUrl(path, params); + + // Build initial config + let config: RequestConfig = { url, ...fetchOptions }; + + // Run request interceptors + for (const interceptor of this.requestInterceptors) { + config = interceptor(config); + } + + const { url: finalUrl, ...rest } = config; + + // Timeout via AbortController + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + try { + let response = await fetch(finalUrl, { + ...rest, + signal: controller.signal, + }); + + // Run response interceptors + for (const interceptor of this.responseInterceptors) { + response = await interceptor(response); + } + + return response; + } finally { + clearTimeout(timeoutId); + } + } + + /** HTTP GET */ + async get( + path: string, + config?: { params?: Record; headers?: Record } + ): Promise { + const response = await this.request(path, { + method: 'GET', + params: config?.params, + headers: config?.headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Request failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? response.json() : (null as T); + } + + /** HTTP POST */ + async post( + path: string, + body?: unknown, + config?: { headers?: Record } + ): Promise { + const response = await this.request(path, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + ...config?.headers, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Request failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? response.json() : (null as T); + } + + /** HTTP PUT */ + async put( + path: string, + body?: unknown, + config?: { headers?: Record } + ): Promise { + const response = await this.request(path, { + method: 'PUT', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + ...config?.headers, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Request failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? response.json() : (null as T); + } + + /** HTTP DELETE */ + async del(path: string): Promise { + const response = await this.request(path, { method: 'DELETE' }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Request failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? response.json() : (null as T); + } + + /** Upload a FormData payload (multipart/form-data) */ + async upload(path: string, formData: FormData): Promise { + // Don't set Content-Type β€” browser sets multipart boundary automatically + const response = await this.request(path, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Upload failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? response.json() : (null as T); + } + + /** HTTP POST without auth (used for login) */ + async postWithoutAuth(path: string, body?: unknown): Promise { + const url = this.baseUrl ? `${this.baseUrl}${path}` : path; + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || 'Request failed'); + } + + const isJson = response.headers.get('content-type')?.includes('application/json'); + return isJson ? response.json() : (null as T); + } +} + +// ────────────────────────────────────────────── +// Singleton instance with interceptors +// ────────────────────────────────────────────── + +const httpClient = new HttpClient(); + +/** + * Auth interceptor β€” single source of truth for userId header. + * Eliminates repeated localStorage.getItem("userId") and manual headerBuilder() calls. + */ +httpClient.addRequestInterceptor((config) => { + const userId = getUserId(); + const token = localStorage.getItem('token'); + + const headers = new Headers(config.headers as HeadersInit); + + if (userId) { + headers.set('x-ms-client-principal-id', String(userId)); + } + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + + return { ...config, headers }; +}); + +export default httpClient; diff --git a/src/frontend/src/api/index.tsx b/src/frontend/src/api/index.tsx index 462775be..c88cde5f 100644 --- a/src/frontend/src/api/index.tsx +++ b/src/frontend/src/api/index.tsx @@ -1,5 +1,11 @@ // Export our API services and utilities export * from './apiClient'; +// Centralized HTTP client with interceptors (Point 2) +export { default as httpClient } from './httpClient'; + +// API utilities: createErrorResponse, retryRequest, RequestCache (Points 6, 8) +export * from './apiUtils'; + // Unified API service - recommended for all new code export { apiService } from './apiService'; diff --git a/src/frontend/src/components/common/TeamSelector.tsx b/src/frontend/src/components/common/TeamSelector.tsx index 9c9aeadd..2709d550 100644 --- a/src/frontend/src/components/common/TeamSelector.tsx +++ b/src/frontend/src/components/common/TeamSelector.tsx @@ -116,7 +116,6 @@ const TeamSelector: React.FC = ({ try { // If this team was just uploaded, skip the selection API call and go directly to homepage if (uploadedTeam && uploadedTeam.team_id === tempSelectedTeam.team_id) { - console.log('Uploaded team selected, going directly to homepage:', tempSelectedTeam.name); onTeamSelect?.(tempSelectedTeam); setIsOpen(false); return; // Skip the selectTeam API call @@ -126,14 +125,12 @@ const TeamSelector: React.FC = ({ const result = await TeamService.selectTeam(tempSelectedTeam.team_id); if (result.success) { - console.log('Team selected:', result.data); onTeamSelect?.(tempSelectedTeam); setIsOpen(false); } else { setError(result.error || 'Failed to select team'); } - } catch (err: any) { - console.error('Error selecting team:', err); + } catch { setError('Failed to select team. Please try again.'); } finally { setSelectionLoading(false); @@ -243,7 +240,7 @@ const TeamSelector: React.FC = ({ let teamData; try { teamData = JSON.parse(fileText); - } catch (parseError) { + } catch { throw new Error('Invalid JSON file format'); } @@ -344,7 +341,7 @@ const TeamSelector: React.FC = ({ let teamData; try { teamData = JSON.parse(fileText); - } catch (parseError) { + } catch { throw new Error('Invalid JSON file format'); } @@ -563,7 +560,6 @@ const TeamSelector: React.FC = ({ placeholder="Search teams..." value={searchQuery} onChange={(e: React.ChangeEvent, data: InputOnChangeData) => { - console.log('Search changed:', data.value); setSearchQuery(data.value || ''); }} contentBefore={} diff --git a/src/frontend/src/components/content/HomeInput.tsx b/src/frontend/src/components/content/HomeInput.tsx index c4684918..4dd2a2ac 100644 --- a/src/frontend/src/components/content/HomeInput.tsx +++ b/src/frontend/src/components/content/HomeInput.tsx @@ -100,7 +100,6 @@ const HomeInput: React.FC = ({ selectedTeam }) => { input.trim(), selectedTeam?.team_id ); - console.log("Plan created:", response); setInput(""); if (textareaRef.current) { @@ -117,15 +116,14 @@ const HomeInput: React.FC = ({ selectedTeam }) => { dismissToast(id); } } catch (error: any) { - console.log("Error creating plan:", error); let errorMessage = "Unable to create plan. Please try again."; dismissToast(id); // Check if this is an RAI validation error try { // errorDetail = JSON.parse(error); errorMessage = error?.message || errorMessage; - } catch (parseError) { - console.error("Error parsing error detail:", parseError); + } catch { + // ignore parse error } showToast(errorMessage, "error"); @@ -290,4 +288,6 @@ const HomeInput: React.FC = ({ selectedTeam }) => { ); }; -export default HomeInput; +const MemoizedHomeInput = React.memo(HomeInput); +MemoizedHomeInput.displayName = 'HomeInput'; +export default MemoizedHomeInput; diff --git a/src/frontend/src/components/content/PlanChat.tsx b/src/frontend/src/components/content/PlanChat.tsx index 81193d74..2a61e21c 100644 --- a/src/frontend/src/components/content/PlanChat.tsx +++ b/src/frontend/src/components/content/PlanChat.tsx @@ -108,4 +108,6 @@ const PlanChat: React.FC = ({ ); }; -export default PlanChat; \ No newline at end of file +const MemoizedPlanChat = React.memo(PlanChat); +MemoizedPlanChat.displayName = 'PlanChat'; +export default MemoizedPlanChat; \ No newline at end of file diff --git a/src/frontend/src/components/content/PlanChatBody.tsx b/src/frontend/src/components/content/PlanChatBody.tsx index d91b3728..210b61b7 100644 --- a/src/frontend/src/components/content/PlanChatBody.tsx +++ b/src/frontend/src/components/content/PlanChatBody.tsx @@ -1,3 +1,4 @@ +import React from "react"; import ChatInput from "@/coral/modules/ChatInput"; import { PlanChatProps } from "@/models"; import { Button } from "@fluentui/react-components"; @@ -74,4 +75,6 @@ const PlanChatBody: React.FC = ({ ); } -export default PlanChatBody; \ No newline at end of file +const MemoizedPlanChatBody = React.memo(PlanChatBody); +MemoizedPlanChatBody.displayName = 'PlanChatBody'; +export default MemoizedPlanChatBody; \ No newline at end of file diff --git a/src/frontend/src/components/content/PlanPanelLeft.tsx b/src/frontend/src/components/content/PlanPanelLeft.tsx index 437fb1ed..ffa48b9d 100644 --- a/src/frontend/src/components/content/PlanPanelLeft.tsx +++ b/src/frontend/src/components/content/PlanPanelLeft.tsx @@ -1,3 +1,4 @@ +import React from "react"; import PanelLeft from "@/coral/components/Panels/PanelLeft"; import PanelLeftToolbar from "@/coral/components/Panels/PanelLeftToolbar"; import { @@ -56,7 +57,6 @@ const PlanPanelLeft: React.FC = ({ const loadPlansData = useCallback(async (forceRefresh = false) => { try { - console.log("Loading plans, forceRefresh:", forceRefresh); setPlansLoading(true); setPlansError(null); const plansData = await apiService.getPlans(undefined, !forceRefresh); // Invert forceRefresh for useCache @@ -67,7 +67,6 @@ const PlanPanelLeft: React.FC = ({ restReload(); } } catch (error) { - console.log("Failed to load plans:", error); setPlansError( error instanceof Error ? error : new Error("Failed to load plans") ); @@ -92,7 +91,6 @@ const PlanPanelLeft: React.FC = ({ useEffect(() => { - console.log("Reload tasks changed:", reloadTasks); if (reloadTasks) { loadPlansData(true); // Force refresh when reloadTasks is true } @@ -265,4 +263,6 @@ const PlanPanelLeft: React.FC = ({ ); }; -export default PlanPanelLeft; +const MemoizedPlanPanelLeft = React.memo(PlanPanelLeft); +MemoizedPlanPanelLeft.displayName = 'PlanPanelLeft'; +export default MemoizedPlanPanelLeft; diff --git a/src/frontend/src/components/content/PlanPanelRight.tsx b/src/frontend/src/components/content/PlanPanelRight.tsx index 48442578..6072b471 100644 --- a/src/frontend/src/components/content/PlanPanelRight.tsx +++ b/src/frontend/src/components/content/PlanPanelRight.tsx @@ -136,4 +136,6 @@ const PlanPanelRight: React.FC = ({ ); }; -export default PlanPanelRight; \ No newline at end of file +const MemoizedPlanPanelRight = React.memo(PlanPanelRight); +MemoizedPlanPanelRight.displayName = 'PlanPanelRight'; +export default MemoizedPlanPanelRight; \ No newline at end of file diff --git a/src/frontend/src/components/content/TaskList.tsx b/src/frontend/src/components/content/TaskList.tsx index aadb626c..4a26f027 100644 --- a/src/frontend/src/components/content/TaskList.tsx +++ b/src/frontend/src/components/content/TaskList.tsx @@ -98,4 +98,6 @@ const TaskList: React.FC = ({ ); }; -export default TaskList; +const MemoizedTaskList = React.memo(TaskList); +MemoizedTaskList.displayName = 'TaskList'; +export default MemoizedTaskList; diff --git a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx index c3bd7c56..6c611754 100644 --- a/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx +++ b/src/frontend/src/components/content/streaming/StreamingBufferMessage.tsx @@ -225,4 +225,6 @@ const StreamingBufferMessage: React.FC = ({ ); }; -export default StreamingBufferMessage; \ No newline at end of file +const MemoizedStreamingBufferMessage = React.memo(StreamingBufferMessage); +MemoizedStreamingBufferMessage.displayName = 'StreamingBufferMessage'; +export default MemoizedStreamingBufferMessage; \ No newline at end of file diff --git a/src/frontend/src/coral/modules/Chat.tsx b/src/frontend/src/coral/modules/Chat.tsx index e178cc10..d7516f96 100644 --- a/src/frontend/src/coral/modules/Chat.tsx +++ b/src/frontend/src/coral/modules/Chat.tsx @@ -62,8 +62,8 @@ const Chat: React.FC = ({ } // const chatMessages = await chatService.getUserHistory(userId); // setMessages(chatMessages); - } catch (err) { - console.log("Failed to load chat history.", err); + } catch { + // Failed to load history β€” silent fail } }; loadHistory(); @@ -102,8 +102,8 @@ const Chat: React.FC = ({ }; const handleCopy = (text: string) => { - navigator.clipboard.writeText(text).catch((err) => { - console.log("Failed to copy text:", err); + navigator.clipboard.writeText(text).catch(() => { + // clipboard copy failed β€” silent }); }; @@ -150,8 +150,7 @@ const Chat: React.FC = ({ // const assistantMessage = { role: "assistant", content: response.assistant_response }; // setMessages([...updatedMessages, assistantMessage]); } - } catch (err) { - console.log("Send Message Error:", err); + } catch { setMessages([ ...updatedMessages, { role: "assistant", content: "Oops! Something went wrong sending your message." }, @@ -169,8 +168,8 @@ const Chat: React.FC = ({ // await chatService.clearChatHistory(userId); } setMessages([]); - } catch (err) { - console.log("Failed to clear chat history:", err); + } catch { + // clear history failed β€” silent } }; @@ -195,7 +194,7 @@ const Chat: React.FC = ({ icon={} />