mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-04 12:56:02 +00:00
fix(web): improve backend retry and sidebar scrolling
This commit is contained in:
@@ -85,7 +85,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { ChevronRight, Plus } from 'lucide-react';
|
||||
import { ChevronDown, ChevronRight, Plus } from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -187,6 +187,7 @@ const ENTITY_ROUTE_MAP: Record<EntityCategoryId, string> = {
|
||||
// localStorage key for collapsible section open/closed state
|
||||
const SIDEBAR_SECTIONS_KEY = 'sidebar_sections';
|
||||
const SIDEBAR_LIST_EXPANSION_KEY = 'sidebar_entity_list_expansion';
|
||||
const SCROLL_HINT_BOTTOM_THRESHOLD = 40;
|
||||
|
||||
type SidebarNavSection = 'home' | 'extensions';
|
||||
type SidebarListExpansionState = Record<
|
||||
@@ -1519,6 +1520,27 @@ export default function HomeSidebar({
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const [starCount, setStarCount] = useState<number | null>(null);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const navigationContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showScrollHint, setShowScrollHint] = useState(false);
|
||||
|
||||
function scrollNavigationToBottom() {
|
||||
const contentEl = navigationContentRef.current;
|
||||
if (!contentEl) return;
|
||||
|
||||
const maxScrollTop = contentEl.scrollHeight - contentEl.clientHeight;
|
||||
contentEl.scrollTo({
|
||||
top: maxScrollTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
setShowScrollHint(false);
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (contentEl.scrollTop < maxScrollTop - 2) {
|
||||
contentEl.scrollTop = maxScrollTop;
|
||||
}
|
||||
setShowScrollHint(false);
|
||||
}, 250);
|
||||
}
|
||||
function handleModelsDialogChange(open: boolean) {
|
||||
setModelsDialogOpen(open);
|
||||
if (open) {
|
||||
@@ -1622,6 +1644,48 @@ export default function HomeSidebar({
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const contentEl = navigationContentRef.current;
|
||||
if (!contentEl) return;
|
||||
|
||||
let animationFrame = 0;
|
||||
const updateScrollHint = () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
animationFrame = requestAnimationFrame(() => {
|
||||
const hasHiddenContent =
|
||||
contentEl.scrollTop + contentEl.clientHeight <
|
||||
contentEl.scrollHeight - SCROLL_HINT_BOTTOM_THRESHOLD;
|
||||
setShowScrollHint(hasHiddenContent);
|
||||
});
|
||||
};
|
||||
|
||||
updateScrollHint();
|
||||
contentEl.addEventListener('scroll', updateScrollHint, { passive: true });
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateScrollHint);
|
||||
resizeObserver.observe(contentEl);
|
||||
if (contentEl.firstElementChild) {
|
||||
resizeObserver.observe(contentEl.firstElementChild);
|
||||
}
|
||||
|
||||
const mutationObserver = new MutationObserver(updateScrollHint);
|
||||
mutationObserver.observe(contentEl, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
window.addEventListener('resize', updateScrollHint);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
contentEl.removeEventListener('scroll', updateScrollHint);
|
||||
resizeObserver.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
window.removeEventListener('resize', updateScrollHint);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update selected state + notify parent without navigating
|
||||
function selectChild(child: SidebarChildVO) {
|
||||
setSelectedChild(child);
|
||||
@@ -1715,37 +1779,57 @@ export default function HomeSidebar({
|
||||
</SidebarHeader>
|
||||
|
||||
{/* Navigation items grouped by section */}
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{t('sidebar.home')}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<NavItems
|
||||
selectedChild={selectedChild}
|
||||
onChildClick={handleChildClick}
|
||||
section="home"
|
||||
sectionOpenState={sectionOpenState}
|
||||
onSectionToggle={handleSectionToggle}
|
||||
/>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{t('sidebar.extensions')}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<NavItems
|
||||
selectedChild={selectedChild}
|
||||
onChildClick={handleChildClick}
|
||||
section="extensions"
|
||||
sectionOpenState={sectionOpenState}
|
||||
onSectionToggle={handleSectionToggle}
|
||||
/>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<PluginPagesNav />
|
||||
</SidebarContent>
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<SidebarContent ref={navigationContentRef} className="min-h-0 pb-8">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{t('sidebar.home')}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<NavItems
|
||||
selectedChild={selectedChild}
|
||||
onChildClick={handleChildClick}
|
||||
section="home"
|
||||
sectionOpenState={sectionOpenState}
|
||||
onSectionToggle={handleSectionToggle}
|
||||
/>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{t('sidebar.extensions')}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<NavItems
|
||||
selectedChild={selectedChild}
|
||||
onChildClick={handleChildClick}
|
||||
section="extensions"
|
||||
sectionOpenState={sectionOpenState}
|
||||
onSectionToggle={handleSectionToggle}
|
||||
/>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<PluginPagesNav />
|
||||
</SidebarContent>
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollNavigationToBottom}
|
||||
disabled={!showScrollHint}
|
||||
aria-label={t('sidebar.scrollToBottom')}
|
||||
aria-hidden={!showScrollHint}
|
||||
tabIndex={showScrollHint ? 0 : -1}
|
||||
className={cn(
|
||||
'absolute inset-x-0 bottom-2 z-10 mx-auto flex w-fit justify-center rounded-full transition-opacity duration-200 group-data-[collapsible=icon]:hidden',
|
||||
showScrollHint
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<span className="flex size-7 items-center justify-center rounded-full border border-sidebar-border bg-sidebar/95 text-sidebar-foreground/70 shadow-sm backdrop-blur transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground">
|
||||
<ChevronDown className="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<SidebarFooter>
|
||||
|
||||
@@ -59,6 +59,8 @@ function isExtensionsRoute(pathname: string): boolean {
|
||||
}
|
||||
|
||||
const HOME_CONTENT_MAX_WIDTH = 'max-w-[1360px]';
|
||||
const BACKEND_UNAVAILABLE_RETURN_TO_STORAGE_KEY =
|
||||
'langbot_backend_unavailable_return_to';
|
||||
|
||||
export default function HomeLayout({
|
||||
children,
|
||||
@@ -66,6 +68,7 @@ export default function HomeLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Initialize user info if not already initialized
|
||||
useEffect(() => {
|
||||
@@ -87,7 +90,15 @@ export default function HomeLayout({
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
navigate('/backend-unavailable', { replace: true });
|
||||
const returnTo = `${location.pathname}${location.search}${location.hash}`;
|
||||
sessionStorage.setItem(
|
||||
BACKEND_UNAVAILABLE_RETURN_TO_STORAGE_KEY,
|
||||
returnTo,
|
||||
);
|
||||
navigate('/backend-unavailable', {
|
||||
replace: true,
|
||||
state: { from: returnTo },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -96,7 +107,7 @@ export default function HomeLayout({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [navigate]);
|
||||
}, [location.hash, location.pathname, location.search, navigate]);
|
||||
|
||||
return (
|
||||
<SidebarDataProvider>
|
||||
|
||||
Reference in New Issue
Block a user