mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
refactor(ui): migrate router settings and shared badges off antd and tremor
Replaces Ant Design and Tremor in the fallbacks views, the router general settings panel, and the two shared banner and badge components. - Tremor Card, Table and Icon become the ui/card, ui/table and lucide equivalents, reproducing Tremor's icon box so click targets keep their size - antd Alert becomes a composed role="alert" region, since the shadcn CLI's alert pulls in class-variance-authority, which this repo does not have - antd InputNumber becomes a native number input, and Switch onChange becomes onCheckedChange - shadcn TableCell ships whitespace-nowrap where Tremor's did not, so cells holding model names and setting descriptions get whitespace-normal back - adds a DeprecationBanner test covering naming, the link, and dismissal, proven against the antd version first and mutation checked - drops the eslint suppressions these files no longer need
This commit is contained in:
parent
423b791ee0
commit
3465ba4914
8 changed files with 306 additions and 213 deletions
|
|
@ -1405,9 +1405,6 @@
|
|||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 2
|
||||
}
|
||||
|
|
@ -1761,11 +1758,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/BetaBadge.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/CloudZeroCostTracking/CloudZeroCreateModal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
@ -1789,11 +1781,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/DeprecationBanner.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/EntityUsageExport/ExportSummary.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
@ -1995,11 +1982,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
|
|
@ -2017,9 +1999,6 @@
|
|||
}
|
||||
},
|
||||
"src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"prefer-const": {
|
||||
"count": 2
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ const settingsRow = async (fieldName: string) => {
|
|||
return row as HTMLElement;
|
||||
};
|
||||
|
||||
const numericValueIn = (row: HTMLElement) => Number((within(row).getByRole("spinbutton") as HTMLInputElement).value);
|
||||
|
||||
describe("GeneralSettings General tab", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getGeneralSettingsCall).mockResolvedValue([...SETTINGS_FIXTURE.map((s) => ({ ...s }))]);
|
||||
|
|
@ -87,7 +89,7 @@ describe("GeneralSettings General tab", () => {
|
|||
|
||||
await user.click(screen.getByText("General"));
|
||||
const row = await settingsRow("max_ui_session_budget");
|
||||
expect(within(row).getByRole("spinbutton")).toHaveValue("7.50");
|
||||
expect(numericValueIn(row)).toBe(7.5);
|
||||
|
||||
const actionCell = row.querySelectorAll("td")[3];
|
||||
const resetIcon = actionCell.querySelector("svg");
|
||||
|
|
@ -95,7 +97,7 @@ describe("GeneralSettings General tab", () => {
|
|||
await user.click(resetIcon as unknown as Element);
|
||||
|
||||
expect(deleteConfigFieldSetting).toHaveBeenCalledWith("token", "max_ui_session_budget");
|
||||
expect(within(row).getByRole("spinbutton")).toHaveValue("1.00");
|
||||
expect(numericValueIn(row)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Title,
|
||||
Text,
|
||||
Button,
|
||||
Icon,
|
||||
Switch,
|
||||
} from "@tremor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking";
|
||||
import { InputNumber, Select as AntdSelect } from "antd";
|
||||
import { TrashIcon } from "@heroicons/react/outline";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { StatusBadge } from "@/components/shared/table_cells";
|
||||
|
||||
import RouterSettings from "@/components/router_settings";
|
||||
|
|
@ -44,16 +36,22 @@ export interface generalSettingsItem {
|
|||
field_default_value?: any;
|
||||
}
|
||||
|
||||
const NUMERIC_INPUT_WIDTH = "w-36";
|
||||
|
||||
const toNumericValue = (raw: string): number | null => (raw === "" ? null : Number(raw));
|
||||
|
||||
const SettingValueEditor: React.FC<{
|
||||
setting: generalSettingsItem;
|
||||
onChange: (fieldName: string, newValue: any) => void;
|
||||
}> = ({ setting, onChange }) => {
|
||||
if (setting.field_type === "Integer") {
|
||||
return (
|
||||
<InputNumber
|
||||
<Input
|
||||
type="number"
|
||||
step={1}
|
||||
value={setting.field_value}
|
||||
onChange={(newValue) => onChange(setting.field_name, newValue)}
|
||||
className={NUMERIC_INPUT_WIDTH}
|
||||
value={setting.field_value ?? ""}
|
||||
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -61,42 +59,55 @@ const SettingValueEditor: React.FC<{
|
|||
return (
|
||||
<Switch
|
||||
checked={setting.field_value === true || setting.field_value === "true"}
|
||||
onChange={(checked) => onChange(setting.field_name, checked)}
|
||||
onCheckedChange={(checked) => onChange(setting.field_name, checked)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (setting.field_type === "Float") {
|
||||
return (
|
||||
<InputNumber
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={setting.field_value}
|
||||
onChange={(newValue) => onChange(setting.field_name, newValue)}
|
||||
className={NUMERIC_INPUT_WIDTH}
|
||||
value={setting.field_value ?? ""}
|
||||
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (setting.field_type === "Dollar") {
|
||||
return (
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
step={0.25}
|
||||
prefix="$"
|
||||
value={setting.field_value}
|
||||
onChange={(newValue) => onChange(setting.field_name, newValue)}
|
||||
/>
|
||||
<InputGroup className={NUMERIC_INPUT_WIDTH}>
|
||||
<InputGroupAddon>$</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
type="number"
|
||||
min={0.01}
|
||||
step={0.25}
|
||||
value={setting.field_value ?? ""}
|
||||
onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
if (setting.field_type === "Select") {
|
||||
return (
|
||||
<AntdSelect
|
||||
allowClear
|
||||
style={{ minWidth: "8rem" }}
|
||||
placeholder="Default"
|
||||
value={setting.field_value || undefined}
|
||||
options={(setting.field_options ?? []).map((option) => ({ label: option, value: option }))}
|
||||
onChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
|
||||
/>
|
||||
<Select
|
||||
value={setting.field_value || null}
|
||||
onValueChange={(newValue) => onChange(setting.field_name, newValue ?? "")}
|
||||
>
|
||||
<SelectTrigger className="min-w-32">
|
||||
<SelectValue placeholder="Default" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={null}>Default</SelectItem>
|
||||
{(setting.field_options ?? []).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
@ -131,33 +142,43 @@ export const PromptCachingPanel: React.FC<{
|
|||
|
||||
return (
|
||||
<Card>
|
||||
<Title>Prompt Caching</Title>
|
||||
<CardContent>
|
||||
<CardTitle>Prompt Caching</CardTitle>
|
||||
|
||||
<div className="mt-6 flex items-start justify-between gap-8">
|
||||
<div className="max-w-2xl">
|
||||
<Text className="font-medium">Automatic Anthropic prompt caching</Text>
|
||||
<p className="mt-1 text-xs text-gray-500">{enableSetting.field_description}</p>
|
||||
</div>
|
||||
<Switch checked={enabled} onChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
|
||||
</div>
|
||||
|
||||
{ttlSetting && (
|
||||
<div className="mt-6 flex items-start justify-between gap-8">
|
||||
<div className="max-w-2xl">
|
||||
<Text className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</Text>
|
||||
<p className="mt-1 text-xs text-gray-500">{ttlSetting.field_description}</p>
|
||||
<div className="min-w-0 max-w-2xl">
|
||||
<p className="font-medium">Automatic Anthropic prompt caching</p>
|
||||
<p className="mt-1 break-words text-xs text-gray-500">{enableSetting.field_description}</p>
|
||||
</div>
|
||||
<AntdSelect
|
||||
allowClear
|
||||
disabled={!enabled}
|
||||
style={{ minWidth: "10rem" }}
|
||||
placeholder="5m (default)"
|
||||
value={ttlSetting.field_value || undefined}
|
||||
options={(ttlSetting.field_options ?? []).map((option) => ({ label: option, value: option }))}
|
||||
onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
|
||||
/>
|
||||
<Switch checked={enabled} onCheckedChange={(checked) => persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ttlSetting && (
|
||||
<div className="mt-6 flex items-start justify-between gap-8">
|
||||
<div className="min-w-0 max-w-2xl">
|
||||
<p className={`font-medium ${enabled ? "" : "text-gray-400"}`}>Cache lifetime (TTL)</p>
|
||||
<p className="mt-1 break-words text-xs text-gray-500">{ttlSetting.field_description}</p>
|
||||
</div>
|
||||
<Select
|
||||
disabled={!enabled}
|
||||
value={ttlSetting.field_value || null}
|
||||
onValueChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")}
|
||||
>
|
||||
<SelectTrigger className="min-w-40">
|
||||
<SelectValue placeholder="5m (default)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={null}>5m (default)</SelectItem>
|
||||
{(ttlSetting.field_options ?? []).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -254,55 +275,60 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
|
|||
</TabsContent>
|
||||
<TabsContent value="general" className="px-8 py-6">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Setting</TableHeaderCell>
|
||||
<TableHeaderCell>Value</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Action</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{generalSettings
|
||||
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
|
||||
.map((value, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Text>{value.field_name}</Text>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.65rem",
|
||||
color: "#808080",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
className="mt-1"
|
||||
>
|
||||
{value.field_description}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SettingValueEditor setting={value} onChange={handleInputChange} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{value.stored_in_db == true ? (
|
||||
<StatusBadge tone="success" label="In DB" />
|
||||
) : value.stored_in_db == false ? (
|
||||
<StatusBadge tone="neutral" label="In Config" />
|
||||
) : (
|
||||
<StatusBadge tone="neutral" label="Not Set" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
|
||||
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name)}>
|
||||
Reset
|
||||
</Icon>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Setting</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{generalSettings
|
||||
.filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB)
|
||||
.map((value, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="whitespace-normal">
|
||||
<p className="break-words">{value.field_name}</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.65rem",
|
||||
color: "#808080",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
className="mt-1 break-words"
|
||||
>
|
||||
{value.field_description}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SettingValueEditor setting={value} onChange={handleInputChange} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{value.stored_in_db == true ? (
|
||||
<StatusBadge tone="success" label="In DB" />
|
||||
) : value.stored_in_db == false ? (
|
||||
<StatusBadge tone="neutral" label="In Config" />
|
||||
) : (
|
||||
<StatusBadge tone="neutral" label="Not Set" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => handleUpdateField(value.field_name)}>Update</Button>
|
||||
<span
|
||||
onClick={() => handleResetField(value.field_name)}
|
||||
className="inline-flex shrink-0 cursor-pointer items-center justify-center px-1.5 py-1.5 text-red-500"
|
||||
>
|
||||
<Trash2 className="h-5 w-5 shrink-0" />
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Badge } from "antd";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge";
|
||||
|
||||
export default function BetaBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) {
|
||||
|
|
@ -8,11 +8,14 @@ export default function BetaBadge({ children, dot = false }: { children?: React.
|
|||
return children ? <>{children}</> : null;
|
||||
}
|
||||
|
||||
const badge = dot ? <Badge className="size-1.5 p-0" /> : <Badge>Beta</Badge>;
|
||||
|
||||
return children ? (
|
||||
<Badge color="blue" count={dot ? undefined : "Beta"} dot={dot}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{children}
|
||||
</Badge>
|
||||
{badge}
|
||||
</span>
|
||||
) : (
|
||||
<Badge color="blue" count={dot ? undefined : "Beta"} dot={dot} />
|
||||
badge
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DeprecationBanner } from "./DeprecationBanner";
|
||||
|
||||
describe("DeprecationBanner", () => {
|
||||
it("names the deprecated feature in the heading and the body", () => {
|
||||
render(<DeprecationBanner featureName="Memory" />);
|
||||
|
||||
expect(screen.getByText("Memory is on a draft deprecation list")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Memory is one of several experimental features/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("states the target removal date and that the list is not final", () => {
|
||||
render(<DeprecationBanner featureName="Memory" />);
|
||||
|
||||
expect(screen.getByText(/as early as September 1, 2026/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/This list is a draft and is not final/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("links to the deprecation discussion in a new tab without leaking the opener", () => {
|
||||
render(<DeprecationBanner featureName="Memory" />);
|
||||
|
||||
const link = screen.getByRole("link", { name: "deprecation discussion" });
|
||||
expect(link).toHaveAttribute("href", "https://github.com/BerriAI/litellm/discussions/32090");
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("exposes a named close control", () => {
|
||||
render(<DeprecationBanner featureName="Memory" />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the banner once the close control is used", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DeprecationBanner featureName="Memory" />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /close/i }));
|
||||
|
||||
expect(screen.queryByText("Memory is on a draft deprecation list")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Alert } from "antd";
|
||||
import { Info, X } from "lucide-react";
|
||||
|
||||
const DEPRECATION_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/32090";
|
||||
const DEPRECATION_TARGET_DATE = "September 1, 2026";
|
||||
|
|
@ -11,21 +11,42 @@ interface DeprecationBannerProps {
|
|||
featureName: string;
|
||||
}
|
||||
|
||||
export const DeprecationBanner: React.FC<DeprecationBannerProps> = ({ featureName }) => (
|
||||
<Alert
|
||||
message={`${featureName} is on a draft deprecation list`}
|
||||
description={
|
||||
<>
|
||||
{`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `}
|
||||
<Link href={DEPRECATION_DISCUSSION_URL} target="_blank" rel="noopener noreferrer">
|
||||
deprecation discussion
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
closable
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
);
|
||||
export const DeprecationBanner: React.FC<DeprecationBannerProps> = ({ featureName }) => {
|
||||
const [isClosed, setIsClosed] = useState(false);
|
||||
|
||||
if (isClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="mb-4 flex items-start gap-3 rounded-lg border border-border bg-muted/50 px-4 py-3 text-sm"
|
||||
>
|
||||
<Info className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">{`${featureName} is on a draft deprecation list`}</p>
|
||||
<p className="mt-1 break-words text-muted-foreground">
|
||||
{`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `}
|
||||
<Link
|
||||
href={DEPRECATION_DISCUSSION_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
deprecation discussion
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
onClick={() => setIsClosed(true)}
|
||||
className="shrink-0 rounded-md p-0.5 text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
* Reuses FallbackGroupConfig with the primary model locked
|
||||
*/
|
||||
|
||||
import { Button } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { LoaderCircle, Pencil } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { fetchAvailableModels } from "@/components/llm_calls/fetch_models";
|
||||
import NotificationManager from "../../../molecules/notifications_manager";
|
||||
|
|
@ -88,16 +88,11 @@ export default function EditFallbacks({
|
|||
disablePrimaryModel
|
||||
/>
|
||||
<div className="flex items-center justify-end space-x-3 pt-6 mt-6 border-t border-gray-100">
|
||||
<Button type="default" onClick={onClose} disabled={isSaving}>
|
||||
<Button variant="outline" onClick={onClose} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Pencil className="w-4 h-4" />}
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || group.fallbackModels.length === 0}
|
||||
loading={isSaving}
|
||||
>
|
||||
<Button onClick={handleSave} disabled={isSaving || group.fallbackModels.length === 0}>
|
||||
{isSaving ? <LoaderCircle className="w-4 h-4 animate-spin" /> : <Pencil className="w-4 h-4" />}
|
||||
{isSaving ? "Saving Changes..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
|
||||
import { ArrowRightIcon, PencilAltIcon, PlayIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
|
||||
import { Tooltip, Typography } from "antd";
|
||||
import { ArrowRight, Pencil, Play, Trash2 } from "lucide-react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import openai from "openai";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DeleteResourceModal from "../../../common_components/DeleteResourceModal";
|
||||
|
|
@ -18,12 +18,14 @@ type Fallbacks = FallbackEntry[];
|
|||
const modelCardClass =
|
||||
"inline-flex items-center gap-2 px-2.5 py-1 rounded-md border border-gray-200 bg-gray-50 text-sm font-medium text-gray-800 shrink-0";
|
||||
|
||||
const iconWrapperClass = "inline-flex shrink-0 items-center justify-center px-1.5 py-1.5";
|
||||
|
||||
function renderModelNameCell(modelName: string, getProviderFromModel?: (modelName: string) => string): React.ReactNode {
|
||||
const provider = getProviderFromModel?.(modelName) ?? modelName;
|
||||
return (
|
||||
<span className={modelCardClass}>
|
||||
<ProviderLogo provider={provider} className="w-4 h-4 shrink-0" />
|
||||
<span>{modelName}</span>
|
||||
<span className="break-words">{modelName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -41,19 +43,23 @@ function renderFallbacksChain(
|
|||
return (
|
||||
<span className={modelCardClass}>
|
||||
<ProviderLogo provider={provider} className="w-4 h-4 shrink-0" />
|
||||
<span>{modelName}</span>
|
||||
<span className="break-words">{modelName}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<span className="grid grid-cols-[auto_1fr] items-start gap-x-2 w-full min-w-0">
|
||||
<span className="inline-flex items-center justify-center w-8 h-8 shrink-0 self-start text-blue-600" aria-hidden>
|
||||
<ArrowRightIcon className="w-5 h-5 stroke-[2.5]" />
|
||||
<ArrowRight className="w-5 h-5 stroke-[2.5]" />
|
||||
</span>
|
||||
<span className="flex flex-wrap items-start gap-1 min-w-0">
|
||||
{list.map((model, i) => (
|
||||
<React.Fragment key={model}>
|
||||
{i > 0 && <Icon icon={ArrowRightIcon} size="xs" className="shrink-0 text-gray-400" />}
|
||||
{i > 0 && (
|
||||
<span className={`${iconWrapperClass} text-gray-400`}>
|
||||
<ArrowRight className="h-3 w-3 shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
<ChainCard modelName={model} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
@ -248,7 +254,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
|
|||
const canModify = isProxyAdminRole(userRole ?? "");
|
||||
|
||||
return (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
{canModify && (
|
||||
<AddFallbacks
|
||||
accessToken={accessToken || ""}
|
||||
|
|
@ -258,62 +264,79 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
|
|||
)}
|
||||
{!hasFallbacks ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-6 text-center">
|
||||
<Typography.Text type="secondary">
|
||||
<span className="text-muted-foreground">
|
||||
No fallbacks configured. Add fallbacks to automatically try another model when the primary fails.
|
||||
</Typography.Text>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Model Name</TableHeaderCell>
|
||||
<TableHeaderCell>Fallbacks</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
<TableHead>Model Name</TableHead>
|
||||
<TableHead>Fallbacks</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{routerSettings["fallbacks"].map((item: FallbackEntry, index: number) =>
|
||||
Object.entries(item).map(([key, value]) => (
|
||||
<TableRow key={index.toString() + key}>
|
||||
<TableCell className="align-top">{renderModelNameCell(key, getProviderFromModel)}</TableCell>
|
||||
<TableCell className="align-top">
|
||||
<TableCell className="align-top whitespace-normal">
|
||||
{renderModelNameCell(key, getProviderFromModel)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top whitespace-normal">
|
||||
{renderFallbacksChain(key, Array.isArray(value) ? value : [], getProviderFromModel)}
|
||||
</TableCell>
|
||||
<TableCell className="align-top">
|
||||
{canModify && (
|
||||
<>
|
||||
<Tooltip title="Test fallback">
|
||||
<Icon
|
||||
icon={PlayIcon}
|
||||
size="sm"
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
|
||||
className="cursor-pointer hover:text-blue-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Edit fallback">
|
||||
<span
|
||||
data-testid="edit-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleEditClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)}
|
||||
className="cursor-pointer inline-flex"
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
onClick={() => testFallbackModelResponse(Object.keys(item)[0], accessToken || "")}
|
||||
className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon icon={PencilAltIcon} size="sm" className="hover:text-blue-600" />
|
||||
</span>
|
||||
<Play className="h-5 w-5 shrink-0" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Test fallback</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete fallback">
|
||||
<span
|
||||
data-testid="delete-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleDeleteClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
|
||||
className="cursor-pointer inline-flex"
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
data-testid="edit-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleEditClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)}
|
||||
className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon icon={TrashIcon} size="sm" className="hover:text-red-600" />
|
||||
</span>
|
||||
<Pencil className="h-5 w-5 shrink-0" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Edit fallback</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span
|
||||
data-testid="delete-fallback-button"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleDeleteClick(item)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)}
|
||||
className={`${iconWrapperClass} cursor-pointer hover:text-red-600`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-5 w-5 shrink-0" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete fallback</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -350,7 +373,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
|
|||
onOk={handleDeleteConfirm}
|
||||
confirmLoading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue