Compare commits

..

4 Commits

5 changed files with 161 additions and 23 deletions

View File

@@ -3,8 +3,8 @@ set -e
cleanup() {
echo "Error occurred. Cleaning up..."
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
}
# Trap errors and output a message, then cleanup
@@ -20,8 +20,8 @@ MINIO_VOLUME=${4:-""} # Default is empty if not provided
# Stop and remove the existing containers
echo "Stopping and removing existing containers..."
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio onyx_code_interpreter 2>/dev/null || true
docker stop onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
docker rm onyx_postgres onyx_vespa onyx_redis onyx_minio 2>/dev/null || true
# Start the PostgreSQL container with optional volume
echo "Starting PostgreSQL container..."
@@ -55,10 +55,6 @@ else
docker run --detach --name onyx_minio --publish 9004:9000 --publish 9005:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data --console-address ":9001"
fi
# Start the Code Interpreter container
echo "Starting Code Interpreter container..."
docker run --detach --name onyx_code_interpreter --publish 8000:8000 --user root -v /var/run/docker.sock:/var/run/docker.sock onyxdotapp/code-interpreter:latest bash ./entrypoint.sh code-interpreter-api
# Ensure alembic runs in the correct directory (backend/)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PARENT_DIR="$(dirname "$SCRIPT_DIR")"

View File

