Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
Lucas Oliveira
bd245f97a0 Merge remote-tracking branch 'origin/main' into fix/onboarding_store 2026-01-08 17:37:56 -03:00
Lucas Oliveira
d839e235e5 changed onboarrding mutation to change the onboarding settings 2026-01-06 13:38:14 -03:00
Lucas Oliveira
527bc7f67e Changed frontend to not reference local storage 2025-12-23 13:22:48 -03:00
Lucas Oliveira
633afde224 Added new endpoint for onboarding 2025-12-23 13:22:35 -03:00
Lucas Oliveira
98ff3e2f8b Added onboarding field on config 2025-12-23 13:22:15 -03:00
Lucas Oliveira
345a3861b0 Added new settings store on the config yaml file 2025-12-23 13:21:58 -03:00
Lucas Oliveira
f7f1553f1d Added required mutations on frontend 2025-12-23 13:21:46 -03:00
15 changed files with 341 additions and 215 deletions

View file

@ -3,7 +3,7 @@ import {
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import { ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY } from "@/lib/constants";
import { useUpdateOnboardingStateMutation } from "./useUpdateOnboardingStateMutation";
export interface OnboardingVariables {
// Provider selection
@ -36,12 +36,14 @@ export const useOnboardingMutation = (
options?: Omit<
UseMutationOptions<OnboardingResponse, Error, OnboardingVariables>,
"mutationFn"
>,
>
) => {
const queryClient = useQueryClient();
const updateOnboardingMutation = useUpdateOnboardingStateMutation();
async function submitOnboarding(
variables: OnboardingVariables,
variables: OnboardingVariables
): Promise<OnboardingResponse> {
const response = await fetch("/api/onboarding", {
method: "POST",
@ -62,10 +64,15 @@ export const useOnboardingMutation = (
return useMutation({
mutationFn: submitOnboarding,
onSuccess: (data) => {
// Store OpenRAG Docs filter ID if returned
if (data.openrag_docs_filter_id && typeof window !== "undefined") {
localStorage.setItem(
ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY,
// Save OpenRAG docs filter ID if sample data was ingested
if (data.openrag_docs_filter_id) {
// Save to backend
updateOnboardingMutation.mutateAsync({
openrag_docs_filter_id: data.openrag_docs_filter_id,
});
console.log(
"Saved OpenRAG docs filter ID:",
data.openrag_docs_filter_id
);
}

View file

@ -0,0 +1,44 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
interface UpdateOnboardingStateVariables {
current_step?: number;
assistant_message?: {
role: string;
content: string;
timestamp: string;
} | null;
selected_nudge?: string | null;
card_steps?: Record<string, unknown> | null;
upload_steps?: Record<string, unknown> | null;
openrag_docs_filter_id?: string | null;
user_doc_filter_id?: string | null;
}
export const useUpdateOnboardingStateMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (variables: UpdateOnboardingStateVariables) => {
const response = await fetch("/api/onboarding/state", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(variables),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to update onboarding state");
}
return response.json();
},
onSuccess: () => {
// Invalidate settings query to refetch updated onboarding state
queryClient.invalidateQueries({ queryKey: ["settings"] });
},
});
};
// Made with Bob

View file

@ -41,12 +41,27 @@ export interface ProviderSettings {
};
}
export interface OnboardingState {
current_step?: number;
assistant_message?: {
role: string;
content: string;
timestamp: string;
} | null;
selected_nudge?: string | null;
card_steps?: Record<string, unknown> | null;
upload_steps?: Record<string, unknown> | null;
openrag_docs_filter_id?: string | null;
user_doc_filter_id?: string | null;
}
export interface Settings {
langflow_url?: string;
flow_id?: string;
ingest_flow_id?: string;
langflow_public_url?: string;
edited?: boolean;
onboarding?: OnboardingState;
providers?: ProviderSettings;
knowledge?: KnowledgeSettings;
agent?: AgentSettings;

View file

@ -12,6 +12,7 @@ import { FILE_CONFIRMATION, FILES_REGEX } from "@/lib/constants";
import { useLoadingStore } from "@/stores/loadingStore";
import { useGetConversationsQuery } from "../api/queries/useGetConversationsQuery";
import { useGetNudgesQuery } from "../api/queries/useGetNudgesQuery";
import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery";
import { AssistantMessage } from "./_components/assistant-message";
import { ChatInput, type ChatInputHandle } from "./_components/chat-input";
import Nudges from "./_components/nudges";
@ -638,27 +639,14 @@ function ChatPage() {
};
}, [endpoint, setPreviousResponseIds, setLoading]);
// Check if onboarding is complete by looking at local storage
const [isOnboardingComplete, setIsOnboardingComplete] = useState(() => {
if (typeof window === "undefined") return false;
return localStorage.getItem("onboarding-step") === null;
});
// Listen for storage changes to detect when onboarding completes
useEffect(() => {
const checkOnboarding = () => {
if (typeof window !== "undefined") {
setIsOnboardingComplete(
localStorage.getItem("onboarding-step") === null,
);
}
};
// Check periodically since storage events don't fire in the same tab
const interval = setInterval(checkOnboarding, 500);
return () => clearInterval(interval);
}, []);
// Get settings to check onboarding completion
const { data: settings } = useGetSettingsQuery();
// Check if onboarding is complete (current_step >= 4 means complete)
const TOTAL_ONBOARDING_STEPS = 4;
const isOnboardingComplete =
settings?.onboarding?.current_step !== undefined &&
settings.onboarding.current_step >= TOTAL_ONBOARDING_STEPS;
// Prepare filters for nudges (same as chat)
const processedFiltersForNudges = parsedFilterData?.filters

View file

@ -10,7 +10,6 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { ONBOARDING_CARD_STEPS_KEY } from "@/lib/constants";
import { cn } from "@/lib/utils";
export function AnimatedProviderSteps({
@ -18,7 +17,6 @@ export function AnimatedProviderSteps({
isCompleted,
setCurrentStep,
steps,
storageKey = ONBOARDING_CARD_STEPS_KEY,
processingStartTime,
hasError = false,
}: {
@ -26,25 +24,19 @@ export function AnimatedProviderSteps({
isCompleted: boolean;
setCurrentStep: (step: number) => void;
steps: string[];
storageKey?: string;
processingStartTime?: number | null;
hasError?: boolean;
}) {
const [startTime, setStartTime] = useState<number | null>(null);
const [elapsedTime, setElapsedTime] = useState<number>(0);
// Initialize start time from prop or local storage
// Initialize start time from prop
useEffect(() => {
const storedElapsedTime = localStorage.getItem(storageKey);
if (isCompleted && storedElapsedTime) {
// If completed, use stored elapsed time
setElapsedTime(parseFloat(storedElapsedTime));
} else if (processingStartTime) {
if (processingStartTime) {
// Use the start time passed from parent (when user clicked Complete)
setStartTime(processingStartTime);
}
}, [storageKey, isCompleted, processingStartTime]);
}, [processingStartTime]);
// Progress through steps
useEffect(() => {
@ -56,14 +48,13 @@ export function AnimatedProviderSteps({
}
}, [currentStep, setCurrentStep, steps, isCompleted]);
// Calculate and store elapsed time when completed
// Calculate elapsed time when completed
useEffect(() => {
if (isCompleted && startTime) {
const elapsed = Date.now() - startTime;
setElapsedTime(elapsed);
localStorage.setItem(storageKey, elapsed.toString());
}
}, [isCompleted, startTime, storageKey]);
}, [isCompleted, startTime]);
const isDone = currentStep >= steps.length && !isCompleted && !hasError;

View file

@ -10,6 +10,7 @@ import {
useOnboardingMutation,
} from "@/app/api/mutations/useOnboardingMutation";
import { useOnboardingRollbackMutation } from "@/app/api/mutations/useOnboardingRollbackMutation";
import { useUpdateOnboardingStateMutation } from "@/app/api/mutations/useUpdateOnboardingStateMutation";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { useGetTasksQuery } from "@/app/api/queries/useGetTasksQuery";
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
@ -25,7 +26,6 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ONBOARDING_CARD_STEPS_KEY } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { AnimatedProviderSteps } from "./animated-provider-steps";
import { AnthropicOnboarding } from "./anthropic-onboarding";
@ -306,15 +306,6 @@ const OnboardingCard = ({
onSuccess: (data) => {
console.log("Onboarding completed successfully", data);
// Save OpenRAG docs filter ID if sample data was ingested
if (data.openrag_docs_filter_id && typeof window !== "undefined") {
localStorage.setItem(
"onboarding_openrag_docs_filter_id",
data.openrag_docs_filter_id
);
console.log("Saved OpenRAG docs filter ID:", data.openrag_docs_filter_id);
}
// Update provider health cache to healthy since backend just validated
const provider =
(isEmbedding ? settings.embedding_provider : settings.llm_provider) ||
@ -674,7 +665,6 @@ const OnboardingCard = ({
setCurrentStep={setCurrentStep}
steps={isEmbedding ? EMBEDDING_STEP_LIST : STEP_LIST}
processingStartTime={processingStartTime}
storageKey={ONBOARDING_CARD_STEPS_KEY}
hasError={!!error}
/>
</motion.div>

View file

@ -3,6 +3,8 @@
import { useEffect, useRef, useState } from "react";
import { StickToBottom } from "use-stick-to-bottom";
import { getFilterById } from "@/app/api/queries/useGetFilterByIdQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { useUpdateOnboardingStateMutation } from "@/app/api/mutations/useUpdateOnboardingStateMutation";
import { AssistantMessage } from "@/app/chat/_components/assistant-message";
import Nudges from "@/app/chat/_components/nudges";
import { UserMessage } from "@/app/chat/_components/user-message";
@ -10,11 +12,6 @@ import type { Message, SelectedFilters } from "@/app/chat/_types/types";
import OnboardingCard from "@/app/onboarding/_components/onboarding-card";
import { useChat } from "@/contexts/chat-context";
import { useChatStreaming } from "@/hooks/useChatStreaming";
import {
ONBOARDING_ASSISTANT_MESSAGE_KEY,
ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY,
ONBOARDING_SELECTED_NUDGE_KEY,
} from "@/lib/constants";
import { OnboardingStep } from "./onboarding-step";
import OnboardingUpload from "./onboarding-upload";
@ -36,43 +33,46 @@ export function OnboardingContent({
currentStep: number;
}) {
const { setConversationFilter, setCurrentConversationId } = useChat();
const { data: settings } = useGetSettingsQuery();
const updateOnboardingMutation = useUpdateOnboardingStateMutation();
const parseFailedRef = useRef(false);
const [responseId, setResponseId] = useState<string | null>(null);
// Initialize from backend settings
const [selectedNudge, setSelectedNudge] = useState<string>(() => {
// Retrieve selected nudge from localStorage on mount
if (typeof window === "undefined") return "";
return localStorage.getItem(ONBOARDING_SELECTED_NUDGE_KEY) || "";
return settings?.onboarding?.selected_nudge || "";
});
const [assistantMessage, setAssistantMessage] = useState<Message | null>(
() => {
// Retrieve assistant message from localStorage on mount
if (typeof window === "undefined") return null;
const savedMessage = localStorage.getItem(
ONBOARDING_ASSISTANT_MESSAGE_KEY,
);
if (savedMessage) {
try {
const parsed = JSON.parse(savedMessage);
// Convert timestamp string back to Date object
return {
...parsed,
timestamp: new Date(parsed.timestamp),
};
} catch (error) {
console.error("Failed to parse saved assistant message:", error);
parseFailedRef.current = true;
// Clear corrupted data - will go back a step in useEffect
if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_ASSISTANT_MESSAGE_KEY);
localStorage.removeItem(ONBOARDING_SELECTED_NUDGE_KEY);
}
return null;
}
// Get from backend settings
if (settings?.onboarding?.assistant_message) {
const msg = settings.onboarding.assistant_message;
return {
role: msg.role as "user" | "assistant",
content: msg.content,
timestamp: new Date(msg.timestamp),
};
}
return null;
},
);
// Sync state when settings change
useEffect(() => {
if (settings?.onboarding?.selected_nudge) {
setSelectedNudge(settings.onboarding.selected_nudge);
}
if (settings?.onboarding?.assistant_message) {
const msg = settings.onboarding.assistant_message;
setAssistantMessage({
role: msg.role as "user" | "assistant",
content: msg.content,
timestamp: new Date(msg.timestamp),
});
}
}, [settings?.onboarding]);
// Handle parse errors by going back a step
useEffect(() => {
if (parseFailedRef.current && currentStep >= 2) {
@ -83,28 +83,23 @@ export function OnboardingContent({
const { streamingMessage, isLoading, sendMessage } = useChatStreaming({
onComplete: async (message, newResponseId) => {
setAssistantMessage(message);
// Save assistant message to localStorage when complete
if (typeof window !== "undefined") {
try {
localStorage.setItem(
ONBOARDING_ASSISTANT_MESSAGE_KEY,
JSON.stringify(message),
);
} catch (error) {
console.error(
"Failed to save assistant message to localStorage:",
error,
);
}
}
// Save assistant message to backend
await updateOnboardingMutation.mutateAsync({
assistant_message: {
role: message.role,
content: message.content,
timestamp: message.timestamp.toISOString(),
},
});
if (newResponseId) {
setResponseId(newResponseId);
// Set the current conversation ID
setCurrentConversationId(newResponseId);
// Save the filter association for this conversation
const openragDocsFilterId = localStorage.getItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY);
// Get filter ID from backend settings
const openragDocsFilterId = settings?.onboarding?.openrag_docs_filter_id;
if (openragDocsFilterId) {
try {
// Load the filter and set it in the context with explicit responseId
@ -136,21 +131,17 @@ export function OnboardingContent({
const handleNudgeClick = async (nudge: string) => {
setSelectedNudge(nudge);
// Save selected nudge to localStorage
if (typeof window !== "undefined") {
localStorage.setItem(ONBOARDING_SELECTED_NUDGE_KEY, nudge);
}
setAssistantMessage(null);
// Clear saved assistant message when starting a new conversation
if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_ASSISTANT_MESSAGE_KEY);
}
// Save selected nudge to backend and clear assistant message
await updateOnboardingMutation.mutateAsync({
selected_nudge: nudge,
assistant_message: null,
});
setTimeout(async () => {
// Check if we have the OpenRAG docs filter ID (sample data was ingested)
const openragDocsFilterId =
typeof window !== "undefined"
? localStorage.getItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY)
: null;
const openragDocsFilterId = settings?.onboarding?.openrag_docs_filter_id;
// Load and set the OpenRAG docs filter if available
let filterToUse = null;

View file

@ -3,14 +3,11 @@ import { AnimatePresence, motion } from "motion/react";
import { type ChangeEvent, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useCreateFilter } from "@/app/api/mutations/useCreateFilter";
import { useUpdateOnboardingStateMutation } from "@/app/api/mutations/useUpdateOnboardingStateMutation";
import { useGetNudgesQuery } from "@/app/api/queries/useGetNudgesQuery";
import { useGetTasksQuery } from "@/app/api/queries/useGetTasksQuery";
import { AnimatedProviderSteps } from "@/app/onboarding/_components/animated-provider-steps";
import { Button } from "@/components/ui/button";
import {
ONBOARDING_UPLOAD_STEPS_KEY,
ONBOARDING_USER_DOC_FILTER_ID_KEY,
} from "@/lib/constants";
import { uploadFile } from "@/lib/upload-utils";
interface OnboardingUploadProps {
@ -27,6 +24,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
const [isCreatingFilter, setIsCreatingFilter] = useState(false);
const createFilterMutation = useCreateFilter();
const updateOnboardingMutation = useUpdateOnboardingStateMutation();
const STEP_LIST = [
"Uploading your document",
@ -103,12 +101,13 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
description: `Filter for ${filename}`,
queryData: queryData,
})
.then((result) => {
if (result.filter?.id && typeof window !== "undefined") {
localStorage.setItem(
ONBOARDING_USER_DOC_FILTER_ID_KEY,
result.filter.id,
);
.then(async (result) => {
if (result.filter?.id) {
// Save to backend
await updateOnboardingMutation.mutateAsync({
user_doc_filter_id: result.filter.id,
});
console.log(
"Created knowledge filter for uploaded document",
result.filter.id,
@ -267,7 +266,6 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
setCurrentStep={setCurrentStep}
isCompleted={false}
steps={STEP_LIST}
storageKey={ONBOARDING_UPLOAD_STEPS_KEY}
/>
</motion.div>
)}

View file

@ -9,6 +9,7 @@ import {
} from "@/app/api/queries/useGetConversationsQuery";
import { getFilterById } from "@/app/api/queries/useGetFilterByIdQuery";
import type { Settings } from "@/app/api/queries/useGetSettingsQuery";
import { useUpdateOnboardingStateMutation } from "@/app/api/mutations/useUpdateOnboardingStateMutation";
import { OnboardingContent } from "@/app/onboarding/_components/onboarding-content";
import { ProgressBar } from "@/app/onboarding/_components/progress-bar";
import { AnimatedConditional } from "@/components/animated-conditional";
@ -19,13 +20,6 @@ import { useChat } from "@/contexts/chat-context";
import {
ANIMATION_DURATION,
HEADER_HEIGHT,
ONBOARDING_ASSISTANT_MESSAGE_KEY,
ONBOARDING_CARD_STEPS_KEY,
ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY,
ONBOARDING_SELECTED_NUDGE_KEY,
ONBOARDING_STEP_KEY,
ONBOARDING_UPLOAD_STEPS_KEY,
ONBOARDING_USER_DOC_FILTER_ID_KEY,
SIDEBAR_WIDTH,
TOTAL_ONBOARDING_STEPS,
} from "@/lib/constants";
@ -50,21 +44,27 @@ export function ChatRenderer({
setOnboardingComplete,
} = useChat();
// Initialize onboarding state based on local storage and settings
// Initialize onboarding state from backend settings
const [currentStep, setCurrentStep] = useState<number>(() => {
if (typeof window === "undefined") return 0;
const savedStep = localStorage.getItem(ONBOARDING_STEP_KEY);
return savedStep !== null ? parseInt(savedStep, 10) : 0;
return settings?.onboarding?.current_step ?? 0;
});
const [showLayout, setShowLayout] = useState<boolean>(() => {
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
const isEdited = settings?.edited ?? true;
return isEdited ? savedStep === null : false;
// Show layout only if onboarding is complete (current_step >= TOTAL_ONBOARDING_STEPS)
// This means onboarding will show even if edited=true, as long as it's not complete
const onboardingStep = settings?.onboarding?.current_step ?? 0;
return onboardingStep >= TOTAL_ONBOARDING_STEPS;
});
// Update currentStep and showLayout when settings change
useEffect(() => {
if (settings?.onboarding?.current_step !== undefined) {
setCurrentStep(settings.onboarding.current_step);
// Update showLayout based on whether onboarding is complete
setShowLayout(settings.onboarding.current_step >= TOTAL_ONBOARDING_STEPS);
}
}, [settings?.onboarding?.current_step]);
// Only fetch conversations on chat page
const isOnChatPage = pathname === "/" || pathname === "/chat";
const { data: conversations = [], isLoading: isConversationsLoading } =
@ -108,18 +108,18 @@ export function ChatRenderer({
}
}
// Try to get the appropriate filter ID
// Try to get the appropriate filter ID from settings
let filterId: string | null = null;
if (preferUserDoc) {
// Completed full onboarding - prefer user document filter
filterId = localStorage.getItem(ONBOARDING_USER_DOC_FILTER_ID_KEY);
filterId = settings?.onboarding?.user_doc_filter_id || null;
console.log("[FILTER] User doc filter ID:", filterId);
}
// Fall back to OpenRAG docs filter
if (!filterId) {
filterId = localStorage.getItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY);
filterId = settings?.onboarding?.openrag_docs_filter_id || null;
console.log("[FILTER] OpenRAG docs filter ID:", filterId);
}
@ -149,25 +149,29 @@ export function ChatRenderer({
[setConversationFilter]
);
// Save current step to local storage whenever it changes
useEffect(() => {
if (typeof window !== "undefined" && !showLayout) {
localStorage.setItem(ONBOARDING_STEP_KEY, currentStep.toString());
}
}, [currentStep, showLayout]);
// Note: Current step is now saved to backend via handleStepComplete
// No need to save on every change, only on completion
const updateOnboardingMutation = useUpdateOnboardingStateMutation();
const handleStepComplete = async () => {
if (currentStep < TOTAL_ONBOARDING_STEPS - 1) {
setCurrentStep(currentStep + 1);
const nextStep = currentStep + 1;
setCurrentStep(nextStep);
// Save step to backend
await updateOnboardingMutation.mutateAsync({ current_step: nextStep });
} else {
// Onboarding is complete - remove from local storage and show layout
if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_STEP_KEY);
localStorage.removeItem(ONBOARDING_ASSISTANT_MESSAGE_KEY);
localStorage.removeItem(ONBOARDING_SELECTED_NUDGE_KEY);
localStorage.removeItem(ONBOARDING_CARD_STEPS_KEY);
localStorage.removeItem(ONBOARDING_UPLOAD_STEPS_KEY);
}
// Onboarding is complete - set step to TOTAL_ONBOARDING_STEPS to indicate completion
// and clear intermediate state in backend
await updateOnboardingMutation.mutateAsync({
current_step: TOTAL_ONBOARDING_STEPS,
assistant_message: null,
selected_nudge: null,
card_steps: null,
upload_steps: null,
openrag_docs_filter_id: null,
user_doc_filter_id: null,
});
// Mark onboarding as complete in context
setOnboardingComplete(true);
@ -180,36 +184,35 @@ export function ChatRenderer({
// This will pick up the default filter we just set
await startNewConversation();
// Clean up onboarding filter IDs now that we've set the default
if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY);
localStorage.removeItem(ONBOARDING_USER_DOC_FILTER_ID_KEY);
console.log("[FILTER] Cleaned up onboarding filter IDs");
}
setShowLayout(true);
}
};
const handleStepBack = () => {
const handleStepBack = async () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
const prevStep = currentStep - 1;
setCurrentStep(prevStep);
// Save step to backend
await updateOnboardingMutation.mutateAsync({ current_step: prevStep });
}
};
const handleSkipOnboarding = () => {
// Skip onboarding by marking it as complete
if (typeof window !== "undefined") {
localStorage.removeItem(ONBOARDING_STEP_KEY);
localStorage.removeItem(ONBOARDING_ASSISTANT_MESSAGE_KEY);
localStorage.removeItem(ONBOARDING_SELECTED_NUDGE_KEY);
localStorage.removeItem(ONBOARDING_CARD_STEPS_KEY);
localStorage.removeItem(ONBOARDING_UPLOAD_STEPS_KEY);
}
const handleSkipOnboarding = async () => {
// Skip onboarding by marking it as complete in backend
await updateOnboardingMutation.mutateAsync({
current_step: TOTAL_ONBOARDING_STEPS,
assistant_message: null,
selected_nudge: null,
card_steps: null,
upload_steps: null,
openrag_docs_filter_id: null,
user_doc_filter_id: null,
});
// Mark onboarding as complete in context
setOnboardingComplete(true);
// Store the OpenRAG docs filter as default for new conversations
storeDefaultFilterForNewConversations(false);
await storeDefaultFilterForNewConversations(false);
setShowLayout(true);
};

View file

@ -10,7 +10,6 @@ import {
useRef,
useState,
} from "react";
import { ONBOARDING_STEP_KEY } from "@/lib/constants";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
export type EndpointType = "chat" | "langflow";
@ -129,28 +128,15 @@ export function ChatProvider({ children }: ChatProviderProps) {
return false;
});
// Sync onboarding completion state with settings.edited and localStorage
// Sync onboarding completion state with settings from backend
useEffect(() => {
const checkOnboarding = () => {
if (typeof window !== "undefined") {
// Onboarding is complete if settings.edited is true AND step key is null
const stepKeyExists = localStorage.getItem(ONBOARDING_STEP_KEY) !== null;
const isEdited = settings?.edited === true;
// Complete if edited is true and step key doesn't exist (onboarding flow finished)
setIsOnboardingComplete(isEdited && !stepKeyExists);
}
};
// Check on mount and when settings change
checkOnboarding();
// Listen for storage events (for cross-tab sync)
window.addEventListener("storage", checkOnboarding);
return () => {
window.removeEventListener("storage", checkOnboarding);
};
}, [settings?.edited]);
const TOTAL_ONBOARDING_STEPS = 4;
// Onboarding is complete if current_step >= 4
const isComplete =
settings?.onboarding?.current_step !== undefined &&
settings.onboarding.current_step >= TOTAL_ONBOARDING_STEPS;
setIsOnboardingComplete(isComplete);
}, [settings?.onboarding?.current_step]);
const setOnboardingComplete = useCallback((complete: boolean) => {
setIsOnboardingComplete(complete);

View file

@ -17,8 +17,8 @@ import {
type TaskFileEntry,
useGetTasksQuery,
} from "@/app/api/queries/useGetTasksQuery";
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
import { useAuth } from "@/contexts/auth-context";
import { ONBOARDING_STEP_KEY } from "@/lib/constants";
// Task interface is now imported from useGetTasksQuery
export type { Task };
@ -90,11 +90,18 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
},
});
// Get settings to check if onboarding is active
const { data: settings } = useGetSettingsQuery();
// 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 TOTAL_ONBOARDING_STEPS = 4;
// Onboarding is active if current_step < 4
return (
settings?.onboarding?.current_step !== undefined &&
settings.onboarding.current_step < TOTAL_ONBOARDING_STEPS
);
}, [settings?.onboarding?.current_step]);
const refetchSearch = useCallback(() => {
queryClient.invalidateQueries({

View file

@ -37,17 +37,6 @@ export const SIDEBAR_WIDTH = 280;
export const HEADER_HEIGHT = 54;
export const TOTAL_ONBOARDING_STEPS = 4;
/**
* Local Storage Keys
*/
export const ONBOARDING_STEP_KEY = "onboarding_current_step";
export const ONBOARDING_ASSISTANT_MESSAGE_KEY = "onboarding_assistant_message";
export const ONBOARDING_SELECTED_NUDGE_KEY = "onboarding_selected_nudge";
export const ONBOARDING_CARD_STEPS_KEY = "onboarding_card_steps";
export const ONBOARDING_UPLOAD_STEPS_KEY = "onboarding_upload_steps";
export const ONBOARDING_OPENRAG_DOCS_FILTER_ID_KEY = "onboarding_openrag_docs_filter_id";
export const ONBOARDING_USER_DOC_FILTER_ID_KEY = "onboarding_user_doc_filter_id";
export const FILES_REGEX =
/(?<=I'm uploading a document called ['"])[^'"]+\.[^.]+(?=['"]\. Here is its content:)/;

View file

@ -65,6 +65,16 @@ async def get_settings(request, session_manager):
"ingest_flow_id": LANGFLOW_INGEST_FLOW_ID,
"langflow_public_url": LANGFLOW_PUBLIC_URL,
"edited": openrag_config.edited,
# Onboarding state
"onboarding": {
"current_step": openrag_config.onboarding.current_step,
"assistant_message": openrag_config.onboarding.assistant_message,
"selected_nudge": openrag_config.onboarding.selected_nudge,
"card_steps": openrag_config.onboarding.card_steps,
"upload_steps": openrag_config.onboarding.upload_steps,
"openrag_docs_filter_id": openrag_config.onboarding.openrag_docs_filter_id,
"user_doc_filter_id": openrag_config.onboarding.user_doc_filter_id,
},
# OpenRAG configuration
"providers": {
"openai": {
@ -1353,6 +1363,65 @@ async def _update_langflow_chunk_settings(config, flows_service):
raise
async def update_onboarding_state(request):
"""Update onboarding state in configuration"""
try:
await TelemetryClient.send_event(Category.ONBOARDING, MessageId.ORB_ONBOARD_START)
# Parse request body
body = await request.json()
# Validate allowed fields
allowed_fields = {
"current_step",
"assistant_message",
"selected_nudge",
"card_steps",
"upload_steps",
"openrag_docs_filter_id",
"user_doc_filter_id",
}
# Check for invalid fields
invalid_fields = set(body.keys()) - allowed_fields
if invalid_fields:
return JSONResponse(
{
"error": f"Invalid fields: {', '.join(invalid_fields)}. Allowed fields: {', '.join(allowed_fields)}"
},
status_code=400,
)
# Update onboarding state using config manager
success = config_manager.update_onboarding_state(**body)
if not success:
return JSONResponse(
{"error": "Failed to update onboarding state"},
status_code=500,
)
logger.info(f"Onboarding state updated: {body}")
return JSONResponse(
{
"message": "Onboarding state updated successfully",
"updated_fields": list(body.keys()),
}
)
except json.JSONDecodeError:
return JSONResponse(
{"error": "Invalid JSON in request body"}, status_code=400
)
except Exception as e:
logger.error(f"Error updating onboarding state: {str(e)}")
return JSONResponse(
{"error": f"Failed to update onboarding state: {str(e)}"},
status_code=500,
)
async def reapply_all_settings(session_manager = None):
"""
Reapply all current configuration settings to Langflow flows and global variables.

View file

@ -4,7 +4,7 @@ import os
import yaml
from pathlib import Path
from typing import Dict, Any, Optional
from dataclasses import dataclass, asdict
from dataclasses import dataclass, asdict, field
from utils.logging_config import get_logger
logger = get_logger(__name__)
@ -85,6 +85,19 @@ class AgentConfig:
system_prompt: str = "You are the OpenRAG Agent. You answer questions using retrieval, reasoning, and tool use.\nYou have access to several tools. Your job is to determine **which tool to use and when**.\n### Available Tools\n- OpenSearch Retrieval Tool:\n Use this to search the indexed knowledge base. Use when the user asks about product details, internal concepts, processes, architecture, documentation, roadmaps, or anything that may be stored in the index.\n- Conversation History:\n Use this to maintain continuity when the user is referring to previous turns. \n Do not treat history as a factual source.\n- Conversation File Context:\n Use this when the user asks about a document they uploaded or refers directly to its contents.\n- URL Ingestion Tool:\n Use this **only** when the user explicitly asks you to read, summarize, or analyze the content of a URL.\n Do not ingest URLs automatically.\n- Calculator / Expression Evaluation Tool:\n Use this when the user asks to compare numbers, compute estimates, calculate totals, analyze pricing, or answer any question requiring mathematics or quantitative reasoning.\n If the answer requires arithmetic, call the calculator tool rather than calculating internally.\n### Retrieval Decision Rules\nUse OpenSearch **whenever**:\n1. The question may be answered from internal or indexed data.\n2. The user references team names, product names, release plans, configurations, requirements, or official information.\n3. The user needs a factual, grounded answer.\nDo **not** use retrieval if:\n- The question is purely creative (e.g., storytelling, analogies) or personal preference.\n- The user simply wants text reformatted or rewritten from what is already present in the conversation.\nWhen uncertain → **Retrieve.** Retrieval is low risk and improves grounding.\n### URL Ingestion Rules\nOnly ingest URLs when the user explicitly says:\n- \"Read this link\"\n- \"Summarize this webpage\"\n- \"What does this site say?\"\n- \"Ingest this URL\"\nIf unclear → ask a clarifying question.\n### Calculator Usage Rules\nUse the calculator when:\n- Performing arithmetic\n- Estimating totals\n- Comparing values\n- Modeling cost, time, effort, scale, or projections\nDo not perform math internally. **Call the calculator tool instead.**\n### Answer Construction Rules\n1. When asked: \"What is OpenRAG\", answer the following:\n\"OpenRAG is an open-source package for building agentic RAG systems. It supports integration with a wide range of orchestration tools, vector databases, and LLM providers. OpenRAG connects and amplifies three popular, proven open-source projects into one powerful platform:\n**Langflow** Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.langflow.org/)\n**OpenSearch** Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://opensearch.org/)\n**Docling** Langflow is a powerful tool to build and deploy AI agents and MCP servers [Read more](https://www.docling.ai/)\"\n2. Synthesize retrieved or ingested content in your own words.\n3. Support factual claims with citations in the format:\n (Source: <document_name_or_id>)\n4. If no supporting evidence is found:\n Say: \"No relevant supporting sources were found for that request.\"\n5. Never invent facts or hallucinate details.\n6. Be concise, direct, and confident. \n7. Do not reveal internal chain-of-thought."
@dataclass
class OnboardingState:
"""Onboarding state configuration."""
current_step: int = 0
assistant_message: Optional[Dict[str, Any]] = field(default=None)
selected_nudge: Optional[str] = field(default=None)
card_steps: Optional[Dict[str, Any]] = field(default=None)
upload_steps: Optional[Dict[str, Any]] = field(default=None)
openrag_docs_filter_id: Optional[str] = field(default=None)
user_doc_filter_id: Optional[str] = field(default=None)
@dataclass
class OpenRAGConfig:
"""Complete OpenRAG configuration."""
@ -92,6 +105,7 @@ class OpenRAGConfig:
providers: ProvidersConfig
knowledge: KnowledgeConfig
agent: AgentConfig
onboarding: OnboardingState
edited: bool = False # Track if manually edited
@classmethod
@ -107,6 +121,7 @@ class OpenRAGConfig:
),
knowledge=KnowledgeConfig(**data.get("knowledge", {})),
agent=AgentConfig(**data.get("agent", {})),
onboarding=OnboardingState(**data.get("onboarding", {})),
edited=data.get("edited", False),
)
@ -156,6 +171,7 @@ class ConfigManager:
},
"knowledge": {},
"agent": {},
"onboarding": {},
}
# Load from config file if it exists
@ -172,7 +188,7 @@ class ConfigManager:
file_config["providers"][provider]
)
for section in ["knowledge", "agent"]:
for section in ["knowledge", "agent", "onboarding"]:
if section in file_config:
config_data[section].update(file_config[section])
@ -294,6 +310,31 @@ class ConfigManager:
logger.error(f"Failed to save configuration to {self.config_file}: {e}")
return False
def update_onboarding_state(self, **kwargs) -> bool:
"""Update onboarding state fields.
Args:
**kwargs: Onboarding state fields to update (current_step, assistant_message, etc.)
Returns:
True if updated successfully, False otherwise.
"""
try:
config = self.get_config()
# Update only the provided fields
for key, value in kwargs.items():
if hasattr(config.onboarding, key):
setattr(config.onboarding, key, value)
else:
logger.warning(f"Unknown onboarding field: {key}")
# Save the updated config
return self.save_config_file(config)
except Exception as e:
logger.error(f"Failed to update onboarding state: {e}")
return False
# Global config manager instance
config_manager = ConfigManager()

View file

@ -1140,6 +1140,13 @@ async def create_app():
),
methods=["POST"],
),
Route(
"/onboarding/state",
require_auth(services["session_manager"])(
settings.update_onboarding_state
),
methods=["POST"],
),
# Provider health check endpoint
Route(
"/provider/health",