Merge pull request #618 from langflow-ai/fix/chat_filter_clearing

fix: chat filter being cleared
This commit is contained in:
Sebastián Estévez 2025-12-05 18:09:07 -05:00 committed by GitHub
commit d908174758
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 252 additions and 368 deletions

View file

@ -25,10 +25,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
const [uploadedTaskId, setUploadedTaskId] = useState<string | null>(null); const [uploadedTaskId, setUploadedTaskId] = useState<string | null>(null);
const [shouldCreateFilter, setShouldCreateFilter] = useState(false); const [shouldCreateFilter, setShouldCreateFilter] = useState(false);
const [isCreatingFilter, setIsCreatingFilter] = useState(false); const [isCreatingFilter, setIsCreatingFilter] = useState(false);
const [error, setError] = useState<string | null>(null);
// Track which tasks we've already handled to prevent infinite loops
const handledFailedTasksRef = useRef<Set<string>>(new Set());
const createFilterMutation = useCreateFilter(); const createFilterMutation = useCreateFilter();
@ -45,6 +41,8 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during upload refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during upload
}); });
const { refetch: refetchNudges } = useGetNudgesQuery(null);
// Monitor tasks and call onComplete when file processing is done // Monitor tasks and call onComplete when file processing is done
useEffect(() => { useEffect(() => {
if (currentStep === null || !tasks || !uploadedTaskId) { if (currentStep === null || !tasks || !uploadedTaskId) {
@ -59,109 +57,14 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
return; return;
} }
// Skip if this task was already handled as a failed task (from a previous failed upload)
// This prevents processing old failed tasks when a new upload starts
if (handledFailedTasksRef.current.has(matchingTask.task_id)) {
// Check if it's a failed task that we've already handled
const hasFailedFile =
matchingTask.files &&
Object.values(matchingTask.files).some(
(file) => file.status === "failed" || file.status === "error",
);
if (hasFailedFile) {
// This is an old failed task that we've already handled, ignore it
console.log(
"Skipping already-handled failed task:",
matchingTask.task_id,
);
return;
}
// If it's not a failed task, remove it from handled list (it might have succeeded on retry)
handledFailedTasksRef.current.delete(matchingTask.task_id);
}
// Check if any file failed in the matching task
const hasFailedFile = (() => {
// Must have files object
if (!matchingTask.files || typeof matchingTask.files !== "object") {
return false;
}
const fileEntries = Object.values(matchingTask.files);
// Must have at least one file
if (fileEntries.length === 0) {
return false;
}
// Check if any file has failed status
return fileEntries.some(
(file) => file.status === "failed" || file.status === "error",
);
})();
// If any file failed, show error and jump back one step (like onboarding-card.tsx)
// Only handle if we haven't already handled this task
if (
hasFailedFile &&
!isCreatingFilter &&
!handledFailedTasksRef.current.has(matchingTask.task_id)
) {
console.error("File failed in task, jumping back one step", matchingTask);
// Mark this task as handled to prevent infinite loops
handledFailedTasksRef.current.add(matchingTask.task_id);
// Extract error messages from failed files
const errorMessages: string[] = [];
if (matchingTask.files) {
Object.values(matchingTask.files).forEach((file) => {
if (
(file.status === "failed" || file.status === "error") &&
file.error
) {
errorMessages.push(file.error);
}
});
}
// Also check task-level error
if (matchingTask.error) {
errorMessages.push(matchingTask.error);
}
// Use the first error message, or a generic message if no errors found
const errorMessage =
errorMessages.length > 0
? errorMessages[0]
: "Document failed to ingest. Please try again with a different file.";
// Set error message and jump back one step
setError(errorMessage);
setCurrentStep(STEP_LIST.length);
// Clear filter creation flags since ingestion failed
setShouldCreateFilter(false);
setUploadedFilename(null);
// Jump back one step after 1 second (go back to upload step)
setTimeout(() => {
setCurrentStep(null);
}, 1000);
return;
}
// Check if the matching task is still active (pending, running, or processing) // Check if the matching task is still active (pending, running, or processing)
const isTaskActive = const isTaskActive =
matchingTask.status === "pending" || matchingTask.status === "pending" ||
matchingTask.status === "running" || matchingTask.status === "running" ||
matchingTask.status === "processing"; matchingTask.status === "processing";
// If task is completed successfully (no failures) and has processed files, complete the onboarding step // If task is completed or has processed files, complete the onboarding step
if ( if (!isTaskActive || (matchingTask.processed_files ?? 0) > 0) {
(!isTaskActive || (matchingTask.processed_files ?? 0) > 0) &&
!hasFailedFile
) {
// Set to final step to show "Done" // Set to final step to show "Done"
setCurrentStep(STEP_LIST.length); setCurrentStep(STEP_LIST.length);
@ -217,6 +120,8 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
}) })
.finally(() => { .finally(() => {
setIsCreatingFilter(false); setIsCreatingFilter(false);
// Refetch nudges to get new ones
refetchNudges();
// Wait a bit before completing (after filter is created) // Wait a bit before completing (after filter is created)
setTimeout(() => { setTimeout(() => {
@ -225,6 +130,8 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
}); });
} else { } else {
// No filter to create, just complete // No filter to create, just complete
// Refetch nudges to get new ones
refetchNudges();
// Wait a bit before completing // Wait a bit before completing
setTimeout(() => { setTimeout(() => {
@ -236,6 +143,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
tasks, tasks,
currentStep, currentStep,
onComplete, onComplete,
refetchNudges,
shouldCreateFilter, shouldCreateFilter,
uploadedFilename, uploadedFilename,
uploadedTaskId, uploadedTaskId,
@ -250,23 +158,11 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
}; };
const handleUploadClick = () => { const handleUploadClick = () => {
// Clear any previous error when user clicks to upload again
setError(null);
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const performUpload = async (file: File) => { const performUpload = async (file: File) => {
setIsUploading(true); setIsUploading(true);
// Clear any previous error when starting a new upload
setError(null);
// Clear handled tasks ref to allow retry
handledFailedTasksRef.current.clear();
// Reset task ID to prevent matching old failed tasks
setUploadedTaskId(null);
// Clear filter creation flags
setShouldCreateFilter(false);
setUploadedFilename(null);
try { try {
setCurrentStep(0); setCurrentStep(0);
const result = await uploadFile(file, true, true); // Pass createFilter=true const result = await uploadFile(file, true, true); // Pass createFilter=true
@ -288,8 +184,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
setCurrentStep(1); setCurrentStep(1);
}, 1500); }, 1500);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : "Upload failed";
error instanceof Error ? error.message : "Upload failed";
console.error("Upload failed", errorMessage); console.error("Upload failed", errorMessage);
// Dispatch event that chat context can listen to // Dispatch event that chat context can listen to
@ -311,9 +206,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
// Reset on error // Reset on error
setCurrentStep(null); setCurrentStep(null);
setUploadedTaskId(null); setUploadedTaskId(null);
setError(errorMessage);
setShouldCreateFilter(false);
setUploadedFilename(null);
} finally { } finally {
setIsUploading(false); setIsUploading(false);
} }
@ -347,24 +239,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
exit={{ opacity: 0, y: -24 }} exit={{ opacity: 0, y: -24 }}
transition={{ duration: 0.4, ease: "easeInOut" }} transition={{ duration: 0.4, ease: "easeInOut" }}
> >
<div className="w-full flex flex-col gap-4">
<AnimatePresence mode="wait">
{error && (
<motion.div
key="error"
initial={{ opacity: 1, y: 0, height: "auto" }}
exit={{ opacity: 0, y: -10, height: 0 }}
>
<div className="pb-2 flex items-center gap-4">
<X className="w-4 h-4 text-destructive shrink-0" />
<span className="text-sm text-muted-foreground">
{error}
</span>
</div>
</motion.div>
)}
</AnimatePresence>
<div>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -373,7 +247,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
> >
<div>{isUploading ? "Uploading..." : "Add a document"}</div> <div>{isUploading ? "Uploading..." : "Add a document"}</div>
</Button> </Button>
</div>
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
@ -381,7 +254,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
className="hidden" className="hidden"
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt" accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
/> />
</div>
</motion.div> </motion.div>
) : ( ) : (
<motion.div <motion.div
@ -396,7 +268,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
isCompleted={false} isCompleted={false}
steps={STEP_LIST} steps={STEP_LIST}
storageKey={ONBOARDING_UPLOAD_STEPS_KEY} storageKey={ONBOARDING_UPLOAD_STEPS_KEY}
hasError={!!error}
/> />
</motion.div> </motion.div>
)} )}

View file

@ -172,12 +172,14 @@ export function ChatRenderer({
// Mark onboarding as complete in context // Mark onboarding as complete in context
setOnboardingComplete(true); setOnboardingComplete(true);
// Clear ALL conversation state so next message starts fresh // Store the user document filter as default for new conversations FIRST
await startNewConversation(); // This must happen before startNewConversation() so the filter is available
// Store the user document filter as default for new conversations and load it
await storeDefaultFilterForNewConversations(true); await storeDefaultFilterForNewConversations(true);
// Clear ALL conversation state so next message starts fresh
// This will pick up the default filter we just set
await startNewConversation();
// Clean up onboarding filter IDs now that we've set the default // Clean up onboarding filter IDs now that we've set the default
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY); localStorage.removeItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY);

View file

@ -262,6 +262,10 @@ export function ChatProvider({ children }: ChatProviderProps) {
const startNewConversation = useCallback(async () => { const startNewConversation = useCallback(async () => {
console.log("[CONVERSATION] Starting new conversation"); console.log("[CONVERSATION] Starting new conversation");
// Check if there's existing conversation data - if so, this is a manual "new conversation" action
// Check state values before clearing them
const hasExistingConversation = conversationData !== null || placeholderConversation !== null;
// Clear current conversation data and reset state // Clear current conversation data and reset state
setCurrentConversationId(null); setCurrentConversationId(null);
setPreviousResponseIds({ chat: null, langflow: null }); setPreviousResponseIds({ chat: null, langflow: null });
@ -295,15 +299,22 @@ export function ChatProvider({ children }: ChatProviderProps) {
setConversationFilterState(null); setConversationFilterState(null);
} }
} else { } else {
console.log("[CONVERSATION] No default filter set"); // No default filter in localStorage
if (hasExistingConversation) {
// User is manually starting a new conversation - clear the filter
console.log("[CONVERSATION] Manual new conversation - clearing filter");
setConversationFilterState(null); setConversationFilterState(null);
}
} else { } else {
setConversationFilterState(null); // First time after onboarding - preserve existing filter if set
// This prevents clearing the filter when startNewConversation is called multiple times during onboarding
console.log("[CONVERSATION] No default filter set, preserving existing filter if any");
// Don't clear the filter - it may have been set by storeDefaultFilterForNewConversations
}
}
} }
// Create a temporary placeholder conversation to show in sidebar // Create a temporary placeholder conversation to show in sidebar
const placeholderConversation: ConversationData = { const newPlaceholderConversation: ConversationData = {
response_id: "new-conversation-" + Date.now(), response_id: "new-conversation-" + Date.now(),
title: "New conversation", title: "New conversation",
endpoint: endpoint, endpoint: endpoint,
@ -318,10 +329,10 @@ export function ChatProvider({ children }: ChatProviderProps) {
last_activity: new Date().toISOString(), last_activity: new Date().toISOString(),
}; };
setPlaceholderConversation(placeholderConversation); setPlaceholderConversation(newPlaceholderConversation);
// Force immediate refresh to ensure sidebar shows correct state // Force immediate refresh to ensure sidebar shows correct state
refreshConversations(true); refreshConversations(true);
}, [endpoint, refreshConversations]); }, [endpoint, refreshConversations, conversationData, placeholderConversation]);
const addConversationDoc = useCallback((filename: string) => { const addConversationDoc = useCallback((filename: string) => {
setConversationDocs((prev) => [ setConversationDocs((prev) => [