-
Notifications
You must be signed in to change notification settings - Fork 584
🐛 Bugfix: fix agent generation cache not restored when switching pages #2806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
007a48f
4c77710
2b74e68
83d62b0
da3c379
1057f16
3770bab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| "use client"; | ||
|
|
||
| import { useState, useEffect, useMemo } from "react"; | ||
| import { useState, useEffect, useMemo, useRef } from "react"; | ||
| import { useTranslation } from "react-i18next"; | ||
| import { | ||
| Button, | ||
|
|
@@ -21,6 +21,13 @@ | |
|
|
||
| import log from "@/lib/logger"; | ||
| import { AgentProfileInfo, AgentBusinessInfo } from "@/types/agentConfig"; | ||
| import { | ||
| getAgentGenerationCache, | ||
| setAgentGenerationStatus, | ||
| saveGeneratedField, | ||
| clearAgentGenerationCache, | ||
| clearExpiredGenerationCaches | ||
| } from "@/lib/agentGenerationCache"; | ||
| import { useAgentList } from "@/hooks/agent/useAgentList"; | ||
| import { | ||
| GENERATE_PROMPT_STREAM_TYPES, | ||
|
|
@@ -108,6 +115,32 @@ | |
| const [expandModalOpen, setExpandModalOpen] = useState(false); | ||
| const [expandModalType, setExpandModalType] = useState<'duty' | 'constraint' | 'few-shots' | null>(null); | ||
|
|
||
| // Use ref to track generation initiator - this doesn't trigger re-renders | ||
| // but is accessible in closures | ||
| const generationInitiatorRef = useRef<number | null>(null); | ||
|
|
||
| // Cleanup invalid cache on mount to prevent stuck "generating" state | ||
| useEffect(() => { | ||
| // Clean up expired caches on startup to prevent stuck states | ||
| // Only removes entries that have exceeded their expiry time | ||
| // Does not interfere with legitimate in-progress caches | ||
| clearExpiredGenerationCaches(); | ||
| }, []); | ||
|
|
||
| // Sync businessInfo local state with store when editedAgent changes | ||
| // This handles navigation scenarios where component remounts but store persists | ||
| useEffect(() => { | ||
| if (editedAgent.business_description !== businessInfo.businessDescription || | ||
| editedAgent.business_logic_model_name !== businessInfo.businessLogicModelName || | ||
| editedAgent.business_logic_model_id !== businessInfo.businessLogicModelId) { | ||
| setBusinessInfo({ | ||
| businessDescription: editedAgent.business_description || "", | ||
| businessLogicModelName: editedAgent.business_logic_model_name || "", | ||
| businessLogicModelId: editedAgent.business_logic_model_id || 0, | ||
| }); | ||
| } | ||
| }, [editedAgent.business_description, editedAgent.business_logic_model_name, editedAgent.business_logic_model_id]); | ||
|
|
||
| // Only show "no edit permission" tooltip when the panel is active and agent is read-only. | ||
| // Note: when no agent is selected, AgentInfoComp shows an overlay and we should not show | ||
| // this tooltip in that state. | ||
|
|
@@ -206,6 +239,23 @@ | |
|
|
||
| // Initialize form values when component mounts or currentAgentId changes | ||
| useEffect(() => { | ||
| const effectiveAgentId = currentAgentId ?? 0; | ||
|
|
||
| // Skip form initialization if we're currently generating for this agent | ||
| // Use generationInitiatorRef to avoid stale closure issues | ||
| if (generationInitiatorRef.current === effectiveAgentId) { | ||
| return; | ||
| } | ||
|
|
||
| // Check if this agent has cached generation content in progress | ||
| const cached = getAgentGenerationCache(effectiveAgentId); | ||
| const hasCachedGeneration = cached?.isGenerating === true; | ||
|
|
||
| // Skip form initialization if we're resuming a cached generation | ||
| // This prevents overwriting the generated content | ||
| if (hasCachedGeneration) { | ||
| return; | ||
| } | ||
|
|
||
| const initialAgentInfo: Record<string, any> = { | ||
| agentName: editedAgent.name || "", | ||
|
|
@@ -289,6 +339,60 @@ | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [editable, currentAgentId, groups, allowedGroupIds, user?.role]); | ||
|
|
||
| // Load cached generation content when switching to a different agent | ||
| useEffect(() => { | ||
| const effectiveAgentId = currentAgentId ?? 0; | ||
|
|
||
| // Check if this agent has cached generation content | ||
| const cached = getAgentGenerationCache(effectiveAgentId); | ||
|
|
||
| // Helper to check if cache has any meaningful content | ||
| const hasContent = cached?.dutyPrompt || cached?.constraintPrompt || cached?.fewShotsPrompt || | ||
| cached?.agentName || cached?.agentDescription || cached?.agentDisplayName; | ||
|
|
||
| // If cache has isGenerating=true, it means a previous session was interrupted | ||
| // Clear it and return - user will need to regenerate | ||
| if (cached?.isGenerating) { | ||
| clearAgentGenerationCache(effectiveAgentId); | ||
| return; | ||
| } | ||
|
|
||
| // For completed generation (isGenerating was cleared), restore the content | ||
| if (cached && hasContent) { | ||
| // Restore cached content to form and local state | ||
| setGeneratedContent({ | ||
| dutyPrompt: cached.dutyPrompt, | ||
| constraintPrompt: cached.constraintPrompt, | ||
| fewShotsPrompt: cached.fewShotsPrompt, | ||
| agentName: cached.agentName, | ||
| agentDescription: cached.agentDescription, | ||
| agentDisplayName: cached.agentDisplayName, | ||
| }); | ||
|
|
||
| // Apply to form fields | ||
| form.setFieldsValue({ | ||
| dutyPrompt: cached.dutyPrompt, | ||
| constraintPrompt: cached.constraintPrompt, | ||
| fewShotsPrompt: cached.fewShotsPrompt, | ||
| agentName: cached.agentName, | ||
| agentDescription: cached.agentDescription, | ||
| agentDisplayName: cached.agentDisplayName, | ||
| }); | ||
|
|
||
| // Update the store's editedAgent so hasUnsavedChanges is correctly set | ||
| // This will trigger hasUnsavedChanges = true when it differs from baselineAgent | ||
| updateProfileInfo({ | ||
| name: cached.agentName, | ||
| display_name: cached.agentDisplayName, | ||
| description: cached.agentDescription, | ||
| duty_prompt: cached.dutyPrompt, | ||
| constraint_prompt: cached.constraintPrompt, | ||
| few_shots_prompt: cached.fewShotsPrompt, | ||
| }); | ||
| } | ||
| // If no valid cache, do nothing - this agent wasn't being generated | ||
| }, [currentAgentId]); | ||
|
|
||
| // Handle business description change | ||
| const handleBusinessDescriptionChange = (value: string) => { | ||
| updateBusinessInfo({ | ||
|
|
@@ -474,12 +578,23 @@ | |
| return; | ||
| } | ||
|
|
||
| const effectiveAgentId = currentAgentId ?? 0; | ||
|
|
||
| setIsGenerating(true); | ||
| generationInitiatorRef.current = effectiveAgentId; | ||
| setActiveTab("few-shots"); | ||
|
|
||
| // Mark generation as in progress in cache | ||
| setAgentGenerationStatus(effectiveAgentId, true, { | ||
| businessDescription: businessInfo.businessDescription, | ||
| businessLogicModelId: businessInfo.businessLogicModelId, | ||
| businessLogicModelName: businessInfo.businessLogicModelName, | ||
| }); | ||
|
|
||
| try { | ||
| await generatePromptStream( | ||
| { | ||
| agent_id: currentAgentId || 0, | ||
| agent_id: effectiveAgentId, | ||
| task_description: businessInfo.businessDescription, | ||
| model_id: businessInfo.businessLogicModelId.toString(), | ||
| sub_agent_ids: editedAgent.sub_agent_id_list, | ||
|
|
@@ -491,79 +606,139 @@ | |
| ) | ||
| : [], | ||
| }, | ||
| (data) => { | ||
|
Check failure on line 609 in frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx
|
||
| // Process streaming response data | ||
| // Track the agent this generation was for | ||
| const generationAgentId = effectiveAgentId; | ||
| const currentVisibleAgentId = useAgentConfigStore.getState().currentAgentId ?? 0; | ||
| const isSameAgent = generationInitiatorRef.current === currentVisibleAgentId; | ||
|
|
||
| switch (data.type) { | ||
| case GENERATE_PROMPT_STREAM_TYPES.DUTY: | ||
| form.setFieldsValue({ dutyPrompt: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| dutyPrompt: data.content, | ||
| })); | ||
| // Only update UI if we're on the same agent | ||
| if (isSameAgent) { | ||
| form.setFieldsValue({ dutyPrompt: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| dutyPrompt: data.content, | ||
| })); | ||
| } | ||
| // Always save to cache for the generation agent | ||
| saveGeneratedField(generationAgentId, 'dutyPrompt', data.content); | ||
| break; | ||
| case GENERATE_PROMPT_STREAM_TYPES.CONSTRAINT: | ||
| form.setFieldsValue({ constraintPrompt: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| constraintPrompt: data.content, | ||
| })); | ||
| if (isSameAgent) { | ||
| form.setFieldsValue({ constraintPrompt: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| constraintPrompt: data.content, | ||
| })); | ||
| } | ||
| saveGeneratedField(generationAgentId, 'constraintPrompt', data.content); | ||
| break; | ||
| case GENERATE_PROMPT_STREAM_TYPES.FEW_SHOTS: | ||
| form.setFieldsValue({ fewShotsPrompt: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| fewShotsPrompt: data.content, | ||
| })); | ||
| if (isSameAgent) { | ||
| form.setFieldsValue({ fewShotsPrompt: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| fewShotsPrompt: data.content, | ||
| })); | ||
| } | ||
| saveGeneratedField(generationAgentId, 'fewShotsPrompt', data.content); | ||
| break; | ||
| case GENERATE_PROMPT_STREAM_TYPES.AGENT_VAR_NAME: | ||
| if (!form.getFieldValue("agentName")?.trim()) { | ||
| form.setFieldsValue({ agentName: data.content }); | ||
| if (isSameAgent) { | ||
| if (!form.getFieldValue("agentName")?.trim()) { | ||
| form.setFieldsValue({ agentName: data.content }); | ||
| } | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| agentName: data.content, | ||
| })); | ||
| } | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| agentName: data.content, | ||
| })); | ||
| saveGeneratedField(generationAgentId, 'agentName', data.content); | ||
| break; | ||
| case GENERATE_PROMPT_STREAM_TYPES.AGENT_DESCRIPTION: | ||
| form.setFieldsValue({ agentDescription: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| agentDescription: data.content, | ||
| })); | ||
| if (isSameAgent) { | ||
| form.setFieldsValue({ agentDescription: data.content }); | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| agentDescription: data.content, | ||
| })); | ||
| } | ||
| saveGeneratedField(generationAgentId, 'agentDescription', data.content); | ||
| break; | ||
| case GENERATE_PROMPT_STREAM_TYPES.AGENT_DISPLAY_NAME: | ||
| // Only update if current agent display name is empty | ||
| if (!form.getFieldValue("agentDisplayName")?.trim()) { | ||
| form.setFieldsValue({ agentDisplayName: data.content }); | ||
| if (isSameAgent) { | ||
| // Only update if current agent display name is empty | ||
| if (!form.getFieldValue("agentDisplayName")?.trim()) { | ||
| form.setFieldsValue({ agentDisplayName: data.content }); | ||
| } | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| agentDisplayName: data.content, | ||
| })); | ||
| } | ||
| setGeneratedContent((prev) => ({ | ||
| ...prev, | ||
| agentDisplayName: data.content, | ||
| })); | ||
| saveGeneratedField(generationAgentId, 'agentDisplayName', data.content); | ||
| break; | ||
| } | ||
| }, | ||
| (error) => { | ||
| log.error("Generate prompt stream error:", error); | ||
| // Try to get i18n translated message using error code, fallback to backend message or default | ||
| let errorMessage = t("businessLogic.config.message.generateError"); | ||
| if (error?.code) { | ||
| const i18nKey = `errorCode.${error.code}`; | ||
| const translated = t(i18nKey); | ||
| // Check if translation exists (i18next returns the key if not found) | ||
| if (translated !== i18nKey) { | ||
| errorMessage = translated; | ||
|
|
||
| // Track the agent this generation was for | ||
| const generationAgentId = effectiveAgentId; | ||
|
|
||
| // Always clear generating state regardless of current agent | ||
| // This prevents stuck "generating" state when user switches agents | ||
| setIsGenerating(false); | ||
| generationInitiatorRef.current = null; | ||
|
|
||
| // If we're on the same agent, show error message | ||
| const currentEffectiveAgentId = useAgentConfigStore.getState().currentAgentId ?? 0; | ||
| if (generationAgentId === currentEffectiveAgentId) { | ||
| // Try to get i18n translated message using error code, fallback to backend message or default | ||
| let errorMessage = t("businessLogic.config.message.generateError"); | ||
| if (error?.code) { | ||
| const i18nKey = `errorCode.${error.code}`; | ||
| const translated = t(i18nKey); | ||
| // Check if translation exists (i18next returns the key if not found) | ||
| if (translated !== i18nKey) { | ||
| errorMessage = translated; | ||
| } else if (error?.message) { | ||
| errorMessage = error.message; | ||
| } | ||
| } else if (error?.message) { | ||
| errorMessage = error.message; | ||
| } | ||
| } else if (error?.message) { | ||
| errorMessage = error.message; | ||
| message.error(errorMessage); | ||
| } | ||
| message.error(errorMessage); | ||
| setIsGenerating(false); | ||
|
|
||
| // Clear cache for this agent | ||
| setAgentGenerationStatus(generationAgentId, false); | ||
| }, | ||
| () => { | ||
| // Track the agent this generation was for | ||
| const generationAgentId = effectiveAgentId; | ||
|
|
||
| // Check if we're still on the same agent | ||
| const currentEffectiveAgentId = useAgentConfigStore.getState().currentAgentId ?? 0; | ||
| const isSameAgent = generationInitiatorRef.current === currentEffectiveAgentId; | ||
|
|
||
| // Clear generating state immediately for ALL cases | ||
| // This prevents the "stuck in generating" state when user switches agents | ||
| setIsGenerating(false); | ||
| generationInitiatorRef.current = null; | ||
|
|
||
| // If not on same agent, keep the cache so user can restore when switching back | ||
| // Do NOT clear cache here - the cache contains the completed generation result | ||
| // Always mark cache as finished (isGenerating=false) so switch-back effect can restore it | ||
| if (!isSameAgent) { | ||
| setAgentGenerationStatus(generationAgentId, false); | ||
| return; | ||
| } | ||
|
Comment on lines
719
to
+738
|
||
|
|
||
| // On same agent: proceed with updating form values and store | ||
|
|
||
| // After generation completes, get all form values and update parent component state | ||
| // Use generatedContent state as fallback to ensure we get the streamed data | ||
| const formValues = form.getFieldsValue(); | ||
|
|
@@ -594,14 +769,20 @@ | |
| agentDisplayName: "", | ||
| }); | ||
|
|
||
| // Clear the cache since generation completed successfully on this agent | ||
| clearAgentGenerationCache(generationAgentId); | ||
|
|
||
|
Comment on lines
+772
to
+774
|
||
| message.success(t("businessLogic.config.message.generateSuccess")); | ||
| setIsGenerating(false); | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| log.error("Generate agent error:", error); | ||
| message.error(t("businessLogic.config.message.generateError")); | ||
|
|
||
| // Clear generating state but keep cache for potential resume | ||
| setIsGenerating(false); | ||
| generationInitiatorRef.current = null; | ||
| setAgentGenerationStatus(effectiveAgentId, false); | ||
| } | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This effect deletes the cache whenever
cached.isGeneratingis true. ButisGeneratingwill be true while a stream is running (including when the user switches away and comes back), so this will drop partial/complete results and prevent restoration. Instead of clearing here, restore cached partial content (and/or set UI generating state) and only clear caches when they are expired or explicitly discarded.