From c5447f6c5d13274d4724d3d859b5c6eb95b1f093 Mon Sep 17 00:00:00 2001
From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Date: Mon, 20 Oct 2025 11:14:13 -0300
Subject: [PATCH] feat: adds provider step status, adds current step to local
storage (#269)
* Added mock provider status when completing the provider selection
* Refactor
* Added transitions to elements on steps
* delete test timeout
* fixed animation glitch
* Added animation to onboarding card status
* bump up time
* Changed interval to 1000
* Adds useEffect to complete onboarding step only when one file is uploaded
* Add local storage storing of step
* formatting
---
.../components/animated-provider-steps.tsx | 91 +++++++
.../onboarding/components/onboarding-card.tsx | 223 ++++++++++++------
frontend/src/components/chat-renderer.tsx | 30 ++-
frontend/src/lib/constants.ts | 7 +-
4 files changed, 271 insertions(+), 80 deletions(-)
create mode 100644 frontend/src/app/onboarding/components/animated-provider-steps.tsx
diff --git a/frontend/src/app/onboarding/components/animated-provider-steps.tsx b/frontend/src/app/onboarding/components/animated-provider-steps.tsx
new file mode 100644
index 00000000..cc52b783
--- /dev/null
+++ b/frontend/src/app/onboarding/components/animated-provider-steps.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { AnimatePresence, motion } from "framer-motion";
+import { CheckIcon } from "lucide-react";
+import { useEffect } from "react";
+import { AnimatedProcessingIcon } from "@/components/ui/animated-processing-icon";
+import { cn } from "@/lib/utils";
+
+export function AnimatedProviderSteps({
+ currentStep,
+ setCurrentStep,
+}: {
+ currentStep: number;
+ setCurrentStep: (step: number) => void;
+}) {
+ const steps = [
+ "Setting up your model provider",
+ "Defining schema",
+ "Configuring Langflow",
+ "Ingesting sample data",
+ ];
+
+ useEffect(() => {
+ if (currentStep < steps.length - 1) {
+ const interval = setInterval(() => {
+ setCurrentStep(currentStep + 1);
+ }, 1000);
+ return () => clearInterval(interval);
+ }
+ }, [currentStep, setCurrentStep]);
+
+ const isDone = currentStep >= steps.length;
+
+ return (
+
+
+
+
+
+ {isDone ? "Done" : "Thinking"}
+
+
+
+
+ {!isDone && (
+
+
+
+
+
+ {steps[currentStep]}
+
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/app/onboarding/components/onboarding-card.tsx b/frontend/src/app/onboarding/components/onboarding-card.tsx
index 58f629f7..898409a3 100644
--- a/frontend/src/app/onboarding/components/onboarding-card.tsx
+++ b/frontend/src/app/onboarding/components/onboarding-card.tsx
@@ -1,11 +1,13 @@
"use client";
-import { useState } from "react";
+import { AnimatePresence, motion } from "framer-motion";
+import { useEffect, useState } from "react";
import { toast } from "sonner";
import {
type OnboardingVariables,
useOnboardingMutation,
} from "@/app/api/mutations/useOnboardingMutation";
+import { useGetTasksQuery } from "@/app/api/queries/useGetTasksQuery";
import { useDoclingHealth } from "@/components/docling-health-banner";
import IBMLogo from "@/components/logo/ibm-logo";
import OllamaLogo from "@/components/logo/ollama-logo";
@@ -23,6 +25,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { AnimatedProviderSteps } from "./animated-provider-steps";
import { IBMOnboarding } from "./ibm-onboarding";
import { OllamaOnboarding } from "./ollama-onboarding";
import { OpenAIOnboarding } from "./openai-onboarding";
@@ -31,11 +34,12 @@ interface OnboardingCardProps {
onComplete: () => void;
}
+const TOTAL_PROVIDER_STEPS = 4;
+
const OnboardingCard = ({ onComplete }: OnboardingCardProps) => {
const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true";
const { isHealthy: isDoclingHealthy } = useDoclingHealth();
-
const [modelProvider, setModelProvider] = useState("openai");
const [sampleDataset, setSampleDataset] = useState(true);
@@ -55,11 +59,47 @@ const OnboardingCard = ({ onComplete }: OnboardingCardProps) => {
llm_model: "",
});
+ const [currentStep, setCurrentStep] = useState(null);
+
+ // Query tasks to track completion
+ const { data: tasks } = useGetTasksQuery({
+ enabled: currentStep !== null, // Only poll when onboarding has started
+ refetchInterval: currentStep !== null ? 1000 : false, // Poll every 1 second during onboarding
+ });
+
+ // Monitor tasks and call onComplete when all tasks are done
+ useEffect(() => {
+ if (currentStep === null || !tasks) {
+ return;
+ }
+
+ // Check if there are any active tasks (pending, running, or processing)
+ const activeTasks = tasks.find(
+ (task) =>
+ task.status === "pending" ||
+ task.status === "running" ||
+ task.status === "processing",
+ );
+
+ // If no active tasks and we've started onboarding, complete it
+ if (
+ (!activeTasks || (activeTasks.processed_files ?? 0) > 0) &&
+ tasks.length > 0
+ ) {
+ // Set to final step to show "Done"
+ setCurrentStep(TOTAL_PROVIDER_STEPS);
+ // Wait a bit before completing
+ setTimeout(() => {
+ onComplete();
+ }, 1000);
+ }
+ }, [tasks, currentStep, onComplete]);
+
// Mutations
const onboardingMutation = useOnboardingMutation({
onSuccess: (data) => {
console.log("Onboarding completed successfully", data);
- onComplete();
+ setCurrentStep(0);
},
onError: (error) => {
toast.error("Failed to complete onboarding", {
@@ -102,81 +142,114 @@ const OnboardingCard = ({ onComplete }: OnboardingCardProps) => {
}
onboardingMutation.mutate(onboardingData);
+ setCurrentStep(0);
};
- const isComplete = !!settings.llm_model && !!settings.embedding_model && isDoclingHealthy;
+ const isComplete =
+ !!settings.llm_model && !!settings.embedding_model && isDoclingHealthy;
return (
-
-
-
-
-
-
- OpenAI
-
-
-
- IBM watsonx.ai
-
-
-
- Ollama
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {!isComplete && (
-
- {!!settings.llm_model && !!settings.embedding_model && !isDoclingHealthy
- ? "docling-serve must be running to continue"
- : "Please fill in all required fields"}
-
- )}
-
-
-
- )
-}
+
+ {currentStep === null ? (
+
+
+
+
+
+
+
+ OpenAI
+
+
+
+ IBM watsonx.ai
+
+
+
+ Ollama
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isComplete && (
+
+ {!!settings.llm_model &&
+ !!settings.embedding_model &&
+ !isDoclingHealthy
+ ? "docling-serve must be running to continue"
+ : "Please fill in all required fields"}
+
+ )}
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
export default OnboardingCard;
diff --git a/frontend/src/components/chat-renderer.tsx b/frontend/src/components/chat-renderer.tsx
index 88174cf2..8721aaa1 100644
--- a/frontend/src/components/chat-renderer.tsx
+++ b/frontend/src/components/chat-renderer.tsx
@@ -2,7 +2,7 @@
import { motion } from "framer-motion";
import { usePathname } from "next/navigation";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import {
type ChatConversation,
useGetConversationsQuery,
@@ -18,6 +18,7 @@ import { useChat } from "@/contexts/chat-context";
import {
ANIMATION_DURATION,
HEADER_HEIGHT,
+ ONBOARDING_STEP_KEY,
SIDEBAR_WIDTH,
TOTAL_ONBOARDING_STEPS,
} from "@/lib/constants";
@@ -39,8 +40,20 @@ export function ChatRenderer({
startNewConversation,
} = useChat();
- // Onboarding animation state
- const [showLayout, setShowLayout] = useState(!!settings?.edited);
+ // Initialize onboarding state based on local storage and settings
+ const [currentStep, setCurrentStep] = useState(() => {
+ if (typeof window === "undefined") return 0;
+ const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
+ return savedStep !== null ? parseInt(savedStep, 10) : 0;
+ });
+
+ const [showLayout, setShowLayout] = useState(() => {
+ if (typeof window === "undefined") return false;
+ const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
+ // Show layout if settings.edited is true and if no onboarding step is saved
+ return !!settings?.edited && savedStep === null;
+ });
+
// Only fetch conversations on chat page
const isOnChatPage = pathname === "/" || pathname === "/chat";
const { data: conversations = [], isLoading: isConversationsLoading } =
@@ -53,12 +66,21 @@ export function ChatRenderer({
startNewConversation();
};
- const [currentStep, setCurrentStep] = useState(0);
+ // Save current step to local storage whenever it changes
+ useEffect(() => {
+ if (typeof window !== "undefined" && !showLayout) {
+ localStorage.setItem(ONBOARDING_STEP_KEY, currentStep.toString());
+ }
+ }, [currentStep, showLayout]);
const handleStepComplete = () => {
if (currentStep < TOTAL_ONBOARDING_STEPS - 1) {
setCurrentStep(currentStep + 1);
} else {
+ // Onboarding is complete - remove from local storage and show layout
+ if (typeof window !== "undefined") {
+ localStorage.removeItem(ONBOARDING_STEP_KEY);
+ }
setShowLayout(true);
}
};
diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts
index 1cd8eb90..43956ae8 100644
--- a/frontend/src/lib/constants.ts
+++ b/frontend/src/lib/constants.ts
@@ -27,4 +27,9 @@ export const UI_CONSTANTS = {
export const ANIMATION_DURATION = 0.4;
export const SIDEBAR_WIDTH = 280;
export const HEADER_HEIGHT = 54;
-export const TOTAL_ONBOARDING_STEPS = 4;
\ No newline at end of file
+export const TOTAL_ONBOARDING_STEPS = 4;
+
+/**
+ * Local Storage Keys
+ */
+export const ONBOARDING_STEP_KEY = "onboarding_current_step";
\ No newline at end of file