diff --git a/core/http/react-ui/src/App.jsx b/core/http/react-ui/src/App.jsx index e14035a3c..b922499b5 100644 --- a/core/http/react-ui/src/App.jsx +++ b/core/http/react-ui/src/App.jsx @@ -75,6 +75,14 @@ export default function App() { } }, [sidebarOpen]) + // Reset scroll to the top on every route change. The default (non-chat) + // layout uses the document as its scroll container, so without this a new + // page opens at the previous page's scroll position - navigating the console + // rail from a scrolled page would land mid-view instead of at the top. + useEffect(() => { + window.scrollTo(0, 0) + }, [location.pathname]) + const layoutClasses = [ 'app-layout', isChatRoute ? 'app-layout-chat' : '', diff --git a/core/http/react-ui/src/components/PageHeader.jsx b/core/http/react-ui/src/components/PageHeader.jsx index bb4bfedd1..59146bdb2 100644 --- a/core/http/react-ui/src/components/PageHeader.jsx +++ b/core/http/react-ui/src/components/PageHeader.jsx @@ -1,10 +1,20 @@ -// Editorial page header: left eyebrow + serif title + supporting line, -// right-aligned meta/actions slot. Asymmetric, left-aligned. +import { useLocation } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { sectionKeyForPath } from '../utils/section' + +// Editorial page header: left eyebrow + title + supporting line, right-aligned +// meta/actions slot. The eyebrow defaults to the page's section/console name +// (derived from the route) so headers stay consistent without per-page wiring; +// pass `eyebrow` to override, or `eyebrow={null}` to suppress it. export default function PageHeader({ eyebrow, title, supporting, actions, className = '' }) { + const { t } = useTranslation('nav') + const { pathname } = useLocation() + const autoKey = sectionKeyForPath(pathname) + const resolvedEyebrow = eyebrow !== undefined ? eyebrow : (autoKey ? t(autoKey) : null) return (
- {eyebrow && {eyebrow}} + {resolvedEyebrow && {resolvedEyebrow}} {title &&

{title}

} {supporting &&

{supporting}

}
diff --git a/core/http/react-ui/src/pages/Settings.jsx b/core/http/react-ui/src/pages/Settings.jsx index 72f92504c..55a032c7b 100644 --- a/core/http/react-ui/src/pages/Settings.jsx +++ b/core/http/react-ui/src/pages/Settings.jsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { settingsApi, resourcesApi, brandingApi } from '../utils/api' import { useBranding } from '../contexts/BrandingContext' import LoadingSpinner from '../components/LoadingSpinner' +import PageHeader from '../components/PageHeader' import SearchableModelSelect from '../components/SearchableModelSelect' import { CAP_CHAT } from '../utils/capabilities' import Toggle from '../components/Toggle' @@ -159,17 +160,16 @@ export default function Settings() { return (
{/* Header */} -
-
-

{t('settings.title')}

-

{t('settings.subtitle')}

-
- +
+ + {saving ? <> Saving... : <> {isDirty ? 'Save Changes' : 'Saved'}} + + } + />
{/* Two-column layout */} diff --git a/core/http/react-ui/src/utils/section.js b/core/http/react-ui/src/utils/section.js new file mode 100644 index 000000000..0613437be --- /dev/null +++ b/core/http/react-ui/src/utils/section.js @@ -0,0 +1,18 @@ +import { consoles, consolePaths } from '../components/console/consoleConfig' + +// Inline "Create" group from the sidebar (these pages live outside a console). +const CREATE_PATHS = ['/app/chat', '/app/studio', '/app/talk'] + +// The section/console an app page belongs to, returned as a `nav` i18n key for +// use as the PageHeader eyebrow. Console pages map to their console title +// (Build / Operate); the inline Create group maps to sections.create; any other +// top-level page (Home, Install Models, Account, ...) has no eyebrow. +export function sectionKeyForPath(pathname) { + for (const c of consoles) { + if (consolePaths(c).some(p => pathname === p || pathname.startsWith(p + '/'))) { + return c.titleKey + } + } + if (CREATE_PATHS.some(p => pathname === p || pathname.startsWith(p + '/'))) return 'sections.create' + return null +}