diff --git a/web/src/components/BetaBanner.tsx b/web/src/components/BetaBanner.tsx
index a6b1dcc5d..87e09a2fa 100644
--- a/web/src/components/BetaBanner.tsx
+++ b/web/src/components/BetaBanner.tsx
@@ -1,80 +1,136 @@
import { useTranslation } from 'react-i18next';
import { AlertCircle, X } from 'lucide-react';
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { backendClient } from '@/app/infra/http';
+const RETRY_DELAYS_MS = [1000, 2000, 4000, 8000];
+const CLOUD_BETA_RETRY_LIMIT = RETRY_DELAYS_MS.length;
+
export function BetaBanner() {
- const { t, i18n } = useTranslation();
+ const { t } = useTranslation();
+ const bannerRef = useRef
(null);
const [show, setShow] = useState(false);
const [dismissed, setDismissed] = useState(false);
+ const visible = show && !dismissed;
+
+ // Publish the banner height so the fixed app shell (sidebar + inset) starts
+ // below the notice instead of painting over it.
+ useEffect(() => {
+ const root = document.documentElement;
+ if (!visible) {
+ root.style.removeProperty('--beta-banner-height');
+ return;
+ }
+ const element = bannerRef.current;
+ if (!element) {
+ return;
+ }
+ const sync = () => {
+ root.style.setProperty(
+ '--beta-banner-height',
+ `${element.getBoundingClientRect().height}px`,
+ );
+ };
+ sync();
+ const observer = new ResizeObserver(sync);
+ observer.observe(element);
+ return () => {
+ observer.disconnect();
+ root.style.removeProperty('--beta-banner-height');
+ };
+ }, [visible]);
useEffect(() => {
- const checkBetaStatus = async () => {
+ let cancelled = false;
+ let retryTimer: number | undefined;
+
+ const checkBetaStatus = async (attempt = 0) => {
try {
const info = await backendClient.getSystemInfo();
- const shouldShow =
- info.deployment_mode === 'cloud' && info.beta === true;
- setShow(shouldShow);
+ if (cancelled) {
+ return;
+ }
+ setShow(info.deployment_mode === 'cloud' && info.beta === true);
} catch {
- setShow(false);
+ if (cancelled) {
+ return;
+ }
+ // A single failed probe (cold start, restarting backend, flaky
+ // network) must not hide the banner for the rest of the session, so
+ // keep the last known state and retry with backoff.
+ if (attempt < CLOUD_BETA_RETRY_LIMIT) {
+ retryTimer = window.setTimeout(
+ () => void checkBetaStatus(attempt + 1),
+ RETRY_DELAYS_MS[attempt],
+ );
+ }
}
};
- checkBetaStatus();
+ void checkBetaStatus();
+
+ const recheck = () => {
+ if (!cancelled) {
+ void checkBetaStatus();
+ }
+ };
+ window.addEventListener('focus', recheck);
+
+ return () => {
+ cancelled = true;
+ clearTimeout(retryTimer);
+ window.removeEventListener('focus', recheck);
+ };
}, []);
- if (!show || dismissed) {
+ if (!visible) {
return null;
}
- const cloudUrl =
- i18n.language === 'zh-Hans' || i18n.language === 'zh_CN'
- ? 'https://langbot.app/zh/cloud'
- : i18n.language === 'ja' || i18n.language === 'ja_JP'
- ? 'https://langbot.app/ja/cloud'
- : 'https://langbot.app/cloud';
-
- const ossUrl =
- i18n.language === 'zh-Hans' || i18n.language === 'zh_CN'
- ? 'https://langbot.app/zh/docs/quick-start/deploy'
- : i18n.language === 'ja' || i18n.language === 'ja_JP'
- ? 'https://langbot.app/ja/docs/quick-start/deploy'
- : 'https://langbot.app/docs/quick-start/deploy';
+ // The cloud product page opens the dedicated (paid v1) environment tab;
+ // space.langbot.app resolves the locale itself, so no path prefix applies.
+ const cloudUrl = 'https://space.langbot.app/cloud?environment=dedicated';
+ const ossUrl = 'https://github.com/langbot-app/LangBot';
return (
-
-
+
);
}
diff --git a/web/src/components/ui/sidebar.tsx b/web/src/components/ui/sidebar.tsx
index fd0daa9dc..3ef69ff30 100644
--- a/web/src/components/ui/sidebar.tsx
+++ b/web/src/components/ui/sidebar.tsx
@@ -203,6 +203,9 @@ function SidebarProvider({
{
'--sidebar-width': `${sidebarWidth}px`,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
+ // Global notices (e.g. the beta banner) publish their height so
+ // the shell starts below them instead of underneath.
+ height: 'calc(100svh - var(--beta-banner-height, 0px))',
...style,
} as React.CSSProperties
}
@@ -298,6 +301,10 @@ function Sidebar({
/>