Merge branch 'feat/new-onboarding' of github.com:langflow-ai/openrag into fix/provider-design

This commit is contained in:
Mike Fortman 2025-10-23 11:42:02 -05:00
commit 545a5f40fb
14 changed files with 364 additions and 187 deletions

1
docs/.gitignore vendored
View file

@ -23,3 +23,4 @@ yarn-error.log*
!package.json !package.json
!package-lock.json !package-lock.json
!yarn.lock !yarn.lock
!scraper.config.json

View file

@ -25,6 +25,9 @@ const config = {
// For GitHub pages deployment, it is often '/<projectName>/' // For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: process.env.BASE_URL ? process.env.BASE_URL : '/', baseUrl: process.env.BASE_URL ? process.env.BASE_URL : '/',
// Control search engine indexing - set to true to prevent indexing
noIndex: true,
// GitHub pages deployment config. // GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these. // If you aren't using GitHub pages, you don't need these.
organizationName: 'langflow-ai', // Usually your GitHub org/user name. organizationName: 'langflow-ai', // Usually your GitHub org/user name.

View file

@ -6,6 +6,7 @@
"docusaurus": "docusaurus", "docusaurus": "docusaurus",
"start": "docusaurus start", "start": "docusaurus start",
"build": "docusaurus build", "build": "docusaurus build",
"build:pdf": "npm run build && npm run serve & sleep 10 && npx docusaurus-to-pdf && pkill -f 'docusaurus serve'",
"swizzle": "docusaurus swizzle", "swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy", "deploy": "docusaurus deploy",
"clear": "docusaurus clear", "clear": "docusaurus clear",

Binary file not shown.

7
docs/scraper.config.json Normal file
View file

@ -0,0 +1,7 @@
{
"baseUrl": "http://localhost:3000",
"entryPoint": "http://localhost:3000",
"outputDir": "./pdf/openrag-documentation.pdf",
"customStyles": "table { max-width: 3500px !important; } .navbar, .footer, .breadcrumbs { display: none !important; }",
"forceImages": true
}

12
docs/static/robots.txt vendored Normal file
View file

@ -0,0 +1,12 @@
# Robots.txt for OpenRAG Documentation
# Block all crawlers by default
User-agent: *
Disallow: /
# Allow specific crawlers if needed (uncomment when ready for launch)
# User-agent: Googlebot
# Allow: /
# Sitemap location (uncomment when ready for launch)
# Sitemap: https://docs.openr.ag/sitemap.xml

View file

@ -26,6 +26,10 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useTask } from "@/contexts/task-context"; import { useTask } from "@/contexts/task-context";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import {
duplicateCheck,
uploadFile as uploadFileUtil,
} from "@/lib/upload-utils";
import type { File as SearchFile } from "@/src/app/api/queries/useGetSearchQuery"; import type { File as SearchFile } from "@/src/app/api/queries/useGetSearchQuery";
export function KnowledgeDropdown() { export function KnowledgeDropdown() {
@ -163,8 +167,17 @@ export function KnowledgeDropdown() {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const resetFileInput = () => {
const files = e.target.files; if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const files = event.target.files;
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
@ -172,37 +185,16 @@ export function KnowledgeDropdown() {
setIsOpen(false); setIsOpen(false);
try { try {
// Check if filename already exists (using ORIGINAL filename)
console.log("[Duplicate Check] Checking file:", file.name); console.log("[Duplicate Check] Checking file:", file.name);
const checkResponse = await fetch( const checkData = await duplicateCheck(file);
`/api/documents/check-filename?filename=${encodeURIComponent(
file.name
)}`
);
console.log("[Duplicate Check] Response status:", checkResponse.status);
if (!checkResponse.ok) {
const errorText = await checkResponse.text();
console.error("[Duplicate Check] Error response:", errorText);
throw new Error(
`Failed to check duplicates: ${checkResponse.statusText}`
);
}
const checkData = await checkResponse.json();
console.log("[Duplicate Check] Result:", checkData); console.log("[Duplicate Check] Result:", checkData);
if (checkData.exists) { if (checkData.exists) {
// Show duplicate handling dialog
console.log("[Duplicate Check] Duplicate detected, showing dialog"); console.log("[Duplicate Check] Duplicate detected, showing dialog");
setPendingFile(file); setPendingFile(file);
setDuplicateFilename(file.name); setDuplicateFilename(file.name);
setShowDuplicateDialog(true); setShowDuplicateDialog(true);
// Reset file input resetFileInput();
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
return; return;
} }
@ -217,105 +209,20 @@ export function KnowledgeDropdown() {
} }
} }
// Reset file input resetFileInput();
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}; };
const uploadFile = async (file: File, replace: boolean) => { const uploadFile = async (file: File, replace: boolean) => {
setFileUploading(true); setFileUploading(true);
// Trigger the same file upload event as the chat page
window.dispatchEvent(
new CustomEvent("fileUploadStart", {
detail: { filename: file.name },
})
);
try { try {
const formData = new FormData(); await uploadFileUtil(file, replace);
formData.append("file", file);
formData.append("replace_duplicates", replace.toString());
// Use router upload and ingest endpoint (automatically routes based on configuration)
const uploadIngestRes = await fetch("/api/router/upload_ingest", {
method: "POST",
body: formData,
});
const uploadIngestJson = await uploadIngestRes.json();
if (!uploadIngestRes.ok) {
throw new Error(uploadIngestJson?.error || "Upload and ingest failed");
}
// Extract results from the response - handle both unified and simple formats
const fileId =
uploadIngestJson?.upload?.id ||
uploadIngestJson?.id ||
uploadIngestJson?.task_id;
const filePath =
uploadIngestJson?.upload?.path || uploadIngestJson?.path || "uploaded";
const runJson = uploadIngestJson?.ingestion;
const deleteResult = uploadIngestJson?.deletion;
console.log("c", uploadIngestJson);
if (!fileId) {
throw new Error("Upload successful but no file id returned");
}
// Check if ingestion actually succeeded
if (
runJson &&
runJson.status !== "COMPLETED" &&
runJson.status !== "SUCCESS"
) {
const errorMsg = runJson.error || "Ingestion pipeline failed";
throw new Error(
`Ingestion failed: ${errorMsg}. Try setting DISABLE_INGEST_WITH_LANGFLOW=true if you're experiencing Langflow component issues.`
);
}
// Log deletion status if provided
if (deleteResult) {
if (deleteResult.status === "deleted") {
console.log(
"File successfully cleaned up from Langflow:",
deleteResult.file_id
);
} else if (deleteResult.status === "delete_failed") {
console.warn(
"Failed to cleanup file from Langflow:",
deleteResult.error
);
}
}
// Notify UI
window.dispatchEvent(
new CustomEvent("fileUploaded", {
detail: {
file: file,
result: {
file_id: fileId,
file_path: filePath,
run: runJson,
deletion: deleteResult,
unified: true,
},
},
})
);
refetchTasks(); refetchTasks();
} catch (error) { } catch (error) {
window.dispatchEvent( toast.error("Upload failed", {
new CustomEvent("fileUploadError", { description: error instanceof Error ? error.message : "Unknown error",
detail: { });
filename: file.name,
error: error instanceof Error ? error.message : "Upload failed",
},
})
);
} finally { } finally {
window.dispatchEvent(new CustomEvent("fileUploadComplete"));
setFileUploading(false); setFileUploading(false);
} }
}; };
@ -332,6 +239,7 @@ export function KnowledgeDropdown() {
}); });
await uploadFile(pendingFile, true); await uploadFile(pendingFile, true);
setPendingFile(null); setPendingFile(null);
setDuplicateFilename(""); setDuplicateFilename("");
} }

