mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
Merge pull request #19831 from BerriAI/litellm_ui_hide_send_feedback
[Feature] UI - Feedback Prompts: Option To Hide Prompts
This commit is contained in:
commit
a97bf452f1
5 changed files with 230 additions and 9 deletions
|
|
@ -0,0 +1,35 @@
|
|||
// hooks/useDisableShowPrompts.ts
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { getLocalStorageItem } from "@/utils/localStorageUtils";
|
||||
import { LOCAL_STORAGE_EVENT } from "@/utils/localStorageUtils";
|
||||
|
||||
function subscribe(callback: () => void) {
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === "disableShowPrompts") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const onCustom = (e: Event) => {
|
||||
const { key } = (e as CustomEvent).detail;
|
||||
if (key === "disableShowPrompts") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", onStorage);
|
||||
window.addEventListener(LOCAL_STORAGE_EVENT, onCustom);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", onStorage);
|
||||
window.removeEventListener(LOCAL_STORAGE_EVENT, onCustom);
|
||||
};
|
||||
}
|
||||
|
||||
function getSnapshot() {
|
||||
return getLocalStorageItem("disableShowPrompts") === "true";
|
||||
}
|
||||
|
||||
export function useDisableShowPrompts() {
|
||||
return useSyncExternalStore(subscribe, getSnapshot);
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ vi.mock("@/utils/proxyUtils", () => ({
|
|||
let mockUseThemeImpl = () => ({ logoUrl: null as string | null });
|
||||
let mockUseHealthReadinessImpl = () => ({ data: null as any });
|
||||
let mockGetLocalStorageItemImpl = () => null as string | null;
|
||||
let mockUseDisableShowPromptsImpl = () => false;
|
||||
|
||||
vi.mock("@/contexts/ThemeContext", () => ({
|
||||
useTheme: () => mockUseThemeImpl(),
|
||||
|
|
@ -25,7 +26,12 @@ vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness", () => ({
|
|||
useHealthReadiness: () => mockUseHealthReadinessImpl(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useDisableShowPrompts", () => ({
|
||||
useDisableShowPrompts: () => mockUseDisableShowPromptsImpl(),
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/localStorageUtils", () => ({
|
||||
LOCAL_STORAGE_EVENT: "local-storage-change",
|
||||
getLocalStorageItem: () => mockGetLocalStorageItemImpl(),
|
||||
setLocalStorageItem: vi.fn(),
|
||||
removeLocalStorageItem: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness";
|
||||
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { clearTokenCookies } from "@/utils/cookieUtils";
|
||||
|
|
@ -51,9 +52,9 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
onToggleSidebar,
|
||||
}) => {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
console.log("baseUrl", baseUrl);
|
||||
const [logoutUrl, setLogoutUrl] = useState("");
|
||||
const [disableShowNewBadge, setDisableShowNewBadge] = useState(false);
|
||||
const disableShowPrompts = useDisableShowPrompts();
|
||||
const { logoUrl } = useTheme();
|
||||
const { data: healthData } = useHealthReadiness();
|
||||
const version = healthData?.litellm_version;
|
||||
|
|
@ -152,6 +153,27 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
aria-label="Toggle hide new feature indicators"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center text-sm pt-2 mt-2 border-t border-gray-100"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span className="text-gray-500 text-xs">Hide All Prompts</span>
|
||||
<Switch
|
||||
className="ml-auto"
|
||||
size="small"
|
||||
checked={disableShowPrompts}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
setLocalStorageItem("disableShowPrompts", "true");
|
||||
emitLocalStorageChange("disableShowPrompts");
|
||||
} else {
|
||||
removeLocalStorageItem("disableShowPrompts");
|
||||
emitLocalStorageChange("disableShowPrompts");
|
||||
}
|
||||
}}
|
||||
aria-label="Toggle hide all prompts"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
101
ui/litellm-dashboard/src/components/survey/NudgePrompt.test.tsx
Normal file
101
ui/litellm-dashboard/src/components/survey/NudgePrompt.test.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { NudgePrompt } from "./NudgePrompt";
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useDisableShowPrompts", () => ({
|
||||
useDisableShowPrompts: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/localStorageUtils", () => ({
|
||||
setLocalStorageItem: vi.fn(),
|
||||
emitLocalStorageChange: vi.fn(),
|
||||
LOCAL_STORAGE_EVENT: "local-storage-change",
|
||||
}));
|
||||
|
||||
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
|
||||
import { emitLocalStorageChange, setLocalStorageItem } from "@/utils/localStorageUtils";
|
||||
|
||||
const mockUseDisableShowPrompts = vi.mocked(useDisableShowPrompts);
|
||||
const mockSetLocalStorageItem = vi.mocked(setLocalStorageItem);
|
||||
const mockEmitLocalStorageChange = vi.mocked(emitLocalStorageChange);
|
||||
|
||||
const defaultProps = {
|
||||
onOpen: vi.fn(),
|
||||
onDismiss: vi.fn(),
|
||||
isVisible: true,
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
buttonText: "Open Modal",
|
||||
icon: MessageSquare,
|
||||
accentColor: "#3b82f6",
|
||||
};
|
||||
|
||||
describe("NudgePrompt", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseDisableShowPrompts.mockReturnValue(false);
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
render(<NudgePrompt {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with all provided props", () => {
|
||||
const { container } = render(<NudgePrompt {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Test Title")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Description")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Open Modal" })).toBeInTheDocument();
|
||||
expect(container.querySelector("svg")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render when isVisible is false", () => {
|
||||
render(<NudgePrompt {...defaultProps} isVisible={false} />);
|
||||
|
||||
expect(screen.queryByText("Test Title")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render when disableShowPrompts is true", () => {
|
||||
mockUseDisableShowPrompts.mockReturnValue(true);
|
||||
|
||||
render(<NudgePrompt {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByText("Test Title")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display progress bar with correct accent color", () => {
|
||||
const { container } = render(<NudgePrompt {...defaultProps} accentColor="#ff0000" />);
|
||||
|
||||
const progressBar = container.querySelector("div[style*='width']");
|
||||
expect(progressBar).toHaveStyle({ backgroundColor: "#ff0000" });
|
||||
});
|
||||
|
||||
it("should reset progress when isVisible becomes false", () => {
|
||||
const { rerender, container } = render(<NudgePrompt {...defaultProps} />);
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
rerender(<NudgePrompt {...defaultProps} isVisible={false} />);
|
||||
|
||||
rerender(<NudgePrompt {...defaultProps} isVisible={true} />);
|
||||
|
||||
const progressBar = container.querySelector("div[style*='width']");
|
||||
expect(progressBar?.getAttribute("style")).toContain("width: 100%");
|
||||
});
|
||||
|
||||
it("should apply custom button style when provided", () => {
|
||||
const buttonStyle = { backgroundColor: "#custom-color" };
|
||||
render(<NudgePrompt {...defaultProps} buttonStyle={buttonStyle} />);
|
||||
|
||||
const openButton = screen.getByRole("button", { name: "Open Modal" });
|
||||
expect(openButton).toHaveStyle(buttonStyle);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { X, LucideIcon } from "lucide-react";
|
||||
import { X, LucideIcon, Check } from "lucide-react";
|
||||
import { Button } from "antd";
|
||||
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
|
||||
import { setLocalStorageItem, emitLocalStorageChange } from "@/utils/localStorageUtils";
|
||||
|
||||
interface NudgePromptProps {
|
||||
onOpen: () => void;
|
||||
|
|
@ -15,6 +17,7 @@ interface NudgePromptProps {
|
|||
}
|
||||
|
||||
const DISMISS_DURATION = 15000; // 15 seconds
|
||||
const CONFIRMATION_DURATION = 5000; // 5 seconds
|
||||
|
||||
export function NudgePrompt({
|
||||
onOpen,
|
||||
|
|
@ -27,11 +30,14 @@ export function NudgePrompt({
|
|||
accentColor,
|
||||
buttonStyle,
|
||||
}: NudgePromptProps) {
|
||||
const disableShowPrompts = useDisableShowPrompts();
|
||||
const [progress, setProgress] = useState(100);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setProgress(100);
|
||||
setShowConfirmation(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -49,13 +55,53 @@ export function NudgePrompt({
|
|||
return () => clearInterval(interval);
|
||||
}, [isVisible]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
useEffect(() => {
|
||||
if (showConfirmation) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowConfirmation(false);
|
||||
onDismiss();
|
||||
}, CONFIRMATION_DURATION);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showConfirmation, onDismiss]);
|
||||
|
||||
const handleDontAskAgain = () => {
|
||||
setLocalStorageItem("disableShowPrompts", "true");
|
||||
emitLocalStorageChange("disableShowPrompts");
|
||||
setShowConfirmation(true);
|
||||
};
|
||||
|
||||
// Show confirmation even if disableShowPrompts is true (since we just set it)
|
||||
if (showConfirmation) {
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 z-40 w-80 bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden transform transition-all duration-300 ease-out ${isVisible ? "translate-y-0 opacity-100 scale-100" : "translate-y-4 opacity-0 scale-95"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<Check className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-gray-700 font-medium">
|
||||
Got it, we will not ask again. Reactivate this at any time in the User Menu.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't show the prompt if disabled (unless we're showing confirmation)
|
||||
if (!isVisible || disableShowPrompts) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 z-40 w-80 bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden transform transition-all duration-300 ease-out ${
|
||||
isVisible ? "translate-y-0 opacity-100 scale-100" : "translate-y-4 opacity-0 scale-95"
|
||||
}`}
|
||||
className={`fixed bottom-6 right-6 z-40 w-80 bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden transform transition-all duration-300 ease-out ${isVisible ? "translate-y-0 opacity-100 scale-100" : "translate-y-4 opacity-0 scale-95"
|
||||
}`}
|
||||
>
|
||||
{/* Progress bar at top showing time remaining */}
|
||||
<div className="h-1 bg-gray-100 w-full">
|
||||
|
|
@ -81,9 +127,20 @@ export function NudgePrompt({
|
|||
|
||||
<p className="text-sm text-gray-600 mb-3">{description}</p>
|
||||
|
||||
<Button type="primary" block onClick={onOpen} style={buttonStyle}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<div className="space-y-2">
|
||||
<Button type="primary" block onClick={onOpen} style={buttonStyle}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
danger
|
||||
block
|
||||
onClick={handleDontAskAgain}
|
||||
className="text-xs"
|
||||
>
|
||||
Don't ask me again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue