feat(ui): common_components \u2014 ModelSelector + PassThroughRoutesSelector + PremiumLoggingSettings

- ModelSelector: antd Select + @tremor TextInput/Text + @ant-design/
  icons RobotOutlined \u2192 shadcn Select + Input + lucide Bot. 'custom'
  option preserved with the inline Input that appears below on
  selection.
- PassThroughRoutesSelector: antd Select mode='tags' (typing an ad-hoc
  value creates it) \u2192 shadcn Popover + chip-list multi-select with
  Enter-to-add-freeform inside the search input.
- PremiumLoggingSettings: @tremor Text \u2192 plain <p>. Emerald/amber
  callout badges preserved as categorical colors; file added to
  .eslintrc.json overrides.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 10:44:09 +00:00
parent 591c2c035a
commit b58fd0b7fc
No known key found for this signature in database
4 changed files with 205 additions and 92 deletions

View file

@ -46,7 +46,8 @@
"src/components/activity_metrics.tsx",
"src/components/skill_hub_table_columns.tsx",
"src/components/common_components/PassThroughSecuritySection.tsx",
"src/components/common_components/DeleteResourceModal.tsx"
"src/components/common_components/DeleteResourceModal.tsx",
"src/components/common_components/PremiumLoggingSettings.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -1,8 +1,18 @@
import React, { useState, useEffect, useRef } from "react";
import { TextInput, Text } from "@tremor/react";
import { Select } from "antd";
import { RobotOutlined } from "@ant-design/icons";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
import React, { useState, useEffect, useRef, useMemo } from "react";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Bot } from "lucide-react";
import { cn } from "@/lib/utils";
import {
fetchAvailableModels,
ModelGroup,
} from "../playground/llm_calls/fetch_models";
interface ModelSelectorProps {
accessToken: string;
@ -28,7 +38,8 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
labelText = "Select Model",
}) => {
const [selectedModel, setSelectedModel] = useState<string | undefined>(value);
const [showCustomModelInput, setShowCustomModelInput] = useState<boolean>(false);
const [showCustomModelInput, setShowCustomModelInput] =
useState<boolean>(false);
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
const customModelTimeout = useRef<NodeJS.Timeout | null>(null);
@ -43,10 +54,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
try {
const uniqueModels = await fetchAvailableModels(accessToken);
console.log("Fetched models for selector:", uniqueModels);
if (uniqueModels.length > 0) {
setModelInfo(uniqueModels);
}
if (uniqueModels.length > 0) setModelInfo(uniqueModels);
} catch (error) {
console.error("Error fetching model info:", error);
}
@ -55,62 +63,62 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({
loadModels();
}, [accessToken]);
const onModelChange = (value: string) => {
if (value === "custom") {
const onModelChange = (v: string) => {
if (v === "custom") {
setShowCustomModelInput(true);
setSelectedModel(undefined);
} else {
setShowCustomModelInput(false);
setSelectedModel(value);
if (onChange) {
onChange(value);
}
setSelectedModel(v);
onChange?.(v);
}
};
const handleCustomModelChange = (value: string) => {
// Using setTimeout to create a simple debounce effect
if (customModelTimeout.current) {
clearTimeout(customModelTimeout.current);
}
const handleCustomModelChange = (v: string) => {
if (customModelTimeout.current) clearTimeout(customModelTimeout.current);
customModelTimeout.current = setTimeout(() => {
setSelectedModel(value);
if (onChange) {
onChange(value);
}
}, 500); // 500ms delay after typing stops
setSelectedModel(v);
onChange?.(v);
}, 500);
};
const uniqueModels = useMemo(
() => Array.from(new Set(modelInfo.map((o) => o.model_group))),
[modelInfo],
);
return (
<div>
{showLabel && (
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
<RobotOutlined className="mr-2" /> {labelText}
</Text>
<div className="font-medium mb-2 text-foreground flex items-center">
<Bot className="mr-2 h-4 w-4" /> {labelText}
</div>
)}
<Select
value={selectedModel}
placeholder={placeholder}
onChange={onModelChange}
options={[
...Array.from(new Set(modelInfo.map((option) => option.model_group))).map((model_group, index) => ({
value: model_group,
label: model_group,
key: index,
})),
{ value: "custom", label: "Enter custom model", key: "custom" },
]}
style={{ width: "100%", ...style }}
showSearch={true}
className={`rounded-md ${className || ""}`}
onValueChange={onModelChange}
disabled={disabled}
/>
>
<SelectTrigger
style={{ width: "100%", ...style }}
className={cn(className)}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{uniqueModels.map((g) => (
<SelectItem key={g} value={g}>
{g}
</SelectItem>
))}
<SelectItem value="custom">Enter custom model</SelectItem>
</SelectContent>
</Select>
{showCustomModelInput && (
<TextInput
<Input
className="mt-2"
placeholder="Enter custom model name"
onValueChange={handleCustomModelChange}
onChange={(e) => handleCustomModelChange(e.target.value)}
disabled={disabled}
/>
)}

View file

@ -1,5 +1,13 @@
import React, { useEffect, useState } from "react";
import { Select } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { getPassThroughEndpointsCall } from "../networking";
interface PassThroughRoutesSelectorProps {
@ -26,8 +34,12 @@ const PassThroughRoutesSelector: React.FC<PassThroughRoutesSelectorProps> = ({
disabled = false,
teamId,
}) => {
const [passThroughRoutes, setPassThroughRoutes] = useState<Array<{ label: string; value: string }>>([]);
const [passThroughRoutes, setPassThroughRoutes] = useState<
Array<{ label: string; value: string }>
>([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
const fetchPassThroughRoutes = async () => {
@ -37,24 +49,21 @@ const PassThroughRoutesSelector: React.FC<PassThroughRoutesSelectorProps> = ({
try {
const response = await getPassThroughEndpointsCall(accessToken, teamId);
if (response.endpoints) {
const routes = response.endpoints.flatMap((endpoint: PassThroughEndpoint) => {
const path = endpoint.path;
const methods = endpoint.methods;
// If methods are specified, create one entry per method
if (methods && methods.length > 0) {
return methods.map((method) => ({
label: `${method} ${path}`,
value: path, // Keep value as path for backward compatibility
}));
}
// If no methods specified, show just the path (all methods supported)
return [{
label: path,
value: path,
}];
});
const routes = response.endpoints.flatMap(
(endpoint: PassThroughEndpoint) => {
const path = endpoint.path;
const methods = endpoint.methods;
if (methods && methods.length > 0) {
return methods.map((method) => ({
label: `${method} ${path}`,
value: path,
}));
}
return [{ label: path, value: path }];
},
);
setPassThroughRoutes(routes);
}
} catch (error) {
@ -67,23 +76,111 @@ const PassThroughRoutesSelector: React.FC<PassThroughRoutesSelectorProps> = ({
fetchPassThroughRoutes();
}, [accessToken, teamId]);
const selected = value ?? [];
const filteredOptions = useMemo(
() =>
passThroughRoutes
.filter((o) => !selected.includes(o.value))
.filter((o) =>
query ? o.label.toLowerCase().includes(query.toLowerCase()) : true,
),
[passThroughRoutes, selected, query],
);
const labelFor = (v: string) =>
passThroughRoutes.find((o) => o.value === v)?.label ?? v;
const addFreeform = () => {
const v = query.trim();
if (!v) return;
if (!selected.includes(v)) onChange([...selected, v]);
setQuery("");
};
return (
<Select
mode="tags"
placeholder={placeholder}
onChange={onChange}
value={value}
loading={loading}
className={className}
allowClear
options={passThroughRoutes}
optionFilterProp="label"
showSearch
style={{ width: "100%" }}
disabled={disabled}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled || loading}
className={cn(
"min-h-9 w-full flex flex-wrap items-center gap-1 rounded-md border border-input bg-background px-2 py-1 text-sm text-left disabled:opacity-50",
className,
)}
>
{selected.length === 0 ? (
<span className="text-muted-foreground px-1">
{loading ? "Loading routes…" : placeholder}
</span>
) : (
selected.map((v) => (
<Badge
key={v}
variant="secondary"
className="gap-1 inline-flex items-center"
>
{labelFor(v)}
<span
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
onChange(selected.filter((s) => s !== v));
}}
className="inline-flex items-center"
aria-label={`Remove ${labelFor(v)}`}
>
<X size={12} />
</span>
</Badge>
))
)}
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-[var(--radix-popover-trigger-width)] p-2"
>
<Input
autoFocus
placeholder="Search or type a custom route…"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addFreeform();
}
}}
className="h-8 mb-2"
/>
<div className="max-h-60 overflow-y-auto">
{filteredOptions.length === 0 ? (
<div className="py-2 px-3 text-sm text-muted-foreground">
{query ? (
<span>
Press Enter to add <span className="font-mono">{query}</span>
</span>
) : (
"No matches"
)}
</div>
) : (
filteredOptions.map((opt) => (
<button
key={opt.value}
type="button"
className="w-full text-left px-2 py-1.5 text-sm rounded hover:bg-accent"
onClick={() => onChange([...selected, opt.value])}
>
{opt.label}
</button>
))
)}
</div>
</PopoverContent>
</Popover>
);
};
export default PassThroughRoutesSelector;

View file

@ -1,9 +1,10 @@
import React from "react";
import { Text } from "@tremor/react";
import LoggingSettings from "../team/LoggingSettings";
interface PremiumLoggingSettingsProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange: (settings: any[]) => void;
premiumUser?: boolean;
disabledCallbacks?: string[];
@ -21,22 +22,28 @@ export function PremiumLoggingSettings({
return (
<div>
<div className="flex flex-wrap gap-2 mb-3">
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-green-50 border border-green-200 text-green-800 text-sm font-medium opacity-50">
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-900 text-emerald-800 dark:text-emerald-200 text-sm font-medium opacity-50">
✨ langfuse-logging
</div>
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-green-50 border border-green-200 text-green-800 text-sm font-medium opacity-50">
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-900 text-emerald-800 dark:text-emerald-200 text-sm font-medium opacity-50">
✨ datadog-logging
</div>
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Text className="text-sm text-yellow-800">
Setting Key/Team logging settings is a LiteLLM Enterprise feature. Global Logging Settings are available for
all free users. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
<div className="p-3 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 rounded-lg">
<p className="text-sm text-amber-800 dark:text-amber-200">
Setting Key/Team logging settings is a LiteLLM Enterprise feature.
Global Logging Settings are available for all free users. Get a
trial key{" "}
<a
href="https://www.litellm.ai/#pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
here
</a>
.
</Text>
</p>
</div>
</div>
);