View file

@ -0,0 +1,138 @@
export interface DuplicateCheckResponse {
exists: boolean;
[key: string]: unknown;
}
export interface UploadFileResult {
fileId: string;
filePath: string;
run: unknown;
deletion: unknown;
unified: boolean;
raw: unknown;
}
export async function duplicateCheck(
file: File
): Promise<DuplicateCheckResponse> {
const response = await fetch(
`/api/documents/check-filename?filename=${encodeURIComponent(file.name)}`
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
errorText || `Failed to check duplicates: ${response.statusText}`
);
}
return response.json();
}
export async function uploadFile(
file: File,
replace = false
): Promise<UploadFileResult> {
window.dispatchEvent(
new CustomEvent("fileUploadStart", {
detail: { filename: file.name },
})
);
try {
const formData = new FormData();
formData.append("file", file);
formData.append("replace_duplicates", replace.toString());
const uploadResponse = await fetch("/api/router/upload_ingest", {
method: "POST",
body: formData,
});
let payload: unknown;
try {
payload = await uploadResponse.json();
} catch (error) {
throw new Error("Upload failed: unable to parse server response");
}
const uploadIngestJson =
typeof payload === "object" && payload !== null ? payload : {};
if (!uploadResponse.ok) {
const errorMessage =
(uploadIngestJson as { error?: string }).error ||
"Upload and ingest failed";
throw new Error(errorMessage);
}
const fileId =
(uploadIngestJson as { upload?: { id?: string } }).upload?.id ||
(uploadIngestJson as { id?: string }).id ||
(uploadIngestJson as { task_id?: string }).task_id;
const filePath =
(uploadIngestJson as { upload?: { path?: string } }).upload?.path ||
(uploadIngestJson as { path?: string }).path ||
"uploaded";
const runJson = (uploadIngestJson as { ingestion?: unknown }).ingestion;
const deletionJson = (uploadIngestJson as { deletion?: unknown }).deletion;
if (!fileId) {
throw new Error("Upload successful but no file id returned");
}
if (
runJson &&
typeof runJson === "object" &&
"status" in (runJson as Record<string, unknown>) &&
(runJson as { status?: string }).status !== "COMPLETED" &&
(runJson as { status?: string }).status !== "SUCCESS"
) {
const errorMsg =
(runJson as { error?: string }).error ||
"Ingestion pipeline failed";
throw new Error(
`Ingestion failed: ${errorMsg}. Try setting DISABLE_INGEST_WITH_LANGFLOW=true if you're experiencing Langflow component issues.`
);
}
const result: UploadFileResult = {
fileId,
filePath,
run: runJson,
deletion: deletionJson,
unified: true,
raw: uploadIngestJson,
};
window.dispatchEvent(
new CustomEvent("fileUploaded", {
detail: {
file,
result: {
file_id: fileId,
file_path: filePath,
run: runJson,
deletion: deletionJson,
unified: true,
},
},
})
);
return result;
} catch (error) {
window.dispatchEvent(
new CustomEvent("fileUploadError", {
detail: {
filename: file.name,
error:
error instanceof Error ? error.message : "Upload failed",
},
})
);
throw error;
} finally {
window.dispatchEvent(new CustomEvent("fileUploadComplete"));
}
}

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { StickToBottom } from "use-stick-to-bottom"; import { StickToBottom } from "use-stick-to-bottom";
import { AssistantMessage } from "@/app/chat/components/assistant-message"; import { AssistantMessage } from "@/app/chat/components/assistant-message";
import { UserMessage } from "@/app/chat/components/user-message"; import { UserMessage } from "@/app/chat/components/user-message";
@ -8,7 +8,9 @@ import Nudges from "@/app/chat/nudges";
import type { Message } from "@/app/chat/types"; import type { Message } from "@/app/chat/types";
import OnboardingCard from "@/app/onboarding/components/onboarding-card"; import OnboardingCard from "@/app/onboarding/components/onboarding-card";
import { useChatStreaming } from "@/hooks/useChatStreaming"; import { useChatStreaming } from "@/hooks/useChatStreaming";
import { OnboardingStep } from "./onboarding-step"; import { OnboardingStep } from "./onboarding-step";
import OnboardingUpload from "./onboarding-upload";
export function OnboardingContent({ export function OnboardingContent({
handleStepComplete, handleStepComplete,
@ -57,6 +59,12 @@ export function OnboardingContent({
// Determine which message to show (streaming takes precedence) // Determine which message to show (streaming takes precedence)
const displayMessage = streamingMessage || assistantMessage; const displayMessage = streamingMessage || assistantMessage;
useEffect(() => {
if (currentStep === 1 && !isLoading && !!displayMessage) {
handleStepComplete();
}
}, [isLoading, displayMessage, handleStepComplete, currentStep]);
return ( return (
<StickToBottom <StickToBottom
className="flex h-full flex-1 flex-col" className="flex h-full flex-1 flex-col"
@ -66,6 +74,7 @@ export function OnboardingContent({
> >
<StickToBottom.Content className="flex flex-col min-h-full overflow-x-hidden px-8 py-6"> <StickToBottom.Content className="flex flex-col min-h-full overflow-x-hidden px-8 py-6">
<div className="flex flex-col place-self-center w-full space-y-6"> <div className="flex flex-col place-self-center w-full space-y-6">
{/* Step 1 */}
<OnboardingStep <OnboardingStep
isVisible={currentStep >= 0} isVisible={currentStep >= 0}
isCompleted={currentStep > 0} isCompleted={currentStep > 0}
@ -74,6 +83,7 @@ export function OnboardingContent({
<OnboardingCard onComplete={handleStepComplete} /> <OnboardingCard onComplete={handleStepComplete} />
</OnboardingStep> </OnboardingStep>
{/* Step 2 */}
<OnboardingStep <OnboardingStep
isVisible={currentStep >= 1} isVisible={currentStep >= 1}
isCompleted={currentStep > 1 || !!selectedNudge} isCompleted={currentStep > 1 || !!selectedNudge}
@ -92,7 +102,7 @@ export function OnboardingContent({
{currentStep >= 1 && !!selectedNudge && ( {currentStep >= 1 && !!selectedNudge && (
<UserMessage <UserMessage
content={selectedNudge} content={selectedNudge}
isCompleted={currentStep > 1} isCompleted={currentStep > 2}
/> />
)} )}
@ -100,65 +110,41 @@ export function OnboardingContent({
{currentStep >= 1 && {currentStep >= 1 &&
!!selectedNudge && !!selectedNudge &&
(displayMessage || isLoading) && ( (displayMessage || isLoading) && (
<> <AssistantMessage
<AssistantMessage content={displayMessage?.content || ""}
content={displayMessage?.content || ""} functionCalls={displayMessage?.functionCalls}
functionCalls={displayMessage?.functionCalls} messageIndex={0}
messageIndex={0} expandedFunctionCalls={new Set()}
expandedFunctionCalls={new Set()} onToggle={() => {}}
onToggle={() => {}} isStreaming={!!streamingMessage}
isStreaming={!!streamingMessage} isCompleted={currentStep > 2}
isCompleted={currentStep > 1} />
/>
{!isLoading && displayMessage && currentStep === 1 && (
<div className="mt-4">
<button
type="button"
onClick={handleStepComplete}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Continue
</button>
</div>
)}
</>
)} )}
{/* Step 3 */}
<OnboardingStep <OnboardingStep
isVisible={currentStep >= 2} isVisible={currentStep >= 2 && !isLoading && !!displayMessage}
isCompleted={currentStep > 2} isCompleted={currentStep > 2}
text="Step 2: Connect your model" text="Now, let's add your data."
hideIcon={true}
> >
<div className="space-y-4"> <OnboardingUpload onComplete={handleStepComplete} />
<p className="text-muted-foreground">
Choose and connect your preferred AI model provider.
</p>
<button
type="button"
onClick={handleStepComplete}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Continue
</button>
</div>
</OnboardingStep> </OnboardingStep>
{/* Step 4 */}
<OnboardingStep <OnboardingStep
isVisible={currentStep >= 3} isVisible={currentStep >= 3}
isCompleted={currentStep > 3} isCompleted={currentStep > 3}
text="Step 3: You're all set!" text="Step 3: You're all set!"
> >
<div className="space-y-4"> <div className="space-y-4">
<p className="text-muted-foreground"> <button
Your account is ready to use. Let's start chatting! type="button"
</p> onClick={handleStepComplete}
<button className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
type="button" >
onClick={handleStepComplete} Go to Chat
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90" </button>
>
Go to Chat
</button>
</div> </div>
</OnboardingStep> </OnboardingStep>
</div> </div>

View file

@ -12,6 +12,7 @@ interface OnboardingStepProps {
isCompleted?: boolean; isCompleted?: boolean;
icon?: ReactNode; icon?: ReactNode;
isMarkdown?: boolean; isMarkdown?: boolean;
hideIcon?: boolean;
} }
export function OnboardingStep({ export function OnboardingStep({
@ -21,6 +22,7 @@ export function OnboardingStep({
isCompleted = false, isCompleted = false,
icon, icon,
isMarkdown = false, isMarkdown = false,
hideIcon = false,
}: OnboardingStepProps) { }: OnboardingStepProps) {
const [displayedText, setDisplayedText] = useState(""); const [displayedText, setDisplayedText] = useState("");
const [showChildren, setShowChildren] = useState(false); const [showChildren, setShowChildren] = useState(false);
@ -66,13 +68,17 @@ export function OnboardingStep({
> >
<Message <Message
icon={ icon={
icon || ( hideIcon ? (
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none"> <div className="w-8 h-8 rounded-lg flex-shrink-0" />
<DogIcon ) : (
className="h-6 w-6 text-accent-foreground transition-colors duration-300" icon || (
disabled={isCompleted} <div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center flex-shrink-0 select-none">
/> <DogIcon
</div> className="h-6 w-6 text-accent-foreground transition-colors duration-300"
disabled={isCompleted}
/>
</div>
)
) )
} }
> >

View file

@ -0,0 +1,114 @@
import { ChangeEvent, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { duplicateCheck, uploadFile } from "@/lib/upload-utils";
import { AnimatePresence, motion } from "motion/react";
import { AnimatedProviderSteps } from "@/app/onboarding/components/animated-provider-steps";
interface OnboardingUploadProps {
onComplete: () => void;
}
const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [currentStep, setCurrentStep] = useState<number | null>(null);
const STEP_LIST = [
"Analyzing your document",
"Ingesting your document",
];
const resetFileInput = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const performUpload = async (file: File, replace = false) => {
setIsUploading(true);
try {
setCurrentStep(1);
await uploadFile(file, replace);
console.log("Document uploaded successfully");
} catch (error) {
console.error("Upload failed", (error as Error).message);
} finally {
setIsUploading(false);
setCurrentStep(STEP_LIST.length);
onComplete();
}
};
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0];
if (!selectedFile) {
resetFileInput();
return;
}
try {
setCurrentStep(0);
const duplicateInfo = await duplicateCheck(selectedFile);
if (duplicateInfo.exists) {
console.log("Duplicate file detected");
return;
}
await performUpload(selectedFile, false);
} catch (error) {
console.error("Unable to prepare file for upload", (error as Error).message);
} finally {
resetFileInput();
}
};
return (
<AnimatePresence mode="wait">
{currentStep === null ? (
<motion.div
key="user-ingest"
initial={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -24 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
>
<Button
size="sm"
variant="outline"
onClick={handleUploadClick}
disabled={isUploading}
>
{isUploading ? "Uploading..." : "Add a Document"}
</Button>
<input
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
/>
</motion.div>
) : (
<motion.div
key="ingest-steps"
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
>
<AnimatedProviderSteps
currentStep={currentStep}
setCurrentStep={setCurrentStep}
steps={STEP_LIST}
/>
</motion.div>
)}
</AnimatePresence>
)
}
export default OnboardingUpload;

View file

@ -3,11 +3,7 @@
import { Suspense, useState } from "react"; import { Suspense, useState } from "react";
import { DoclingHealthBanner } from "@/components/docling-health-banner"; import { DoclingHealthBanner } from "@/components/docling-health-banner";
import { ProtectedRoute } from "@/components/protected-route"; import { ProtectedRoute } from "@/components/protected-route";
import { DotPattern } from "@/components/ui/dot-pattern";
import { cn } from "@/lib/utils";
import OnboardingCard from "../onboarding/components/onboarding-card";
import { OnboardingContent } from "./components/onboarding-content"; import { OnboardingContent } from "./components/onboarding-content";
import { OnboardingStep } from "./components/onboarding-step";
import { ProgressBar } from "./components/progress-bar"; import { ProgressBar } from "./components/progress-bar";
const TOTAL_STEPS = 4; const TOTAL_STEPS = 4;
@ -28,7 +24,7 @@ function NewOnboardingPage() {
{/* Chat-like content area */} {/* Chat-like content area */}
<div className="flex flex-col items-center gap-5 w-full max-w-3xl z-10"> <div className="flex flex-col items-center gap-5 w-full max-w-3xl z-10">
<div className="w-full h-[872px] bg-background border rounded-lg p-4 shadow-sm overflow-y-auto"> <div className="w-full h-[872px] bg-background border rounded-lg p-4 shadow-sm overflow-y-auto">
<OnboardingContent /> <OnboardingContent handleStepComplete={handleStepComplete} currentStep={currentStep} />
</div> </div>
<ProgressBar currentStep={currentStep} totalSteps={TOTAL_STEPS} /> <ProgressBar currentStep={currentStep} totalSteps={TOTAL_STEPS} />

View file

@ -9,16 +9,12 @@ import { cn } from "@/lib/utils";
export function AnimatedProviderSteps({ export function AnimatedProviderSteps({
currentStep, currentStep,
setCurrentStep, setCurrentStep,
steps,
}: { }: {
currentStep: number; currentStep: number;
setCurrentStep: (step: number) => void; setCurrentStep: (step: number) => void;
steps: string[];
}) { }) {
const steps = [
"Setting up your model provider",
"Defining schema",
"Configuring Langflow",
"Ingesting sample data",
];
useEffect(() => { useEffect(() => {
if (currentStep < steps.length - 1) { if (currentStep < steps.length - 1) {
@ -27,7 +23,7 @@ export function AnimatedProviderSteps({
}, 1000); }, 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
} }
}, [currentStep, setCurrentStep]); }, [currentStep, setCurrentStep, steps]);
const isDone = currentStep >= steps.length; const isDone = currentStep >= steps.length;

View file

@ -35,7 +35,15 @@ interface OnboardingCardProps {
onComplete: () => void; onComplete: () => void;
} }
const TOTAL_PROVIDER_STEPS = 4;
const STEP_LIST = [
"Setting up your model provider",
"Defining schema",
"Configuring Langflow",
"Ingesting sample data",
];
const TOTAL_PROVIDER_STEPS = STEP_LIST.length;
const OnboardingCard = ({ onComplete }: OnboardingCardProps) => { const OnboardingCard = ({ onComplete }: OnboardingCardProps) => {
const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true"; const updatedOnboarding = process.env.UPDATED_ONBOARDING === "true";
@ -250,9 +258,10 @@ const OnboardingCard = ({ onComplete }: OnboardingCardProps) => {
transition={{ duration: 0.4, ease: "easeInOut" }} transition={{ duration: 0.4, ease: "easeInOut" }}
> >
<AnimatedProviderSteps <AnimatedProviderSteps
currentStep={currentStep} currentStep={currentStep}
setCurrentStep={setCurrentStep} setCurrentStep={setCurrentStep}
/> steps={STEP_LIST}
/>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>