@@ -1,8 +1,13 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { useOnboardingState } from "@/refresh-components/onboarding/useOnboardingState";
function getOnboardingCompletedKey(userId: string): string {
return `onyx:onboardingCompleted:${userId}`;
}
interface UseShowOnboardingParams {
liveAssistant: MinimalPersonaSnapshot | undefined;
isLoadingProviders: boolean;
@@ -21,6 +26,15 @@ export function useShowOnboarding({
userId,
}: UseShowOnboardingParams) {
const [showOnboarding, setShowOnboarding] = useState(false);
const [onboardingDismissed, setOnboardingDismissed] = useState(false);
// Read localStorage once userId is available to check if onboarding was dismissed
useEffect(() => {
if (userId === undefined) return;
const dismissed =
localStorage.getItem(getOnboardingCompletedKey(userId)) === "true";
setOnboardingDismissed(dismissed);
}, [userId]);
// Initialize onboarding state
const {
@@ -38,13 +52,23 @@ export function useShowOnboarding({
// Show onboarding only if no LLM providers are configured.
// Skip entirely if user has existing chat sessions.
useEffect(() => {
// If onboarding was previously dismissed, never show it again
if (onboardingDismissed) {
setShowOnboarding(false);
return;
}
// Wait for data to load
if (isLoadingProviders || isLoadingChatSessions || userId === undefined) {
return;
}
// Only check once per user
// Only check once per user — but allow self-correction from true→false
// when provider data arrives (e.g. after a transient fetch error).
if (hasCheckedOnboardingForUserId.current === userId) {
if (showOnboarding && hasAnyProvider && onboardingState.stepIndex === 0) {
setShowOnboarding(false);
}
return;
}
hasCheckedOnboardingForUserId.current = userId;
@@ -63,18 +87,24 @@ export function useShowOnboarding({
hasAnyProvider,
chatSessionsCount,
userId,
showOnboarding,
onboardingDismissed,
onboardingState.stepIndex,
]);
const hideOnboarding = () => {
const dismissOnboarding = useCallback(() => {
if (userId === undefined) return;
setShowOnboarding(false);
};
setOnboardingDismissed(true);
localStorage.setItem(getOnboardingCompletedKey(userId), "true");
}, [userId]);
const finishOnboarding = () => {
setShowOnboarding(false);
};
const hideOnboarding = dismissOnboarding;
const finishOnboarding = dismissOnboarding;
return {
showOnboarding,
onboardingDismissed,
onboardingState,
onboardingActions,
llmDescriptors,

View File

@@ -17,11 +17,13 @@ const mockActions = {
reset: jest.fn(),
};
let mockStepIndex = 0;
jest.mock("@/refresh-components/onboarding/useOnboardingState", () => ({
useOnboardingState: () => ({
state: {
currentStep: OnboardingStep.Welcome,
stepIndex: 0,
stepIndex: mockStepIndex,
totalSteps: 3,
data: {},
isButtonActive: true,
@@ -60,6 +62,8 @@ function renderUseShowOnboarding(
describe("useShowOnboarding", () => {
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
mockStepIndex = 0;
});
it("returns showOnboarding=false while providers are loading", () => {
@@ -107,7 +111,7 @@ describe("useShowOnboarding", () => {
expect(result.current.showOnboarding).toBe(false);
});
it("only evaluates once per userId", () => {
it("self-corrects showOnboarding to false when providers arrive late", () => {
const { result, rerender } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
@@ -115,7 +119,7 @@ describe("useShowOnboarding", () => {
});
expect(result.current.showOnboarding).toBe(true);
// Re-render with same userId but different provider state
// Re-render with same userId but provider data now available
rerender({
liveAssistant: undefined,
isLoadingProviders: false,
@@ -125,7 +129,32 @@ describe("useShowOnboarding", () => {
userId: "user-1",
});
// Should still be true because it was already evaluated for this userId
// Should correct to false — providers exist, no need for LLM setup flow
expect(result.current.showOnboarding).toBe(false);
});
it("does not self-correct when user has advanced past Welcome step", () => {
const { result, rerender } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
userId: "user-1",
});
expect(result.current.showOnboarding).toBe(true);
// Simulate user advancing past Welcome (e.g. they configured an LLM provider)
mockStepIndex = 1;
// Re-render with same userId but provider data now available
rerender({
liveAssistant: undefined,
isLoadingProviders: false,
hasAnyProvider: true,
isLoadingChatSessions: false,
chatSessionsCount: 0,
userId: "user-1",
});
// Should stay true — user is actively using onboarding
expect(result.current.showOnboarding).toBe(true);
});
@@ -186,4 +215,83 @@ describe("useShowOnboarding", () => {
expect(result.current.onboardingActions).toBeDefined();
expect(result.current.llmDescriptors).toEqual([]);
});
describe("localStorage persistence", () => {
it("finishOnboarding sets localStorage flag and onboardingDismissed", () => {
const { result } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
});
expect(result.current.showOnboarding).toBe(true);
expect(result.current.onboardingDismissed).toBe(false);
act(() => {
result.current.finishOnboarding();
});
expect(result.current.showOnboarding).toBe(false);
expect(result.current.onboardingDismissed).toBe(true);
expect(localStorage.getItem("onyx:onboardingCompleted:user-1")).toBe(
"true"
);
});
it("hideOnboarding sets localStorage flag and onboardingDismissed", () => {
const { result } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
});
act(() => {
result.current.hideOnboarding();
});
expect(result.current.onboardingDismissed).toBe(true);
expect(localStorage.getItem("onyx:onboardingCompleted:user-1")).toBe(
"true"
);
});
it("showOnboarding stays false when localStorage flag is set", () => {
localStorage.setItem("onyx:onboardingCompleted:user-1", "true");
const { result } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
});
expect(result.current.showOnboarding).toBe(false);
expect(result.current.onboardingDismissed).toBe(true);
});
it("onboardingDismissed is false when localStorage flag is not set", () => {
const { result } = renderUseShowOnboarding();
expect(result.current.onboardingDismissed).toBe(false);
});
it("dismissal for user-1 does not suppress onboarding for user-2", () => {
const { result: result1 } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
userId: "1",
});
expect(result1.current.showOnboarding).toBe(true);
act(() => {
result1.current.finishOnboarding();
});
expect(result1.current.onboardingDismissed).toBe(true);
expect(localStorage.getItem("onyx:onboardingCompleted:1")).toBe("true");
// user-2 should still see onboarding
const { result: result2 } = renderUseShowOnboarding({
hasAnyProvider: false,
chatSessionsCount: 0,
userId: "2",
});
expect(result2.current.showOnboarding).toBe(true);
expect(result2.current.onboardingDismissed).toBe(false);
expect(localStorage.getItem("onyx:onboardingCompleted:2")).toBeNull();
});
});
});

View File

@@ -51,7 +51,7 @@ export default function NonAdminStep() {
<>
{showHeader && (
<div
className="flex items-center justify-between w-full max-w-[800px] min-h-11 py-1 pl-3 pr-2 bg-background-tint-00 rounded-16 shadow-01 mb-2"
className="flex items-center justify-between w-full min-h-11 py-1 pl-3 pr-2 bg-background-tint-00 rounded-16 shadow-01 mb-2"
aria-label="non-admin-confirmation"
>
<div className="flex items-center gap-1">

View File

@@ -228,6 +228,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
const {
showOnboarding,
onboardingDismissed,
onboardingState,
onboardingActions,
llmDescriptors,
@@ -463,7 +464,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
currentMessageFiles,
deepResearch: deepResearchEnabled,
});
if (showOnboarding) {
if (showOnboarding || !onboardingDismissed) {
finishOnboarding();
}
},
@@ -473,6 +474,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
currentMessageFiles,
deepResearchEnabled,
showOnboarding,
onboardingDismissed,
finishOnboarding,
]
);
@@ -506,7 +508,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
currentMessageFiles,
deepResearch: deepResearchEnabled,
});
if (showOnboarding) {
if (showOnboarding || !onboardingDismissed) {
finishOnboarding();
}
return;
@@ -527,6 +529,7 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
currentMessageFiles,
deepResearchEnabled,
showOnboarding,
onboardingDismissed,
finishOnboarding,
]
);
@@ -804,7 +807,8 @@ export default function AppPage({ firstMessage }: ChatPageProps) {
{/* OnboardingUI */}
{(appFocus.isNewSession() || appFocus.isAgent()) &&
!classification &&
(showOnboarding || !user?.personalization?.name) && (
(showOnboarding || !user?.personalization?.name) &&
!onboardingDismissed && (
<OnboardingFlow
showOnboarding={showOnboarding}
handleHideOnboarding={hideOnboarding}