mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(onboarding): brand logo on welcome + back-to-welcome from first step (#3366)
This commit is contained in:
@@ -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 (متقدم)',
|
||||
|
||||
@@ -161,6 +161,7 @@ const messages: TranslationMap = {
|
||||
'clearData.failed': 'ডেটা মুছতে ও লগ আউট করতে ব্যর্থ হয়েছে। আবার চেষ্টা করুন।',
|
||||
'clearData.failedLogout': 'লগ আউট করতে ব্যর্থ হয়েছে। আবার চেষ্টা করুন।',
|
||||
'clearData.failedPersist': 'অ্যাপ স্টেট মুছতে ব্যর্থ হয়েছে। আবার চেষ্টা করুন।',
|
||||
'welcome.logoAlt': 'OpenHuman',
|
||||
'welcome.title': 'OpenHuman-এ স্বাগতম',
|
||||
'welcome.subtitle':
|
||||
'আপনার ব্যক্তিগত AI সুপার ইন্টেলিজেন্স। ব্যক্তিগত, সহজ এবং অত্যন্ত শক্তিশালী।',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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 कॉन्फिगर करें (एडवांस्ड)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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 구성(고급)',
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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 (дополнительно)',
|
||||
|
||||
@@ -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 地址以开始使用',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user