mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
feat: initial commit, adding in-product nudge for claude code usage
helps us talk to more litellm x claude code users
This commit is contained in:
parent
e2593bdda2
commit
31855e5cf9
7 changed files with 646 additions and 67 deletions
|
|
@ -17,7 +17,7 @@ import { Team } from "@/components/key_team_helpers/key_list";
|
|||
import { MCPServers } from "@/components/mcp_tools";
|
||||
import ModelHubTable from "@/components/AIHub/ModelHubTable";
|
||||
import Navbar from "@/components/navbar";
|
||||
import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName, getInProductNudgesCall } from "@/components/networking";
|
||||
import NewUsagePage from "@/components/UsagePage/components/UsagePageView";
|
||||
import OldTeams from "@/components/OldTeams";
|
||||
import { fetchUserModels } from "@/components/organisms/create_key_button";
|
||||
|
|
@ -27,7 +27,7 @@ import PromptsPanel from "@/components/prompts";
|
|||
import PublicModelHub from "@/components/public_model_hub";
|
||||
import { SearchTools } from "@/components/search_tools";
|
||||
import Settings from "@/components/settings";
|
||||
import { SurveyPrompt, SurveyModal } from "@/components/survey";
|
||||
import { SurveyPrompt, SurveyModal, ClaudeCodePrompt, ClaudeCodeModal } from "@/components/survey";
|
||||
import TagManagement from "@/components/tag_management";
|
||||
import TransformRequestPanel from "@/components/transform_request";
|
||||
import UIThemeSettings from "@/components/ui_theme_settings";
|
||||
|
|
@ -124,6 +124,11 @@ export default function CreateKeyPage() {
|
|||
const [showSurveyPrompt, setShowSurveyPrompt] = useState(true);
|
||||
const [showSurveyModal, setShowSurveyModal] = useState(false);
|
||||
|
||||
// Claude Code feedback state
|
||||
const [isClaudeCode, setIsClaudeCode] = useState(false);
|
||||
const [showClaudeCodePrompt, setShowClaudeCodePrompt] = useState(false);
|
||||
const [showClaudeCodeModal, setShowClaudeCodeModal] = useState(false);
|
||||
|
||||
const invitation_id = searchParams.get("invitation_id");
|
||||
|
||||
// Get page from URL, default to 'api-keys' if not present
|
||||
|
|
@ -267,6 +272,29 @@ export default function CreateKeyPage() {
|
|||
}
|
||||
}, [accessToken, userID, userRole]);
|
||||
|
||||
// Fetch in-product nudges configuration from backend
|
||||
useEffect(() => {
|
||||
if (accessToken && token) {
|
||||
(async () => {
|
||||
try {
|
||||
const nudgesConfig = await getInProductNudgesCall(accessToken);
|
||||
const isUsingClaudeCode = nudgesConfig?.is_claude_code_enabled || false;
|
||||
setIsClaudeCode(isUsingClaudeCode);
|
||||
|
||||
// Show Claude Code prompt on login if enabled
|
||||
if (isUsingClaudeCode) {
|
||||
setShowClaudeCodePrompt(true);
|
||||
// Don't show the regular survey prompt if showing Claude Code prompt
|
||||
setShowSurveyPrompt(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch in-product nudges:", error);
|
||||
// Silently fail and don't show Claude Code nudge
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [accessToken, token]);
|
||||
|
||||
// Auto-dismiss survey prompt after 15 seconds
|
||||
useEffect(() => {
|
||||
if (showSurveyPrompt && !showSurveyModal) {
|
||||
|
|
@ -277,6 +305,16 @@ export default function CreateKeyPage() {
|
|||
}
|
||||
}, [showSurveyPrompt, showSurveyModal]);
|
||||
|
||||
// Auto-dismiss Claude Code prompt after 15 seconds
|
||||
useEffect(() => {
|
||||
if (showClaudeCodePrompt && !showClaudeCodeModal) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowClaudeCodePrompt(false);
|
||||
}, 15000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [showClaudeCodePrompt, showClaudeCodeModal]);
|
||||
|
||||
const handleOpenSurvey = () => {
|
||||
setShowSurveyPrompt(false);
|
||||
setShowSurveyModal(true);
|
||||
|
|
@ -296,6 +334,25 @@ export default function CreateKeyPage() {
|
|||
setShowSurveyPrompt(true);
|
||||
};
|
||||
|
||||
const handleOpenClaudeCode = () => {
|
||||
setShowClaudeCodePrompt(false);
|
||||
setShowClaudeCodeModal(true);
|
||||
};
|
||||
|
||||
const handleDismissClaudeCodePrompt = () => {
|
||||
setShowClaudeCodePrompt(false);
|
||||
};
|
||||
|
||||
const handleClaudeCodeComplete = () => {
|
||||
setShowClaudeCodeModal(false);
|
||||
};
|
||||
|
||||
const handleClaudeCodeModalClose = () => {
|
||||
// If they close the modal without completing, show the prompt again
|
||||
setShowClaudeCodeModal(false);
|
||||
setShowClaudeCodePrompt(true);
|
||||
};
|
||||
|
||||
if (authLoading || redirectToLogin) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
|
@ -503,6 +560,18 @@ export default function CreateKeyPage() {
|
|||
onClose={handleSurveyModalClose}
|
||||
onComplete={handleSurveyComplete}
|
||||
/>
|
||||
|
||||
{/* Claude Code Components */}
|
||||
<ClaudeCodePrompt
|
||||
isVisible={showClaudeCodePrompt}
|
||||
onOpen={handleOpenClaudeCode}
|
||||
onDismiss={handleDismissClaudeCodePrompt}
|
||||
/>
|
||||
<ClaudeCodeModal
|
||||
isOpen={showClaudeCodeModal}
|
||||
onClose={handleClaudeCodeModalClose}
|
||||
onComplete={handleClaudeCodeComplete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,36 @@ export const getCallbackConfigsCall = async (accessToken: string) => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getInProductNudgesCall = async (accessToken: string) => {
|
||||
/**
|
||||
* Get in-product nudges configuration.
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/in_product_nudges` : `/in_product_nudges`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get in-product nudges:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Helper file for calls being made to proxy
|
||||
*/
|
||||
|
|
|
|||
412
ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx
Normal file
412
ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
import React, { useState } from "react";
|
||||
import { X, Code, ArrowRight, ArrowLeft } from "lucide-react";
|
||||
import { Button, Input, Radio, Space, Progress, Checkbox } from "antd";
|
||||
|
||||
interface ClaudeCodeModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
const FEATURE_OPTIONS = [
|
||||
{
|
||||
id: "model_compatibility",
|
||||
label: "Model Compatibility",
|
||||
description: "Works seamlessly with Claude models through LiteLLM",
|
||||
},
|
||||
{
|
||||
id: "performance",
|
||||
label: "Performance & Speed",
|
||||
description: "Fast response times and reliable connections",
|
||||
},
|
||||
{
|
||||
id: "debugging",
|
||||
label: "Debugging & Observability",
|
||||
description: "Easy to debug and monitor API calls",
|
||||
},
|
||||
{
|
||||
id: "cost_tracking",
|
||||
label: "Cost Tracking",
|
||||
description: "Helpful for tracking usage and costs",
|
||||
},
|
||||
{
|
||||
id: "other",
|
||||
label: "Other",
|
||||
description: "Something else not listed above",
|
||||
},
|
||||
];
|
||||
|
||||
const IMPROVEMENT_OPTIONS = [
|
||||
{
|
||||
id: "documentation",
|
||||
label: "Documentation",
|
||||
description: "Better guides for Claude Code integration",
|
||||
},
|
||||
{
|
||||
id: "error_messages",
|
||||
label: "Error Messages",
|
||||
description: "Clearer error messages and debugging info",
|
||||
},
|
||||
{
|
||||
id: "performance_optimization",
|
||||
label: "Performance",
|
||||
description: "Faster response times or lower latency",
|
||||
},
|
||||
{
|
||||
id: "additional_features",
|
||||
label: "Additional Features",
|
||||
description: "More features specific to Claude Code workflows",
|
||||
},
|
||||
{
|
||||
id: "nothing",
|
||||
label: "Nothing - It's great!",
|
||||
description: "I'm satisfied with the current experience",
|
||||
},
|
||||
{
|
||||
id: "other",
|
||||
label: "Other",
|
||||
description: "Something else not listed above",
|
||||
},
|
||||
];
|
||||
|
||||
type SurveyData = {
|
||||
satisfaction: string;
|
||||
features: string[];
|
||||
improvements: string[];
|
||||
improvementOther: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export function ClaudeCodeModal({ isOpen, onClose, onComplete }: ClaudeCodeModalProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [data, setData] = useState<SurveyData>({
|
||||
satisfaction: "",
|
||||
features: [],
|
||||
improvements: [],
|
||||
improvementOther: "",
|
||||
email: "",
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const totalSteps = 4;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleNext = () => {
|
||||
if (step < 4) {
|
||||
setStep(step + 1);
|
||||
} else {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setStep(step - 1);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Map IDs to readable labels
|
||||
const featureLabels: Record<string, string> = {
|
||||
model_compatibility: "Model Compatibility",
|
||||
performance: "Performance & Speed",
|
||||
debugging: "Debugging & Observability",
|
||||
cost_tracking: "Cost Tracking",
|
||||
};
|
||||
|
||||
const improvementLabels: Record<string, string> = {
|
||||
documentation: "Documentation",
|
||||
error_messages: "Error Messages",
|
||||
performance_optimization: "Performance",
|
||||
additional_features: "Additional Features",
|
||||
nothing: "Nothing - It's great!",
|
||||
};
|
||||
|
||||
const readableFeatures = data.features.map((f) => {
|
||||
if (f === "other") return "Other";
|
||||
return featureLabels[f] || f;
|
||||
});
|
||||
|
||||
const readableImprovements = data.improvements.map((i) => {
|
||||
if (i === "other" && data.improvementOther) {
|
||||
return `Other: ${data.improvementOther}`;
|
||||
}
|
||||
return improvementLabels[i] || i;
|
||||
});
|
||||
|
||||
await fetch("https://hooks.zapier.com/hooks/catch/16331268/ugms6w0/", {
|
||||
method: "POST",
|
||||
mode: "no-cors",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
surveyType: "Claude Code Feedback",
|
||||
satisfaction: data.satisfaction,
|
||||
features: readableFeatures.join(", "),
|
||||
improvements: readableImprovements.join(", "),
|
||||
improvementOther: data.improvementOther || null,
|
||||
email: data.email || null,
|
||||
submittedAt: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail - don't block the user experience
|
||||
console.error("Failed to submit Claude Code feedback:", error);
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
onComplete();
|
||||
};
|
||||
|
||||
const updateData = (key: keyof SurveyData, value: string | string[]) => {
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleFeature = (featureId: string) => {
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
features: prev.features.includes(featureId)
|
||||
? prev.features.filter((f) => f !== featureId)
|
||||
: [...prev.features, featureId],
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleImprovement = (improvementId: string) => {
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
improvements: prev.improvements.includes(improvementId)
|
||||
? prev.improvements.filter((i) => i !== improvementId)
|
||||
: [...prev.improvements, improvementId],
|
||||
}));
|
||||
};
|
||||
|
||||
const isStepValid = () => {
|
||||
if (step === 1) return data.satisfaction !== "";
|
||||
if (step === 2) return data.features.length > 0;
|
||||
if (step === 3) {
|
||||
// If "other" is selected, require the text field
|
||||
if (data.improvements.includes("other")) {
|
||||
return data.improvements.length > 0 && data.improvementOther.trim().length > 0;
|
||||
}
|
||||
return data.improvements.length > 0;
|
||||
}
|
||||
if (step === 4) return true; // Email is optional
|
||||
return false;
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
// Step 1: Satisfaction rating
|
||||
if (step === 1) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">How satisfied are you with using LiteLLM in Claude Code?</h2>
|
||||
<Radio.Group
|
||||
value={data.satisfaction}
|
||||
onChange={(e) => updateData("satisfaction", e.target.value)}
|
||||
className="w-full"
|
||||
>
|
||||
<Space direction="vertical" className="w-full">
|
||||
{[
|
||||
"Very Satisfied",
|
||||
"Satisfied",
|
||||
"Neutral",
|
||||
"Dissatisfied",
|
||||
"Very Dissatisfied",
|
||||
].map((option) => (
|
||||
<label
|
||||
key={option}
|
||||
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all w-full ${
|
||||
data.satisfaction === option
|
||||
? "border-purple-600 bg-purple-50 ring-1 ring-purple-600"
|
||||
: "border-gray-200 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<Radio value={option}>{option}</Radio>
|
||||
</label>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: What features do you value?
|
||||
if (step === 2) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">What do you value most about using LiteLLM with Claude Code?</h2>
|
||||
<p className="text-gray-500">Select all that apply.</p>
|
||||
<div className="space-y-3">
|
||||
{FEATURE_OPTIONS.map((option) => {
|
||||
const isSelected = data.features.includes(option.id);
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleFeature(option.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggleFeature(option.id);
|
||||
}
|
||||
}}
|
||||
className={`flex items-start p-4 rounded-lg border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? "border-purple-600 bg-purple-50 ring-1 ring-purple-600"
|
||||
: "border-gray-200 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<Checkbox checked={isSelected} className="mt-0.5 pointer-events-none" />
|
||||
<div className="ml-3">
|
||||
<span className="block font-medium text-gray-900">{option.label}</span>
|
||||
<span className="text-sm text-gray-500">{option.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: What could be improved?
|
||||
if (step === 3) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">What could we improve for Claude Code users?</h2>
|
||||
<p className="text-gray-500">Select all that apply.</p>
|
||||
<div className="space-y-3">
|
||||
{IMPROVEMENT_OPTIONS.map((option) => {
|
||||
const isSelected = data.improvements.includes(option.id);
|
||||
return (
|
||||
<div key={option.id}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleImprovement(option.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggleImprovement(option.id);
|
||||
}
|
||||
}}
|
||||
className={`flex items-start p-4 rounded-lg border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? "border-purple-600 bg-purple-50 ring-1 ring-purple-600"
|
||||
: "border-gray-200 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<Checkbox checked={isSelected} className="mt-0.5 pointer-events-none" />
|
||||
<div className="ml-3">
|
||||
<span className="block font-medium text-gray-900">{option.label}</span>
|
||||
<span className="text-sm text-gray-500">{option.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Show text input if "Other" is selected */}
|
||||
{option.id === "other" && isSelected && (
|
||||
<Input
|
||||
className="mt-2 ml-7"
|
||||
placeholder="Please specify..."
|
||||
value={data.improvementOther}
|
||||
onChange={(e) => updateData("improvementOther", e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 4: Email (optional)
|
||||
if (step === 4) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Want to discuss further?</h2>
|
||||
<p className="text-gray-500">
|
||||
Leave your email and we may reach out to learn more about your Claude Code experience. This is completely optional.
|
||||
</p>
|
||||
<Input
|
||||
size="large"
|
||||
type="email"
|
||||
placeholder="your@email.com (optional)"
|
||||
value={data.email}
|
||||
onChange={(e) => updateData("email", e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-gray-400">
|
||||
We will only use this to follow up on your feedback. No spam, ever.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isLastStep = step === 4;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative w-full max-w-lg bg-white rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh] transform transition-all duration-300 ease-out">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
|
||||
<div className="flex items-center gap-2 text-purple-600">
|
||||
<Code className="h-5 w-5" />
|
||||
<span className="font-semibold text-sm tracking-wide uppercase">Claude Code Feedback</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded-full hover:bg-gray-100"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Progress percent={(step / totalSteps) * 100} showInfo={false} strokeColor="#7c3aed" className="m-0" />
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8 flex-1 overflow-y-auto">{renderStepContent()}</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 font-medium">
|
||||
Step {step} of {totalSteps}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{step > 1 && (
|
||||
<Button onClick={handleBack} disabled={isSubmitting} icon={<ArrowLeft className="h-4 w-4" />}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={!isStepValid() || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
className="min-w-[100px]"
|
||||
style={{ backgroundColor: '#7c3aed', borderColor: '#7c3aed' }}
|
||||
>
|
||||
{isLastStep ? "Submit" : "Next"}
|
||||
{!isLastStep && <ArrowRight className="ml-2 h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
import { Code } from "lucide-react";
|
||||
import { NudgePrompt } from "./NudgePrompt";
|
||||
|
||||
interface ClaudeCodePromptProps {
|
||||
onOpen: () => void;
|
||||
onDismiss: () => void;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export function ClaudeCodePrompt({ onOpen, onDismiss, isVisible }: ClaudeCodePromptProps) {
|
||||
return (
|
||||
<NudgePrompt
|
||||
onOpen={onOpen}
|
||||
onDismiss={onDismiss}
|
||||
isVisible={isVisible}
|
||||
title="Claude Code Feedback"
|
||||
description="Help us improve your Claude Code experience with LiteLLM! Share your feedback in 4 quick questions."
|
||||
buttonText="Share feedback"
|
||||
icon={Code}
|
||||
accentColor="#7c3aed"
|
||||
buttonStyle={{ backgroundColor: '#7c3aed', borderColor: '#7c3aed' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
91
ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx
Normal file
91
ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { X, LucideIcon } from "lucide-react";
|
||||
import { Button } from "antd";
|
||||
|
||||
interface NudgePromptProps {
|
||||
onOpen: () => void;
|
||||
onDismiss: () => void;
|
||||
isVisible: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
icon: LucideIcon;
|
||||
accentColor: string;
|
||||
buttonStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const DISMISS_DURATION = 15000; // 15 seconds
|
||||
|
||||
export function NudgePrompt({
|
||||
onOpen,
|
||||
onDismiss,
|
||||
isVisible,
|
||||
title,
|
||||
description,
|
||||
buttonText,
|
||||
icon: Icon,
|
||||
accentColor,
|
||||
buttonStyle,
|
||||
}: NudgePromptProps) {
|
||||
const [progress, setProgress] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setProgress(100);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const remaining = Math.max(0, 100 - (elapsed / DISMISS_DURATION) * 100);
|
||||
setProgress(remaining);
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isVisible]);
|
||||
|
||||
if (!isVisible) 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"
|
||||
}`}
|
||||
>
|
||||
{/* Progress bar at top showing time remaining */}
|
||||
<div className="h-1 bg-gray-100 w-full">
|
||||
<div
|
||||
className="h-full transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progress}%`, backgroundColor: accentColor }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2" style={{ color: accentColor }}>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="font-semibold text-sm">{title}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors p-0.5 rounded hover:bg-gray-100"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-3">{description}</p>
|
||||
|
||||
<Button type="primary" block onClick={onOpen} style={buttonStyle}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { MessageSquare, X } from "lucide-react";
|
||||
import { Button } from "antd";
|
||||
import React from "react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { NudgePrompt } from "./NudgePrompt";
|
||||
|
||||
interface SurveyPromptProps {
|
||||
onOpen: () => void;
|
||||
|
|
@ -8,70 +8,18 @@ interface SurveyPromptProps {
|
|||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const DISMISS_DURATION = 15000; // 15 seconds
|
||||
|
||||
export function SurveyPrompt({ onOpen, onDismiss, isVisible }: SurveyPromptProps) {
|
||||
const [progress, setProgress] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setProgress(100);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const remaining = Math.max(0, 100 - (elapsed / DISMISS_DURATION) * 100);
|
||||
setProgress(remaining);
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isVisible]);
|
||||
|
||||
if (!isVisible) 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"
|
||||
}`}
|
||||
>
|
||||
{/* Progress bar at top showing time remaining */}
|
||||
<div className="h-1 bg-gray-100 w-full">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-blue-600">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<span className="font-semibold text-sm">Quick feedback</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors p-0.5 rounded hover:bg-gray-100"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Help us improve LiteLLM! Share your experience in 5 quick questions.
|
||||
</p>
|
||||
|
||||
<Button type="primary" block onClick={onOpen}>
|
||||
Share feedback
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<NudgePrompt
|
||||
onOpen={onOpen}
|
||||
onDismiss={onDismiss}
|
||||
isVisible={isVisible}
|
||||
title="Quick feedback"
|
||||
description="Help us improve LiteLLM! Share your experience in 5 quick questions."
|
||||
buttonText="Share feedback"
|
||||
icon={MessageSquare}
|
||||
accentColor="#3b82f6"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
export { SurveyPrompt } from "./SurveyPrompt";
|
||||
export { SurveyModal } from "./SurveyModal";
|
||||
export { ClaudeCodePrompt } from "./ClaudeCodePrompt";
|
||||
export { ClaudeCodeModal } from "./ClaudeCodeModal";
|
||||
export { NudgePrompt } from "./NudgePrompt";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue