Merge pull request #21004 from BerriAI/litellm_ui_auto_router

[Fix] UI - Add Auto Router: Description Text Input Focus
This commit is contained in:
yuneng-jiang 2026-02-11 20:48:44 -08:00 committed by GitHub
commit 9cee51abb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 575 additions and 283 deletions

View file

@ -0,0 +1,296 @@
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import RouterConfigBuilder from "./RouterConfigBuilder";
const MOCK_MODEL_INFO = [
{ model_group: "gpt-4", mode: "chat" },
{ model_group: "gpt-3.5-turbo", mode: "chat" },
{ model_group: "claude-3-opus", mode: "chat" },
];
describe("RouterConfigBuilder", () => {
it("should render", () => {
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
expect(screen.getByText("Routes Configuration")).toBeInTheDocument();
});
it("should display Add Route button", () => {
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
expect(screen.getByRole("button", { name: /add route/i })).toBeInTheDocument();
});
it("should show empty state when no routes are configured", () => {
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
expect(screen.getByText(/no routes configured/i)).toBeInTheDocument();
});
it("should add a route when Add Route is clicked", async () => {
const user = userEvent.setup();
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
await user.click(screen.getByRole("button", { name: /add route/i }));
expect(screen.getByText("Route 1: Unnamed")).toBeInTheDocument();
});
it("should call onChange when a route is added", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} onChange={onChange} />);
await user.click(screen.getByRole("button", { name: /add route/i }));
expect(onChange).toHaveBeenCalledWith({
routes: [
expect.objectContaining({
name: "",
utterances: [],
description: "",
score_threshold: 0.5,
}),
],
});
});
it("should initialize routes from value prop", async () => {
const value = {
routes: [
{
name: "gpt-4",
utterances: ["hello", "hi"],
description: "For greetings",
score_threshold: 0.7,
},
],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
});
it("should support both name and model fields in value prop", async () => {
const value = {
routes: [{ model: "gpt-3.5-turbo", utterances: [], description: "", score_threshold: 0.5 }],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-3.5-turbo")).toBeInTheDocument();
});
});
it("should remove a route when delete button is clicked", async () => {
const user = userEvent.setup();
const value = {
routes: [
{
name: "gpt-4",
utterances: [],
description: "",
score_threshold: 0.5,
},
],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
const deleteButton = screen.getByRole("button", { name: "delete" });
await user.click(deleteButton);
await waitFor(() => {
expect(screen.queryByText("Route 1: gpt-4")).not.toBeInTheDocument();
expect(screen.getByText(/no routes configured/i)).toBeInTheDocument();
});
});
it("should call onChange when route is removed", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const value = {
routes: [
{
name: "gpt-4",
utterances: [],
description: "",
score_threshold: 0.5,
},
],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} onChange={onChange} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
const deleteButton = screen.getByRole("button", { name: "delete" });
await user.click(deleteButton);
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith({ routes: [] });
});
});
it("should update route when description is changed", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const value = {
routes: [
{
name: "gpt-4",
utterances: [],
description: "",
score_threshold: 0.5,
},
],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} onChange={onChange} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
const descriptionInput = screen.getByPlaceholderText("Describe when this route should be used...");
await user.type(descriptionInput, "For code generation");
await waitFor(() => {
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
expect(lastCall[0].routes[0].description).toBe("For code generation");
});
});
it("should update route when score threshold is changed", async () => {
const onChange = vi.fn();
const value = {
routes: [
{
name: "gpt-4",
utterances: [],
description: "",
score_threshold: 0.5,
},
],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} onChange={onChange} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
const scoreInput = screen.getByRole("spinbutton");
fireEvent.change(scoreInput, { target: { value: "0.9" } });
await waitFor(() => {
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
expect(lastCall[0].routes[0].score_threshold).toBe(0.9);
});
});
it("should add multiple routes", async () => {
const user = userEvent.setup();
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
await user.click(screen.getByRole("button", { name: /add route/i }));
await user.click(screen.getByRole("button", { name: /add route/i }));
expect(screen.getByText("Route 1: Unnamed")).toBeInTheDocument();
expect(screen.getByText("Route 2: Unnamed")).toBeInTheDocument();
});
it("should toggle JSON preview visibility", async () => {
const user = userEvent.setup();
const { container } = render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
expect(screen.getByText("JSON Preview")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Show" })).toBeInTheDocument();
expect(container.querySelector("pre")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Show" }));
expect(screen.getByRole("button", { name: "Hide" })).toBeInTheDocument();
expect(container.querySelector("pre")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Hide" }));
expect(screen.getByRole("button", { name: "Show" })).toBeInTheDocument();
expect(container.querySelector("pre")).not.toBeInTheDocument();
});
it("should display JSON preview with route data when routes exist", async () => {
const user = userEvent.setup();
const { container } = render(
<RouterConfigBuilder
modelInfo={MOCK_MODEL_INFO}
value={{
routes: [
{ name: "gpt-4", utterances: ["hello"], description: "test", score_threshold: 0.8 },
],
}}
/>,
);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: "Show" }));
const preElement = container.querySelector("pre");
expect(preElement).toBeInTheDocument();
expect(preElement?.textContent).toContain("gpt-4");
expect(preElement?.textContent).toContain("hello");
expect(preElement?.textContent).toContain("0.8");
});
it("should display model selector with options from modelInfo", async () => {
const value = {
routes: [
{ name: "", utterances: [], description: "", score_threshold: 0.5 },
],
};
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
await waitFor(() => {
expect(screen.getByText("Route 1: Unnamed")).toBeInTheDocument();
});
expect(screen.getByText("Model")).toBeInTheDocument();
const comboboxes = screen.getAllByRole("combobox");
expect(comboboxes.length).toBeGreaterThan(0);
});
it("should clear routes when value prop changes to empty", async () => {
const value = {
routes: [
{
name: "gpt-4",
utterances: [],
description: "",
score_threshold: 0.5,
},
],
};
const { rerender } = render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
await waitFor(() => {
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
});
rerender(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={{ routes: [] }} />);
await waitFor(() => {
expect(screen.getByText(/no routes configured/i)).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,277 @@
import { DeleteOutlined, InfoCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { Select as AntdSelect, Button, Card, Collapse, Divider, Empty, Flex, Input, InputNumber, Space, Tooltip, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { ModelGroup } from "../playground/llm_calls/fetch_models";
const { Text } = Typography;
const { TextArea } = Input;
interface Route {
id: string;
model: string;
utterances: string[];
description: string;
score_threshold: number;
}
interface SavedRoute {
id?: string;
name?: string;
model?: string;
utterances?: string[];
description?: string;
score_threshold?: number;
}
interface RouterConfig {
routes?: SavedRoute[];
}
interface RouterConfigBuilderProps {
modelInfo: ModelGroup[];
value?: RouterConfig;
onChange?: (config: any) => void;
}
const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, value, onChange }) => {
const [routes, setRoutes] = useState<Route[]>([]);
const [showJsonPreview, setShowJsonPreview] = useState<boolean>(false);
const [expandedRoutes, setExpandedRoutes] = useState<string[]>([]);
// Initialize routes from value prop - preserve existing route IDs to avoid focus loss when parent re-renders
useEffect(() => {
const routesFromValue = value?.routes;
if (routesFromValue) {
const routeIds: string[] = [];
setRoutes((prevRoutes) => {
const initializedRoutes = routesFromValue.map((route: SavedRoute, index: number) => {
const existingRoute = prevRoutes[index];
const id = existingRoute?.id || route.id || `route-${index}-${Date.now()}`;
routeIds.push(id);
return {
id,
model: route.name || route.model || "", // handle both 'name' and 'model' fields
utterances: route.utterances || [],
description: route.description || "",
score_threshold: route.score_threshold ?? 0.5,
};
});
return initializedRoutes;
});
setExpandedRoutes(routeIds);
} else {
setRoutes([]);
setExpandedRoutes([]);
}
}, [value]);
// Handle adding a new route
const addRoute = () => {
const newRouteId = `route-${Date.now()}`;
const newRoute: Route = {
id: newRouteId,
model: "",
utterances: [],
description: "",
score_threshold: 0.5,
};
const updatedRoutes = [...routes, newRoute];
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
// Automatically expand the new route
setExpandedRoutes((prev) => [...prev, newRouteId]);
};
// Handle removing a route
const removeRoute = (routeId: string) => {
const updatedRoutes = routes.filter((route) => route.id !== routeId);
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
// Remove from expanded routes as well
setExpandedRoutes((prev) => prev.filter((id) => id !== routeId));
};
// Handle updating a route
const updateRoute = (routeId: string, field: keyof Route, value: any) => {
const updatedRoutes = routes.map((route) => (route.id === routeId ? { ...route, [field]: value } : route));
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
};
// Update the overall configuration
const updateConfig = (updatedRoutes: Route[]) => {
const config = {
routes: updatedRoutes.map((route) => ({
name: route.model,
utterances: route.utterances,
description: route.description,
score_threshold: route.score_threshold,
})),
};
onChange?.(config);
};
// Handle utterances change (convert textarea string to array)
const handleUtterancesChange = (routeId: string, utterancesText: string) => {
const utterancesArray = utterancesText
.split("\n")
.map((line) => line.trim()) // Only trims leading/trailing whitespace, preserves internal spaces
.filter((line) => line.length > 0);
updateRoute(routeId, "utterances", utterancesArray);
};
// Prepare model options for dropdowns
const modelOptions = modelInfo.map((model) => ({
value: model.model_group,
label: model.model_group,
}));
const generateConfig = () => {
return {
routes: routes.map((route) => ({
name: route.model,
utterances: route.utterances,
description: route.description,
score_threshold: route.score_threshold,
})),
};
};
return (
<div className="w-full max-w-none">
<Flex justify="space-between" align="center" gap="middle" style={{ width: "100%", marginBottom: 24 }}>
<Space align="center">
<Typography.Title level={4} style={{ margin: 0 }}>Routes Configuration</Typography.Title>
<Tooltip title="Configure routing logic to automatically select the best model based on user input patterns">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</Space>
<Button type="primary" icon={<PlusOutlined />} onClick={addRoute} className="bg-blue-600 hover:bg-blue-700">
Add Route
</Button>
</Flex>
{/* Routes */}
{routes.length === 0 ? (
<Card>
<Empty description="No routes configured. Click &quot;Add Route&quot; to get started." />
</Card>
) : (
<Collapse
activeKey={expandedRoutes}
onChange={(keys) => setExpandedRoutes(Array.isArray(keys) ? keys : [keys].filter(Boolean))}
style={{ width: "100%" }}
items={routes.map((route, index) => ({
key: route.id,
label: (
<Text style={{ fontSize: 16 }}>
Route {index + 1}: {route.model || "Unnamed"}
</Text>
),
extra: (
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
removeRoute(route.id);
}}
/>
),
children: (
<Card key={route.id}>
{/* Model Selection */}
<div className="mb-4 w-full">
<Text className="text-sm font-medium mb-2 block">Model</Text>
<AntdSelect
value={route.model}
onChange={(value) => updateRoute(route.id, "model", value)}
placeholder="Select model"
showSearch
style={{ width: "100%" }}
options={modelOptions}
/>
</div>
{/* Description */}
<div className="mb-4 w-full">
<Text className="text-sm font-medium mb-2 block">Description</Text>
<TextArea
value={route.description}
onChange={(e) => updateRoute(route.id, "description", e.target.value)}
placeholder="Describe when this route should be used..."
rows={2}
style={{ width: "100%" }}
/>
</div>
{/* Score Threshold */}
<div className="mb-4 w-full">
<div className="flex items-center gap-2 mb-2">
<Text className="text-sm font-medium">Score Threshold</Text>
<Tooltip title="Minimum similarity score to route to this model (0-1)">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<InputNumber
value={route.score_threshold}
onChange={(value) => updateRoute(route.id, "score_threshold", value || 0)}
min={0}
max={1}
step={0.1}
style={{ width: "100%" }}
placeholder="0.5"
/>
</div>
{/* Example Utterances */}
<div className="w-full">
<div className="flex items-center gap-2 mb-2">
<Text className="text-sm font-medium">Example Utterances</Text>
<Tooltip title="Training examples for this route. Type an utterance and press Enter to add it.">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Text className="text-xs text-gray-500 mb-2">
Type an utterance and press Enter to add it. You can also paste multiple lines.
</Text>
<AntdSelect
mode="tags"
value={route.utterances}
onChange={(utterances) => updateRoute(route.id, "utterances", utterances)}
placeholder="Type an utterance and press Enter..."
style={{ width: "100%" }}
tokenSeparators={["\n"]}
maxTagCount="responsive"
allowClear
/>
</div>
</Card>
),
}))}
/>
)}
{/* JSON Preview */}
<Divider />
<div className="flex justify-between items-center mb-4 w-full">
<Text className="text-lg font-semibold">JSON Preview</Text>
<Button type="link" onClick={() => setShowJsonPreview(!showJsonPreview)} className="text-blue-600 p-0">
{showJsonPreview ? "Hide" : "Show"}
</Button>
</div>
{showJsonPreview && (
<Card className="bg-gray-50 w-full">
<pre className="text-sm overflow-auto max-h-64 w-full">{JSON.stringify(generateConfig(), null, 2)}</pre>
</Card>
)}
</div>
);
};
export default RouterConfigBuilder;

View file

@ -7,7 +7,7 @@ import ConnectionErrorDisplay from "./model_connection_test";
import { all_admin_roles } from "@/utils/roles";
import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
import RouterConfigBuilder from "./router_config_builder";
import RouterConfigBuilder from "./RouterConfigBuilder";
import NotificationManager from "../molecules/notifications_manager";
interface AddAutoRouterTabProps {

View file

@ -1,281 +0,0 @@
import React, { useState, useEffect } from "react";
import { Card, Button, Input, InputNumber, Select as AntdSelect, Tooltip, Collapse } from "antd";
import { PlusOutlined, DeleteOutlined, InfoCircleOutlined, DownOutlined } from "@ant-design/icons";
import { Text } from "@tremor/react";
import { ModelGroup } from "../playground/llm_calls/fetch_models";
const { TextArea } = Input;
const { Panel } = Collapse;
interface Route {
id: string;
model: string;
utterances: string[];
description: string;
score_threshold: number;
}
interface SavedRoute {
id?: string;
name?: string;
model?: string;
utterances?: string[];
description?: string;
score_threshold?: number;
}
interface RouterConfig {
routes?: SavedRoute[];
}
interface RouterConfigBuilderProps {
modelInfo: ModelGroup[];
value?: RouterConfig;
onChange?: (config: any) => void;
}
const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({ modelInfo, value, onChange }) => {
const [routes, setRoutes] = useState<Route[]>([]);
const [showJsonPreview, setShowJsonPreview] = useState<boolean>(false);
const [expandedRoutes, setExpandedRoutes] = useState<string[]>([]);
// Initialize routes from value prop
useEffect(() => {
if (value?.routes) {
const initializedRoutes = value.routes.map((route: SavedRoute, index: number) => ({
id: route.id || `route-${index}-${Date.now()}`,
model: route.name || route.model || "", // handle both 'name' and 'model' fields
utterances: route.utterances || [],
description: route.description || "",
score_threshold: route.score_threshold || 0.5,
}));
setRoutes(initializedRoutes);
// Set expanded routes for existing routes
const routeIds = initializedRoutes.map((route) => route.id);
setExpandedRoutes(routeIds);
} else {
setRoutes([]);
setExpandedRoutes([]);
}
}, [value]);
// Handle adding a new route
const addRoute = () => {
const newRouteId = `route-${Date.now()}`;
const newRoute: Route = {
id: newRouteId,
model: "",
utterances: [],
description: "",
score_threshold: 0.5,
};
const updatedRoutes = [...routes, newRoute];
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
// Automatically expand the new route
setExpandedRoutes((prev) => [...prev, newRouteId]);
};
// Handle removing a route
const removeRoute = (routeId: string) => {
const updatedRoutes = routes.filter((route) => route.id !== routeId);
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
// Remove from expanded routes as well
setExpandedRoutes((prev) => prev.filter((id) => id !== routeId));
};
// Handle updating a route
const updateRoute = (routeId: string, field: keyof Route, value: any) => {
const updatedRoutes = routes.map((route) => (route.id === routeId ? { ...route, [field]: value } : route));
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
};
// Update the overall configuration
const updateConfig = (updatedRoutes: Route[]) => {
const config = {
routes: updatedRoutes.map((route) => ({
name: route.model,
utterances: route.utterances,
description: route.description,
score_threshold: route.score_threshold,
})),
};
onChange?.(config);
};
// Handle utterances change (convert textarea string to array)
const handleUtterancesChange = (routeId: string, utterancesText: string) => {
const utterancesArray = utterancesText
.split("\n")
.map((line) => line.trim()) // Only trims leading/trailing whitespace, preserves internal spaces
.filter((line) => line.length > 0);
updateRoute(routeId, "utterances", utterancesArray);
};
// Prepare model options for dropdowns
const modelOptions = modelInfo.map((model) => ({
value: model.model_group,
label: model.model_group,
}));
const generateConfig = () => {
return {
routes: routes.map((route) => ({
name: route.model,
utterances: route.utterances,
description: route.description,
score_threshold: route.score_threshold,
})),
};
};
return (
<div className="w-full max-w-none">
{/* Routes Configuration Header */}
<div className="flex justify-between items-center mb-6 w-full">
<div className="flex items-center gap-2">
<Text className="text-lg font-semibold">Routes Configuration</Text>
<Tooltip title="Configure routing logic to automatically select the best model based on user input patterns">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={addRoute} className="bg-blue-600 hover:bg-blue-700">
Add Route
</Button>
</div>
{/* Routes */}
{routes.length === 0 ? (
<div className="text-center py-12 text-gray-500 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200 mb-6">
<Text>No routes configured. Click &ldquo;Add Route&rdquo; to get started.</Text>
</div>
) : (
<div className="space-y-3 mb-6 w-full">
{routes.map((route, index) => (
<Card key={route.id} className="border border-gray-200 shadow-sm w-full" bodyStyle={{ padding: 0 }}>
<Collapse
ghost
expandIcon={({ isActive }) => <DownOutlined rotate={isActive ? 180 : 0} />}
activeKey={expandedRoutes}
onChange={(keys) => setExpandedRoutes(Array.isArray(keys) ? keys : [keys].filter(Boolean))}
items={[
{
key: route.id,
label: (
<div className="flex justify-between items-center py-2">
<Text className="font-medium text-base">
Route {index + 1}: {route.model || "Unnamed"}
</Text>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
removeRoute(route.id);
}}
className="mr-2"
/>
</div>
),
children: (
<div className="px-6 pb-6 w-full">
{/* Model Selection */}
<div className="mb-4 w-full">
<Text className="text-sm font-medium mb-2 block">Model</Text>
<AntdSelect
value={route.model}
onChange={(value) => updateRoute(route.id, "model", value)}
placeholder="Select model"
showSearch
style={{ width: "100%" }}
options={modelOptions}
/>
</div>
{/* Description */}
<div className="mb-4 w-full">
<Text className="text-sm font-medium mb-2 block">Description</Text>
<TextArea
value={route.description}
onChange={(e) => updateRoute(route.id, "description", e.target.value)}
placeholder="Describe when this route should be used..."
rows={2}
style={{ width: "100%" }}
/>
</div>
{/* Score Threshold */}
<div className="mb-4 w-full">
<div className="flex items-center gap-2 mb-2">
<Text className="text-sm font-medium">Score Threshold</Text>
<Tooltip title="Minimum similarity score to route to this model (0-1)">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<InputNumber
value={route.score_threshold}
onChange={(value) => updateRoute(route.id, "score_threshold", value || 0)}
min={0}
max={1}
step={0.1}
style={{ width: "100%" }}
placeholder="0.5"
/>
</div>
{/* Example Utterances */}
<div className="w-full">
<div className="flex items-center gap-2 mb-2">
<Text className="text-sm font-medium">Example Utterances</Text>
<Tooltip title="Training examples for this route. Type an utterance and press Enter to add it.">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Text className="text-xs text-gray-500 mb-2">
Type an utterance and press Enter to add it. You can also paste multiple lines.
</Text>
<AntdSelect
mode="tags"
value={route.utterances}
onChange={(utterances) => updateRoute(route.id, "utterances", utterances)}
placeholder="Type an utterance and press Enter..."
style={{ width: "100%" }}
tokenSeparators={["\n"]}
maxTagCount="responsive"
allowClear
/>
</div>
</div>
),
},
]}
/>
</Card>
))}
</div>
)}
{/* JSON Preview */}
<div className="border-t pt-6 w-full">
<div className="flex justify-between items-center mb-4 w-full">
<Text className="text-lg font-semibold">JSON Preview</Text>
<Button type="link" onClick={() => setShowJsonPreview(!showJsonPreview)} className="text-blue-600 p-0">
{showJsonPreview ? "Hide" : "Show"}
</Button>
</div>
{showJsonPreview && (
<Card className="bg-gray-50 w-full">
<pre className="text-sm overflow-auto max-h-64 w-full">{JSON.stringify(generateConfig(), null, 2)}</pre>
</Card>
)}
</div>
</div>
);
};
export default RouterConfigBuilder;

View file

@ -3,7 +3,7 @@ import { Modal, Form, Button, Select as AntdSelect } from "antd";
import { Text, TextInput } from "@tremor/react";
import { modelAvailableCall, modelPatchUpdateCall } from "../networking";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
import RouterConfigBuilder from "../add_model/router_config_builder";
import RouterConfigBuilder from "../add_model/RouterConfigBuilder";
import NotificationsManager from "../molecules/notifications_manager";
interface EditAutoRouterModalProps {