fix(onboarding): brand logo on welcome + back-to-welcome from first step (#3366)

This commit is contained in:
Mega Mind
2026-06-04 18:42:19 +05:30
committed by GitHub
parent 7c08704e51
commit 5247c26700
19 changed files with 88 additions and 14 deletions
+1
View File
@@ -161,6 +161,7 @@ const messages: TranslationMap = {
'clearData.failed': 'فشل مسح البيانات وتسجيل الخروج. يرجى المحاولة مرة أخرى.',
'clearData.failedLogout': 'فشل تسجيل الخروج. يرجى المحاولة مرة أخرى.',
'clearData.failedPersist': 'فشل مسح حالة التطبيق المحفوظة. يرجى المحاولة مرة أخرى.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'مرحبًا بك في OpenHuman',
'welcome.subtitle': 'مساعدك الذكي الشخصي. خاص وبسيط وبالغ القوة.',
'welcome.connectPrompt': 'ضبط عنوان URL للـ RPC (متقدم)',
+1
View File
@@ -161,6 +161,7 @@ const messages: TranslationMap = {
'clearData.failed': 'ডেটা মুছতে ও লগ আউট করতে ব্যর্থ হয়েছে। আবার চেষ্টা করুন।',
'clearData.failedLogout': 'লগ আউট করতে ব্যর্থ হয়েছে। আবার চেষ্টা করুন।',
'clearData.failedPersist': 'অ্যাপ স্টেট মুছতে ব্যর্থ হয়েছে। আবার চেষ্টা করুন।',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'OpenHuman-এ স্বাগতম',
'welcome.subtitle':
'আপনার ব্যক্তিগত AI সুপার ইন্টেলিজেন্স। ব্যক্তিগত, সহজ এবং অত্যন্ত শক্তিশালী।',
+1
View File
@@ -169,6 +169,7 @@ const messages: TranslationMap = {
'clearData.failedLogout': 'Abmelden fehlgeschlagen. Bitte versuche es erneut.',
'clearData.failedPersist':
'Der persistente App-Status konnte nicht gelöscht werden. Bitte versuche es erneut.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Willkommen bei OpenHuman',
'welcome.subtitle':
'Deine persönliche KI-Superintelligenz. Privat, einfach und äußerst leistungsstark.',
+1
View File
@@ -178,6 +178,7 @@ const en: TranslationMap = {
'clearData.failedPersist': 'Failed to clear persisted app state. Please try again.',
// Welcome page
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Welcome to OpenHuman',
'welcome.subtitle':
'Your personal AI super intelligence. Private, simple and extremely powerful.',
+1
View File
@@ -170,6 +170,7 @@ const messages: TranslationMap = {
'clearData.failedLogout': 'No se pudo cerrar sesión. Inténtalo de nuevo.',
'clearData.failedPersist':
'No se pudo borrar el estado persistido de la app. Inténtalo de nuevo.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Bienvenido a OpenHuman',
'welcome.subtitle':
'Tu super inteligencia artificial personal. Privada, simple y extremadamente poderosa.',
+1
View File
@@ -168,6 +168,7 @@ const messages: TranslationMap = {
'clearData.failed': "Échec de l'effacement des données et de la déconnexion. Réessaie.",
'clearData.failedLogout': 'Échec de la déconnexion. Réessaie.',
'clearData.failedPersist': "Échec de l'effacement de l'état persisté. Réessaie.",
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Bienvenue sur OpenHuman',
'welcome.subtitle':
'Ton assistant IA personnel super-intelligent. Privé, simple et extrêmement puissant.',
+1
View File
@@ -162,6 +162,7 @@ const messages: TranslationMap = {
'clearData.failed': 'डेटा क्लियर करने और लॉगआउट में दिक्कत आई। दोबारा कोशिश करें।',
'clearData.failedLogout': 'लॉग आउट नहीं हो पाया। दोबारा कोशिश करें।',
'clearData.failedPersist': 'सेव्ड ऐप स्टेट क्लियर नहीं हो पाई। दोबारा कोशिश करें।',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'OpenHuman में आपका स्वागत है',
'welcome.subtitle': 'आपकी पर्सनल AI सुपर इंटेलिजेंस। प्राइवेट, सिंपल और बेहद पावरफुल।',
'welcome.connectPrompt': 'RPC URL कॉन्फिगर करें (एडवांस्ड)',
+1
View File
@@ -163,6 +163,7 @@ const messages: TranslationMap = {
'clearData.failed': 'Gagal membersihkan data dan keluar. Silakan coba lagi.',
'clearData.failedLogout': 'Gagal keluar. Silakan coba lagi.',
'clearData.failedPersist': 'Gagal membersihkan status aplikasi tersimpan. Silakan coba lagi.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Selamat datang di OpenHuman',
'welcome.subtitle': 'Asisten AI Anda untuk komunitas',
'welcome.connectPrompt': 'Konfigurasikan RPC URL (Lanjutan)',
+1
View File
@@ -167,6 +167,7 @@ const messages: TranslationMap = {
'clearData.failed': 'Impossibile cancellare i dati ed effettuare il logout. Riprova.',
'clearData.failedLogout': 'Impossibile effettuare il logout. Riprova.',
'clearData.failedPersist': "Impossibile cancellare lo stato persistente dell'app. Riprova.",
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Benvenuto in OpenHuman',
'welcome.subtitle':
'La tua super intelligenza AI personale. Privata, semplice ed estremamente potente.',
+1
View File
@@ -161,6 +161,7 @@ const messages: TranslationMap = {
'clearData.failed': '데이터 삭제 및 로그아웃에 실패했습니다. 다시 시도해 주세요.',
'clearData.failedLogout': '로그아웃에 실패했습니다. 다시 시도해 주세요.',
'clearData.failedPersist': '저장된 앱 상태를 삭제하지 못했습니다. 다시 시도해 주세요.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'OpenHuman에 오신 것을 환영합니다',
'welcome.subtitle': '개인용 AI 슈퍼 인텔리전스입니다. 비공개이며, 간단하고, 매우 강력합니다.',
'welcome.connectPrompt': 'RPC URL 구성(고급)',
+1
View File
@@ -165,6 +165,7 @@ const messages: TranslationMap = {
'clearData.failedLogout': 'Nie udało się wylogować. Spróbuj ponownie.',
'clearData.failedPersist':
'Nie udało się wyczyścić utrwalonego stanu aplikacji. Spróbuj ponownie.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Witaj w OpenHuman',
'welcome.subtitle': 'Twoja osobista superinteligencja AI. Prywatna, prosta i niezwykle potężna.',
'welcome.connectPrompt': 'Skonfiguruj URL RPC (zaawansowane)',
+1
View File
@@ -168,6 +168,7 @@ const messages: TranslationMap = {
'clearData.failedLogout': 'Falha ao sair. Por favor, tente novamente.',
'clearData.failedPersist':
'Falha ao limpar o estado persistido do app. Por favor, tente novamente.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Bem-vindo ao OpenHuman',
'welcome.subtitle':
'Sua super inteligência artificial pessoal. Privada, simples e extremamente poderosa.',
+1
View File
@@ -164,6 +164,7 @@ const messages: TranslationMap = {
'clearData.failed': 'Не удалось очистить данные и выйти. Попробуй ещё раз.',
'clearData.failedLogout': 'Не удалось выйти. Попробуй ещё раз.',
'clearData.failedPersist': 'Не удалось сбросить состояние приложения. Попробуй ещё раз.',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': 'Добро пожаловать в OpenHuman',
'welcome.subtitle': 'Твой персональный суперинтеллект. Приватный, простой и невероятно мощный.',
'welcome.connectPrompt': 'Настроить RPC URL (дополнительно)',
+1
View File
@@ -158,6 +158,7 @@ const messages: TranslationMap = {
'clearData.failed': '清除数据失败,请重试。',
'clearData.failedLogout': '退出登录失败,请重试。',
'clearData.failedPersist': '清除持久化应用状态失败,请重试。',
'welcome.logoAlt': 'OpenHuman',
'welcome.title': '欢迎使用 OpenHuman',
'welcome.subtitle': '你的私人 AI 超级智能。私密、简单、强大。',
'welcome.connectPrompt': '输入 Core RPC 地址以开始使用',
+5 -4
View File
@@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
import OAuthProviderButton from '../components/oauth/OAuthProviderButton';
import { oauthProviderConfigs } from '../components/oauth/providerConfigs';
import RotatingTetrahedronCanvas from '../components/RotatingTetrahedronCanvas';
import Button from '../components/ui/Button';
import { useT } from '../lib/i18n/I18nContext';
import { useCoreState } from '../providers/CoreStateProvider';
@@ -127,9 +126,11 @@ const Welcome = () => {
</button>
</div>
<div className="flex justify-center mb-6">
<div className="h-20 w-20">
<RotatingTetrahedronCanvas />
</div>
<img
src={isDark ? '/brand/OpenhumanLogo-white.svg' : '/brand/OpenhumanLogo-Black.svg'}
alt={t('welcome.logoAlt')}
className="h-20 w-20"
/>
</div>
<h1 className="text-2xl font-bold text-stone-900 dark:text-neutral-100 text-center mb-2">
-4
View File
@@ -23,10 +23,6 @@ vi.mock('../../providers/CoreStateProvider', () => ({
const oauthButtonSpy = vi.fn();
const oauthOverrideSpy = vi.fn();
vi.mock('../../components/RotatingTetrahedronCanvas', () => ({
default: () => <div data-testid="welcome-logo" />,
}));
vi.mock('../../components/oauth/OAuthProviderButton', () => ({
default: ({
provider,
@@ -1,5 +1,5 @@
import { configureStore } from '@reduxjs/toolkit';
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -11,6 +11,7 @@ import CustomInferencePage from './CustomInferencePage';
const navigateMock = vi.fn();
const setDraftMock = vi.fn();
const clearSessionMock = vi.fn().mockResolvedValue(undefined);
vi.mock('react-router-dom', async importOriginal => {
const actual = await importOriginal<typeof import('react-router-dom')>();
@@ -22,7 +23,10 @@ vi.mock('../../../components/settings/panels/AIPanel', () => ({
}));
vi.mock('../../../providers/CoreStateProvider', () => ({
useCoreState: () => ({ snapshot: { sessionToken: 'header.payload.local' } }),
useCoreState: () => ({
snapshot: { sessionToken: 'header.payload.local' },
clearSession: clearSessionMock,
}),
}));
vi.mock('../OnboardingContext', () => ({
@@ -54,6 +58,7 @@ describe('CustomInferencePage', () => {
beforeEach(() => {
navigateMock.mockReset();
setDraftMock.mockReset();
clearSessionMock.mockClear();
});
it('forces configure mode and hides the default/configure chooser for local sessions', () => {
@@ -67,4 +72,39 @@ describe('CustomInferencePage', () => {
screen.queryByTestId('onboarding-custom-inference-step-configure')
).not.toBeInTheDocument();
});
it('waits for clearSession to finish before navigating back to the welcome page', async () => {
let resolveClearSession!: () => void;
clearSessionMock.mockReturnValueOnce(
new Promise<void>(resolve => {
resolveClearSession = resolve;
})
);
renderPage();
fireEvent.click(screen.getByRole('button', { name: 'Back' }));
// Navigation must not race ahead of the session being cleared — otherwise
// PublicRoute bounces "/" back to /home with the session still live.
expect(clearSessionMock).toHaveBeenCalledTimes(1);
expect(navigateMock).not.toHaveBeenCalled();
resolveClearSession();
await waitFor(() => expect(navigateMock).toHaveBeenCalledWith('/'));
});
it('stays on the step and does not navigate when clearSession fails', async () => {
clearSessionMock.mockRejectedValueOnce(new Error('clear failed'));
renderPage();
fireEvent.click(screen.getByRole('button', { name: 'Back' }));
await waitFor(() => expect(clearSessionMock).toHaveBeenCalledTimes(1));
// Give the rejected promise a chance to settle, then confirm we did not
// navigate to "/" with a still-active session.
await Promise.resolve();
expect(navigateMock).not.toHaveBeenCalled();
});
});
@@ -4,7 +4,7 @@ import CustomWizardConfigPage from './CustomWizardConfigPage';
const CustomInferencePage = () => (
<CustomWizardConfigPage
stepKey="inference"
backRoute="/onboarding/runtime-choice"
backRoute="/"
configureContent={<AIPanel embedded />}
/>
);
@@ -1,3 +1,4 @@
import createDebug from 'debug';
import { type ReactNode, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -13,6 +14,10 @@ import {
} from '../OnboardingContext';
import CustomWizardStep from '../steps/CustomWizardStep';
const log = createDebug('app:onboarding:custom');
const describeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
const LOCAL_DEFAULT_DISABLED_REASON =
'Managed setup requires OpenHuman sign-in and is unavailable in local mode.';
@@ -29,7 +34,7 @@ export default function CustomWizardConfigPage({
}: CustomWizardConfigPageProps) {
const { t } = useT();
const navigate = useNavigate();
const { snapshot } = useCoreState();
const { snapshot, clearSession } = useCoreState();
const { draft, setDraft, completeAndExit } = useOnboardingContext();
const isLocalSession = isLocalSessionToken(snapshot.sessionToken);
const stepIndex = CUSTOM_WIZARD_STEPS.indexOf(stepKey);
@@ -52,8 +57,26 @@ export default function CustomWizardConfigPage({
};
const isLast = stepIndex === CUSTOM_WIZARD_STEPS.length - 1;
const isFirst = stepIndex === 0;
const namespace = `onboarding.custom.${stepKey}`;
const handleBack = async () => {
// Going back from the first step returns to the welcome/login screen.
// A session is always present here (OAuth or "Continue Locally"), so we
// must clear it first — otherwise PublicRoute bounces "/" to /home.
if (isFirst) {
try {
await clearSession();
} catch (err) {
// Navigating to "/" with a live session would just bounce back to /home
// via PublicRoute — so stay on the step and surface a dev-only diagnostic.
log('[onboarding:custom-%s] clearSession on back failed: %s', stepKey, describeError(err));
return;
}
}
navigate(backRoute ?? CUSTOM_WIZARD_ROUTES[CUSTOM_WIZARD_STEPS[stepIndex - 1]]);
};
return (
<CustomWizardStep
testId={`onboarding-custom-${stepKey}-step`}
@@ -69,7 +92,7 @@ export default function CustomWizardConfigPage({
hideChoiceCards={isLocalSession}
choice={choice}
onChoiceChange={persistChoice}
onBack={() => navigate(backRoute ?? CUSTOM_WIZARD_ROUTES[CUSTOM_WIZARD_STEPS[stepIndex - 1]])}
onBack={() => void handleBack()}
onContinue={async () => {
trackEvent('onboarding_step_complete', {
step_name: `custom_${stepKey}`,
@@ -79,7 +102,7 @@ export default function CustomWizardConfigPage({
try {
await completeAndExit();
} catch (err) {
console.error(`[onboarding:custom-${stepKey}] completeAndExit failed`, err);
log('[onboarding:custom-%s] completeAndExit failed: %s', stepKey, describeError(err));
}
return;
}