From 3a537cce4d9ba30b30e23e3346d4fbfe6dc0b1c2 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 14 Aug 2026 04:48:49 -0700 Subject: [PATCH] refactor(ui): move the model hub and model select onto shadcn primitives Rebuilds public_model_hub, MakeSkillPublicForm, ModelSelect and the guardrail LogViewer on the in-repo shadcn layer, so they inherit the dashboard's design tokens instead of styling themselves through Ant Design and Tremor. Public prop signatures are unchanged, so no caller moves. The two teams e2e steps that reached into antd's Select internals now drive the combobox through its test id, role and data-slot instead. --- tests/e2e/ui/tests/proxy-admin/teams.spec.ts | 16 +- ui/litellm-dashboard/eslint-suppressions.json | 14 - .../GuardrailsMonitor/LogViewer.tsx | 25 +- .../ModelSelect/ModelSelect.test.tsx | 280 +-- .../components/ModelSelect/ModelSelect.tsx | 228 ++- .../MakeSkillPublicForm.tsx | 165 +- .../src/components/public_model_hub.test.tsx | 15 + .../src/components/public_model_hub.tsx | 1741 +++++++++-------- 8 files changed, 1261 insertions(+), 1223 deletions(-) diff --git a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts index 92d22f11f4d..3a63c5be940 100644 --- a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts +++ b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts @@ -47,10 +47,10 @@ test.describe("Proxy Admin - Teams", () => { // Fill Team Name — the input has id="team_alias" await dialog.locator("#team_alias").fill(uniqueAlias); - // Select models — the models multi-select is inside the modal - // Click to open dropdown, select "All Proxy Models" - await dialog.locator(".ant-select-selection-overflow").first().click(); - await page.locator(".ant-select-dropdown:visible").getByText("All Proxy Models").click(); + // Select models — the models multi-select is inside the modal. Its popup is + // portaled to the body, so scope the option lookup to the page, not the dialog. + await dialog.getByTestId("create-team-models-select").getByRole("combobox").click(); + await page.getByRole("option", { name: "All Proxy Models", exact: true }).click(); await page.keyboard.press("Escape"); // Submit — click the submit button inside the dialog (not the header button) @@ -191,11 +191,11 @@ test.describe("Proxy Admin - Teams", () => { const modelsSelect = page.locator("[data-testid='models-select']"); await expect(modelsSelect).toBeVisible({ timeout: 10_000 }); - const anthropicTag = modelsSelect - .locator(".ant-select-selection-item") + const anthropicChip = modelsSelect + .locator('[data-slot="combobox-chip"]') .filter({ hasText: "fake-anthropic-claude" }); - await expect(anthropicTag).toBeVisible({ timeout: 5_000 }); - await anthropicTag.locator(".ant-select-selection-item-remove").click(); + await expect(anthropicChip).toBeVisible({ timeout: 5_000 }); + await anthropicChip.locator('[data-slot="combobox-chip-remove"]').click(); await page.getByRole("button", { name: "Save Changes" }).click(); diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 5e322598a10..eb3352a0e30 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1830,9 +1830,6 @@ "src/components/GuardrailsMonitor/LogViewer.tsx": { "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/HelpLink.test.tsx": { @@ -1845,11 +1842,6 @@ "count": 1 } }, - "src/components/ModelSelect/ModelSelect.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Navbar/BlogDropdown/BlogDropdown.test.tsx": { "max-nested-callbacks": { "count": 12 @@ -2341,9 +2333,6 @@ } }, "src/components/claude_code_plugins/MakeSkillPublicForm.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -2982,9 +2971,6 @@ }, "max-lines": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/components/query_param_input.tsx": { diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx index c3671fc9e2c..d41b29f8218 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx @@ -1,8 +1,9 @@ -import { CheckCircleOutlined, CloseOutlined, DownOutlined, WarningOutlined } from "@ant-design/icons"; +import { CircleCheck, ChevronDown, TriangleAlert, X } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import moment from "moment"; -import { Button, Spin } from "antd"; import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { uiSpendLogsCall } from "@/components/networking"; import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer"; import type { LogEntry as ViewLogsLogEntry } from "@/components/view_logs/columns"; @@ -13,21 +14,21 @@ const actionConfig: Record< { icon: React.ElementType; color: string; bg: string; border: string; label: string } > = { blocked: { - icon: CloseOutlined, + icon: X, color: "text-red-600", bg: "bg-red-50", border: "border-red-200", label: "Blocked", }, passed: { - icon: CheckCircleOutlined, + icon: CircleCheck, color: "text-green-600", bg: "bg-green-50", border: "border-green-200", label: "Passed", }, flagged: { - icon: WarningOutlined, + icon: TriangleAlert, color: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200", @@ -125,8 +126,8 @@ export function LogViewer({ {filters.map((f) => ( ); })} diff --git a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx index e253bc4c0ef..eeaeac541bd 100644 --- a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx +++ b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx @@ -1,6 +1,6 @@ import type { ProxyModel } from "@/app/(dashboard)/hooks/models/useModels"; import type { Organization } from "@/components/networking"; -import { screen, waitFor } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "../../../tests/test-utils"; @@ -22,64 +22,6 @@ vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({ useCurrentUser: vi.fn(), })); -vi.mock("antd", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - Select: ({ - value, - onChange, - options, - "data-testid": dataTestId, - allowClear, - maxTagCount, - maxTagPlaceholder, - mode, - ...props - }: any) => { - // Simulate maxTagCount responsive behavior - if value length > 5, call maxTagPlaceholder - const shouldShowPlaceholder = maxTagCount === "responsive" && Array.isArray(value) && value.length > 5; - const visibleValues = shouldShowPlaceholder ? value.slice(0, 5) : value; - const omittedValues = shouldShowPlaceholder ? value.slice(5).map((v: string) => ({ value: v, label: v })) : []; - - return ( -
- - {shouldShowPlaceholder && maxTagPlaceholder && ( -
{maxTagPlaceholder(omittedValues)}
- )} -
- ); - }, - Skeleton: { - Input: ({ active, block }: any) =>
, - }, - Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, - }; -}); - import { useAllProxyModels } from "@/app/(dashboard)/hooks/models/useModels"; import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; @@ -108,6 +50,14 @@ const createMockOrganization = (models: string[]): Organization => ({ members: null, }); +const openModelList = async (user: ReturnType) => { + await user.click(screen.getAllByRole("combobox")[0]); + await screen.findByRole("listbox"); +}; + +const expectOffered = (label: string) => expect(screen.queryAllByText(label).length).toBeGreaterThan(0); +const expectNotOffered = (label: string) => expect(screen.queryAllByText(label)).toHaveLength(0); + describe("ModelSelect", () => { const mockProxyModels: ProxyModel[] = [ { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, @@ -138,21 +88,26 @@ describe("ModelSelect", () => { } as any); }); - it("should render with all option groups", async () => { + it("should offer every model and wildcard under its group heading", async () => { + const user = userEvent.setup(); renderWithProviders( , ); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - expect(screen.getByText("gpt-4")).toBeInTheDocument(); - expect(screen.getByText("claude-3")).toBeInTheDocument(); - expect(screen.getByText("All Openai models")).toBeInTheDocument(); - expect(screen.getByText("All Anthropic models")).toBeInTheDocument(); - }); + await openModelList(user); + + expectOffered("Wildcard Options"); + expectOffered("gpt-4"); + expectOffered("claude-3"); + expectOffered("All Openai models"); + expectOffered("All Anthropic models"); }); - it("should show skeleton loader when any data is loading", () => { + it("should offer nothing to select while any dependency is loading", () => { + const { unmount: unmountReady } = renderWithProviders(); + expect(screen.getAllByRole("combobox")).toHaveLength(1); + unmountReady(); + const loadingScenarios = [ { hook: mockUseAllProxyModels, context: "user" as const }, { hook: mockUseTeam, context: "team" as const, props: { teamID: "team-1" } }, @@ -168,30 +123,24 @@ describe("ModelSelect", () => { const { unmount } = renderWithProviders(); - expect(screen.getByTestId("skeleton-input")).toBeInTheDocument(); + expect(screen.queryAllByRole("combobox")).toHaveLength(0); unmount(); }); }); - it("should handle model selection and onChange", async () => { + it("should report the picked model to onChange", async () => { const user = userEvent.setup(); renderWithProviders( , ); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - }); + await openModelList(user); + await user.click(screen.getAllByText("gpt-4")[0]); - const select = screen.getByRole("listbox"); - await user.selectOptions(select, "gpt-4"); expect(mockOnChange).toHaveBeenCalledWith(["gpt-4"]); - - await user.selectOptions(select, ["gpt-4", "claude-3"]); - expect(mockOnChange).toHaveBeenCalled(); }); - it("should handle special options correctly", async () => { + it("should offer both special options when they are enabled", async () => { const user = userEvent.setup(); mockUseOrganization.mockReturnValue({ data: createMockOrganization(["all-proxy-models"]), @@ -207,33 +156,32 @@ describe("ModelSelect", () => { />, ); - await waitFor(() => { - expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); - expect(screen.getByText("No Default Models")).toBeInTheDocument(); - }); + await openModelList(user); - const select = screen.getByRole("listbox"); - await user.selectOptions(select, ["all-proxy-models", "no-default-models"]); - expect(mockOnChange).toHaveBeenCalledWith(["no-default-models"]); + expectOffered("Special Options"); + expectOffered("All Proxy Models"); + expectOffered("No Default Models"); }); - it("should disable models when special option is selected", async () => { + it("should replace an existing selection when a special option is picked", async () => { + const user = userEvent.setup(); + renderWithProviders( , ); - await waitFor(() => { - expect(screen.getByRole("option", { name: "gpt-4" })).toBeDisabled(); - expect(screen.getByRole("option", { name: "All Openai models" })).toBeDisabled(); - }); + await openModelList(user); + await user.click(screen.getAllByText("No Default Models")[0]); + + expect(mockOnChange).toHaveBeenCalledWith(["no-default-models"]); }); - it("should filter models based on context", async () => { + it("should filter the offered models by context", async () => { const testCases = [ { name: "user context with includeUserModels", @@ -340,6 +288,7 @@ describe("ModelSelect", () => { ]; for (const testCase of testCases) { + const user = userEvent.setup(); testCase.setup(); const { unmount } = renderWithProviders( { />, ); - await waitFor(() => { - testCase.expectedVisible.forEach((model) => { - expect(screen.getByText(model)).toBeInTheDocument(); - }); - testCase.expectedHidden.forEach((model) => { - expect(screen.queryByText(model)).not.toBeInTheDocument(); - }); - }); + await openModelList(user); + testCase.expectedVisible.forEach(expectOffered); + testCase.expectedHidden.forEach(expectNotOffered); unmount(); vi.clearAllMocks(); @@ -368,7 +312,7 @@ describe("ModelSelect", () => { } }); - it("should show All Proxy Models option based on conditions", async () => { + it("should offer All Proxy Models only when the context allows it", async () => { const testCases = [ { name: "when showAllProxyModelsOverride is true", @@ -426,6 +370,7 @@ describe("ModelSelect", () => { ]; for (const testCase of testCases) { + const user = userEvent.setup(); testCase.setup(); const { unmount } = renderWithProviders( { />, ); - await waitFor(() => { - if (testCase.shouldShow) { - expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); - } else { - expect(screen.queryByText("All Proxy Models")).not.toBeInTheDocument(); - expect(screen.getByText("No Default Models")).toBeInTheDocument(); - } - }); + await openModelList(user); + if (testCase.shouldShow) { + expectOffered("All Proxy Models"); + } else { + expectNotOffered("All Proxy Models"); + expectOffered("No Default Models"); + } unmount(); vi.clearAllMocks(); @@ -454,27 +398,6 @@ describe("ModelSelect", () => { } }); - it("should deduplicate models with same id", async () => { - const duplicateModels: ProxyModel[] = [ - { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, - { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, - ]; - - mockUseAllProxyModels.mockReturnValue({ - data: { data: duplicateModels }, - isLoading: false, - } as any); - - renderWithProviders( - , - ); - - await waitFor(() => { - const gpt4Options = screen.getAllByText("gpt-4"); - expect(gpt4Options.length).toBeGreaterThan(0); - }); - }); - it("should use custom dataTestId when provided", async () => { renderWithProviders( { />, ); - await waitFor(() => { - expect(screen.getByTestId("custom-test-id")).toBeInTheDocument(); - }); + expect(await screen.findByTestId("custom-test-id")).toBeInTheDocument(); }); it("should return all proxy models for team context when organization has empty models array", async () => { + const user = userEvent.setup(); mockUseTeam.mockReturnValue({ data: { team_id: "team-1", team_alias: "Test Team", models: [] }, isLoading: false, @@ -503,52 +425,66 @@ describe("ModelSelect", () => { renderWithProviders(); - await waitFor(() => { - expect(screen.getByText("gpt-4")).toBeInTheDocument(); - expect(screen.getByText("claude-3")).toBeInTheDocument(); - }); + await openModelList(user); + + expectOffered("gpt-4"); + expectOffered("claude-3"); }); - it("should disable No Default Models when all-proxy-models is selected", async () => { - mockUseOrganization.mockReturnValue({ - data: createMockOrganization(["all-proxy-models"]), - isLoading: false, - } as any); + it("should not offer a special options group when includeSpecialOptions is omitted", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await openModelList(user); + + expectNotOffered("Special Options"); + expectNotOffered("All Proxy Models"); + expectNotOffered("No Default Models"); + expectOffered("Models"); + }); + + it("should mark models and wildcards unselectable while a special option is selected", async () => { + const user = userEvent.setup(); renderWithProviders( , ); - await waitFor(() => { - const noDefaultOption = screen.getByRole("option", { name: "No Default Models" }); - expect(noDefaultOption).toBeDisabled(); - }); + await openModelList(user); + + expect(screen.getByRole("option", { name: "gpt-4" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("option", { name: "All Openai models" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("option", { name: "No Default Models" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("option", { name: "All Proxy Models" })).not.toHaveAttribute("aria-disabled", "true"); }); - it("should not render an empty optgroup when includeSpecialOptions is omitted", async () => { - renderWithProviders(); + it("should list a duplicated proxy model only once", async () => { + const user = userEvent.setup(); + mockUseAllProxyModels.mockReturnValue({ + data: { + data: [ + { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, + { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, + ], + }, + isLoading: false, + } as any); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - }); + renderWithProviders( + , + ); - const optgroups = document.querySelectorAll("optgroup"); - // Wildcard Options + Models — no blank leading group - expect(optgroups.length).toBe(2); - optgroups.forEach((g) => { - expect(g.getAttribute("label")).toBeTruthy(); - }); + await openModelList(user); + + expect(screen.getAllByRole("option", { name: "gpt-4" })).toHaveLength(1); }); - it("should render maxTagPlaceholder when many items are selected", async () => { - // Create many models to trigger maxTagCount responsive behavior - const manyModels: ProxyModel[] = Array.from({ length: 20 }, (_, i) => ({ + it("should collapse selections past the chip limit into a labelled overflow count", async () => { + const manyModels: ProxyModel[] = Array.from({ length: 8 }, (_, i) => ({ id: `model-${i}`, object: "model", created: 1234567890, @@ -560,22 +496,18 @@ describe("ModelSelect", () => { isLoading: false, } as any); - const selectedValues = manyModels.slice(0, 10).map((m) => m.id); - renderWithProviders( m.id)} context="user" options={{ showAllProxyModelsOverride: true }} />, ); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - // Verify maxTagPlaceholder is rendered with omitted values - expect(screen.getByTestId("max-tag-placeholder")).toBeInTheDocument(); - expect(screen.getByText(/\+5 more/)).toBeInTheDocument(); - }); + expect(await screen.findByText("+3 more")).toBeInTheDocument(); + expect(screen.getByLabelText("model-0")).toBeInTheDocument(); + expect(screen.getByLabelText("model-4")).toBeInTheDocument(); + expect(screen.queryByLabelText("model-5")).not.toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx index e993fed2408..55aa1f1ec5f 100644 --- a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx +++ b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx @@ -2,7 +2,22 @@ import { ProxyModel, useAllProxyModels } from "@/app/(dashboard)/hooks/models/us import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; -import { Select, Skeleton, Tooltip } from "antd"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxCollection, + ComboboxContent, + ComboboxEmpty, + ComboboxGroup, + ComboboxItem, + ComboboxLabel, + ComboboxList, + ComboboxValue, +} from "@/components/ui/combobox"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Organization, Team } from "../networking"; import { splitWildcardModels } from "./modelUtils"; @@ -21,6 +36,8 @@ export const MODEL_SENTINEL_OPTIONS = [ MODEL_SELECT_NO_DEFAULT_MODELS_SPECIAL_VALUE, ] as const; +const MAX_VISIBLE_MODEL_CHIPS = 5; + export interface ModelSelectProps { teamID?: string; organizationID?: string; @@ -37,6 +54,17 @@ export interface ModelSelectProps { style?: React.CSSProperties; } +type ModelOption = { + label: string; + value: string; + disabled?: boolean; +}; + +type ModelOptionGroup = { + label: string; + items: ModelOption[]; +}; + type FilterContextArgs = { allProxyModels: string[]; selectedTeam?: Team; @@ -109,10 +137,11 @@ export const ModelSelect = (props: ModelSelectProps) => { showAllProxyModelsOverride || (organizationHasAllProxyModels && includeSpecialOptions) || context === "global"; if (isLoading) { - return ; + return ; } - const handleChange = (values: string[]) => { + const handleChange = (selected: ModelOption[]) => { + const values = selected.map((option) => option.value); const specialValues = values.filter(isSpecialOption); let finalValues: string[]; @@ -133,85 +162,122 @@ export const ModelSelect = (props: ModelSelectProps) => { }); const { wildcard, regular } = splitWildcardModels(filteredModels); - return ( - setSearchTerm(e.target.value)} - className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" - /> -
- -
- Provider: - -
-
- Mode: - -
-
- Features: - -
- - - model.model_group || String(index)} - sortingMode="client" - sorting={modelSorting} - onSortingChange={setModelSorting} - isLoading={loading} - loadingMessage="Loading models…" - noDataMessage={ - - } - size="compact" - /> - -
- - Showing {filteredData.length} of {modelHubData?.length || 0} models - -
- - - {/* Agents Tab */} - {agentHubData && Array.isArray(agentHubData) && agentHubData.length > 0 && ( - -
- Available Agents -
- - {/* Filters */} -
-
-
- Search Agents: - - - -
-
- - setAgentSearchTerm(e.target.value)} - className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" - /> -
-
-
- Skills: - -
-
- - agent.name || String(index)} - sortingMode="client" - sorting={agentSorting} - onSortingChange={setAgentSorting} - isLoading={agentLoading} - loadingMessage="Loading agents…" - noDataMessage={ - - } - size="compact" - /> - -
- - Showing {filteredAgentData.length} of {agentHubData?.length || 0} agents - -
-
- )} - - {/* MCP Servers Tab */} - {mcpHubData && Array.isArray(mcpHubData) && mcpHubData.length > 0 && ( - -
- Available MCP Servers -
- - {/* Filters */} -
-
-
- Search MCP Servers: - - - -
-
- - setMcpSearchTerm(e.target.value)} - className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" - /> -
-
-
- Transport: - -
-
- - server.server_id || String(index)} - sortingMode="client" - sorting={mcpSorting} - onSortingChange={setMcpSorting} - isLoading={mcpLoading} - loadingMessage="Loading MCP servers…" - noDataMessage={ - - } - size="compact" - /> - -
- - Showing {filteredMcpData.length} of {mcpHubData?.length || 0} MCP servers - -
-
- )} - - {/* Skill Hub Tab */} - - - - - - - - {/* Model Details Modal */} - - {selectedModel?.model_group || "Model Details"} - {selectedModel && ( - - copyToClipboard(selectedModel.model_group)} - className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4" - /> - - )} - - } - width={1000} - open={isModalVisible} - footer={null} - onOk={handleModalOk} - onCancel={handleModalCancel} - > - {selectedModel && ( -
- {/* Model Overview */} -
- Model Overview -
-
- Model Name: - {selectedModel.model_group} -
-
- Mode: - {selectedModel.mode || "Not specified"} -
-
- Providers: -
- {(selectedModel.providers ?? []).map((provider) => { - const { logo } = getProviderLogoAndName(provider); - return ( - -
- {logo && ( - {provider} { - (e.target as HTMLImageElement).style.display = "none"; - }} - /> - )} - {provider} -
-
- ); - })} -
-
-
- - {/* Wildcard Routing Note */} - {selectedModel.model_group.includes("*") && ( -
-
- -
- Wildcard Routing - - This model uses wildcard routing. You can pass any value where you see the{" "} - * symbol. - - - For example, with{" "} - - {selectedModel.model_group} - - , you can use any string ( - - {selectedModel.model_group.replaceAll("*", "my-custom-value")} - - ) that matches this pattern. - -
-
-
- )} -
- - {/* Token and Cost Information */} -
- Token & Cost Information -
-
- Max Input Tokens: - {selectedModel.max_input_tokens?.toLocaleString() || "Not specified"} -
-
- Max Output Tokens: - {selectedModel.max_output_tokens?.toLocaleString() || "Not specified"} -
-
- Input Cost per 1M Tokens: - - {selectedModel.input_cost_per_token - ? formatCost(selectedModel.input_cost_per_token) - : "Not specified"} - -
-
- Output Cost per 1M Tokens: - - {selectedModel.output_cost_per_token - ? formatCost(selectedModel.output_cost_per_token) - : "Not specified"} - -
-
-
- - {/* Capabilities */} -
- Capabilities -
- {(() => { - const capabilities = getModelCapabilities(selectedModel); - const colors = ["green", "blue", "purple", "orange", "red", "yellow"]; - - if (capabilities.length === 0) { - return No special capabilities listed; - } - - return capabilities.map((capability, index) => ( - - {formatCapabilityName(capability)} - - )); - })()} -
-
- - {/* Rate Limits */} - {(selectedModel.tpm || selectedModel.rpm) && ( -
- Rate Limits -
- {selectedModel.tpm && ( -
- Tokens per Minute: - {selectedModel.tpm.toLocaleString()} -
- )} - {selectedModel.rpm && ( -
- Requests per Minute: - {selectedModel.rpm.toLocaleString()} -
- )} -
-
- )} - - {/* Supported OpenAI Parameters */} - {selectedModel.supported_openai_params && selectedModel.supported_openai_params.length > 0 && ( -
- Supported OpenAI Parameters -
- {selectedModel.supported_openai_params.map((param) => ( - - {param} - + +

{title}

+ ))} -
- )} + + )} - {/* Usage Example */} -
- Usage Example -
-
-                    {(() => {
-                      const codeSnippet = generateCodeSnippet({
-                        apiKeySource: "custom",
-                        accessToken: null,
-                        apiKey: "your_api_key",
-                        inputMessage: "Hello, how are you?",
-                        chatHistory: [{ role: "user", content: "Hello, how are you?", isImage: false } as MessageType],
-                        selectedTags: [],
-                        selectedVectorStores: [],
-                        selectedGuardrails: [],
-                        selectedPolicies: [],
-                        selectedMCPServers: [],
-                        endpointType: getEndpointType(selectedModel.mode || "chat"),
-                        selectedModel: selectedModel.model_group,
-                        selectedSdk: "openai",
-                      });
-                      return codeSnippet;
-                    })()}
-                  
+ {/* Health and Endpoint Status - only shown when not embedded */} + {!isEmbedded && ( + +

Health and Endpoint Status

+
+

Service status: {serviceStatus}

-
- -
-
-
- )} - + + )} - {/* Agent Details Modal */} - - {selectedAgent?.name || "Agent Details"} - {selectedAgent && ( - - copyToClipboard(selectedAgent.name)} - className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4" - /> - - )} -
- } - width={1000} - open={isAgentModalVisible} - footer={null} - onOk={handleAgentModalOk} - onCancel={handleAgentModalCancel} - > - {selectedAgent && ( -
- {/* Agent Overview */} -
- Agent Overview -
-
- Name: - {selectedAgent.name} + {/* Tabs for Models and Agents */} + + + + Model Hub + {hasAgents && Agent Hub} + {hasMcpServers && MCP Hub} + Skill Hub + + + {/* Models Tab */} + +
+

Available Models

-
- Version: - {selectedAgent.version} -
-
- Description: - {selectedAgent.description} -
- {selectedAgent.url && ( + + {/* Filters */} +
- URL: - - {selectedAgent.url} - +
+

Search Models:

+ + } /> + + Smart search with relevance ranking - finds models containing your search terms, ranked by + relevance. Try searching 'xai grok-4', 'claude-4', 'gpt-4', or + 'sonnet' + + +
+
+ + setSearchTerm(e.target.value)} + className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" + /> +
+
+
+

Provider:

+ setSelectedProviders(values)} + > + + + {(values: string[]) => + values.map((provider) => ( + + {provider} + + )) + } + + + + + No providers found + + {(provider: string) => { + const { logo } = getProviderLogoAndName(provider); + return ( + + + {logo && ( + {provider} { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} + {provider} + + + ); + }} + + + +
+
+

Mode:

+ +
+
+

Features:

+
- )} -
-
- - {/* Capabilities */} - {selectedAgent.capabilities && ( -
- Capabilities -
- {Object.entries(selectedAgent.capabilities) - .filter(([_, value]) => value === true) - .map(([key]) => ( - - {key} - - ))}
-
- )} - {/* Skills */} - {selectedAgent.skills && selectedAgent.skills.length > 0 && ( -
- Skills -
- {selectedAgent.skills.map((skill, index) => ( -
-
+ model.model_group || String(index)} + sortingMode="client" + sorting={modelSorting} + onSortingChange={setModelSorting} + isLoading={loading} + loadingMessage="Loading models…" + noDataMessage={ + + } + size="compact" + /> + +
+

+ Showing {filteredData.length} of {modelHubData?.length || 0} models +

+
+ + + {/* Agents Tab */} + {hasAgents && ( + +
+

Available Agents

+
+ + {/* Filters */} +
+
+
+

Search Agents:

+ + } /> + Search agents by name or description + +
+
+ + setAgentSearchTerm(e.target.value)} + className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" + /> +
+
+
+

Skills:

+ +
+
+ + agent.name || String(index)} + sortingMode="client" + sorting={agentSorting} + onSortingChange={setAgentSorting} + isLoading={agentLoading} + loadingMessage="Loading agents…" + noDataMessage={ + + } + size="compact" + /> + +
+

+ Showing {filteredAgentData.length} of {agentHubData?.length || 0} agents +

+
+
+ )} + + {/* MCP Servers Tab */} + {hasMcpServers && ( + +
+

Available MCP Servers

+
+ + {/* Filters */} +
+
+
+

Search MCP Servers:

+ + } /> + Search MCP servers by name or description + +
+
+ + setMcpSearchTerm(e.target.value)} + className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" + /> +
+
+
+

Transport:

+ +
+
+ + server.server_id || String(index)} + sortingMode="client" + sorting={mcpSorting} + onSortingChange={setMcpSorting} + isLoading={mcpLoading} + loadingMessage="Loading MCP servers…" + noDataMessage={ + + } + size="compact" + /> + +
+

+ Showing {filteredMcpData.length} of {mcpHubData?.length || 0} MCP servers +

+
+
+ )} + + {/* Skill Hub Tab */} + + + + + +
+ + {/* Model Details Modal */} + !open && handleModalCancel()}> + + + + {selectedModel?.model_group || "Model Details"} + {selectedModel && ( + + copyToClipboard(selectedModel.model_group)} + className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4 shrink-0" + /> + } + /> + Copy model name + + )} + + + {selectedModel && ( +
+ {/* Model Overview */} +
+

Model Overview

+
+
+

Model Name:

+

{selectedModel.model_group}

+
+
+

Mode:

+

{selectedModel.mode || "Not specified"}

+
+
+

Providers:

+
+ {(selectedModel.providers ?? []).map((provider) => { + const { logo } = getProviderLogoAndName(provider); + return ( + +
+ {logo && ( + {provider} { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} + {provider} +
+
+ ); + })} +
+
+
+ + {/* Wildcard Routing Note */} + {selectedModel.model_group.includes("*") && ( +
+
+
- {skill.name} - {skill.description} +

Wildcard Routing

+

+ This model uses wildcard routing. You can pass any value where you see the{" "} + * symbol. +

+

+ For example, with{" "} + + {selectedModel.model_group} + + , you can use any string ( + + {selectedModel.model_group.replaceAll("*", "my-custom-value")} + + ) that matches this pattern. +

- {skill.tags && skill.tags.length > 0 && ( -
- {skill.tags.map((tag) => ( - - {tag} - - ))} +
+ )} +
+ + {/* Token and Cost Information */} +
+

Token & Cost Information

+
+
+

Max Input Tokens:

+

{selectedModel.max_input_tokens?.toLocaleString() || "Not specified"}

+
+
+

Max Output Tokens:

+

{selectedModel.max_output_tokens?.toLocaleString() || "Not specified"}

+
+
+

Input Cost per 1M Tokens:

+

+ {selectedModel.input_cost_per_token + ? formatCost(selectedModel.input_cost_per_token) + : "Not specified"} +

+
+
+

Output Cost per 1M Tokens:

+

+ {selectedModel.output_cost_per_token + ? formatCost(selectedModel.output_cost_per_token) + : "Not specified"} +

+
+
+
+ + {/* Capabilities */} +
+

Capabilities

+
+ {(() => { + const capabilities = getModelCapabilities(selectedModel); + + if (capabilities.length === 0) { + return

No special capabilities listed

; + } + + return capabilities.map((capability) => ( + + {formatCapabilityName(capability)} + + )); + })()} +
+
+ + {/* Rate Limits */} + {(selectedModel.tpm || selectedModel.rpm) && ( +
+

Rate Limits

+
+ {selectedModel.tpm && ( +
+

Tokens per Minute:

+

{selectedModel.tpm.toLocaleString()}

+
+ )} + {selectedModel.rpm && ( +
+

Requests per Minute:

+

{selectedModel.rpm.toLocaleString()}

)}
- ))} -
-
- )} - - {/* Input/Output Modes */} -
- Input/Output Modes -
-
- Input Modes: -
- {(selectedAgent.defaultInputModes ?? []).map((mode) => ( - - {mode} - - ))}
-
+ )} + + {/* Supported OpenAI Parameters */} + {selectedModel.supported_openai_params && selectedModel.supported_openai_params.length > 0 && ( +
+

Supported OpenAI Parameters

+
+ {selectedModel.supported_openai_params.map((param) => ( + + {param} + + ))} +
+
+ )} + + {/* Usage Example */}
- Output Modes: -
- {(selectedAgent.defaultOutputModes ?? []).map((mode) => ( - - {mode} - - ))} +

Usage Example

+
+
+                        {(() => {
+                          const codeSnippet = generateCodeSnippet({
+                            apiKeySource: "custom",
+                            accessToken: null,
+                            apiKey: "your_api_key",
+                            inputMessage: "Hello, how are you?",
+                            chatHistory: [
+                              { role: "user", content: "Hello, how are you?", isImage: false } as MessageType,
+                            ],
+                            selectedTags: [],
+                            selectedVectorStores: [],
+                            selectedGuardrails: [],
+                            selectedPolicies: [],
+                            selectedMCPServers: [],
+                            endpointType: getEndpointType(selectedModel.mode || "chat"),
+                            selectedModel: selectedModel.model_group,
+                            selectedSdk: "openai",
+                          });
+                          return codeSnippet;
+                        })()}
+                      
+
+
+
-
- - {/* Documentation */} - {selectedAgent.documentationUrl && ( -
- Documentation - - - View Documentation - -
)} + +
- {/* A2A Usage Example */} -
- Usage Example (A2A Protocol) + {/* Agent Details Modal */} + !open && handleAgentModalCancel()}> + + + + {selectedAgent?.name || "Agent Details"} + {selectedAgent && ( + + copyToClipboard(selectedAgent.name)} + className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4 shrink-0" + /> + } + /> + Copy agent name + + )} + + + {selectedAgent && ( +
+ {/* Agent Overview */} +
+

Agent Overview

+
+
+

Name:

+

{selectedAgent.name}

+
+
+

Version:

+

{selectedAgent.version}

+
+
+

Description:

+

{selectedAgent.description}

+
+ {selectedAgent.url && ( + + )} +
+
- {/* Step 1: Retrieve Agent Card */} -
- Step 1: Retrieve Agent Card -
-
-                      {`base_url = '${selectedAgent.url}'
+                  {/* Capabilities */}
+                  {selectedAgent.capabilities && (
+                    
+

Capabilities

+
+ {Object.entries(selectedAgent.capabilities) + .filter(([_, value]) => value === true) + .map(([key]) => ( + + {key} + + ))} +
+
+ )} + + {/* Skills */} + {selectedAgent.skills && selectedAgent.skills.length > 0 && ( +
+

Skills

+
+ {selectedAgent.skills.map((skill, index) => ( +
+
+
+

{skill.name}

+

{skill.description}

+
+
+ {skill.tags && skill.tags.length > 0 && ( +
+ {skill.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ ))} +
+
+ )} + + {/* Input/Output Modes */} +
+

Input/Output Modes

+
+
+

Input Modes:

+
+ {(selectedAgent.defaultInputModes ?? []).map((mode) => ( + + {mode} + + ))} +
+
+
+

Output Modes:

+
+ {(selectedAgent.defaultOutputModes ?? []).map((mode) => ( + + {mode} + + ))} +
+
+
+
+ + {/* Documentation */} + {selectedAgent.documentationUrl && ( +
+

Documentation

+ + + View Documentation + +
+ )} + + {/* A2A Usage Example */} +
+

Usage Example (A2A Protocol)

+ + {/* Step 1: Retrieve Agent Card */} +
+

Step 1: Retrieve Agent Card

+
+
+                          {`base_url = '${selectedAgent.url}'
 
 resolver = A2ACardResolver(
     httpx_client=httpx_client,
@@ -1251,12 +1275,12 @@ if _public_card.supports_authenticated_extended_card:
             f'Failed to fetch extended agent card: {e_extended}. Will proceed with public card.',
             exc_info=True,
         )`}
-                    
-
-
-
+
+
+ -
-
+ copyToClipboard(codeSnippet); + }} + className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer" + > + Copy to clipboard + +
+
- {/* Step 2: Call the Agent */} -
- Step 2: Call the Agent -
-
-                      {`client = A2AClient(
+                    {/* Step 2: Call the Agent */}
+                    
+

Step 2: Call the Agent

+
+
+                          {`client = A2AClient(
     httpx_client=httpx_client, agent_card=final_agent_card_to_use
 )
 
@@ -1333,12 +1357,12 @@ request = SendMessageRequest(
 
 response = await client.send_message(request)
 print(response.model_dump(mode='json', exclude_none=True))`}
-                    
-
-
-
+
+
+ + copyToClipboard(codeSnippet); + }} + className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer" + > + Copy to clipboard + +
+
-
-
- )} - - - {/* MCP Server Details Modal */} - - {selectedMcpServer?.server_name || "MCP Server Details"} - {selectedMcpServer && ( - - copyToClipboard(selectedMcpServer.server_name)} - className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4" - /> - )} -
- } - width={1000} - open={isMcpModalVisible} - footer={null} - onOk={handleMcpModalOk} - onCancel={handleMcpModalCancel} - > - {selectedMcpServer && ( -
- {/* Server Overview */} -
- Server Overview -
+ + + + {/* MCP Server Details Modal */} + !open && handleMcpModalCancel()}> + + + + {selectedMcpServer?.server_name || "MCP Server Details"} + {selectedMcpServer && ( + + copyToClipboard(selectedMcpServer.server_name)} + className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4 shrink-0" + /> + } + /> + Copy server name + + )} + + + {selectedMcpServer && ( +
+ {/* Server Overview */}
- Server Name: - {selectedMcpServer.server_name} +

Server Overview

+
+
+

Server Name:

+

{selectedMcpServer.server_name}

+
+
+

Transport:

+ {selectedMcpServer.transport} +
+ {selectedMcpServer.alias && ( +
+

Alias:

+

{selectedMcpServer.alias}

+
+ )} +
+

Auth Type:

+ + {selectedMcpServer.auth_type} + +
+
+

Description:

+

{selectedMcpServer.mcp_info?.description || "-"}

+
+
-
- Transport: - {selectedMcpServer.transport} -
- {selectedMcpServer.alias && ( + + {/* Additional Info */} + {selectedMcpServer.mcp_info && Object.keys(selectedMcpServer.mcp_info).length > 0 && (
- Alias: - {selectedMcpServer.alias} +

Additional Information

+
+
+                          {JSON.stringify(selectedMcpServer.mcp_info, null, 2)}
+                        
+
)} + + {/* Usage Example */}
- Auth Type: - - {selectedMcpServer.auth_type} - -
-
- Description: - {selectedMcpServer.mcp_info?.description || "-"} -
-
-
- - {/* Additional Info */} - {selectedMcpServer.mcp_info && Object.keys(selectedMcpServer.mcp_info).length > 0 && ( -
- Additional Information -
-
{JSON.stringify(selectedMcpServer.mcp_info, null, 2)}
-
-
- )} - - {/* Usage Example */} -
- Usage Example -
-
-                    {`# Using MCP Server with Python FastMCP
+                    

Usage Example

+
+
+                        {`# Using MCP Server with Python FastMCP
 
 from fastmcp import Client
 import asyncio
@@ -1474,12 +1501,12 @@ async def main():
 
 if __name__ == "__main__":
     asyncio.run(main())`}
-                  
-
-
-
+
+
+ + copyToClipboard(codeSnippet); + }} + className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer" + > + Copy to clipboard + +
+
-
-
- )} -
- + )} + + + + ); };