core/apps/webapp/app/components/onboarding/onboarding-modal.tsx
Elias Stepanik da30ec8a6b Add skip button to onboarding modal with persistence
Add skip functionality to the Welcome to Core onboarding modal that
prevents it from showing again after the user clicks skip.

Changes:
- Add skip button in modal header with ghost styling
- Persist onboarding completion state to localStorage
- Check localStorage before showing modal on future visits
- Apply persistence to both skip and complete flows

The modal will no longer appear after being skipped or completed,
improving user experience for returning users.
2025-10-30 12:01:17 +02:00

255 lines
7.7 KiB
TypeScript

import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog";
import { type Provider, OnboardingStep } from "./types";
import { ProviderSelectionStep } from "./provider-selection-step";
import { IngestionStep } from "./ingestion-step";
import { VerificationStep } from "./verification-step";
import { PROVIDER_CONFIGS } from "./provider-config";
import { Progress } from "../ui/progress";
import { Button } from "../ui/button";
interface OnboardingModalProps {
isOpen: boolean;
onClose: () => void;
onComplete: () => void;
}
export function OnboardingModal({
isOpen,
onClose,
onComplete,
}: OnboardingModalProps) {
const [currentStep, setCurrentStep] = useState<OnboardingStep>(
OnboardingStep.PROVIDER_SELECTION,
);
const [selectedProvider, setSelectedProvider] = useState<Provider>();
const [ingestionStatus, setIngestionStatus] = useState<
"idle" | "waiting" | "processing" | "complete" | "error"
>("idle");
const [verificationResult, setVerificationResult] = useState<string>();
const [isCheckingRecall, setIsCheckingRecall] = useState(false);
const [error, setError] = useState<string>();
// Calculate progress
const getProgress = () => {
switch (currentStep) {
case OnboardingStep.PROVIDER_SELECTION:
return 33;
case OnboardingStep.FIRST_INGESTION:
return 66;
case OnboardingStep.VERIFICATION:
return 100;
default:
return 0;
}
};
// Poll for ingestion status
const pollIngestion = async () => {
setIngestionStatus("waiting");
try {
const maxAttempts = 30; // 60 seconds (30 * 2s)
let attempts = 0;
// Store the timestamp when polling starts
const startTime = Date.now();
const poll = async (): Promise<boolean> => {
if (attempts >= maxAttempts) {
throw new Error("Ingestion timeout - please try again");
}
// Check for new ingestion logs from the last 5 minutes
const response = await fetch("/api/v1/logs?limit=1");
const data = await response.json();
// Check if there's a recent ingestion (created after we started polling)
if (data.logs && data.logs.length > 0) {
const latestLog = data.logs[0];
const logTime = new Date(latestLog.time).getTime();
// If the log was created after we started polling, we found a new ingestion
if (logTime >= startTime) {
return true;
}
}
await new Promise((resolve) => setTimeout(resolve, 2000));
attempts++;
return poll();
};
const success = await poll();
if (success) {
setIngestionStatus("complete");
// Auto-advance to verification step after 2 seconds
setTimeout(() => {
setCurrentStep(OnboardingStep.VERIFICATION);
}, 2000);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error occurred");
setIngestionStatus("error");
}
};
const handleProviderSelect = (provider: Provider) => {
setSelectedProvider(provider);
};
const handleContinueFromProvider = () => {
setCurrentStep(OnboardingStep.FIRST_INGESTION);
};
const handleStartWaiting = () => {
pollIngestion();
};
const handleComplete = () => {
// Mark onboarding as completed in localStorage
if (typeof window !== "undefined") {
localStorage.setItem("onboarding_completed", "true");
}
setCurrentStep(OnboardingStep.COMPLETE);
onComplete();
onClose();
};
const handleSkip = () => {
// Mark onboarding as completed in localStorage
if (typeof window !== "undefined") {
localStorage.setItem("onboarding_completed", "true");
}
onComplete();
onClose();
};
// Poll for recall logs to detect verification
const pollRecallLogs = async () => {
setIsCheckingRecall(true);
try {
const maxAttempts = 30; // 60 seconds
let attempts = 0;
const startTime = Date.now();
const poll = async (): Promise<string | null> => {
if (attempts >= maxAttempts) {
throw new Error("Verification timeout - please try again");
}
// Check for new recall logs
const response = await fetch("/api/v1/recall-logs?limit=1");
const data = await response.json();
// Check if there's a recent recall (created after we started polling)
if (data.recallLogs && data.recallLogs.length > 0) {
const latestRecall = data.recallLogs[0];
const recallTime = new Date(latestRecall.createdAt).getTime();
// If the recall was created after we started polling
if (recallTime >= startTime) {
// Return the query as verification result
return latestRecall.query || "Recall detected successfully";
}
}
await new Promise((resolve) => setTimeout(resolve, 2000));
attempts++;
return poll();
};
const result = await poll();
if (result) {
setVerificationResult(result);
setIsCheckingRecall(false);
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error occurred");
setIsCheckingRecall(false);
}
};
const getStepTitle = () => {
switch (currentStep) {
case OnboardingStep.PROVIDER_SELECTION:
return "Step 1 of 3";
case OnboardingStep.FIRST_INGESTION:
return "Step 2 of 3";
case OnboardingStep.VERIFICATION:
return "Step 3 of 3";
default:
return "";
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto p-4">
<DialogHeader>
<div className="space-y-3">
<div className="flex items-center justify-between">
<DialogTitle className="text-2xl">Welcome to Core</DialogTitle>
<Button
variant="ghost"
size="sm"
onClick={handleSkip}
className="text-muted-foreground hover:text-foreground"
>
Skip
</Button>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-muted-foreground text-sm">
{getStepTitle()}
</p>
</div>
<Progress
segments={[{ value: getProgress() }]}
className="mb-2"
color="#c15e50"
/>
</div>
</div>
</DialogHeader>
<div>
{currentStep === OnboardingStep.PROVIDER_SELECTION && (
<ProviderSelectionStep
selectedProvider={selectedProvider}
onSelectProvider={handleProviderSelect}
onContinue={handleContinueFromProvider}
/>
)}
{currentStep === OnboardingStep.FIRST_INGESTION &&
selectedProvider && (
<IngestionStep
providerName={PROVIDER_CONFIGS[selectedProvider].name}
ingestionStatus={ingestionStatus}
onStartWaiting={handleStartWaiting}
error={error}
/>
)}
{currentStep === OnboardingStep.VERIFICATION && selectedProvider && (
<VerificationStep
providerName={PROVIDER_CONFIGS[selectedProvider].name}
verificationResult={verificationResult}
isCheckingRecall={isCheckingRecall}
onStartChecking={pollRecallLogs}
onComplete={handleComplete}
/>
)}
</div>
</DialogContent>
</Dialog>
);
}