Skip to content

Commit b87f15c

Browse files
committed
♻️ Repeated calls in chat page (Agent list & conversation list) #2082
[Specification Details] 1.Remove the repeated calls to the /agent/list, /conversation/list, and deployment_version interfaces. The first call results contain content for use elsewhere.
1 parent 4dfb164 commit b87f15c

File tree

9 files changed

+177
-60
lines changed

9 files changed

+177
-60
lines changed

frontend/app/[locale]/chat/components/chatAgentSelector.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function ChatAgentSelector({
1515
onAgentSelect,
1616
disabled = false,
1717
isInitialMode = false,
18+
initialAgents,
1819
}: ChatAgentSelectorProps) {
1920
const [agents, setAgents] = useState<Agent[]>([]);
2021
const [isOpen, setIsOpen] = useState(false);
@@ -94,6 +95,13 @@ export function ChatAgentSelector({
9495
};
9596

9697
useEffect(() => {
98+
// If parent provided initialAgents, reuse them to avoid duplicate network calls.
99+
if (initialAgents && initialAgents.length > 0) {
100+
setAgents(initialAgents);
101+
setIsLoading(false);
102+
return;
103+
}
104+
97105
loadAgents();
98106
}, []);
99107

frontend/app/[locale]/chat/components/chatInput.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { chatConfig } from "@/const/chatConfig";
3131
import { FilePreview } from "@/types/chat";
3232

3333
import { ChatAgentSelector } from "./chatAgentSelector";
34+
import type { Agent } from "@/types/chat";
3435

3536
// Image viewer component
3637
function ImageViewer({
@@ -310,6 +311,8 @@ interface ChatInputProps {
310311
onAttachmentsChange?: (attachments: FilePreview[]) => void;
311312
selectedAgentId?: number | null;
312313
onAgentSelect?: (agentId: number | null) => void;
314+
// Optional agents list passed from parent to avoid redundant fetching
315+
initialAgents?: Agent[];
313316
}
314317

315318
export function ChatInput({
@@ -328,6 +331,7 @@ export function ChatInput({
328331
onAttachmentsChange,
329332
selectedAgentId = null,
330333
onAgentSelect,
334+
initialAgents,
331335
}: ChatInputProps) {
332336
const [isRecording, setIsRecording] = useState(false);
333337
const [recordingStatus, setRecordingStatus] = useState<
@@ -1026,6 +1030,7 @@ export function ChatInput({
10261030
onAgentSelect={onAgentSelect || (() => {})}
10271031
disabled={isLoading || isStreaming}
10281032
isInitialMode={isInitialMode}
1033+
initialAgents={initialAgents}
10291034
/>
10301035
</div>
10311036

frontend/app/[locale]/chat/internal/chatInterface.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { useAuth } from "@/hooks/useAuth";
1515
import { conversationService } from "@/services/conversationService";
1616
import { storageService, convertImageUrlToApiUrl } from "@/services/storageService";
1717
import { useConversationManagement } from "@/hooks/chat/useConversationManagement";
18+
import { fetchAllAgents } from "@/services/agentConfigService";
19+
import type { Agent } from "@/types/chat";
1820

1921
import { ChatSidebar } from "../components/chatLeftSidebar";
2022
import { FilePreview } from "@/types/chat";
@@ -137,6 +139,7 @@ export function ChatInterface() {
137139

138140
// Add agent selection state
139141
const [selectedAgentId, setSelectedAgentId] = useState<number | null>(null);
142+
const [agents, setAgents] = useState<Agent[]>([]);
140143

141144
// Reset scroll to bottom state
142145
useEffect(() => {
@@ -201,6 +204,24 @@ export function ChatInterface() {
201204
}
202205
}, [appConfig]); // Add appConfig as dependency
203206

207+
// Load agent list once and reuse across child components to avoid duplicate /agent/list calls.
208+
useEffect(() => {
209+
let mounted = true;
210+
(async () => {
211+
try {
212+
const res = await fetchAllAgents();
213+
if (mounted && res?.success) {
214+
setAgents(res.data || []);
215+
}
216+
} catch (e) {
217+
log.error("Failed to fetch agents for chat page:", e);
218+
}
219+
})();
220+
return () => {
221+
mounted = false;
222+
};
223+
}, []);
224+
204225
// Add useEffect to listen for conversationId changes, ensure right sidebar is always closed when conversation switches
205226
useEffect(() => {
206227
// Ensure right sidebar is reset to closed state whenever conversation ID changes
@@ -1459,6 +1480,7 @@ export function ChatInterface() {
14591480
shouldScrollToBottom={shouldScrollToBottom}
14601481
selectedAgentId={selectedAgentId}
14611482
onAgentSelect={setSelectedAgentId}
1483+
initialAgents={agents}
14621484
onCitationHover={clearCompletedIndicator}
14631485
onScroll={clearCompletedIndicator}
14641486
/>

frontend/app/[locale]/chat/streaming/chatStreamMain.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function ChatStreamMain({
4040
onAgentSelect,
4141
onCitationHover,
4242
onScroll,
43+
initialAgents,
4344
}: ChatStreamMainProps) {
4445
const { t } = useTranslation();
4546
// Animation variants for ChatInput
@@ -375,6 +376,7 @@ export function ChatStreamMain({
375376
onImageUpload={onImageUpload}
376377
selectedAgentId={selectedAgentId}
377378
onAgentSelect={onAgentSelect}
379+
initialAgents={initialAgents}
378380
/>
379381
</motion.div>
380382
</AnimatePresence>
@@ -472,6 +474,7 @@ export function ChatStreamMain({
472474
onImageUpload={onImageUpload}
473475
selectedAgentId={selectedAgentId}
474476
onAgentSelect={onAgentSelect}
477+
initialAgents={initialAgents}
475478
/>
476479
</motion.div>
477480
</AnimatePresence>

frontend/hooks/useAuth.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,32 @@ export function AuthProvider({ children }: { children: (value: AuthContextType)
4848
}, [isLoginModalOpen, isRegisterModalOpen])
4949

5050
// Check deployment version and handle speed mode
51+
// This function deduplicates network requests across the app instance using a module/global promise.
5152
const checkDeploymentVersion = async () => {
5253
try {
5354
setIsReady(false);
54-
const response = await fetch(API_ENDPOINTS.tenantConfig.deploymentVersion);
55-
if (response.ok) {
56-
const data = await response.json();
57-
const version = data.content?.deployment_version || data.deployment_version;
58-
59-
setIsSpeedMode(version === 'speed');
60-
// In speed mode, do not perform any auto login; UI should not depend on login
55+
if (!(globalThis as any).__deploymentVersionPromise) {
56+
(globalThis as any).__deploymentVersionPromise = (async () => {
57+
try {
58+
const resp = await fetch(API_ENDPOINTS.tenantConfig.deploymentVersion);
59+
if (!resp.ok) return null;
60+
const data = await resp.json();
61+
return data.content?.deployment_version || data.deployment_version || null;
62+
} catch (e) {
63+
return null;
64+
}
65+
})();
66+
}
67+
68+
const version = await (globalThis as any).__deploymentVersionPromise;
69+
if (version) {
70+
setIsSpeedMode(version === "speed");
71+
} else {
72+
setIsSpeedMode(false);
6173
}
74+
// In speed mode, do not perform any auto login; UI should not depend on login
6275
} catch (error) {
63-
log.error('Failed to check deployment version:', error);
76+
log.error("Failed to check deployment version:", error);
6477
setIsSpeedMode(false);
6578
} finally {
6679
setIsReady(true);

frontend/services/agentConfigService.ts

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -646,38 +646,52 @@ export const searchAgentInfo = async (agentId: number) => {
646646
* @returns list of available agents with agent_id, name, description, is_available
647647
*/
648648
export const fetchAllAgents = async () => {
649-
try {
650-
const response = await fetch(API_ENDPOINTS.agent.list, {
651-
headers: getAuthHeaders(),
652-
});
653-
if (!response.ok) {
654-
throw new Error(`Request failed: ${response.status}`);
655-
}
656-
const data = await response.json();
649+
// Deduplicate concurrent fetches: reuse the in-flight promise if present.
650+
// Use module-level promise variable.
651+
if ((fetchAllAgents as any).__promise) {
652+
return (fetchAllAgents as any).__promise;
653+
}
657654

658-
// convert backend data to frontend format
659-
const formattedAgents = data.map((agent: any) => ({
660-
agent_id: agent.agent_id,
661-
name: agent.name,
662-
display_name: agent.display_name || agent.name,
663-
description: agent.description,
664-
author: agent.author,
665-
is_available: agent.is_available,
666-
}));
655+
const p = (async () => {
656+
try {
657+
const response = await fetch(API_ENDPOINTS.agent.list, {
658+
headers: getAuthHeaders(),
659+
});
660+
if (!response.ok) {
661+
throw new Error(`Request failed: ${response.status}`);
662+
}
663+
const data = await response.json();
664+
665+
// convert backend data to frontend format
666+
const formattedAgents = data.map((agent: any) => ({
667+
agent_id: agent.agent_id,
668+
name: agent.name,
669+
display_name: agent.display_name || agent.name,
670+
description: agent.description,
671+
author: agent.author,
672+
is_available: agent.is_available,
673+
}));
667674

668-
return {
669-
success: true,
670-
data: formattedAgents,
671-
message: "",
672-
};
673-
} catch (error) {
674-
log.error("Failed to get all Agent list:", error);
675-
return {
676-
success: false,
677-
data: [],
678-
message: "agentConfig.agents.listFetchFailed",
679-
};
680-
}
675+
return {
676+
success: true,
677+
data: formattedAgents,
678+
message: "",
679+
};
680+
} catch (error) {
681+
log.error("Failed to get all Agent list:", error);
682+
return {
683+
success: false,
684+
data: [],
685+
message: "agentConfig.agents.listFetchFailed",
686+
};
687+
} finally {
688+
// clear the in-flight promise after completion so future calls can refetch
689+
delete (fetchAllAgents as any).__promise;
690+
}
691+
})();
692+
693+
(fetchAllAgents as any).__promise = p;
694+
return p;
681695
};
682696

683697
/**

frontend/services/conversationService.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import log from "@/lib/logger";
1212
// @ts-ignore
1313
const fetch = fetchWithAuth;
1414

15+
// Simple in-memory cache to ensure conversation list is fetched once per app session
16+
// and reused by callers. Provide an invalidate function if callers need to refresh.
17+
let conversationListCache: { ts: number; data: ConversationListItem[] } | null = null;
18+
1519
// This helper function now ALWAYS connects through the current host and port.
1620
// This relies on our custom `server.js` to handle the proxying in all environments.
1721
const getWebSocketUrl = (endpoint: string): string => {
@@ -24,15 +28,34 @@ const getWebSocketUrl = (endpoint: string): string => {
2428
export const conversationService = {
2529
// Get conversation list
2630
async getList(): Promise<ConversationListItem[]> {
27-
const response = await fetch(API_ENDPOINTS.conversation.list);
31+
try {
32+
// If we already fetched the conversation list earlier in this session, return it.
33+
if (conversationListCache) {
34+
return conversationListCache.data;
35+
}
2836

29-
const data = await response.json() as ConversationListResponse;
30-
31-
if (data.code === 0) {
32-
return data.data || [];
37+
const response = await fetch(API_ENDPOINTS.conversation.list);
38+
const data = (await response.json()) as ConversationListResponse;
39+
40+
if (data.code === 0) {
41+
const list = data.data || [];
42+
conversationListCache = { ts: Date.now(), data: list };
43+
return list;
44+
}
45+
46+
throw new ApiError(data.code, data.message);
47+
} catch (error) {
48+
// On error, if we have cached data return it as a fallback
49+
if (conversationListCache) {
50+
return conversationListCache.data;
51+
}
52+
throw error;
3353
}
34-
35-
throw new ApiError(data.code, data.message);
54+
},
55+
56+
// Invalidate the cached conversation list (call when you need to force refresh)
57+
invalidateListCache() {
58+
conversationListCache = null;
3659
},
3760

3861
// Create new conversation
@@ -48,6 +71,8 @@ export const conversationService = {
4871
const data = await response.json();
4972

5073
if (data.code === 0) {
74+
// Invalidate conversation list cache so callers will fetch updated list
75+
conversationListCache = null;
5176
return data.data;
5277
}
5378

@@ -68,6 +93,8 @@ export const conversationService = {
6893
const data = await response.json();
6994

7095
if (data.code === 0) {
96+
// Invalidate conversation list cache since titles changed
97+
conversationListCache = null;
7198
return data.data;
7299
}
73100

@@ -114,6 +141,8 @@ export const conversationService = {
114141
const data = await response.json();
115142

116143
if (data.code === 0) {
144+
// Invalidate conversation list cache because an item was removed
145+
conversationListCache = null;
117146
return true;
118147
}
119148

@@ -802,7 +831,21 @@ export const conversationService = {
802831
const data = await response.json();
803832

804833
if (data.code === 0) {
805-
return data.data;
834+
const title = data.data;
835+
// If we have a cached conversation list, update the corresponding item's title
836+
if (conversationListCache && Array.isArray(conversationListCache.data)) {
837+
const idx = conversationListCache.data.findIndex(
838+
(c) => c.conversation_id === params.conversation_id
839+
);
840+
if (idx >= 0) {
841+
// Update in-place and refresh timestamp
842+
conversationListCache.data[idx].conversation_title =
843+
title || conversationListCache.data[idx].conversation_title;
844+
conversationListCache.ts = Date.now();
845+
}
846+
}
847+
848+
return title;
806849
}
807850

808851
throw new ApiError(data.code, data.message);

frontend/services/versionService.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,27 @@ export class VersionService {
1717
*/
1818
async getAppVersion(): Promise<string> {
1919
try {
20-
const response = await fetchWithErrorHandling(
21-
API_ENDPOINTS.tenantConfig.deploymentVersion
22-
);
23-
24-
if (response.status !== STATUS_CODES.SUCCESS) {
25-
log.warn("Failed to fetch app version, using fallback");
26-
return APP_VERSION;
27-
}
28-
29-
const data: DeploymentVersionResponse = await response.json();
30-
const version = data.app_version || data.content?.app_version;
31-
32-
if (version) {
33-
return version;
20+
// Reuse a global promise to deduplicate deployment_version calls across the app
21+
if (!(globalThis as any).__deploymentVersionPromise) {
22+
(globalThis as any).__deploymentVersionPromise = (async () => {
23+
try {
24+
const response = await fetchWithErrorHandling(
25+
API_ENDPOINTS.tenantConfig.deploymentVersion
26+
);
27+
if (response.status !== STATUS_CODES.SUCCESS) {
28+
return null;
29+
}
30+
const data: DeploymentVersionResponse = await response.json();
31+
return data.app_version || data.content?.app_version || null;
32+
} catch (e) {
33+
log.error("Error fetching app version (inner):", e);
34+
return null;
35+
}
36+
})();
3437
}
3538

39+
const version = await (globalThis as any).__deploymentVersionPromise;
40+
if (version) return version;
3641
log.warn("App version not found in response, using fallback");
3742
return APP_VERSION;
3843
} catch (error) {

0 commit comments

Comments
 (0)