From 49ea058cfefd4f47e73f96c4359ef22478995384 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:27:22 -0300 Subject: [PATCH 1/6] fix: added onboarding steps accordion, changed bg color (#313) * fixed accordion chevron side * Added steps history to animated provider steps * always reserve space for thinking, and pass required things for children * update onboarding card to reflect changes * added showCompleted * passed required props * Added background to other pages * Made tasks just appear when not on onboarding * changed task context to not have error * changed default to closed * fixed onboarding card * changed line to show up centered --- frontend/components/ui/accordion.tsx | 6 +- .../components/onboarding-content.tsx | 6 +- .../components/onboarding-step.tsx | 336 +++++---- .../components/onboarding-upload.tsx | 184 ++--- .../components/animated-provider-steps.tsx | 269 ++++++-- .../onboarding/components/onboarding-card.tsx | 56 +- frontend/src/components/chat-renderer.tsx | 3 +- frontend/src/contexts/task-context.tsx | 639 +++++++++--------- 8 files changed, 815 insertions(+), 684 deletions(-) diff --git a/frontend/components/ui/accordion.tsx b/frontend/components/ui/accordion.tsx index 37ec5c3c..16330e30 100644 --- a/frontend/components/ui/accordion.tsx +++ b/frontend/components/ui/accordion.tsx @@ -1,7 +1,7 @@ "use client"; import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDown } from "lucide-react"; +import { ChevronRight } from "lucide-react"; import * as React from "react"; import { cn } from "@/lib/utils"; @@ -28,12 +28,12 @@ const AccordionTrigger = React.forwardRef< svg]:rotate-180", + "flex flex-1 items-center p-4 py-2.5 gap-2 font-medium !text-mmd text-muted-foreground transition-all [&[data-state=open]>svg]:rotate-90", className, )} {...props} > - + {children} diff --git a/frontend/src/app/new-onboarding/components/onboarding-content.tsx b/frontend/src/app/new-onboarding/components/onboarding-content.tsx index 6ca77e60..c328d1f3 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-content.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-content.tsx @@ -26,7 +26,6 @@ export function OnboardingContent({ ); const [isLoadingModels, setIsLoadingModels] = useState(false); const [loadingStatus, setLoadingStatus] = useState([]); - const [hasStartedOnboarding, setHasStartedOnboarding] = useState(false); const { streamingMessage, isLoading, sendMessage } = useChatStreaming({ onComplete: (message, newResponseId) => { @@ -81,16 +80,17 @@ export function OnboardingContent({ = 0} isCompleted={currentStep > 0} + showCompleted={true} text="Let's get started by setting up your model provider." isLoadingModels={isLoadingModels} loadingStatus={loadingStatus} - reserveSpaceForThinking={!hasStartedOnboarding} + reserveSpaceForThinking={true} > { - setHasStartedOnboarding(true); handleStepComplete(); }} + isCompleted={currentStep > 0} setIsLoadingModels={setIsLoadingModels} setLoadingStatus={setLoadingStatus} /> diff --git a/frontend/src/app/new-onboarding/components/onboarding-step.tsx b/frontend/src/app/new-onboarding/components/onboarding-step.tsx index 93f76a74..9ce97c87 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-step.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-step.tsx @@ -2,191 +2,189 @@ import { AnimatePresence, motion } from "motion/react"; import { type ReactNode, useEffect, useState } from "react"; import { Message } from "@/app/chat/components/message"; import DogIcon from "@/components/logo/dog-icon"; -import AnimatedProcessingIcon from "@/components/ui/animated-processing-icon"; import { MarkdownRenderer } from "@/components/markdown-renderer"; +import AnimatedProcessingIcon from "@/components/ui/animated-processing-icon"; import { cn } from "@/lib/utils"; interface OnboardingStepProps { - text: string; - children?: ReactNode; - isVisible: boolean; - isCompleted?: boolean; - icon?: ReactNode; - isMarkdown?: boolean; - hideIcon?: boolean; - isLoadingModels?: boolean; - loadingStatus?: string[]; - reserveSpaceForThinking?: boolean; + text: string; + children?: ReactNode; + isVisible: boolean; + isCompleted?: boolean; + showCompleted?: boolean; + icon?: ReactNode; + isMarkdown?: boolean; + hideIcon?: boolean; + isLoadingModels?: boolean; + loadingStatus?: string[]; + reserveSpaceForThinking?: boolean; } export function OnboardingStep({ - text, - children, - isVisible, - isCompleted = false, - icon, - isMarkdown = false, - hideIcon = false, - isLoadingModels = false, - loadingStatus = [], - reserveSpaceForThinking = false, + text, + children, + isVisible, + isCompleted = false, + showCompleted = false, + icon, + isMarkdown = false, + hideIcon = false, + isLoadingModels = false, + loadingStatus = [], + reserveSpaceForThinking = false, }: OnboardingStepProps) { - const [displayedText, setDisplayedText] = useState(""); - const [showChildren, setShowChildren] = useState(false); - const [currentStatusIndex, setCurrentStatusIndex] = useState(0); + const [displayedText, setDisplayedText] = useState(""); + const [showChildren, setShowChildren] = useState(false); + const [currentStatusIndex, setCurrentStatusIndex] = useState(0); - // Cycle through loading status messages once - useEffect(() => { - if (!isLoadingModels || loadingStatus.length === 0) { - setCurrentStatusIndex(0); - return; - } + // Cycle through loading status messages once + useEffect(() => { + if (!isLoadingModels || loadingStatus.length === 0) { + setCurrentStatusIndex(0); + return; + } - const interval = setInterval(() => { - setCurrentStatusIndex((prev) => { - const nextIndex = prev + 1; - // Stop at the last message - if (nextIndex >= loadingStatus.length - 1) { - clearInterval(interval); - return loadingStatus.length - 1; - } - return nextIndex; - }); - }, 1500); // Change status every 1.5 seconds + const interval = setInterval(() => { + setCurrentStatusIndex((prev) => { + const nextIndex = prev + 1; + // Stop at the last message + if (nextIndex >= loadingStatus.length - 1) { + clearInterval(interval); + return loadingStatus.length - 1; + } + return nextIndex; + }); + }, 1500); // Change status every 1.5 seconds - return () => clearInterval(interval); - }, [isLoadingModels, loadingStatus]); + return () => clearInterval(interval); + }, [isLoadingModels, loadingStatus]); - useEffect(() => { - if (!isVisible) { - setDisplayedText(""); - setShowChildren(false); - return; - } + useEffect(() => { + if (!isVisible) { + setDisplayedText(""); + setShowChildren(false); + return; + } - if (isCompleted) { - setDisplayedText(text); - setShowChildren(true); - return; - } + if (isCompleted) { + setDisplayedText(text); + setShowChildren(true); + return; + } - let currentIndex = 0; - setDisplayedText(""); - setShowChildren(false); + let currentIndex = 0; + setDisplayedText(""); + setShowChildren(false); - const interval = setInterval(() => { - if (currentIndex < text.length) { - setDisplayedText(text.slice(0, currentIndex + 1)); - currentIndex++; - } else { - clearInterval(interval); - setShowChildren(true); - } - }, 20); // 20ms per character + const interval = setInterval(() => { + if (currentIndex < text.length) { + setDisplayedText(text.slice(0, currentIndex + 1)); + currentIndex++; + } else { + clearInterval(interval); + setShowChildren(true); + } + }, 20); // 20ms per character - return () => clearInterval(interval); - }, [text, isVisible, isCompleted]); + return () => clearInterval(interval); + }, [text, isVisible, isCompleted]); - if (!isVisible) return null; + if (!isVisible) return null; - return ( - - - ) : ( - icon || ( -
- -
- ) - ) - } - > -
- {isLoadingModels && loadingStatus.length > 0 ? ( -
-
-
- -
- - Thinking - -
-
-
-
-
- - - {loadingStatus[currentStatusIndex]} - - -
-
-
-
- ) : isMarkdown ? ( - - ) : ( - <> -

- {displayedText} - {!showChildren && !isCompleted && ( - - )} -

- {reserveSpaceForThinking && ( -
- )} - - )} - {children && ( - - {((showChildren && !isCompleted) || isMarkdown) && ( - -
- {children}
-
- )} -
- )} -
- - - ); + return ( + + + ) : ( + icon || ( +
+ +
+ ) + ) + } + > +
+ {isLoadingModels && loadingStatus.length > 0 ? ( +
+
+
+ +
+ + Thinking + +
+
+
+
+
+ + + {loadingStatus[currentStatusIndex]} + + +
+
+
+
+ ) : isMarkdown ? ( + + ) : ( + <> +

+ {displayedText} + {!showChildren && !isCompleted && ( + + )} +

+ {reserveSpaceForThinking &&
} + + )} + {children && ( + + {((showChildren && (!isCompleted || showCompleted)) || + isMarkdown) && ( + +
{children}
+
+ )} +
+ )} +
+ + + ); } diff --git a/frontend/src/app/new-onboarding/components/onboarding-upload.tsx b/frontend/src/app/new-onboarding/components/onboarding-upload.tsx index dab12e53..366b420e 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-upload.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-upload.tsx @@ -1,108 +1,108 @@ -import { ChangeEvent, useRef, useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; +import { type ChangeEvent, useRef, useState } from "react"; +import { AnimatedProviderSteps } from "@/app/onboarding/components/animated-provider-steps"; import { Button } from "@/components/ui/button"; import { uploadFileForContext } from "@/lib/upload-utils"; -import { AnimatePresence, motion } from "motion/react"; -import { AnimatedProviderSteps } from "@/app/onboarding/components/animated-provider-steps"; interface OnboardingUploadProps { - onComplete: () => void; + onComplete: () => void; } const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => { - const fileInputRef = useRef(null); - const [isUploading, setIsUploading] = useState(false); - const [currentStep, setCurrentStep] = useState(null); + const fileInputRef = useRef(null); + const [isUploading, setIsUploading] = useState(false); + const [currentStep, setCurrentStep] = useState(null); - const STEP_LIST = [ - "Uploading your document", - "Processing your document", - ]; + const STEP_LIST = ["Uploading your document", "Processing your document"]; - const resetFileInput = () => { - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }; + const resetFileInput = () => { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; - const handleUploadClick = () => { - fileInputRef.current?.click(); - }; + const handleUploadClick = () => { + fileInputRef.current?.click(); + }; - const performUpload = async (file: File) => { - setIsUploading(true); - try { - setCurrentStep(0); - await uploadFileForContext(file); - console.log("Document uploaded successfully"); - } catch (error) { - console.error("Upload failed", (error as Error).message); - } finally { - setIsUploading(false); - await new Promise(resolve => setTimeout(resolve, 1000)); - setCurrentStep(STEP_LIST.length); - await new Promise(resolve => setTimeout(resolve, 500)); - onComplete(); - } - }; + const performUpload = async (file: File) => { + setIsUploading(true); + try { + setCurrentStep(0); + await uploadFileForContext(file); + console.log("Document uploaded successfully"); + } catch (error) { + console.error("Upload failed", (error as Error).message); + } finally { + setIsUploading(false); + await new Promise((resolve) => setTimeout(resolve, 1000)); + setCurrentStep(STEP_LIST.length); + await new Promise((resolve) => setTimeout(resolve, 500)); + onComplete(); + } + }; - const handleFileChange = async (event: ChangeEvent) => { - const selectedFile = event.target.files?.[0]; - if (!selectedFile) { - resetFileInput(); - return; - } + const handleFileChange = async (event: ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + if (!selectedFile) { + resetFileInput(); + return; + } - try { - await performUpload(selectedFile); - } catch (error) { - console.error("Unable to prepare file for upload", (error as Error).message); - } finally { - resetFileInput(); - } - }; + try { + await performUpload(selectedFile); + } catch (error) { + console.error( + "Unable to prepare file for upload", + (error as Error).message, + ); + } finally { + resetFileInput(); + } + }; - - return ( - - {currentStep === null ? ( - - - - - ) : ( - - - - )} - - ) -} + return ( + + {currentStep === null ? ( + + + + + ) : ( + + + + )} + + ); +}; export default OnboardingUpload; diff --git a/frontend/src/app/onboarding/components/animated-provider-steps.tsx b/frontend/src/app/onboarding/components/animated-provider-steps.tsx index 90708d4a..0dc334aa 100644 --- a/frontend/src/app/onboarding/components/animated-provider-steps.tsx +++ b/frontend/src/app/onboarding/components/animated-provider-steps.tsx @@ -2,86 +2,211 @@ import { AnimatePresence, motion } from "framer-motion"; import { CheckIcon } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import AnimatedProcessingIcon from "@/components/ui/animated-processing-icon"; import { cn } from "@/lib/utils"; export function AnimatedProviderSteps({ - currentStep, - setCurrentStep, - steps, + currentStep, + isCompleted, + setCurrentStep, + steps, + storageKey = "provider-steps", }: { - currentStep: number; - setCurrentStep: (step: number) => void; - steps: string[]; + currentStep: number; + isCompleted: boolean; + setCurrentStep: (step: number) => void; + steps: string[]; + storageKey?: string; }) { + const [startTime, setStartTime] = useState(null); + const [elapsedTime, setElapsedTime] = useState(0); - useEffect(() => { - if (currentStep < steps.length - 1) { - const interval = setInterval(() => { - setCurrentStep(currentStep + 1); - }, 1500); - return () => clearInterval(interval); - } - }, [currentStep, setCurrentStep, steps]); + // Initialize start time from local storage or set new one + useEffect(() => { + const storedStartTime = localStorage.getItem(`${storageKey}-start`); + const storedElapsedTime = localStorage.getItem(`${storageKey}-elapsed`); - const isDone = currentStep >= steps.length; + if (isCompleted && storedElapsedTime) { + // If completed, use stored elapsed time + setElapsedTime(parseFloat(storedElapsedTime)); + } else if (storedStartTime) { + // If in progress, use stored start time + setStartTime(parseInt(storedStartTime)); + } else { + // First time, set new start time + const now = Date.now(); + setStartTime(now); + localStorage.setItem(`${storageKey}-start`, now.toString()); + } + }, [storageKey, isCompleted]); - return ( -
-
-
- - -
+ // Progress through steps + useEffect(() => { + if (currentStep < steps.length - 1 && !isCompleted) { + const interval = setInterval(() => { + setCurrentStep(currentStep + 1); + }, 1500); + return () => clearInterval(interval); + } + }, [currentStep, setCurrentStep, steps, isCompleted]); - - {isDone ? "Done" : "Thinking"} - -
-
- - {!isDone && ( - -
-
- - - {steps[currentStep]} - - -
- - )} - -
-
- ); + // Calculate and store elapsed time when completed + useEffect(() => { + if (isCompleted && startTime) { + const elapsed = Date.now() - startTime; + setElapsedTime(elapsed); + localStorage.setItem(`${storageKey}-elapsed`, elapsed.toString()); + localStorage.removeItem(`${storageKey}-start`); + } + }, [isCompleted, startTime, storageKey]); + + const isDone = currentStep >= steps.length && !isCompleted; + + return ( + + {!isCompleted ? ( + +
+
+ + +
+ + + {isDone ? "Done" : "Thinking"} + +
+
+ + {!isDone && ( + +
+
+ + + {steps[currentStep]} + + +
+ + )} + +
+
+ ) : ( + + + + +
+ + {`Initialized in ${(elapsedTime / 1000).toFixed(1)} seconds`} + +
+
+ +
+ {/* Connecting line on the left */} + + +
+ + {steps.map((step, index) => ( + + + + + + + + {step} + + + ))} + +
+
+
+
+
+
+ )} +
+ ); } diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx index fb3ce3b9..e379976b 100644 --- a/frontend/src/app/onboarding/components/onboarding-card.tsx +++ b/frontend/src/app/onboarding/components/onboarding-card.tsx @@ -27,6 +27,7 @@ import { OpenAIOnboarding } from "./openai-onboarding"; interface OnboardingCardProps { onComplete: () => void; + isCompleted?: boolean; setIsLoadingModels?: (isLoading: boolean) => void; setLoadingStatus?: (status: string[]) => void; } @@ -43,6 +44,7 @@ const TOTAL_PROVIDER_STEPS = STEP_LIST.length; const OnboardingCard = ({ onComplete, + isCompleted = false, setIsLoadingModels: setIsLoadingModelsParent, setLoadingStatus: setLoadingStatusParent, }: OnboardingCardProps) => { @@ -104,7 +106,7 @@ const OnboardingCard = ({ llm_model: "", }); - const [currentStep, setCurrentStep] = useState(null); + const [currentStep, setCurrentStep] = useState(isCompleted ? TOTAL_PROVIDER_STEPS : null); // Query tasks to track completion const { data: tasks } = useGetTasksQuery({ @@ -130,6 +132,7 @@ const OnboardingCard = ({ if ( (!activeTasks || (activeTasks.processed_files ?? 0) > 0) && tasks.length > 0 + && !isCompleted ) { // Set to final step to show "Done" setCurrentStep(TOTAL_PROVIDER_STEPS); @@ -138,7 +141,7 @@ const OnboardingCard = ({ onComplete(); }, 1000); } - }, [tasks, currentStep, onComplete]); + }, [tasks, currentStep, onComplete, isCompleted]); // Mutations const onboardingMutation = useOnboardingMutation({ @@ -267,31 +270,29 @@ const OnboardingCard = ({ - {!isLoadingModels && ( - - -
- -
-
- {!isComplete && ( - - {!!settings.llm_model && - !!settings.embedding_model && - !isDoclingHealthy - ? "docling-serve must be running to continue" - : "Please fill in all required fields"} - - )} -
- )} + + +
+ +
+
+ {!isComplete && ( + + {isLoadingModels ? "Loading models..." : (!!settings.llm_model && + !!settings.embedding_model && + !isDoclingHealthy + ? "docling-serve must be running to continue" + : "Please fill in all required fields")} + + )} +
) : ( @@ -303,6 +304,7 @@ const OnboardingCard = ({ > diff --git a/frontend/src/components/chat-renderer.tsx b/frontend/src/components/chat-renderer.tsx index 925ddb76..9d6402e6 100644 --- a/frontend/src/components/chat-renderer.tsx +++ b/frontend/src/components/chat-renderer.tsx @@ -149,6 +149,7 @@ export function ChatRenderer({ className={cn( "flex h-full w-full max-w-full max-h-full items-center justify-center overflow-hidden", !showLayout && "absolute", + showLayout && !isOnChatPage && "bg-background", )} >
{children} diff --git a/frontend/src/contexts/task-context.tsx b/frontend/src/contexts/task-context.tsx index 8bb2bb24..a8e7747e 100644 --- a/frontend/src/contexts/task-context.tsx +++ b/frontend/src/contexts/task-context.tsx @@ -3,368 +3,373 @@ import { useQueryClient } from "@tanstack/react-query"; import type React from "react"; import { - createContext, - useCallback, - useContext, - useEffect, - useRef, - useState, + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, } from "react"; import { toast } from "sonner"; import { useCancelTaskMutation } from "@/app/api/mutations/useCancelTaskMutation"; import { - type Task, - type TaskFileEntry, - useGetTasksQuery, + type Task, + type TaskFileEntry, + useGetTasksQuery, } from "@/app/api/queries/useGetTasksQuery"; import { useAuth } from "@/contexts/auth-context"; +import { ONBOARDING_STEP_KEY } from "@/lib/constants"; // Task interface is now imported from useGetTasksQuery export type { Task }; export interface TaskFile { - filename: string; - mimetype: string; - source_url: string; - size: number; - connector_type: string; - status: "active" | "failed" | "processing"; - task_id: string; - created_at: string; - updated_at: string; - error?: string; - embedding_model?: string; - embedding_dimensions?: number; + filename: string; + mimetype: string; + source_url: string; + size: number; + connector_type: string; + status: "active" | "failed" | "processing"; + task_id: string; + created_at: string; + updated_at: string; + error?: string; + embedding_model?: string; + embedding_dimensions?: number; } interface TaskContextType { - tasks: Task[]; - files: TaskFile[]; - addTask: (taskId: string) => void; - addFiles: (files: Partial[], taskId: string) => void; - refreshTasks: () => Promise; - cancelTask: (taskId: string) => Promise; - isPolling: boolean; - isFetching: boolean; - isMenuOpen: boolean; - toggleMenu: () => void; - isRecentTasksExpanded: boolean; - setRecentTasksExpanded: (expanded: boolean) => void; - // React Query states - isLoading: boolean; - error: Error | null; + tasks: Task[]; + files: TaskFile[]; + addTask: (taskId: string) => void; + addFiles: (files: Partial[], taskId: string) => void; + refreshTasks: () => Promise; + cancelTask: (taskId: string) => Promise; + isPolling: boolean; + isFetching: boolean; + isMenuOpen: boolean; + toggleMenu: () => void; + isRecentTasksExpanded: boolean; + setRecentTasksExpanded: (expanded: boolean) => void; + // React Query states + isLoading: boolean; + error: Error | null; } const TaskContext = createContext(undefined); export function TaskProvider({ children }: { children: React.ReactNode }) { - const [files, setFiles] = useState([]); - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isRecentTasksExpanded, setIsRecentTasksExpanded] = useState(false); - const previousTasksRef = useRef([]); - const { isAuthenticated, isNoAuthMode } = useAuth(); + const [files, setFiles] = useState([]); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isRecentTasksExpanded, setIsRecentTasksExpanded] = useState(false); + const previousTasksRef = useRef([]); + const { isAuthenticated, isNoAuthMode } = useAuth(); - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - // Use React Query hooks - const { - data: tasks = [], - isLoading, - error, - refetch: refetchTasks, - isFetching, - } = useGetTasksQuery({ - enabled: isAuthenticated || isNoAuthMode, - }); + // Use React Query hooks + const { + data: tasks = [], + isLoading, + error, + refetch: refetchTasks, + isFetching, + } = useGetTasksQuery({ + enabled: isAuthenticated || isNoAuthMode, + }); - const cancelTaskMutation = useCancelTaskMutation({ - onSuccess: () => { - toast.success("Task cancelled", { - description: "Task has been cancelled successfully", - }); - }, - onError: (error) => { - toast.error("Failed to cancel task", { - description: error.message, - }); - }, - }); + const cancelTaskMutation = useCancelTaskMutation({ + onSuccess: () => { + toast.success("Task cancelled", { + description: "Task has been cancelled successfully", + }); + }, + onError: (error) => { + toast.error("Failed to cancel task", { + description: error.message, + }); + }, + }); - const refetchSearch = useCallback(() => { - queryClient.invalidateQueries({ - queryKey: ["search"], - exact: false, - }); - }, [queryClient]); + // Helper function to check if onboarding is active + const isOnboardingActive = useCallback(() => { + if (typeof window === "undefined") return false; + return localStorage.getItem(ONBOARDING_STEP_KEY) !== null; + }, []); - const addFiles = useCallback( - (newFiles: Partial[], taskId: string) => { - const now = new Date().toISOString(); - const filesToAdd: TaskFile[] = newFiles.map((file) => ({ - filename: file.filename || "", - mimetype: file.mimetype || "", - source_url: file.source_url || "", - size: file.size || 0, - connector_type: file.connector_type || "local", - status: "processing", - task_id: taskId, - created_at: now, - updated_at: now, - error: file.error, - embedding_model: file.embedding_model, - embedding_dimensions: file.embedding_dimensions, - })); + const refetchSearch = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: ["search"], + exact: false, + }); + }, [queryClient]); - setFiles((prevFiles) => [...prevFiles, ...filesToAdd]); - }, - [], - ); + const addFiles = useCallback( + (newFiles: Partial[], taskId: string) => { + const now = new Date().toISOString(); + const filesToAdd: TaskFile[] = newFiles.map((file) => ({ + filename: file.filename || "", + mimetype: file.mimetype || "", + source_url: file.source_url || "", + size: file.size || 0, + connector_type: file.connector_type || "local", + status: "processing", + task_id: taskId, + created_at: now, + updated_at: now, + error: file.error, + embedding_model: file.embedding_model, + embedding_dimensions: file.embedding_dimensions, + })); - // Handle task status changes and file updates - useEffect(() => { - if (tasks.length === 0) { - // Store current tasks as previous for next comparison - previousTasksRef.current = tasks; - return; - } + setFiles((prevFiles) => [...prevFiles, ...filesToAdd]); + }, + [], + ); - // Check for task status changes by comparing with previous tasks - tasks.forEach((currentTask) => { - const previousTask = previousTasksRef.current.find( - (prev) => prev.task_id === currentTask.task_id, - ); + // Handle task status changes and file updates + useEffect(() => { + if (tasks.length === 0) { + // Store current tasks as previous for next comparison + previousTasksRef.current = tasks; + return; + } - // Only show toasts if we have previous data and status has changed - if ( - (previousTask && previousTask.status !== currentTask.status) || - (!previousTask && previousTasksRef.current.length !== 0) - ) { - // Process files from failed task and add them to files list - if (currentTask.files && typeof currentTask.files === "object") { - const taskFileEntries = Object.entries(currentTask.files); - const now = new Date().toISOString(); + // Check for task status changes by comparing with previous tasks + tasks.forEach((currentTask) => { + const previousTask = previousTasksRef.current.find( + (prev) => prev.task_id === currentTask.task_id, + ); - taskFileEntries.forEach(([filePath, fileInfo]) => { - if (typeof fileInfo === "object" && fileInfo) { - const fileInfoEntry = fileInfo as TaskFileEntry; - // Use the filename from backend if available, otherwise extract from path - const fileName = - fileInfoEntry.filename || - filePath.split("/").pop() || - filePath; - const fileStatus = fileInfoEntry.status ?? "processing"; + // Only show toasts if we have previous data and status has changed + if ( + (previousTask && previousTask.status !== currentTask.status) || + (!previousTask && previousTasksRef.current.length !== 0) + ) { + // Process files from failed task and add them to files list + if (currentTask.files && typeof currentTask.files === "object") { + const taskFileEntries = Object.entries(currentTask.files); + const now = new Date().toISOString(); - // Map backend file status to our TaskFile status - let mappedStatus: TaskFile["status"]; - switch (fileStatus) { - case "pending": - case "running": - mappedStatus = "processing"; - break; - case "completed": - mappedStatus = "active"; - break; - case "failed": - mappedStatus = "failed"; - break; - default: - mappedStatus = "processing"; - } + taskFileEntries.forEach(([filePath, fileInfo]) => { + if (typeof fileInfo === "object" && fileInfo) { + const fileInfoEntry = fileInfo as TaskFileEntry; + // Use the filename from backend if available, otherwise extract from path + const fileName = + fileInfoEntry.filename || filePath.split("/").pop() || filePath; + const fileStatus = fileInfoEntry.status ?? "processing"; - const fileError = (() => { - if ( - typeof fileInfoEntry.error === "string" && - fileInfoEntry.error.trim().length > 0 - ) { - return fileInfoEntry.error.trim(); - } - if ( - mappedStatus === "failed" && - typeof currentTask.error === "string" && - currentTask.error.trim().length > 0 - ) { - return currentTask.error.trim(); - } - return undefined; - })(); + // Map backend file status to our TaskFile status + let mappedStatus: TaskFile["status"]; + switch (fileStatus) { + case "pending": + case "running": + mappedStatus = "processing"; + break; + case "completed": + mappedStatus = "active"; + break; + case "failed": + mappedStatus = "failed"; + break; + default: + mappedStatus = "processing"; + } - setFiles((prevFiles) => { - const existingFileIndex = prevFiles.findIndex( - (f) => - f.source_url === filePath && - f.task_id === currentTask.task_id, - ); + const fileError = (() => { + if ( + typeof fileInfoEntry.error === "string" && + fileInfoEntry.error.trim().length > 0 + ) { + return fileInfoEntry.error.trim(); + } + if ( + mappedStatus === "failed" && + typeof currentTask.error === "string" && + currentTask.error.trim().length > 0 + ) { + return currentTask.error.trim(); + } + return undefined; + })(); - // Detect connector type based on file path or other indicators - let connectorType = "local"; - if (filePath.includes("/") && !filePath.startsWith("/")) { - // Likely S3 key format (bucket/path/file.ext) - connectorType = "s3"; - } + setFiles((prevFiles) => { + const existingFileIndex = prevFiles.findIndex( + (f) => + f.source_url === filePath && + f.task_id === currentTask.task_id, + ); - const fileEntry: TaskFile = { - filename: fileName, - mimetype: "", // We don't have this info from the task - source_url: filePath, - size: 0, // We don't have this info from the task - connector_type: connectorType, - status: mappedStatus, - task_id: currentTask.task_id, - created_at: - typeof fileInfoEntry.created_at === "string" - ? fileInfoEntry.created_at - : now, - updated_at: - typeof fileInfoEntry.updated_at === "string" - ? fileInfoEntry.updated_at - : now, - error: fileError, - embedding_model: - typeof fileInfoEntry.embedding_model === "string" - ? fileInfoEntry.embedding_model - : undefined, - embedding_dimensions: - typeof fileInfoEntry.embedding_dimensions === "number" - ? fileInfoEntry.embedding_dimensions - : undefined, - }; + // Detect connector type based on file path or other indicators + let connectorType = "local"; + if (filePath.includes("/") && !filePath.startsWith("/")) { + // Likely S3 key format (bucket/path/file.ext) + connectorType = "s3"; + } - if (existingFileIndex >= 0) { - // Update existing file - const updatedFiles = [...prevFiles]; - updatedFiles[existingFileIndex] = fileEntry; - return updatedFiles; - } else { - // Add new file - return [...prevFiles, fileEntry]; - } - }); - } - }); - } - if ( - previousTask && - previousTask.status !== "completed" && - currentTask.status === "completed" - ) { - // Task just completed - show success toast with file counts - const successfulFiles = currentTask.successful_files || 0; - const failedFiles = currentTask.failed_files || 0; + const fileEntry: TaskFile = { + filename: fileName, + mimetype: "", // We don't have this info from the task + source_url: filePath, + size: 0, // We don't have this info from the task + connector_type: connectorType, + status: mappedStatus, + task_id: currentTask.task_id, + created_at: + typeof fileInfoEntry.created_at === "string" + ? fileInfoEntry.created_at + : now, + updated_at: + typeof fileInfoEntry.updated_at === "string" + ? fileInfoEntry.updated_at + : now, + error: fileError, + embedding_model: + typeof fileInfoEntry.embedding_model === "string" + ? fileInfoEntry.embedding_model + : undefined, + embedding_dimensions: + typeof fileInfoEntry.embedding_dimensions === "number" + ? fileInfoEntry.embedding_dimensions + : undefined, + }; - let description = ""; - if (failedFiles > 0) { - description = `${successfulFiles} file${ - successfulFiles !== 1 ? "s" : "" - } uploaded successfully, ${failedFiles} file${ - failedFiles !== 1 ? "s" : "" - } failed`; - } else { - description = `${successfulFiles} file${ - successfulFiles !== 1 ? "s" : "" - } uploaded successfully`; - } + if (existingFileIndex >= 0) { + // Update existing file + const updatedFiles = [...prevFiles]; + updatedFiles[existingFileIndex] = fileEntry; + return updatedFiles; + } else { + // Add new file + return [...prevFiles, fileEntry]; + } + }); + } + }); + } + if ( + previousTask && + previousTask.status !== "completed" && + currentTask.status === "completed" + ) { + // Task just completed - show success toast with file counts + const successfulFiles = currentTask.successful_files || 0; + const failedFiles = currentTask.failed_files || 0; - toast.success("Task completed", { - description, - action: { - label: "View", - onClick: () => { - setIsMenuOpen(true); - setIsRecentTasksExpanded(true); - }, - }, - }); - setTimeout(() => { - setFiles((prevFiles) => - prevFiles.filter( - (file) => - file.task_id !== currentTask.task_id || - file.status === "failed", - ), - ); - refetchSearch(); - }, 500); - } else if ( - previousTask && - previousTask.status !== "failed" && - previousTask.status !== "error" && - (currentTask.status === "failed" || currentTask.status === "error") - ) { - // Task just failed - show error toast - toast.error("Task failed", { - description: `Task ${currentTask.task_id} failed: ${ - currentTask.error || "Unknown error" - }`, - }); - } - } - }); + let description = ""; + if (failedFiles > 0) { + description = `${successfulFiles} file${ + successfulFiles !== 1 ? "s" : "" + } uploaded successfully, ${failedFiles} file${ + failedFiles !== 1 ? "s" : "" + } failed`; + } else { + description = `${successfulFiles} file${ + successfulFiles !== 1 ? "s" : "" + } uploaded successfully`; + } + if (!isOnboardingActive()) { + toast.success("Task completed", { + description, + action: { + label: "View", + onClick: () => { + setIsMenuOpen(true); + setIsRecentTasksExpanded(true); + }, + }, + }); + } + setTimeout(() => { + setFiles((prevFiles) => + prevFiles.filter( + (file) => + file.task_id !== currentTask.task_id || + file.status === "failed", + ), + ); + refetchSearch(); + }, 500); + } else if ( + previousTask && + previousTask.status !== "failed" && + previousTask.status !== "error" && + (currentTask.status === "failed" || currentTask.status === "error") + ) { + // Task just failed - show error toast + toast.error("Task failed", { + description: `Task ${currentTask.task_id} failed: ${ + currentTask.error || "Unknown error" + }`, + }); + } + } + }); - // Store current tasks as previous for next comparison - previousTasksRef.current = tasks; - }, [tasks, refetchSearch]); + // Store current tasks as previous for next comparison + previousTasksRef.current = tasks; + }, [tasks, refetchSearch, isOnboardingActive]); - const addTask = useCallback( - (_taskId: string) => { - // React Query will automatically handle polling when tasks are active - // Just trigger a refetch to get the latest data - setTimeout(() => { - refetchTasks(); - }, 500); - }, - [refetchTasks], - ); + const addTask = useCallback( + (_taskId: string) => { + // React Query will automatically handle polling when tasks are active + // Just trigger a refetch to get the latest data + setTimeout(() => { + refetchTasks(); + }, 500); + }, + [refetchTasks], + ); - const refreshTasks = useCallback(async () => { - setFiles([]); - await refetchTasks(); - }, [refetchTasks]); + const refreshTasks = useCallback(async () => { + setFiles([]); + await refetchTasks(); + }, [refetchTasks]); + const cancelTask = useCallback( + async (taskId: string) => { + cancelTaskMutation.mutate({ taskId }); + }, + [cancelTaskMutation], + ); - const cancelTask = useCallback( - async (taskId: string) => { - cancelTaskMutation.mutate({ taskId }); - }, - [cancelTaskMutation], - ); + const toggleMenu = useCallback(() => { + setIsMenuOpen((prev) => !prev); + }, []); - const toggleMenu = useCallback(() => { - setIsMenuOpen((prev) => !prev); - }, []); + // Determine if we're polling based on React Query's refetch interval + const isPolling = + isFetching && + tasks.some( + (task) => + task.status === "pending" || + task.status === "running" || + task.status === "processing", + ); - // Determine if we're polling based on React Query's refetch interval - const isPolling = - isFetching && - tasks.some( - (task) => - task.status === "pending" || - task.status === "running" || - task.status === "processing", - ); + const value: TaskContextType = { + tasks, + files, + addTask, + addFiles, + refreshTasks, + cancelTask, + isPolling, + isFetching, + isMenuOpen, + toggleMenu, + isRecentTasksExpanded, + setRecentTasksExpanded: setIsRecentTasksExpanded, + isLoading, + error, + }; - const value: TaskContextType = { - tasks, - files, - addTask, - addFiles, - refreshTasks, - cancelTask, - isPolling, - isFetching, - isMenuOpen, - toggleMenu, - isRecentTasksExpanded, - setRecentTasksExpanded: setIsRecentTasksExpanded, - isLoading, - error, - }; - - return {children}; + return {children}; } export function useTask() { - const context = useContext(TaskContext); - if (context === undefined) { - throw new Error("useTask must be used within a TaskProvider"); - } - return context; + const context = useContext(TaskContext); + if (context === undefined) { + throw new Error("useTask must be used within a TaskProvider"); + } + return context; } From efa4b91736510841c0444362ee8f67afc77f0c4d Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 27 Oct 2025 16:59:00 -0400 Subject: [PATCH 2/6] update symlinks --- src/tui/_assets/documents/2506.08231v1.pdf | 1 - src/tui/_assets/documents/ai-human-resources.pdf | 1 - src/tui/_assets/documents/docling.pdf | 1 + src/tui/_assets/documents/ibm_anthropic.pdf | 1 + src/tui/_assets/documents/openrag-documentation.pdf | 1 + src/tui/_assets/flows/openrag_ingest_docling.json | 1 - 6 files changed, 3 insertions(+), 3 deletions(-) delete mode 120000 src/tui/_assets/documents/2506.08231v1.pdf delete mode 120000 src/tui/_assets/documents/ai-human-resources.pdf create mode 120000 src/tui/_assets/documents/docling.pdf create mode 120000 src/tui/_assets/documents/ibm_anthropic.pdf create mode 120000 src/tui/_assets/documents/openrag-documentation.pdf delete mode 120000 src/tui/_assets/flows/openrag_ingest_docling.json diff --git a/src/tui/_assets/documents/2506.08231v1.pdf b/src/tui/_assets/documents/2506.08231v1.pdf deleted file mode 120000 index 079e1ace..00000000 --- a/src/tui/_assets/documents/2506.08231v1.pdf +++ /dev/null @@ -1 +0,0 @@ -../../../../documents/2506.08231v1.pdf \ No newline at end of file diff --git a/src/tui/_assets/documents/ai-human-resources.pdf b/src/tui/_assets/documents/ai-human-resources.pdf deleted file mode 120000 index ba76acc5..00000000 --- a/src/tui/_assets/documents/ai-human-resources.pdf +++ /dev/null @@ -1 +0,0 @@ -../../../../documents/ai-human-resources.pdf \ No newline at end of file diff --git a/src/tui/_assets/documents/docling.pdf b/src/tui/_assets/documents/docling.pdf new file mode 120000 index 00000000..a550ad37 --- /dev/null +++ b/src/tui/_assets/documents/docling.pdf @@ -0,0 +1 @@ +../../../../documents/docling.pdf \ No newline at end of file diff --git a/src/tui/_assets/documents/ibm_anthropic.pdf b/src/tui/_assets/documents/ibm_anthropic.pdf new file mode 120000 index 00000000..2c72f137 --- /dev/null +++ b/src/tui/_assets/documents/ibm_anthropic.pdf @@ -0,0 +1 @@ +../../../../documents/ibm_anthropic.pdf \ No newline at end of file diff --git a/src/tui/_assets/documents/openrag-documentation.pdf b/src/tui/_assets/documents/openrag-documentation.pdf new file mode 120000 index 00000000..4e381e27 --- /dev/null +++ b/src/tui/_assets/documents/openrag-documentation.pdf @@ -0,0 +1 @@ +../../../../documents/openrag-documentation.pdf \ No newline at end of file diff --git a/src/tui/_assets/flows/openrag_ingest_docling.json b/src/tui/_assets/flows/openrag_ingest_docling.json deleted file mode 120000 index a23a93dc..00000000 --- a/src/tui/_assets/flows/openrag_ingest_docling.json +++ /dev/null @@ -1 +0,0 @@ -../../../../flows/openrag_ingest_docling.json \ No newline at end of file From 688d31e6b4ca6aeafd3cd3fe72180cfc0405cb93 Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Mon, 27 Oct 2025 16:02:53 -0500 Subject: [PATCH 3/6] Allow skip onboarding --- .../components/progress-bar.tsx | 26 ++++++++++++++++--- frontend/src/components/chat-renderer.tsx | 9 +++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/new-onboarding/components/progress-bar.tsx b/frontend/src/app/new-onboarding/components/progress-bar.tsx index 419a2ae4..ef16dc54 100644 --- a/frontend/src/app/new-onboarding/components/progress-bar.tsx +++ b/frontend/src/app/new-onboarding/components/progress-bar.tsx @@ -1,15 +1,20 @@ +import { ArrowRight } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + interface ProgressBarProps { currentStep: number; totalSteps: number; + onSkip?: () => void; } -export function ProgressBar({ currentStep, totalSteps }: ProgressBarProps) { +export function ProgressBar({ currentStep, totalSteps, onSkip }: ProgressBarProps) { const progressPercentage = ((currentStep + 1) / totalSteps) * 100; return ( -
-
-
+
+
+
+
+
+ {currentStep > 0 && onSkip && ( + + )} +
); } diff --git a/frontend/src/components/chat-renderer.tsx b/frontend/src/components/chat-renderer.tsx index 9d6402e6..d20864a8 100644 --- a/frontend/src/components/chat-renderer.tsx +++ b/frontend/src/components/chat-renderer.tsx @@ -86,6 +86,14 @@ export function ChatRenderer({ } }; + const handleSkipOnboarding = () => { + // Skip onboarding by marking it as complete + if (typeof window !== "undefined") { + localStorage.removeItem(ONBOARDING_STEP_KEY); + } + setShowLayout(true); + }; + // List of paths with smaller max-width const smallWidthPaths = ["/settings/connector/new"]; const isSmallWidthPath = smallWidthPaths.includes(pathname); @@ -196,6 +204,7 @@ export function ChatRenderer({ From a7af519d0160f13db24c36cbdc15f9ecc88c06d6 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:08:27 -0300 Subject: [PATCH 4/6] fix: added empty message on file upload handling, fixed time calculation on onboarding (#316) * fixed miscalculation of provider steps * Handled empty message with file * fixed same key --- frontend/components/navigation.tsx | 4 +- .../src/app/chat/components/user-message.tsx | 2 +- frontend/src/app/chat/page.tsx | 13 ++- .../components/animated-provider-steps.tsx | 19 ++-- .../onboarding/components/onboarding-card.tsx | 101 ++++++++++++------ frontend/src/lib/constants.ts | 4 +- 6 files changed, 90 insertions(+), 53 deletions(-) diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index 2d4d64df..28422445 100644 --- a/frontend/components/navigation.tsx +++ b/frontend/components/navigation.tsx @@ -594,8 +594,8 @@ export function Navigation({ No documents yet
) : ( - newConversationFiles?.map((file) => ( -
+ newConversationFiles?.map((file, index) => ( +
{file}
diff --git a/frontend/src/app/chat/components/user-message.tsx b/frontend/src/app/chat/components/user-message.tsx index 21c97f4f..bb3ff10f 100644 --- a/frontend/src/app/chat/components/user-message.tsx +++ b/frontend/src/app/chat/components/user-message.tsx @@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"; import { Message } from "./message"; interface UserMessageProps { - content: string; + content: string | undefined; isCompleted?: boolean; animate?: boolean; files?: string; diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 81144644..f34b1efb 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -9,7 +9,7 @@ import { type EndpointType, useChat } from "@/contexts/chat-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useTask } from "@/contexts/task-context"; import { useChatStreaming } from "@/hooks/useChatStreaming"; -import { FILES_REGEX } from "@/lib/constants"; +import { FILE_CONFIRMATION, FILES_REGEX } from "@/lib/constants"; import { useLoadingStore } from "@/stores/loadingStore"; import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery"; import { AssistantMessage } from "./components/assistant-message"; @@ -911,9 +911,9 @@ function ChatPage() { } // Only send message if there's input text - if (input.trim()) { + if (input.trim() || uploadedFile) { // Pass the responseId from upload (if any) to handleSendMessage - handleSendMessage(input, uploadedResponseId || undefined); + handleSendMessage(!input.trim() ? FILE_CONFIRMATION : input, uploadedResponseId || undefined); } }; @@ -1154,6 +1154,8 @@ function ChatPage() { } }; + console.log(messages) + return ( <> {/* Debug header - only show in debug mode */} @@ -1236,7 +1238,10 @@ function ChatPage() { ? message.source !== "langflow" : false } - content={message.content} + content={index >= 2 + && (messages[index - 2]?.content.match( + FILES_REGEX, + )?.[0] ?? undefined) && message.content === FILE_CONFIRMATION ? undefined : message.content} files={ index >= 2 ? messages[index - 2]?.content.match( diff --git a/frontend/src/app/onboarding/components/animated-provider-steps.tsx b/frontend/src/app/onboarding/components/animated-provider-steps.tsx index 0dc334aa..b97113dc 100644 --- a/frontend/src/app/onboarding/components/animated-provider-steps.tsx +++ b/frontend/src/app/onboarding/components/animated-provider-steps.tsx @@ -19,34 +19,30 @@ export function AnimatedProviderSteps({ setCurrentStep, steps, storageKey = "provider-steps", + processingStartTime, }: { currentStep: number; isCompleted: boolean; setCurrentStep: (step: number) => void; steps: string[]; storageKey?: string; + processingStartTime?: number | null; }) { const [startTime, setStartTime] = useState(null); const [elapsedTime, setElapsedTime] = useState(0); - // Initialize start time from local storage or set new one + // Initialize start time from prop or local storage useEffect(() => { - const storedStartTime = localStorage.getItem(`${storageKey}-start`); const storedElapsedTime = localStorage.getItem(`${storageKey}-elapsed`); if (isCompleted && storedElapsedTime) { // If completed, use stored elapsed time setElapsedTime(parseFloat(storedElapsedTime)); - } else if (storedStartTime) { - // If in progress, use stored start time - setStartTime(parseInt(storedStartTime)); - } else { - // First time, set new start time - const now = Date.now(); - setStartTime(now); - localStorage.setItem(`${storageKey}-start`, now.toString()); + } else if (processingStartTime) { + // Use the start time passed from parent (when user clicked Complete) + setStartTime(processingStartTime); } - }, [storageKey, isCompleted]); + }, [storageKey, isCompleted, processingStartTime]); // Progress through steps useEffect(() => { @@ -64,7 +60,6 @@ export function AnimatedProviderSteps({ const elapsed = Date.now() - startTime; setElapsedTime(elapsed); localStorage.setItem(`${storageKey}-elapsed`, elapsed.toString()); - localStorage.removeItem(`${storageKey}-start`); } }, [isCompleted, startTime, storageKey]); diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx index e379976b..4414b601 100644 --- a/frontend/src/app/onboarding/components/onboarding-card.tsx +++ b/frontend/src/app/onboarding/components/onboarding-card.tsx @@ -32,12 +32,11 @@ interface OnboardingCardProps { setLoadingStatus?: (status: string[]) => void; } - const STEP_LIST = [ - "Setting up your model provider", - "Defining schema", - "Configuring Langflow", - "Ingesting sample data", + "Setting up your model provider", + "Defining schema", + "Configuring Langflow", + "Ingesting sample data", ]; const TOTAL_PROVIDER_STEPS = STEP_LIST.length; @@ -106,7 +105,13 @@ const OnboardingCard = ({ llm_model: "", }); - const [currentStep, setCurrentStep] = useState(isCompleted ? TOTAL_PROVIDER_STEPS : null); + const [currentStep, setCurrentStep] = useState( + isCompleted ? TOTAL_PROVIDER_STEPS : null, + ); + + const [processingStartTime, setProcessingStartTime] = useState( + null, + ); // Query tasks to track completion const { data: tasks } = useGetTasksQuery({ @@ -131,8 +136,8 @@ const OnboardingCard = ({ // If no active tasks and we've started onboarding, complete it if ( (!activeTasks || (activeTasks.processed_files ?? 0) > 0) && - tasks.length > 0 - && !isCompleted + tasks.length > 0 && + !isCompleted ) { // Set to final step to show "Done" setCurrentStep(TOTAL_PROVIDER_STEPS); @@ -189,6 +194,8 @@ const OnboardingCard = ({ onboardingData.project_id = settings.project_id; } + // Record the start time when user clicks Complete + setProcessingStartTime(Date.now()); onboardingMutation.mutate(onboardingData); setCurrentStep(0); }; @@ -211,30 +218,55 @@ const OnboardingCard = ({ onValueChange={handleSetModelProvider} > - -
- + +
+
OpenAI
- -
- + +
+
IBM watsonx.ai
- -
+ +
@@ -285,11 +317,13 @@ const OnboardingCard = ({ {!isComplete && ( - {isLoadingModels ? "Loading models..." : (!!settings.llm_model && - !!settings.embedding_model && - !isDoclingHealthy - ? "docling-serve must be running to continue" - : "Please fill in all required fields")} + {isLoadingModels + ? "Loading models..." + : !!settings.llm_model && + !!settings.embedding_model && + !isDoclingHealthy + ? "docling-serve must be running to continue" + : "Please fill in all required fields"} )} @@ -303,11 +337,12 @@ const OnboardingCard = ({ transition={{ duration: 0.4, ease: "easeInOut" }} > + currentStep={currentStep} + isCompleted={isCompleted} + setCurrentStep={setCurrentStep} + steps={STEP_LIST} + processingStartTime={processingStartTime} + /> )} diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index dfd7358a..4b74a5db 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -35,4 +35,6 @@ export const TOTAL_ONBOARDING_STEPS = 3; export const ONBOARDING_STEP_KEY = "onboarding_current_step"; export const FILES_REGEX = - /(?<=I'm uploading a document called ['"])[^'"]+\.[^.]+(?=['"]\. Here is its content:)/; \ No newline at end of file + /(?<=I'm uploading a document called ['"])[^'"]+\.[^.]+(?=['"]\. Here is its content:)/; + +export const FILE_CONFIRMATION = "Confirm that you received this file."; \ No newline at end of file From 93ba498d4b19142d57ffd620acaf9e74b7bf6c83 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:21:33 -0300 Subject: [PATCH 5/6] fix: remove provider loading (#314) * Remove provider loading * Added loading to the steps --- .../components/onboarding-content.tsx | 7 -- .../components/onboarding-step.tsx | 87 +++---------------- .../onboarding/components/ibm-onboarding.tsx | 18 +--- .../components/ollama-onboarding.tsx | 17 +--- .../onboarding/components/onboarding-card.tsx | 67 +++++++------- .../components/openai-onboarding.tsx | 20 ++--- 6 files changed, 53 insertions(+), 163 deletions(-) diff --git a/frontend/src/app/new-onboarding/components/onboarding-content.tsx b/frontend/src/app/new-onboarding/components/onboarding-content.tsx index c328d1f3..9215edd1 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-content.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-content.tsx @@ -24,8 +24,6 @@ export function OnboardingContent({ const [assistantMessage, setAssistantMessage] = useState( null, ); - const [isLoadingModels, setIsLoadingModels] = useState(false); - const [loadingStatus, setLoadingStatus] = useState([]); const { streamingMessage, isLoading, sendMessage } = useChatStreaming({ onComplete: (message, newResponseId) => { @@ -82,17 +80,12 @@ export function OnboardingContent({ isCompleted={currentStep > 0} showCompleted={true} text="Let's get started by setting up your model provider." - isLoadingModels={isLoadingModels} - loadingStatus={loadingStatus} - reserveSpaceForThinking={true} > { handleStepComplete(); }} isCompleted={currentStep > 0} - setIsLoadingModels={setIsLoadingModels} - setLoadingStatus={setLoadingStatus} /> diff --git a/frontend/src/app/new-onboarding/components/onboarding-step.tsx b/frontend/src/app/new-onboarding/components/onboarding-step.tsx index 9ce97c87..8670009f 100644 --- a/frontend/src/app/new-onboarding/components/onboarding-step.tsx +++ b/frontend/src/app/new-onboarding/components/onboarding-step.tsx @@ -3,7 +3,6 @@ import { type ReactNode, useEffect, useState } from "react"; import { Message } from "@/app/chat/components/message"; import DogIcon from "@/components/logo/dog-icon"; import { MarkdownRenderer } from "@/components/markdown-renderer"; -import AnimatedProcessingIcon from "@/components/ui/animated-processing-icon"; import { cn } from "@/lib/utils"; interface OnboardingStepProps { @@ -15,9 +14,6 @@ interface OnboardingStepProps { icon?: ReactNode; isMarkdown?: boolean; hideIcon?: boolean; - isLoadingModels?: boolean; - loadingStatus?: string[]; - reserveSpaceForThinking?: boolean; } export function OnboardingStep({ @@ -29,35 +25,9 @@ export function OnboardingStep({ icon, isMarkdown = false, hideIcon = false, - isLoadingModels = false, - loadingStatus = [], - reserveSpaceForThinking = false, }: OnboardingStepProps) { const [displayedText, setDisplayedText] = useState(""); const [showChildren, setShowChildren] = useState(false); - const [currentStatusIndex, setCurrentStatusIndex] = useState(0); - - // Cycle through loading status messages once - useEffect(() => { - if (!isLoadingModels || loadingStatus.length === 0) { - setCurrentStatusIndex(0); - return; - } - - const interval = setInterval(() => { - setCurrentStatusIndex((prev) => { - const nextIndex = prev + 1; - // Stop at the last message - if (nextIndex >= loadingStatus.length - 1) { - clearInterval(interval); - return loadingStatus.length - 1; - } - return nextIndex; - }); - }, 1500); // Change status every 1.5 seconds - - return () => clearInterval(interval); - }, [isLoadingModels, loadingStatus]); useEffect(() => { if (!isVisible) { @@ -115,37 +85,7 @@ export function OnboardingStep({ } >
- {isLoadingModels && loadingStatus.length > 0 ? ( -
-
-
- -
- - Thinking - -
-
-
-
-
- - - {loadingStatus[currentStatusIndex]} - - -
-
-
-
- ) : isMarkdown ? ( + {isMarkdown ? ( ) : ( - <> -

- {displayedText} - {!showChildren && !isCompleted && ( - - )} -

- {reserveSpaceForThinking &&
} - +

+ {displayedText} + {!showChildren && !isCompleted && ( + + )} +

)} {children && ( @@ -178,7 +115,7 @@ export function OnboardingStep({ exit={{ opacity: 0, height: 0 }} transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }} > -
{children}
+
{children}
)}
diff --git a/frontend/src/app/onboarding/components/ibm-onboarding.tsx b/frontend/src/app/onboarding/components/ibm-onboarding.tsx index cd638025..272ab1b2 100644 --- a/frontend/src/app/onboarding/components/ibm-onboarding.tsx +++ b/frontend/src/app/onboarding/components/ibm-onboarding.tsx @@ -15,13 +15,11 @@ export function IBMOnboarding({ sampleDataset, setSampleDataset, setIsLoadingModels, - setLoadingStatus, }: { setSettings: (settings: OnboardingVariables) => void; sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; setIsLoadingModels?: (isLoading: boolean) => void; - setLoadingStatus?: (status: string[]) => void; }) { const [endpoint, setEndpoint] = useState("https://us-south.ml.cloud.ibm.com"); const [apiKey, setApiKey] = useState(""); @@ -91,6 +89,10 @@ export function IBMOnboarding({ setSampleDataset(dataset); }; + useEffect(() => { + setIsLoadingModels?.(isLoadingModels); + }, [isLoadingModels, setIsLoadingModels]); + // Update settings when values change useUpdateSettings( "watsonx", @@ -104,18 +106,6 @@ export function IBMOnboarding({ setSettings, ); - // Notify parent about loading state - useEffect(() => { - setIsLoadingModels?.(isLoadingModels); - - // Set detailed loading status - if (isLoadingModels) { - const status = ["Connecting to IBM watsonx.ai", "Fetching language models", "Fetching embedding models"]; - setLoadingStatus?.(status); - } else { - setLoadingStatus?.([]); - } - }, [isLoadingModels, setIsLoadingModels, setLoadingStatus]); return ( <>
diff --git a/frontend/src/app/onboarding/components/ollama-onboarding.tsx b/frontend/src/app/onboarding/components/ollama-onboarding.tsx index 82d86d83..b085fa95 100644 --- a/frontend/src/app/onboarding/components/ollama-onboarding.tsx +++ b/frontend/src/app/onboarding/components/ollama-onboarding.tsx @@ -14,13 +14,11 @@ export function OllamaOnboarding({ sampleDataset, setSampleDataset, setIsLoadingModels, - setLoadingStatus, }: { setSettings: (settings: OnboardingVariables) => void; sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; setIsLoadingModels?: (isLoading: boolean) => void; - setLoadingStatus?: (status: string[]) => void; }) { const [endpoint, setEndpoint] = useState(`http://localhost:11434`); const [showConnecting, setShowConnecting] = useState(false); @@ -74,20 +72,7 @@ export function OllamaOnboarding({ }, setSettings, ); - - // Notify parent about loading state - useEffect(() => { - setIsLoadingModels?.(isLoadingModels); - - // Set detailed loading status - if (isLoadingModels) { - const status = ["Connecting to Ollama", "Fetching language models", "Fetching embedding models"]; - setLoadingStatus?.(status); - } else { - setLoadingStatus?.([]); - } - }, [isLoadingModels, setIsLoadingModels, setLoadingStatus]); - + // Check validation state based on models query const hasConnectionError = debouncedEndpoint && modelsError; const hasNoModels = diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx index 4414b601..a1c8b79f 100644 --- a/frontend/src/app/onboarding/components/onboarding-card.tsx +++ b/frontend/src/app/onboarding/components/onboarding-card.tsx @@ -44,8 +44,6 @@ const TOTAL_PROVIDER_STEPS = STEP_LIST.length; const OnboardingCard = ({ onComplete, isCompleted = false, - setIsLoadingModels: setIsLoadingModelsParent, - setLoadingStatus: setLoadingStatusParent, }: OnboardingCardProps) => { const { isHealthy: isDoclingHealthy } = useDoclingHealth(); @@ -55,40 +53,14 @@ const OnboardingCard = ({ const [isLoadingModels, setIsLoadingModels] = useState(false); - const [loadingStatus, setLoadingStatus] = useState([]); + const [loadingStep, setLoadingStep] = useState(0); - const [currentStatusIndex, setCurrentStatusIndex] = useState(0); - - // Pass loading state to parent + // Reset loading step when models start loading useEffect(() => { - setIsLoadingModelsParent?.(isLoadingModels); - }, [isLoadingModels, setIsLoadingModelsParent]); - - useEffect(() => { - setLoadingStatusParent?.(loadingStatus); - }, [loadingStatus, setLoadingStatusParent]); - - // Cycle through loading status messages once - useEffect(() => { - if (!isLoadingModels || loadingStatus.length === 0) { - setCurrentStatusIndex(0); - return; + if (isLoadingModels) { + setLoadingStep(0); } - - const interval = setInterval(() => { - setCurrentStatusIndex((prev) => { - const nextIndex = prev + 1; - // Stop at the last message - if (nextIndex >= loadingStatus.length - 1) { - clearInterval(interval); - return loadingStatus.length - 1; - } - return nextIndex; - }); - }, 1500); // Change status every 1.5 seconds - - return () => clearInterval(interval); - }, [isLoadingModels, loadingStatus]); + }, [isLoadingModels]); const handleSetModelProvider = (provider: string) => { setModelProvider(provider); @@ -273,13 +245,36 @@ const OnboardingCard = ({ Ollama + + {isLoadingModels && ( + +
+
+
+ )} +
@@ -288,7 +283,6 @@ const OnboardingCard = ({ sampleDataset={sampleDataset} setSampleDataset={setSampleDataset} setIsLoadingModels={setIsLoadingModels} - setLoadingStatus={setLoadingStatus} /> @@ -297,11 +291,12 @@ const OnboardingCard = ({ sampleDataset={sampleDataset} setSampleDataset={setSampleDataset} setIsLoadingModels={setIsLoadingModels} - setLoadingStatus={setLoadingStatus} /> + +
diff --git a/frontend/src/app/onboarding/components/openai-onboarding.tsx b/frontend/src/app/onboarding/components/openai-onboarding.tsx index 01646ad9..2997cea4 100644 --- a/frontend/src/app/onboarding/components/openai-onboarding.tsx +++ b/frontend/src/app/onboarding/components/openai-onboarding.tsx @@ -15,13 +15,11 @@ export function OpenAIOnboarding({ sampleDataset, setSampleDataset, setIsLoadingModels, - setLoadingStatus, }: { setSettings: (settings: OnboardingVariables) => void; sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; setIsLoadingModels?: (isLoading: boolean) => void; - setLoadingStatus?: (status: string[]) => void; }) { const [apiKey, setApiKey] = useState(""); const [getFromEnv, setGetFromEnv] = useState(true); @@ -62,6 +60,10 @@ export function OpenAIOnboarding({ setEmbeddingModel(""); }; + useEffect(() => { + setIsLoadingModels?.(isLoadingModels); + }, [isLoadingModels, setIsLoadingModels]); + // Update settings when values change useUpdateSettings( "openai", @@ -72,19 +74,7 @@ export function OpenAIOnboarding({ }, setSettings, ); - - // Notify parent about loading state - useEffect(() => { - setIsLoadingModels?.(isLoadingModels); - - // Set detailed loading status - if (isLoadingModels) { - const status = ["Connecting to OpenAI", "Fetching language models", "Fetching embedding models"]; - setLoadingStatus?.(status); - } else { - setLoadingStatus?.([]); - } - }, [isLoadingModels, setIsLoadingModels, setLoadingStatus]); + return ( <>
From 511d309c7e2c9b1c74c3d3492c090bb360800e15 Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 27 Oct 2025 17:33:03 -0400 Subject: [PATCH 6/6] v0.1.25 --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ea8e4a03..93e68c9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openrag" -version = "0.1.24" +version = "0.1.25" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" diff --git a/uv.lock b/uv.lock index 3b095861..e4f7578b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" resolution-markers = [ "platform_machine == 'x86_64' and sys_platform == 'linux'", @@ -2352,7 +2352,7 @@ wheels = [ [[package]] name = "openrag" -version = "0.1.24" +version = "0.1.25" source = { editable = "." } dependencies = [ { name = "agentd" },