From f7f1553f1db3c8df7bba042805f751296ff771fd Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Dec 2025 13:21:46 -0300 Subject: [PATCH 1/6] Added required mutations on frontend --- .../useUpdateOnboardingStateMutation.ts | 44 +++++++++++++++++++ .../app/api/queries/useGetSettingsQuery.ts | 15 +++++++ 2 files changed, 59 insertions(+) create mode 100644 frontend/app/api/mutations/useUpdateOnboardingStateMutation.ts diff --git a/frontend/app/api/mutations/useUpdateOnboardingStateMutation.ts b/frontend/app/api/mutations/useUpdateOnboardingStateMutation.ts new file mode 100644 index 00000000..4a3b3a4b --- /dev/null +++ b/frontend/app/api/mutations/useUpdateOnboardingStateMutation.ts @@ -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 | null; + upload_steps?: Record | 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 diff --git a/frontend/app/api/queries/useGetSettingsQuery.ts b/frontend/app/api/queries/useGetSettingsQuery.ts index c14c5140..5ee5e74c 100644 --- a/frontend/app/api/queries/useGetSettingsQuery.ts +++ b/frontend/app/api/queries/useGetSettingsQuery.ts @@ -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 | null; + upload_steps?: Record | 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; From 345a3861b0b18f195586707c0d6ac15c341155ea Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Dec 2025 13:21:58 -0300 Subject: [PATCH 2/6] Added new settings store on the config yaml file --- src/api/settings.py | 69 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/api/settings.py b/src/api/settings.py index 982d9272..e348d5df 100644 --- a/src/api/settings.py +++ b/src/api/settings.py @@ -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. From 98ff3e2f8bb9a3b9016dceec07c04b00d866d0c7 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Dec 2025 13:22:15 -0300 Subject: [PATCH 3/6] Added onboarding field on config --- src/config/config_manager.py | 45 ++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/config/config_manager.py b/src/config/config_manager.py index e5231889..6af07db4 100644 --- a/src/config/config_manager.py +++ b/src/config/config_manager.py @@ -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: )\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() From 633afde2248f524c33dbae3d36ef5587bbdcae1d Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Dec 2025 13:22:35 -0300 Subject: [PATCH 4/6] Added new endpoint for onboarding --- src/main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main.py b/src/main.py index 710b3dab..3d25e0e0 100644 --- a/src/main.py +++ b/src/main.py @@ -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", From 527bc7f67e5e2440e361c18ffc9603c059f64553 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Dec 2025 13:22:48 -0300 Subject: [PATCH 5/6] Changed frontend to not reference local storage --- frontend/app/chat/page.tsx | 30 ++--- .../_components/animated-provider-steps.tsx | 19 +-- .../_components/onboarding-card.tsx | 16 +-- .../_components/onboarding-content.tsx | 109 ++++++++---------- .../_components/onboarding-upload.tsx | 20 ++-- frontend/components/chat-renderer.tsx | 109 +++++++++--------- frontend/contexts/chat-context.tsx | 30 ++--- frontend/contexts/task-context.tsx | 15 ++- frontend/lib/constants.ts | 11 -- 9 files changed, 157 insertions(+), 202 deletions(-) diff --git a/frontend/app/chat/page.tsx b/frontend/app/chat/page.tsx index f15cf788..663bc36b 100644 --- a/frontend/app/chat/page.tsx +++ b/frontend/app/chat/page.tsx @@ -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 diff --git a/frontend/app/onboarding/_components/animated-provider-steps.tsx b/frontend/app/onboarding/_components/animated-provider-steps.tsx index 026b330f..27ddde54 100644 --- a/frontend/app/onboarding/_components/animated-provider-steps.tsx +++ b/frontend/app/onboarding/_components/animated-provider-steps.tsx @@ -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(null); const [elapsedTime, setElapsedTime] = useState(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; diff --git a/frontend/app/onboarding/_components/onboarding-card.tsx b/frontend/app/onboarding/_components/onboarding-card.tsx index be7a25ef..5ea6218a 100644 --- a/frontend/app/onboarding/_components/onboarding-card.tsx +++ b/frontend/app/onboarding/_components/onboarding-card.tsx @@ -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"; @@ -173,6 +173,8 @@ const OnboardingCard = ({ // Track which tasks we've already handled to prevent infinite loops const handledFailedTasksRef = useRef>(new Set()); + + const updateOnboardingMutation = useUpdateOnboardingStateMutation(); // Query tasks to track completion const { data: tasks } = useGetTasksQuery({ @@ -307,11 +309,12 @@ const OnboardingCard = ({ 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 - ); + 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); } @@ -674,7 +677,6 @@ const OnboardingCard = ({ setCurrentStep={setCurrentStep} steps={isEmbedding ? EMBEDDING_STEP_LIST : STEP_LIST} processingStartTime={processingStartTime} - storageKey={ONBOARDING_CARD_STEPS_KEY} hasError={!!error} /> diff --git a/frontend/app/onboarding/_components/onboarding-content.tsx b/frontend/app/onboarding/_components/onboarding-content.tsx index 1127e725..094c540b 100644 --- a/frontend/app/onboarding/_components/onboarding-content.tsx +++ b/frontend/app/onboarding/_components/onboarding-content.tsx @@ -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(null); + + // Initialize from backend settings const [selectedNudge, setSelectedNudge] = useState(() => { - // 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( () => { - // 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; diff --git a/frontend/app/onboarding/_components/onboarding-upload.tsx b/frontend/app/onboarding/_components/onboarding-upload.tsx index b434cce9..bef2610d 100644 --- a/frontend/app/onboarding/_components/onboarding-upload.tsx +++ b/frontend/app/onboarding/_components/onboarding-upload.tsx @@ -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} /> )} diff --git a/frontend/components/chat-renderer.tsx b/frontend/components/chat-renderer.tsx index a0e2a9d4..110586ea 100644 --- a/frontend/components/chat-renderer.tsx +++ b/frontend/components/chat-renderer.tsx @@ -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(() => { - 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(() => { - 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); }; diff --git a/frontend/contexts/chat-context.tsx b/frontend/contexts/chat-context.tsx index f6f1a1e4..5803cde4 100644 --- a/frontend/contexts/chat-context.tsx +++ b/frontend/contexts/chat-context.tsx @@ -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); diff --git a/frontend/contexts/task-context.tsx b/frontend/contexts/task-context.tsx index 780bbc7e..cd70b598 100644 --- a/frontend/contexts/task-context.tsx +++ b/frontend/contexts/task-context.tsx @@ -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({ diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts index 88baf8d0..f0e1a70e 100644 --- a/frontend/lib/constants.ts +++ b/frontend/lib/constants.ts @@ -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:)/; From d839e235e5c9175ac4b48802ee7547f4fd18cc94 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 6 Jan 2026 13:38:14 -0300 Subject: [PATCH 6/6] changed onboarrding mutation to change the onboarding settings --- .../api/mutations/useOnboardingMutation.ts | 21 ++++++++++++------- .../_components/onboarding-card.tsx | 12 ----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/frontend/app/api/mutations/useOnboardingMutation.ts b/frontend/app/api/mutations/useOnboardingMutation.ts index 42b95236..911e196c 100644 --- a/frontend/app/api/mutations/useOnboardingMutation.ts +++ b/frontend/app/api/mutations/useOnboardingMutation.ts @@ -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, "mutationFn" - >, + > ) => { const queryClient = useQueryClient(); + const updateOnboardingMutation = useUpdateOnboardingStateMutation(); + async function submitOnboarding( - variables: OnboardingVariables, + variables: OnboardingVariables ): Promise { 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 ); } diff --git a/frontend/app/onboarding/_components/onboarding-card.tsx b/frontend/app/onboarding/_components/onboarding-card.tsx index 5ea6218a..3d7d3e3f 100644 --- a/frontend/app/onboarding/_components/onboarding-card.tsx +++ b/frontend/app/onboarding/_components/onboarding-card.tsx @@ -173,8 +173,6 @@ const OnboardingCard = ({ // Track which tasks we've already handled to prevent infinite loops const handledFailedTasksRef = useRef>(new Set()); - - const updateOnboardingMutation = useUpdateOnboardingStateMutation(); // Query tasks to track completion const { data: tasks } = useGetTasksQuery({ @@ -308,16 +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) { - // 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); - } - // Update provider health cache to healthy since backend just validated const provider = (isEmbedding ? settings.embedding_provider : settings.llm_provider) ||