mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
refactor(ui): migrate playground to shadcn
Replaces antd and Tremor with shadcn primitives across the six route-owned, form-free playground components: the compare view and its panel, message input and unified selector, plus the realtime playground and the agent builder. Markup only, no behaviour change. The characterisation tests added in the previous commit are untouched here and stay green through the swap. Adds ui/slider.tsx via the shadcn CLI and retires the six antd no-restricted-imports suppressions the migration made obsolete.
This commit is contained in:
parent
923a49a6f8
commit
9a26da90a4
8 changed files with 514 additions and 414 deletions
|
|
@ -1055,9 +1055,6 @@
|
|||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 5
|
||||
}
|
||||
|
|
@ -1098,9 +1095,6 @@
|
|||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/immutability": {
|
||||
"count": 2
|
||||
},
|
||||
|
|
@ -1115,9 +1109,6 @@
|
|||
"no-nested-ternary": {
|
||||
"count": 4
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
|
|
@ -1125,9 +1116,6 @@
|
|||
"src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 2
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.tsx": {
|
||||
|
|
@ -1135,21 +1123,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 2
|
||||
|
|
@ -3582,6 +3560,14 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/ui/slider.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
},
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/ui/switch.tsx": {
|
||||
"local/filename-pascal-case": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
CommentOutlined,
|
||||
DeleteOutlined,
|
||||
ExperimentOutlined,
|
||||
LinkOutlined,
|
||||
PlusOutlined,
|
||||
RobotOutlined,
|
||||
SaveOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Input, Modal, Select, Spin, Tabs } from "antd";
|
||||
import { Bot, FlaskConical, Link as LinkIcon, MessageSquare, Plus, Save, Trash2 } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { MultiSelect } from "@/components/shared/MultiSelect";
|
||||
import { useVisitedTabs } from "@/hooks/useVisitedTabs";
|
||||
import CodeBlock from "@/components/CodeBlock";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import {
|
||||
|
|
@ -27,8 +35,6 @@ import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_m
|
|||
import ComplianceUI from "../complianceUI/ComplianceUI";
|
||||
import ChatUI from "./ChatUI";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export interface AgentBuilderViewProps {
|
||||
accessToken: string | null;
|
||||
token: string | null;
|
||||
|
|
@ -45,6 +51,8 @@ export interface AgentBuilderViewProps {
|
|||
|
||||
const NEW_AGENT_ID = "__new__";
|
||||
|
||||
type AgentTab = "configure" | "chat" | "test" | "connect";
|
||||
|
||||
function getConnectTabBaseUrl(
|
||||
proxySettings: AgentBuilderViewProps["proxySettings"],
|
||||
customProxyBaseUrl?: string,
|
||||
|
|
@ -116,7 +124,7 @@ function ConnectTabContent({
|
|||
Create a virtual key that can only call this agent. The key will be scoped to you (user_id) and restricted to
|
||||
the model <span className="font-mono text-gray-800">{agentName}</span>.
|
||||
</p>
|
||||
<Button type="primary" onClick={onCreateKey} loading={creatingKey} disabled={disabledPersonalKeyCreation}>
|
||||
<Button onClick={onCreateKey} disabled={creatingKey || disabledPersonalKeyCreation}>
|
||||
Create key for this agent
|
||||
</Button>
|
||||
{disabledPersonalKeyCreation && (
|
||||
|
|
@ -190,7 +198,12 @@ export default function AgentBuilderView({
|
|||
const [modelGroups, setModelGroups] = useState<ModelGroup[]>([]);
|
||||
const [loadingAgents, setLoadingAgents] = useState(true);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"configure" | "chat" | "test" | "connect">("configure");
|
||||
const [activeTab, setActiveTab] = useState<AgentTab>("configure");
|
||||
const { onTabChange, hasVisited } = useVisitedTabs("configure");
|
||||
const goToTab = (tab: AgentTab) => {
|
||||
setActiveTab(tab);
|
||||
onTabChange(tab);
|
||||
};
|
||||
const [creatingKey, setCreatingKey] = useState(false);
|
||||
const [createdKeyValue, setCreatedKeyValue] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -207,6 +220,7 @@ export default function AgentBuilderView({
|
|||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||
|
||||
const effectiveApiKey = apiKey || accessToken || "";
|
||||
const selectedAgent =
|
||||
|
|
@ -314,7 +328,7 @@ export default function AgentBuilderView({
|
|||
setDraftTemperature(0.7);
|
||||
setDraftMaxTokens(4096);
|
||||
setDraftTools([]);
|
||||
setActiveTab("configure");
|
||||
goToTab("configure");
|
||||
};
|
||||
|
||||
const handleSaveAgent = async () => {
|
||||
|
|
@ -344,7 +358,7 @@ export default function AgentBuilderView({
|
|||
? list.find((a) => getAgentModelId(a) === createdId) ?? list.find((a) => a.model_name === draftName.trim())
|
||||
: list.find((a) => a.model_name === draftName.trim());
|
||||
setSelectedId(created ? getAgentSelectionKey(created) : list[0] ? getAgentSelectionKey(list[0]) : null);
|
||||
setActiveTab("chat");
|
||||
goToTab("chat");
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Failed to save agent");
|
||||
} finally {
|
||||
|
|
@ -411,27 +425,24 @@ export default function AgentBuilderView({
|
|||
|
||||
const handleDeleteAgent = () => {
|
||||
if (!selectedAgent || !selectedAgentModelId || !accessToken) return;
|
||||
Modal.confirm({
|
||||
title: "Delete agent",
|
||||
content: `Are you sure you want to delete "${selectedAgent.model_name}"? This cannot be undone.`,
|
||||
okText: "Delete",
|
||||
okType: "danger",
|
||||
cancelText: "Cancel",
|
||||
onOk: async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await modelDeleteCall(accessToken, selectedAgentModelId);
|
||||
NotificationsManager.success("Agent deleted");
|
||||
const list = await loadAgents();
|
||||
const remaining = list.filter((a) => getAgentModelId(a) !== selectedAgentModelId);
|
||||
setSelectedId(remaining.length > 0 ? getAgentSelectionKey(remaining[0]) : null);
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Failed to delete agent");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
setConfirmingDelete(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!selectedAgent || !selectedAgentModelId || !accessToken) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await modelDeleteCall(accessToken, selectedAgentModelId);
|
||||
NotificationsManager.success("Agent deleted");
|
||||
const list = await loadAgents();
|
||||
const remaining = list.filter((a) => getAgentModelId(a) !== selectedAgentModelId);
|
||||
setSelectedId(remaining.length > 0 ? getAgentSelectionKey(remaining[0]) : null);
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Failed to delete agent");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
setConfirmingDelete(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!accessToken || !userID || !userRole) {
|
||||
|
|
@ -446,13 +457,8 @@ export default function AgentBuilderView({
|
|||
<div className="flex h-12 items-center justify-between px-4">
|
||||
<span className="text-sm font-medium text-gray-900">Agent Builder</span>
|
||||
{isNewAgent ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSaveAgent}
|
||||
loading={saving}
|
||||
disabled={!draftName?.trim() || !draftUnderlyingModel}
|
||||
>
|
||||
<Button onClick={handleSaveAgent} disabled={saving || !draftName?.trim() || !draftUnderlyingModel}>
|
||||
<Save />
|
||||
Save Agent
|
||||
</Button>
|
||||
) : (
|
||||
|
|
@ -460,7 +466,7 @@ export default function AgentBuilderView({
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-t border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800">
|
||||
<ExperimentOutlined className="shrink-0 text-amber-600" />
|
||||
<FlaskConical className="size-4 shrink-0 text-amber-600" />
|
||||
<span>
|
||||
Agent Builder is experimental and may change or be removed without notice. We’d love your feedback—email us
|
||||
at{" "}
|
||||
|
|
@ -477,12 +483,14 @@ export default function AgentBuilderView({
|
|||
<div className="w-60 shrink-0 border-r border-gray-200 bg-white flex flex-col">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-gray-500">Agents</span>
|
||||
<Button type="text" size="small" icon={<PlusOutlined />} onClick={handleAddAgent} aria-label="Add agent" />
|
||||
<Button variant="ghost" size="icon-sm" onClick={handleAddAgent} aria-label="Add agent">
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{loadingAgents ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Spin size="small" />
|
||||
<div className="flex justify-center py-4" aria-busy="true">
|
||||
<UiLoadingSpinner className="size-4 text-gray-400" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -509,7 +517,7 @@ export default function AgentBuilderView({
|
|||
onClick={handleAddAgent}
|
||||
className="mb-1 w-full rounded-md border border-dashed border-gray-300 px-3 py-2 text-left text-sm text-gray-500 hover:border-blue-400 hover:bg-blue-50/50 hover:text-gray-700"
|
||||
>
|
||||
<PlusOutlined className="mr-1" /> New agent
|
||||
<Plus className="mr-1 inline size-4" /> New agent
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -526,227 +534,231 @@ export default function AgentBuilderView({
|
|||
{(selectedId !== null || isNewAgent) && (
|
||||
<>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(k) => setActiveTab(k as "configure" | "chat" | "test" | "connect")}
|
||||
className="flex-1 overflow-hidden [&_.ant-tabs-content]:h-full [&_.ant-tabs-tabpane]:h-full [&_.ant-tabs-nav]:pl-4"
|
||||
items={[
|
||||
{
|
||||
key: "configure",
|
||||
label: (
|
||||
<span>
|
||||
<RobotOutlined className="mr-1" /> Configure
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
{isNewAgent || selectedAgent ? (
|
||||
<div className="mx-auto max-w-xl space-y-4">
|
||||
{!selectedAgentModelId && selectedAgent && (
|
||||
<div className="rounded-sm border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||
This agent cannot be updated or deleted here (missing model id). Manage it from Models
|
||||
& Endpoints.
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Agent name</label>
|
||||
<Input
|
||||
value={draftName}
|
||||
onChange={(e) => setDraftName(e.target.value)}
|
||||
placeholder="My Agent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">System prompt</label>
|
||||
<TextArea
|
||||
value={draftSystemPrompt}
|
||||
onChange={(e) => setDraftSystemPrompt(e.target.value)}
|
||||
placeholder="You are a helpful assistant..."
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Underlying LLM</label>
|
||||
<Select
|
||||
value={draftUnderlyingModel}
|
||||
onChange={setDraftUnderlyingModel}
|
||||
className="w-full"
|
||||
options={modelGroups.map((m) => ({ value: m.model_group, label: m.model_group }))}
|
||||
placeholder="Select model"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Temperature</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={draftTemperature}
|
||||
onChange={(e) => setDraftTemperature(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Max tokens</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={draftMaxTokens}
|
||||
onChange={(e) => setDraftMaxTokens(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">MCP servers</label>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select MCP servers to attach (same format as chat completions API)"
|
||||
value={selectedMCPServerIds}
|
||||
onChange={handleMCPServerChange}
|
||||
loading={loadingMCPServers}
|
||||
className="w-full"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={mcpServers.map((s) => ({
|
||||
value: s.server_id,
|
||||
label: s.alias || s.server_name || s.server_id,
|
||||
}))}
|
||||
/>
|
||||
{selectedAgent && draftTools.length > 0 && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{draftTools.length} MCP server{draftTools.length !== 1 ? "s" : ""} saved. Use the same{" "}
|
||||
<code className="rounded-sm bg-gray-100 px-1">tools</code> array in chat completions
|
||||
when calling this agent.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{selectedAgent && (
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
{selectedAgentModelId && (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleUpdateAgent}
|
||||
loading={saving}
|
||||
disabled={!draftName?.trim() || !draftUnderlyingModel}
|
||||
>
|
||||
Update Agent
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDeleteAgent}
|
||||
loading={deleting}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button type="primary" icon={<CommentOutlined />} onClick={() => setActiveTab("chat")}>
|
||||
Test in Chat
|
||||
value={activeTab}
|
||||
onValueChange={(value) => goToTab(value as AgentTab)}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<TabsList variant="line" className="h-auto w-full justify-start rounded-none border-b p-0 pl-4">
|
||||
<TabsTrigger value="configure" className="flex-none rounded-none px-4 py-2">
|
||||
<Bot />
|
||||
Configure
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="chat" disabled={isNewAgent} className="flex-none rounded-none px-4 py-2">
|
||||
<MessageSquare />
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="test" disabled={isNewAgent} className="flex-none rounded-none px-4 py-2">
|
||||
<FlaskConical />
|
||||
Batch Test
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="connect" disabled={isNewAgent} className="flex-none rounded-none px-4 py-2">
|
||||
<LinkIcon />
|
||||
Connect
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
value="configure"
|
||||
keepMounted={hasVisited("configure")}
|
||||
className="min-h-0 overflow-hidden"
|
||||
>
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
{isNewAgent || selectedAgent ? (
|
||||
<div className="mx-auto max-w-xl space-y-4">
|
||||
{!selectedAgentModelId && selectedAgent && (
|
||||
<div className="rounded-sm border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||
This agent cannot be updated or deleted here (missing model id). Manage it from Models &
|
||||
Endpoints.
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Agent name</label>
|
||||
<Input
|
||||
value={draftName}
|
||||
onChange={(e) => setDraftName(e.target.value)}
|
||||
placeholder="My Agent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">System prompt</label>
|
||||
<Textarea
|
||||
value={draftSystemPrompt}
|
||||
onChange={(e) => setDraftSystemPrompt(e.target.value)}
|
||||
placeholder="You are a helpful assistant..."
|
||||
rows={6}
|
||||
className="field-sizing-fixed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Underlying LLM</label>
|
||||
<Select
|
||||
value={draftUnderlyingModel ?? null}
|
||||
onValueChange={(model: string | null) => setDraftUnderlyingModel(model ?? undefined)}
|
||||
>
|
||||
<SelectTrigger className="w-full" aria-label="Underlying LLM">
|
||||
<SelectValue placeholder="Select model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{modelGroups.map((m) => (
|
||||
<SelectItem key={m.model_group} value={m.model_group}>
|
||||
{m.model_group}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Temperature</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
value={draftTemperature}
|
||||
onChange={(e) => setDraftTemperature(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Max tokens</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={draftMaxTokens}
|
||||
onChange={(e) => setDraftMaxTokens(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">MCP servers</label>
|
||||
<MultiSelect
|
||||
placeholder="Select MCP servers to attach (same format as chat completions API)"
|
||||
value={selectedMCPServerIds}
|
||||
onValueChange={handleMCPServerChange}
|
||||
loading={loadingMCPServers}
|
||||
className="w-full"
|
||||
options={mcpServers.map((s) => ({
|
||||
value: s.server_id,
|
||||
label: s.alias || s.server_name || s.server_id,
|
||||
}))}
|
||||
/>
|
||||
{selectedAgent && draftTools.length > 0 && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{draftTools.length} MCP server{draftTools.length !== 1 ? "s" : ""} saved. Use the same{" "}
|
||||
<code className="rounded-sm bg-gray-100 px-1">tools</code> array in chat completions when
|
||||
calling this agent.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{selectedAgent && (
|
||||
<div className="flex flex-wrap items-center gap-2 pt-2">
|
||||
{selectedAgentModelId && (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleUpdateAgent}
|
||||
disabled={saving || !draftName?.trim() || !draftUnderlyingModel}
|
||||
>
|
||||
<Save />
|
||||
Update Agent
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="destructive" onClick={handleDeleteAgent} disabled={deleting}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "chat",
|
||||
label: (
|
||||
<span>
|
||||
<CommentOutlined className="mr-1" /> Chat
|
||||
</span>
|
||||
),
|
||||
disabled: isNewAgent,
|
||||
children: (
|
||||
<div className="flex h-full flex-col min-h-0">
|
||||
{selectedAgent ? (
|
||||
<ChatUI
|
||||
key={selectedAgent.model_name}
|
||||
simplified
|
||||
fixedModel={selectedAgent.model_name}
|
||||
accessToken={accessToken}
|
||||
token={token}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Save an agent first to test in Chat.
|
||||
<Button onClick={() => goToTab("chat")}>
|
||||
<MessageSquare />
|
||||
Test in Chat
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "test",
|
||||
label: (
|
||||
<span>
|
||||
<ExperimentOutlined className="mr-1" /> Batch Test
|
||||
</span>
|
||||
),
|
||||
disabled: isNewAgent,
|
||||
children: (
|
||||
<div className="flex h-full flex-col min-h-0">
|
||||
{selectedAgent ? (
|
||||
<ComplianceUI
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
backendMode="chat_completions"
|
||||
fixedModel={selectedAgent.model_name}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Select an agent to run batch tests.
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="chat" keepMounted={hasVisited("chat")} className="min-h-0 overflow-hidden">
|
||||
<div className="flex h-full flex-col min-h-0">
|
||||
{selectedAgent ? (
|
||||
<ChatUI
|
||||
key={selectedAgent.model_name}
|
||||
simplified
|
||||
fixedModel={selectedAgent.model_name}
|
||||
accessToken={accessToken}
|
||||
token={token}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Save an agent first to test in Chat.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "connect",
|
||||
label: (
|
||||
<span>
|
||||
<LinkOutlined className="mr-1" /> Connect
|
||||
</span>
|
||||
),
|
||||
disabled: isNewAgent,
|
||||
children: (
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
{selectedAgent ? (
|
||||
<ConnectTabContent
|
||||
agentName={selectedAgent.model_name}
|
||||
proxySettings={proxySettings}
|
||||
customProxyBaseUrl={customProxyBaseUrl}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
creatingKey={creatingKey}
|
||||
createdKeyValue={createdKeyValue}
|
||||
onCreateKey={handleCreateKeyForAgent}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Select an agent to see how to connect.
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="test" keepMounted={hasVisited("test")} className="min-h-0 overflow-hidden">
|
||||
<div className="flex h-full flex-col min-h-0">
|
||||
{selectedAgent ? (
|
||||
<ComplianceUI
|
||||
accessToken={accessToken}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
backendMode="chat_completions"
|
||||
fixedModel={selectedAgent.model_name}
|
||||
proxySettings={proxySettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Select an agent to run batch tests.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="connect" keepMounted={hasVisited("connect")} className="min-h-0 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
{selectedAgent ? (
|
||||
<ConnectTabContent
|
||||
agentName={selectedAgent.model_name}
|
||||
proxySettings={proxySettings}
|
||||
customProxyBaseUrl={customProxyBaseUrl}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
disabledPersonalKeyCreation={disabledPersonalKeyCreation}
|
||||
creatingKey={creatingKey}
|
||||
createdKeyValue={createdKeyValue}
|
||||
onCreateKey={handleCreateKeyForAgent}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center text-gray-500">
|
||||
Select an agent to see how to connect.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={confirmingDelete} onOpenChange={setConfirmingDelete}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete agent</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{selectedAgent?.model_name}"? This cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction variant="outline">Cancel</AlertDialogAction>
|
||||
{/* Not AlertDialogAction: that closes on click, and the dialog must stay up while the delete is in flight */}
|
||||
<Button variant="destructive" onClick={handleConfirmDelete} disabled={deleting}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { AudioMutedOutlined, AudioOutlined, CloseCircleOutlined, SendOutlined, SoundOutlined } from "@ant-design/icons";
|
||||
import { Button, Input, Select, Typography } from "antd";
|
||||
import { CircleX, Mic, MicOff, Send, Volume2 } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { getProxyBaseUrl } from "@/components/networking";
|
||||
import { OPEN_AI_VOICE_SELECT_OPTIONS } from "./chatConstants";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface RealtimeMessage {
|
||||
role: "user" | "assistant" | "system" | "status";
|
||||
content: string;
|
||||
|
|
@ -364,28 +364,37 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
|
|||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<SoundOutlined className="text-lg text-blue-500" />
|
||||
<Text className="font-semibold text-gray-800">Realtime Voice Chat</Text>
|
||||
<Volume2 className="size-5 text-blue-500" />
|
||||
<span className="font-semibold text-gray-800">Realtime Voice Chat</span>
|
||||
<span className={`inline-block w-2 h-2 rounded-full ${isConnected ? "bg-green-500" : "bg-gray-300"}`} />
|
||||
<Text className="text-xs text-gray-500">
|
||||
<span className="text-xs text-gray-500">
|
||||
{isConnected ? "Connected" : isConnecting ? "Connecting..." : "Disconnected"}
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
size="small"
|
||||
value={selectedVoice}
|
||||
onChange={setSelectedVoice}
|
||||
options={OPEN_AI_VOICE_SELECT_OPTIONS}
|
||||
style={{ width: 220 }}
|
||||
onValueChange={(voice) => setSelectedVoice(voice ?? selectedVoice)}
|
||||
disabled={isConnected}
|
||||
/>
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-[220px]" aria-label="Voice">
|
||||
<SelectValue>{OPEN_AI_VOICE_SELECT_OPTIONS.find((v) => v.value === selectedVoice)?.label}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OPEN_AI_VOICE_SELECT_OPTIONS.map((voice) => (
|
||||
<SelectItem key={voice.value} value={voice.value}>
|
||||
{voice.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{!isConnected ? (
|
||||
<Button type="primary" onClick={connect} loading={isConnecting} size="small">
|
||||
<Button onClick={connect} disabled={isConnecting} size="sm">
|
||||
Connect
|
||||
</Button>
|
||||
) : (
|
||||
<Button danger onClick={disconnect} size="small" icon={<CloseCircleOutlined />}>
|
||||
<Button variant="destructive" onClick={disconnect} size="sm">
|
||||
<CircleX />
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -396,12 +405,12 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
|
|||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{messages.length === 0 && !isConnected && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 gap-3">
|
||||
<SoundOutlined style={{ fontSize: 48 }} />
|
||||
<Text className="text-lg text-gray-500">Realtime Voice Playground</Text>
|
||||
<Text className="text-sm text-gray-400 text-center max-w-md">
|
||||
<Volume2 className="size-12" />
|
||||
<span className="text-lg text-gray-500">Realtime Voice Playground</span>
|
||||
<p className="text-sm text-gray-400 text-center max-w-md">
|
||||
Click <b>Connect</b> to start a realtime session. You can speak using your microphone or type messages.
|
||||
The AI will respond with voice and text.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
|
|
@ -433,30 +442,26 @@ const RealtimePlayground: React.FC<RealtimePlaygroundProps> = ({
|
|||
<div className="border-t border-gray-200 p-3 bg-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
shape="circle"
|
||||
size="large"
|
||||
type={isRecording ? "primary" : "default"}
|
||||
danger={isRecording}
|
||||
icon={isRecording ? <AudioMutedOutlined /> : <AudioOutlined />}
|
||||
size="icon-lg"
|
||||
variant={isRecording ? "destructive" : "outline"}
|
||||
onClick={isRecording ? stopRecording : startRecording}
|
||||
title={isRecording ? "Stop recording" : "Start recording"}
|
||||
className={isRecording ? "animate-pulse" : ""}
|
||||
/>
|
||||
className={`rounded-full ${isRecording ? "animate-pulse" : ""}`}
|
||||
>
|
||||
{isRecording ? <MicOff /> : <Mic />}
|
||||
</Button>
|
||||
<Input
|
||||
placeholder="Type a message or use the mic..."
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
onPressEnter={sendTextMessage}
|
||||
className="flex-1"
|
||||
size="large"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
onClick={sendTextMessage}
|
||||
disabled={!inputText.trim()}
|
||||
size="large"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") sendTextMessage();
|
||||
}}
|
||||
className="h-10 flex-1"
|
||||
/>
|
||||
<Button size="icon-lg" onClick={sendTextMessage} disabled={!inputText.trim()} aria-label="Send">
|
||||
<Send />
|
||||
</Button>
|
||||
</div>
|
||||
{isRecording && (
|
||||
<div className="mt-2 flex items-center gap-2 text-red-500 text-xs">
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants";
|
||||
import { ClearOutlined, DeleteOutlined, FilePdfOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Eraser, FileText, Plus, Trash2 } from "lucide-react";
|
||||
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
|
||||
import { Button, Input, Select, Tooltip } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import ChatImageUpload from "../chat_ui/ChatImageUpload";
|
||||
|
|
@ -692,17 +695,22 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
|
|||
<span className="text-sm font-medium text-gray-600">Virtual Key Source</span>
|
||||
<Select
|
||||
value={apiKeySource}
|
||||
onChange={(value) => setApiKeySource(value as "session" | "custom")}
|
||||
onValueChange={(value) => setApiKeySource(value as "session" | "custom")}
|
||||
disabled={disabledPersonalKeyCreation}
|
||||
className="w-48"
|
||||
>
|
||||
<Select.Option value="session" disabled={!canUseSessionKey}>
|
||||
Current UI Session
|
||||
</Select.Option>
|
||||
<Select.Option value="custom">Virtual Key</Select.Option>
|
||||
<SelectTrigger className="w-48" aria-label="Virtual Key Source">
|
||||
<SelectValue>{apiKeySource === "custom" ? "Virtual Key" : "Current UI Session"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="session" disabled={!canUseSessionKey}>
|
||||
Current UI Session
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">Virtual Key</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{apiKeySource === "custom" && (
|
||||
<Input.Password
|
||||
<Input
|
||||
type="password"
|
||||
value={customApiKey}
|
||||
onChange={(event) => setCustomApiKey(event.target.value)}
|
||||
placeholder="Enter Virtual Key"
|
||||
|
|
@ -712,30 +720,35 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-600">Endpoint</span>
|
||||
<Select
|
||||
value={selectedEndpoint}
|
||||
onChange={(value) => setSelectedEndpoint(value as EndpointIdType)}
|
||||
className="w-56"
|
||||
>
|
||||
{getAvailableEndpoints().map((endpoint) => (
|
||||
<Select.Option key={endpoint.value} value={endpoint.value}>
|
||||
{endpoint.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
<Select value={selectedEndpoint} onValueChange={(value) => setSelectedEndpoint(value as EndpointIdType)}>
|
||||
<SelectTrigger className="w-56" aria-label="Endpoint">
|
||||
<SelectValue>{endpointConfig.label}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getAvailableEndpoints().map((endpoint) => (
|
||||
<SelectItem key={endpoint.value} value={endpoint.value}>
|
||||
{endpoint.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={clearAllChats} disabled={!hasMessages} icon={<ClearOutlined />}>
|
||||
<Button variant="outline" onClick={clearAllChats} disabled={!hasMessages}>
|
||||
<Eraser />
|
||||
Clear All Chats
|
||||
</Button>
|
||||
<Tooltip
|
||||
title={
|
||||
comparisons.length >= maxComparisons ? "Compare up to 3 models at a time" : "Add another comparison"
|
||||
}
|
||||
>
|
||||
<Button onClick={addComparison} disabled={comparisons.length >= maxComparisons} icon={<PlusOutlined />}>
|
||||
Add Comparison
|
||||
</Button>
|
||||
<Tooltip>
|
||||
{/* A disabled button emits no pointer events, so the hover target has to be a wrapper */}
|
||||
<TooltipTrigger render={<span className="inline-flex" />}>
|
||||
<Button variant="outline" onClick={addComparison} disabled={comparisons.length >= maxComparisons}>
|
||||
<Plus />
|
||||
Add Comparison
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{comparisons.length >= maxComparisons ? "Compare up to 3 models at a time" : "Add another comparison"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -807,8 +820,8 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
|
|||
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="relative inline-block">
|
||||
{isUploadedFilePdf ? (
|
||||
<div className="w-10 h-10 rounded-md bg-red-500 flex items-center justify-center">
|
||||
<FilePdfOutlined style={{ fontSize: "16px", color: "white" }} />
|
||||
<div className="w-10 h-10 rounded-md bg-red-500 flex items-center justify-center text-white">
|
||||
<FileText className="size-4" aria-label="file-pdf" />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
|
|
@ -825,8 +838,9 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
|
|||
<button
|
||||
className="flex items-center justify-center w-6 h-6 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-full transition-colors"
|
||||
onClick={handleRemoveFile}
|
||||
aria-label="Remove attachment"
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: "12px" }} />
|
||||
<Trash2 className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { Settings, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useId, useState } from "react";
|
||||
import { ComparisonInstance } from "../CompareUI";
|
||||
import { MessageDisplay } from "./MessageDisplay";
|
||||
import { UnifiedSelector } from "./UnifiedSelector";
|
||||
import TagSelector from "@/components/tag_management/TagSelector";
|
||||
import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector";
|
||||
import GuardrailSelector from "@/components/guardrails/GuardrailSelector";
|
||||
import { Checkbox, Divider, Popover, Slider } from "antd";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { SelectorOption, EndpointConfig, isAgentEndpoint, getComparisonSelection } from "../endpoint_config";
|
||||
|
||||
interface ComparisonPanelProps {
|
||||
|
|
@ -35,6 +38,8 @@ export function ComparisonPanel({
|
|||
const isA2AMode = isAgentEndpoint(endpointConfig.id);
|
||||
const currentSelection = getComparisonSelection(comparison, endpointConfig.id);
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const syncId = useId();
|
||||
const advancedParamsId = useId();
|
||||
|
||||
const handleSyncChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
|
|
@ -103,12 +108,18 @@ export function ComparisonPanel({
|
|||
<div className="space-y-2">
|
||||
{/* Sync Checkbox */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox checked={comparison.applyAcrossModels} onChange={(e) => handleSyncChange(e.target.checked)}>
|
||||
<span className="text-xs font-medium">Sync Settings Across Models</span>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
id={syncId}
|
||||
checked={comparison.applyAcrossModels}
|
||||
onCheckedChange={handleSyncChange}
|
||||
aria-label="Sync Settings Across Models"
|
||||
/>
|
||||
<label htmlFor={syncId} className="cursor-pointer text-xs font-medium">
|
||||
Sync Settings Across Models
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Divider className="border-gray-200" />
|
||||
<Separator className="my-3" />
|
||||
|
||||
{/* General Settings */}
|
||||
<div>
|
||||
|
|
@ -146,11 +157,14 @@ export function ComparisonPanel({
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 pb-1">
|
||||
<Checkbox
|
||||
id={advancedParamsId}
|
||||
checked={comparison.useAdvancedParams}
|
||||
onChange={(e) => handleAdvancedParamsChange(e.target.checked)}
|
||||
>
|
||||
<span className="text-sm font-medium">Use Advanced Parameters</span>
|
||||
</Checkbox>
|
||||
onCheckedChange={handleAdvancedParamsChange}
|
||||
aria-label="Use Advanced Parameters"
|
||||
/>
|
||||
<label htmlFor={advancedParamsId} className="cursor-pointer text-sm font-medium">
|
||||
Use Advanced Parameters
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-2 transition-opacity duration-200" style={{ opacity: disabledOpacity }}>
|
||||
<div>
|
||||
|
|
@ -162,8 +176,8 @@ export function ComparisonPanel({
|
|||
min={0}
|
||||
max={2}
|
||||
step={0.01}
|
||||
value={comparison.temperature}
|
||||
onChange={(value) => {
|
||||
value={[comparison.temperature]}
|
||||
onValueChange={(value) => {
|
||||
const nextValue = Array.isArray(value) ? value[0] : value;
|
||||
const clamped = Math.min(2, Math.max(0, Number(nextValue.toFixed(2))));
|
||||
handleSettingChange("temperature", clamped);
|
||||
|
|
@ -180,8 +194,8 @@ export function ComparisonPanel({
|
|||
min={1}
|
||||
max={32768}
|
||||
step={1}
|
||||
value={comparison.maxTokens}
|
||||
onChange={(value) => {
|
||||
value={[comparison.maxTokens]}
|
||||
onValueChange={(value) => {
|
||||
const nextValue = Array.isArray(value) ? value[0] : value;
|
||||
const clamped = Math.min(32768, Math.max(1, Math.round(nextValue)));
|
||||
handleSettingChange("maxTokens", clamped);
|
||||
|
|
@ -209,26 +223,29 @@ export function ComparisonPanel({
|
|||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Popover
|
||||
content={settingsContent}
|
||||
trigger={[]}
|
||||
open={popoverVisible}
|
||||
onOpenChange={() => {
|
||||
// Prevent automatic closing - we control it manually
|
||||
}}
|
||||
placement="bottomRight"
|
||||
destroyTooltipOnHide={false}
|
||||
>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleTogglePopover();
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
popoverVisible ? "bg-gray-200 text-gray-700" : "hover:bg-gray-100 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleTogglePopover();
|
||||
}}
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
popoverVisible ? "bg-gray-200 text-gray-700" : "hover:bg-gray-100 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent side="bottom" align="end" className="w-auto">
|
||||
{settingsContent}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React from "react";
|
||||
import { Input, Button } from "antd";
|
||||
import { ArrowUpOutlined } from "@ant-design/icons";
|
||||
|
||||
const { TextArea } = Input;
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
interface MessageInputProps {
|
||||
value: string;
|
||||
|
|
@ -29,25 +28,25 @@ export function MessageInput({ value, onChange, onSend, disabled, hasAttachment,
|
|||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center flex-1 bg-white border border-gray-300 rounded-xl px-3 py-1 min-h-[44px]">
|
||||
{uploadComponent && <div className="shrink-0 mr-2">{uploadComponent}</div>}
|
||||
<TextArea
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type your message... (Shift+Enter for new line)"
|
||||
disabled={disabled}
|
||||
className="flex-1"
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
style={{
|
||||
resize: "none",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
background: "transparent",
|
||||
padding: "4px 0",
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
}}
|
||||
rows={1}
|
||||
className="max-h-20 min-h-0 flex-1 resize-none overflow-y-auto border-0 bg-transparent px-0 py-1 text-sm leading-5 shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
<Button onClick={onSend} disabled={!canSend} icon={<ArrowUpOutlined />} shape="circle" />
|
||||
<Button
|
||||
onClick={onSend}
|
||||
disabled={!canSend}
|
||||
size="icon-sm"
|
||||
variant="outline"
|
||||
className="rounded-full"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<ArrowUp />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@
|
|||
* based on the current endpoint configuration.
|
||||
*/
|
||||
|
||||
import { Select, Spin } from "antd";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
} from "@/components/ui/combobox";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { SelectorOption, EndpointConfig } from "../endpoint_config";
|
||||
|
||||
interface UnifiedSelectorProps {
|
||||
|
|
@ -14,26 +22,44 @@ interface UnifiedSelectorProps {
|
|||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const matchesQuery = (option: SelectorOption, query: string): boolean =>
|
||||
option.label.toLowerCase().includes(query.trim().toLowerCase());
|
||||
|
||||
export function UnifiedSelector({ value, options, loading, config, onChange }: UnifiedSelectorProps) {
|
||||
const selected = options.find((option) => option.value === value) ?? null;
|
||||
const noun = config.selectorLabel.toLowerCase();
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value || undefined}
|
||||
placeholder={loading ? `Loading ${config.selectorLabel.toLowerCase()}s...` : config.selectorPlaceholder}
|
||||
onChange={onChange}
|
||||
loading={loading}
|
||||
showSearch
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
options={options}
|
||||
className="w-48 md:w-64 lg:w-72"
|
||||
notFoundContent={
|
||||
loading ? (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
) : (
|
||||
`No ${config.selectorLabel.toLowerCase()}s available`
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Combobox
|
||||
items={options}
|
||||
value={selected}
|
||||
onValueChange={(option: SelectorOption | null) => onChange(option?.value ?? "")}
|
||||
isItemEqualToValue={(a: SelectorOption, b: SelectorOption) => a.value === b.value}
|
||||
itemToStringLabel={(option: SelectorOption) => option.label}
|
||||
filter={matchesQuery}
|
||||
>
|
||||
<ComboboxInput
|
||||
placeholder={loading ? `Loading ${noun}s...` : config.selectorPlaceholder}
|
||||
className="w-48 md:w-64 lg:w-72"
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>
|
||||
{loading ? (
|
||||
<span aria-busy="true" className="flex items-center justify-center py-2">
|
||||
<UiLoadingSpinner className="size-4" />
|
||||
</span>
|
||||
) : (
|
||||
`No ${noun}s available`
|
||||
)}
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(option: SelectorOption) => (
|
||||
<ComboboxItem key={option.value} value={option}>
|
||||
{option.label}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
41
ui/litellm-dashboard/src/components/ui/slider.tsx
Normal file
41
ui/litellm-dashboard/src/components/ui/slider.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
|
||||
|
||||
import { cn } from "@/lib/cva.config";
|
||||
|
||||
function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }: SliderPrimitive.Root.Props) {
|
||||
const _values = Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max];
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
className={cn("data-horizontal:w-full data-vertical:h-full", className)}
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
thumbAlignment="edge"
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className="relative grow overflow-hidden rounded-full bg-muted select-none data-horizontal:h-1.5 data-horizontal:w-full data-vertical:h-full data-vertical:w-1.5"
|
||||
>
|
||||
<SliderPrimitive.Indicator
|
||||
data-slot="slider-range"
|
||||
className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] select-none hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Control>
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider };
|
||||
Loading…
Add table
Reference in a new issue