Compare commits
7 commits
main
...
fix/onboar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd245f97a0 | ||
|
|
d839e235e5 | ||
|
|
527bc7f67e | ||
|
|
633afde224 | ||
|
|
98ff3e2f8b | ||
|
|
345a3861b0 | ||
|
|
f7f1553f1d |
15 changed files with 341 additions and 215 deletions
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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:)/;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue