Merge branch 'feat/graceful-degradation' into feat/on-demand-ai-features

This commit is contained in:
ashim-hq
2026-04-21 09:24:37 +08:00
14 changed files with 801 additions and 125 deletions
@@ -0,0 +1,40 @@
import { CheckCircle2, Loader2, WifiOff } from "lucide-react";
import { useConnectionStore } from "@/stores/connection-store";
export function ConnectionBanner() {
const status = useConnectionStore((s) => s.status);
if (status === "connected") return null;
const config = {
disconnected: {
bg: "bg-amber-500 dark:bg-amber-600",
text: "text-amber-950 dark:text-amber-50",
icon: <Loader2 className="h-4 w-4 animate-spin" />,
message: "Reconnecting to server\u2026",
},
offline: {
bg: "bg-amber-500 dark:bg-amber-600",
text: "text-amber-950 dark:text-amber-50",
icon: <WifiOff className="h-4 w-4" />,
message: "You\u2019re offline",
},
reconnected: {
bg: "bg-emerald-500 dark:bg-emerald-600",
text: "text-emerald-950 dark:text-emerald-50",
icon: <CheckCircle2 className="h-4 w-4" />,
message: "Connected",
},
}[status];
return (
<div
role="status"
aria-live="polite"
className={`fixed top-0 left-0 right-0 z-[60] flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-transform duration-300 ${config.bg} ${config.text}`}
>
{config.icon}
{config.message}
</div>
);
}
+15 -2
View File
@@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
import { useMobile } from "@/hooks/use-mobile";
import { apiGet } from "@/lib/api";
import { cn } from "@/lib/utils";
import { useConnectionStore } from "@/stores/connection-store";
import { Dropzone } from "../common/dropzone";
import { GemLogo } from "../common/gem-logo";
import { HelpDialog } from "../help/help-dialog";
@@ -25,6 +26,8 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const isMobile = useMobile();
const [customLogo, setCustomLogo] = useState(false);
const connectionStatus = useConnectionStore((s) => s.status);
const bannerVisible = connectionStatus !== "connected";
useEffect(() => {
apiGet<{ settings: Record<string, string> }>("/v1/settings")
@@ -33,7 +36,12 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
}, []);
return (
<div className="flex h-screen bg-background text-foreground overflow-hidden">
<div
className={cn(
"flex h-screen bg-background text-foreground overflow-hidden",
bannerVisible && "pt-9",
)}
>
{/* Desktop sidebar */}
{!isMobile && (
<Sidebar
@@ -92,7 +100,12 @@ export function AppLayout({ children, showToolPanel = true, onFiles }: AppLayout
{/* Mobile top bar */}
{isMobile && (
<div className="fixed top-0 left-0 right-0 z-30 bg-background/95 backdrop-blur-sm border-b border-border px-3 py-2 flex items-center gap-3">
<div
className={cn(
"fixed left-0 right-0 z-30 bg-background/95 backdrop-blur-sm border-b border-border px-3 py-2 flex items-center gap-3",
bannerVisible ? "top-9" : "top-0",
)}
>
<button
type="button"
onClick={() => setMobileSidebarOpen(true)}