From 31855e5cf905284289301e07f81469c462e12c4d Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sat, 17 Jan 2026 17:44:55 -0800 Subject: [PATCH] feat: initial commit, adding in-product nudge for claude code usage helps us talk to more litellm x claude code users --- ui/litellm-dashboard/src/app/page.tsx | 73 +++- .../src/components/networking.tsx | 30 ++ .../src/components/survey/ClaudeCodeModal.tsx | 412 ++++++++++++++++++ .../components/survey/ClaudeCodePrompt.tsx | 26 ++ .../src/components/survey/NudgePrompt.tsx | 91 ++++ .../src/components/survey/SurveyPrompt.tsx | 78 +--- .../src/components/survey/index.tsx | 3 + 7 files changed, 646 insertions(+), 67 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx create mode 100644 ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx create mode 100644 ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index ac019a7a8c5..8a56287e156 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -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 ; } @@ -503,6 +560,18 @@ export default function CreateKeyPage() { onClose={handleSurveyModalClose} onComplete={handleSurveyComplete} /> + + {/* Claude Code Components */} + + )} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 82894dfb0e2..c975877d06c 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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 */ diff --git a/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx b/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx new file mode 100644 index 00000000000..1c0db00340f --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/ClaudeCodeModal.tsx @@ -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({ + 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 = { + model_compatibility: "Model Compatibility", + performance: "Performance & Speed", + debugging: "Debugging & Observability", + cost_tracking: "Cost Tracking", + }; + + const improvementLabels: Record = { + 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 ( +
+

How satisfied are you with using LiteLLM in Claude Code?

+ updateData("satisfaction", e.target.value)} + className="w-full" + > + + {[ + "Very Satisfied", + "Satisfied", + "Neutral", + "Dissatisfied", + "Very Dissatisfied", + ].map((option) => ( + + ))} + + +
+ ); + } + + // Step 2: What features do you value? + if (step === 2) { + return ( +
+

What do you value most about using LiteLLM with Claude Code?

+

Select all that apply.

+
+ {FEATURE_OPTIONS.map((option) => { + const isSelected = data.features.includes(option.id); + return ( +
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" + }`} + > + +
+ {option.label} + {option.description} +
+
+ ); + })} +
+
+ ); + } + + // Step 3: What could be improved? + if (step === 3) { + return ( +
+

What could we improve for Claude Code users?

+

Select all that apply.

+
+ {IMPROVEMENT_OPTIONS.map((option) => { + const isSelected = data.improvements.includes(option.id); + return ( +
+
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" + }`} + > + +
+ {option.label} + {option.description} +
+
+ {/* Show text input if "Other" is selected */} + {option.id === "other" && isSelected && ( + updateData("improvementOther", e.target.value)} + onClick={(e) => e.stopPropagation()} + autoFocus + /> + )} +
+ ); + })} +
+
+ ); + } + + // Step 4: Email (optional) + if (step === 4) { + return ( +
+

Want to discuss further?

+

+ Leave your email and we may reach out to learn more about your Claude Code experience. This is completely optional. +

+ updateData("email", e.target.value)} + autoFocus + /> +

+ We will only use this to follow up on your feedback. No spam, ever. +

+
+ ); + } + + return null; + }; + + const isLastStep = step === 4; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+ + Claude Code Feedback +
+ +
+ + {/* Progress Bar */} + + + {/* Content */} +
{renderStepContent()}
+ + {/* Footer */} +
+
+ Step {step} of {totalSteps} +
+
+ {step > 1 && ( + + )} + +
+
+
+
+ ); +} + diff --git a/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx b/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx new file mode 100644 index 00000000000..9006575ce0a --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/ClaudeCodePrompt.tsx @@ -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 ( + + ); +} + diff --git a/ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx b/ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx new file mode 100644 index 00000000000..9095c6c21c5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/survey/NudgePrompt.tsx @@ -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 ( +
+ {/* Progress bar at top showing time remaining */} +
+
+
+ +
+
+
+ + {title} +
+ +
+ +

{description}

+ + +
+
+ ); +} + diff --git a/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx b/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx index 69a886bddeb..a41acb265a7 100644 --- a/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx +++ b/ui/litellm-dashboard/src/components/survey/SurveyPrompt.tsx @@ -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 ( -
- {/* Progress bar at top showing time remaining */} -
-
-
- -
-
-
- - Quick feedback -
- -
- -

- Help us improve LiteLLM! Share your experience in 5 quick questions. -

- - -
-
+ ); } diff --git a/ui/litellm-dashboard/src/components/survey/index.tsx b/ui/litellm-dashboard/src/components/survey/index.tsx index 7c36419980c..fde05084b7e 100644 --- a/ui/litellm-dashboard/src/components/survey/index.tsx +++ b/ui/litellm-dashboard/src/components/survey/index.tsx @@ -1,3 +1,6 @@ export { SurveyPrompt } from "./SurveyPrompt"; export { SurveyModal } from "./SurveyModal"; +export { ClaudeCodePrompt } from "./ClaudeCodePrompt"; +export { ClaudeCodeModal } from "./ClaudeCodeModal"; +export { NudgePrompt } from "./NudgePrompt";