mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
feat(ui): migrate SearchTools subtree to shadcn
Migrates the SearchTools CRUD surface: - SearchToolColumn.tsx: antd ColumnsType -> tanstack-table ColumnDef<SearchTool>[] (matches the DataTable wrapper already used elsewhere; header/accessorKey/cell shape). - SearchTools.tsx: antd Table -> shadcn DataTable (reusing view_logs/table.tsx which is already tremor-inside-shadcn-chrome). antd Form.useForm (edit modal) -> react-hook-form (FormProvider + register + Controller). antd Input/Select/TextArea -> shadcn Input/ Select/Textarea with Label. Dialog wrapper unchanged. - CreateSearchTools.tsx: antd Modal -> shadcn Dialog. antd Form.useForm + onValuesChange -> react-hook-form (watch() replaces the manual formValues state). antd Select + Option (with logo-prefixed label) -> shadcn Select + SelectItem rendering SearchProviderLabel directly. antd Tooltip + Typography.Link -> shadcn Tooltip + plain anchor. antd Input/Input.Password/Input.TextArea -> shadcn Input (type=password for API key) + Textarea. Form.Item info-icon helper tooltips preserved via shadcn Tooltip. - SearchToolTester.tsx: antd Button/Input/Spin/Typography + tremor Card/Title -> shadcn Button/Input/Card + plain h3/p. Loader2 for the Spin replacement. Categorical palette (blue accent, green success) kept raw with eslint override. SearchToolTester.tsx gets added to .eslintrc.json's no-raw-tailwind-colors override list (blue accent / green success palette on result cards). Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
68bbcae478
commit
298489075a
6 changed files with 620 additions and 556 deletions
|
|
@ -161,7 +161,8 @@
|
|||
"src/components/UsagePage/components/EntityUsage/EntityUsage.tsx",
|
||||
"src/components/UsagePage/components/UsageAIChatPanel.tsx",
|
||||
"src/components/UsagePage/components/UsagePageView.tsx",
|
||||
"src/components/prompts/prompt_info.tsx"
|
||||
"src/components/prompts/prompt_info.tsx",
|
||||
"src/components/SearchTools/SearchToolTester.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"litellm-ui/no-banned-ui-imports": "off",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,38 @@
|
|||
import { isAdminRole } from "@/utils/roles";
|
||||
import { Info as InfoCircleOutlined } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import { Form, Input, Modal, Select, Tooltip, Typography } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import Image from "next/image";
|
||||
import React, { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { createSearchTool, fetchAvailableSearchProviders } from "../networking";
|
||||
import SearchConnectionTest from "./SearchConnectionTest";
|
||||
import { AvailableSearchProvider, SearchTool } from "./types";
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
// Search provider logos folder path (matches existing provider logo pattern)
|
||||
const searchProviderLogosFolder = "../ui/assets/logos/";
|
||||
|
||||
|
|
@ -21,7 +41,6 @@ const getSearchProviderLogo = (providerName: string): string => {
|
|||
return `${searchProviderLogosFolder}${providerName}.png`;
|
||||
};
|
||||
|
||||
// Component to display search provider logo and name
|
||||
interface SearchProviderLabelProps {
|
||||
providerName: string;
|
||||
displayName: string;
|
||||
|
|
@ -54,6 +73,16 @@ interface CreateSearchToolProps {
|
|||
setModalVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
interface CreateFormValues {
|
||||
search_tool_name: string;
|
||||
search_provider: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const SEARCH_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
|
||||
userRole,
|
||||
accessToken,
|
||||
|
|
@ -61,14 +90,20 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
|
|||
isModalVisible,
|
||||
setModalVisible,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const form = useForm<CreateFormValues>({
|
||||
defaultValues: {
|
||||
search_tool_name: "",
|
||||
search_provider: "",
|
||||
api_key: "",
|
||||
api_base: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
||||
const [isTestModalVisible, setIsTestModalVisible] = useState(false);
|
||||
const [isTestingConnection, setIsTestingConnection] = useState(false);
|
||||
const [connectionTestId, setConnectionTestId] = useState<string>("");
|
||||
|
||||
// Fetch available search providers
|
||||
const {
|
||||
data: providersResponse,
|
||||
isLoading: isLoadingProviders,
|
||||
|
|
@ -83,23 +118,20 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
|
|||
|
||||
const availableProviders = providersResponse?.providers || [];
|
||||
|
||||
const handleCreate = async (formValues: Record<string, any>) => {
|
||||
const onSubmit = form.handleSubmit(async (formValues) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Prepare the payload
|
||||
const payload = {
|
||||
search_tool_name: formValues.search_tool_name,
|
||||
litellm_params: {
|
||||
search_provider: formValues.search_provider,
|
||||
api_key: formValues.api_key,
|
||||
api_base: formValues.api_base,
|
||||
timeout: formValues.timeout ? parseFloat(formValues.timeout) : undefined,
|
||||
max_retries: formValues.max_retries ? parseInt(formValues.max_retries) : undefined,
|
||||
},
|
||||
search_tool_info: formValues.description
|
||||
? {
|
||||
description: formValues.description,
|
||||
}
|
||||
description: formValues.description,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
|
|
@ -107,10 +139,8 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
|
|||
|
||||
if (accessToken != null) {
|
||||
const response = await createSearchTool(accessToken, payload);
|
||||
|
||||
NotificationsManager.success("Search tool created successfully");
|
||||
form.resetFields();
|
||||
setFormValues({});
|
||||
form.reset();
|
||||
setModalVisible(false);
|
||||
onCreateSuccess(response);
|
||||
}
|
||||
|
|
@ -119,219 +149,237 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
|
|||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
setFormValues({});
|
||||
form.reset();
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
try {
|
||||
// Validate required fields for testing
|
||||
await form.validateFields(["search_provider", "api_key"]);
|
||||
|
||||
setIsTestingConnection(true);
|
||||
// Generate a new test ID (using timestamp for uniqueness)
|
||||
setConnectionTestId(`test-${Date.now()}`);
|
||||
// Show the modal with the fresh test
|
||||
setIsTestModalVisible(true);
|
||||
} catch (error) {
|
||||
const valid = await form.trigger(["search_provider", "api_key"]);
|
||||
if (!valid) {
|
||||
NotificationsManager.error("Please fill in Search Provider and API Key before testing");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTestingConnection(true);
|
||||
setConnectionTestId(`test-${Date.now()}`);
|
||||
setIsTestModalVisible(true);
|
||||
};
|
||||
|
||||
// Clear formValues when modal closes to reset
|
||||
// Clear form when modal closes to reset
|
||||
React.useEffect(() => {
|
||||
if (!isModalVisible) {
|
||||
setFormValues({});
|
||||
form.reset();
|
||||
}
|
||||
}, [isModalVisible]);
|
||||
}, [isModalVisible, form]);
|
||||
|
||||
const formValues = form.watch();
|
||||
|
||||
if (!isAdminRole(userRole)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
|
||||
<span className="text-2xl">🔍</span>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Add New Search Tool</h2>
|
||||
</div>
|
||||
}
|
||||
open={isModalVisible}
|
||||
width={800}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
className="top-8"
|
||||
styles={{
|
||||
body: { padding: "24px" },
|
||||
header: { padding: "24px 24px 0 24px", border: "none" },
|
||||
}}
|
||||
>
|
||||
<div className="mt-6">
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreate}
|
||||
onValuesChange={(_, allValues) => setFormValues(allValues)}
|
||||
layout="vertical"
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Search Tool Name
|
||||
<Tooltip title="A unique name to identify this search tool configuration (e.g., 'perplexity-search', 'tavily-news-search').">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="search_tool_name"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter a search tool name" },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message: "Name can only contain letters, numbers, hyphens, and underscores",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="e.g., perplexity-search, my-tavily-tool"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
Search Provider
|
||||
<Tooltip title="Select the search provider you want to use. Each provider has different capabilities and pricing.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="search_provider"
|
||||
rules={[{ required: true, message: "Please select a search provider" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select a search provider"
|
||||
className="rounded-lg"
|
||||
size="large"
|
||||
loading={isLoadingProviders}
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
optionLabelProp="label"
|
||||
>
|
||||
{availableProviders.map((provider) => (
|
||||
<Select.Option
|
||||
key={provider.provider_name}
|
||||
value={provider.provider_name}
|
||||
label={
|
||||
<SearchProviderLabel
|
||||
providerName={provider.provider_name}
|
||||
displayName={provider.ui_friendly_name}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SearchProviderLabel
|
||||
providerName={provider.provider_name}
|
||||
displayName={provider.ui_friendly_name}
|
||||
/>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
API Key
|
||||
<Tooltip title="The API key for authenticating with the search provider. This will be securely stored.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="api_key"
|
||||
rules={[{ required: false, message: "Please enter an API key" }]}
|
||||
>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="Enter your API key"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Description (Optional)</span>}
|
||||
name="description"
|
||||
>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder="Brief description of this search tool's purpose"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-6 border-t border-gray-100">
|
||||
<Tooltip title="Get help on our github">
|
||||
<Typography.Link href="https://github.com/BerriAI/litellm/issues" target="_blank">
|
||||
Need Help?
|
||||
</Typography.Link>
|
||||
</Tooltip>
|
||||
<div className="space-x-2">
|
||||
<Button onClick={handleTestConnection} loading={isTestingConnection}>
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button loading={isLoading} type="submit">
|
||||
Add Search Tool
|
||||
</Button>
|
||||
<Dialog open={isModalVisible} onOpenChange={(o) => (!o ? handleCancel() : undefined)}>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle asChild>
|
||||
<div className="flex items-center space-x-3 pb-4 border-b border-border">
|
||||
<span className="text-2xl">🔍</span>
|
||||
<h2 className="text-xl font-semibold text-foreground">Add New Search Tool</h2>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Test Connection Results Modal */}
|
||||
<Modal
|
||||
title="Connection Test Results"
|
||||
open={isTestModalVisible}
|
||||
onCancel={() => {
|
||||
setIsTestModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={onSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-search-tool-name" className="flex items-center">
|
||||
Search Tool Name <span className="text-destructive ml-1">*</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircleOutlined className="ml-2 h-4 w-4 text-muted-foreground hover:text-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
A unique name to identify this search tool configuration
|
||||
(e.g., "perplexity-search", "tavily-news-search").
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="create-search-tool-name"
|
||||
placeholder="e.g., perplexity-search, my-tavily-tool"
|
||||
{...form.register("search_tool_name", {
|
||||
required: "Please enter a search tool name",
|
||||
pattern: {
|
||||
value: SEARCH_TOOL_NAME_PATTERN,
|
||||
message: "Name can only contain letters, numbers, hyphens, and underscores",
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{form.formState.errors.search_tool_name && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.search_tool_name.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-search-provider" className="flex items-center">
|
||||
Search Provider <span className="text-destructive ml-1">*</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircleOutlined className="ml-2 h-4 w-4 text-muted-foreground hover:text-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Select the search provider you want to use. Each provider has different capabilities and pricing.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="search_provider"
|
||||
rules={{ required: "Please select a search provider" }}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange} disabled={isLoadingProviders}>
|
||||
<SelectTrigger id="create-search-provider">
|
||||
<SelectValue placeholder="Select a search provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableProviders.map((provider) => (
|
||||
<SelectItem key={provider.provider_name} value={provider.provider_name}>
|
||||
<SearchProviderLabel
|
||||
providerName={provider.provider_name}
|
||||
displayName={provider.ui_friendly_name}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
{form.formState.errors.search_provider && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.search_provider.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-api-key" className="flex items-center">
|
||||
API Key
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircleOutlined className="ml-2 h-4 w-4 text-muted-foreground hover:text-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
The API key for authenticating with the search provider. This will be securely stored.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="create-api-key"
|
||||
type="password"
|
||||
placeholder="Enter your API key"
|
||||
{...form.register("api_key")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-description">Description (Optional)</Label>
|
||||
<Textarea
|
||||
id="create-description"
|
||||
rows={3}
|
||||
placeholder="Brief description of this search tool's purpose"
|
||||
{...form.register("description")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-6 border-t border-border flex !justify-between">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href="https://github.com/BerriAI/litellm/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline text-sm"
|
||||
>
|
||||
Need Help?
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Get help on our github</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={isTestingConnection}
|
||||
>
|
||||
{isTestingConnection ? "Testing..." : "Test Connection"}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? "Adding..." : "Add Search Tool"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
{/* Test Connection Results Dialog */}
|
||||
<Dialog
|
||||
open={isTestModalVisible}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) {
|
||||
setIsTestModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{/* Only render the SearchConnectionTest when modal is visible and we have a test ID */}
|
||||
{isTestModalVisible && accessToken && (
|
||||
<SearchConnectionTest
|
||||
key={connectionTestId}
|
||||
litellmParams={{
|
||||
search_provider: formValues.search_provider,
|
||||
api_key: formValues.api_key,
|
||||
api_base: formValues.api_base,
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
onTestComplete={() => setIsTestingConnection(false)}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</Modal>
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connection Test Results</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isTestModalVisible && accessToken && (
|
||||
<SearchConnectionTest
|
||||
key={connectionTestId}
|
||||
litellmParams={{
|
||||
search_provider: formValues.search_provider,
|
||||
api_key: formValues.api_key,
|
||||
api_base: formValues.api_base,
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
onTestComplete={() => setIsTestingConnection(false)}
|
||||
/>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsTestModalVisible(false);
|
||||
setIsTestingConnection(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateSearchTool;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ColumnsType } from "antd/es/table";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
|
||||
import { SearchTool } from "./types";
|
||||
|
||||
|
|
@ -9,115 +9,116 @@ export const searchToolColumns = (
|
|||
onEdit: (searchToolId: string) => void,
|
||||
onDelete: (searchToolId: string) => void,
|
||||
availableProviders: Array<{ provider_name: string; ui_friendly_name: string }>,
|
||||
): ColumnsType<SearchTool> => [
|
||||
{
|
||||
title: "Search Tool ID",
|
||||
dataIndex: "search_tool_id",
|
||||
key: "search_tool_id",
|
||||
render: (_, tool) => {
|
||||
const isFromConfig = tool.is_from_config;
|
||||
): ColumnDef<SearchTool>[] => [
|
||||
{
|
||||
header: "Search Tool ID",
|
||||
accessorKey: "search_tool_id",
|
||||
cell: ({ row }) => {
|
||||
const tool = row.original;
|
||||
const isFromConfig = tool.is_from_config;
|
||||
|
||||
if (isFromConfig) {
|
||||
return <span className="text-xs">-</span>;
|
||||
}
|
||||
if (isFromConfig) {
|
||||
return <span className="text-xs">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onView(tool.search_tool_id!)}
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 text-xs font-normal px-2 py-0.5 text-left cursor-pointer max-w-40 rounded"
|
||||
>
|
||||
<span className="truncate block">{tool.search_tool_id}</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onView(tool.search_tool_id!)}
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 text-xs font-normal px-2 py-0.5 text-left cursor-pointer max-w-40 rounded"
|
||||
>
|
||||
<span className="truncate block">{tool.search_tool_id}</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "search_tool_name",
|
||||
key: "search_tool_name",
|
||||
render: (name: string) => <span className="font-medium">{name}</span>,
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "search_tool_name",
|
||||
cell: (info) => <span className="font-medium">{info.getValue() as string}</span>,
|
||||
},
|
||||
{
|
||||
header: "Provider",
|
||||
id: "provider",
|
||||
cell: ({ row }) => {
|
||||
const tool = row.original;
|
||||
const provider = tool.litellm_params.search_provider;
|
||||
const providerInfo = availableProviders.find((p) => p.provider_name === provider);
|
||||
const displayName = providerInfo?.ui_friendly_name || provider;
|
||||
return <span className="text-sm">{displayName}</span>;
|
||||
},
|
||||
{
|
||||
title: "Provider",
|
||||
key: "provider",
|
||||
render: (_, tool) => {
|
||||
const provider = tool.litellm_params.search_provider;
|
||||
const providerInfo = availableProviders.find((p) => p.provider_name === provider);
|
||||
const displayName = providerInfo?.ui_friendly_name || provider;
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-xs">
|
||||
{row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Updated At",
|
||||
accessorKey: "updated_at",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-xs">
|
||||
{row.original.updated_at ? new Date(row.original.updated_at).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Source",
|
||||
id: "source",
|
||||
cell: ({ row }) => {
|
||||
const isFromConfig = row.original.is_from_config ?? false;
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"text-xs",
|
||||
isFromConfig
|
||||
? "bg-muted text-muted-foreground"
|
||||
: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
|
||||
)}
|
||||
>
|
||||
{isFromConfig ? "Config" : "DB"}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const tool = row.original;
|
||||
const toolId = tool.search_tool_id;
|
||||
const isFromConfig = tool.is_from_config ?? false;
|
||||
|
||||
return <span className="text-sm">{displayName}</span>;
|
||||
},
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit search tool"
|
||||
disabled={isFromConfig}
|
||||
disabledTooltipText="Config search tool cannot be edited on the dashboard. Please edit it from the config file."
|
||||
onClick={() => {
|
||||
if (toolId && !isFromConfig) {
|
||||
onEdit(toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete search tool"
|
||||
disabled={isFromConfig}
|
||||
disabledTooltipText="Config search tool cannot be deleted on the dashboard. Please delete it from the config file."
|
||||
onClick={() => {
|
||||
if (toolId && !isFromConfig) {
|
||||
onDelete(toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "Created At",
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
render: (_, tool) => {
|
||||
return <span className="text-xs">{tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Updated At",
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
render: (_, tool) => {
|
||||
return <span className="text-xs">{tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
key: "source",
|
||||
render: (_, tool) => {
|
||||
const isFromConfig = tool.is_from_config ?? false;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"text-xs",
|
||||
isFromConfig
|
||||
? "bg-muted text-muted-foreground"
|
||||
: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
|
||||
)}
|
||||
>
|
||||
{isFromConfig ? "Config" : "DB"}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
key: "actions",
|
||||
render: (_, tool) => {
|
||||
const toolId = tool.search_tool_id;
|
||||
const isFromConfig = tool.is_from_config ?? false;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit search tool"
|
||||
disabled={isFromConfig}
|
||||
disabledTooltipText="Config search tool cannot be edited on the dashboard. Please edit it from the config file."
|
||||
onClick={() => {
|
||||
if (toolId && !isFromConfig) {
|
||||
onEdit(toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete search tool"
|
||||
disabled={isFromConfig}
|
||||
disabledTooltipText="Config search tool cannot be deleted on the dashboard. Please delete it from the config file."
|
||||
onClick={() => {
|
||||
if (toolId && !isFromConfig) {
|
||||
onDelete(toolId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button, Input, Typography, Spin } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { Search as SearchOutlined, Loader2 as LoadingOutlined } from "lucide-react";
|
||||
import { Loader2, Search as SearchOutlined } from "lucide-react";
|
||||
import { searchToolQueryCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { Card, Title as TremorTitle } from "@tremor/react";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SearchResult {
|
||||
title: string;
|
||||
|
|
@ -61,7 +59,6 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
};
|
||||
|
||||
setSearchHistory((prev) => [historyEntry, ...prev]);
|
||||
// Don't clear query after search so user can modify it
|
||||
} catch (error) {
|
||||
console.error("Error querying search tool:", error);
|
||||
NotificationsManager.fromBackend("Failed to query search tool");
|
||||
|
|
@ -88,64 +85,58 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
}));
|
||||
};
|
||||
|
||||
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} className="animate-spin" />;
|
||||
|
||||
const latestResults = searchHistory.length > 0 ? searchHistory[0] : null;
|
||||
|
||||
return (
|
||||
<Card className="mt-6">
|
||||
<Card className={`mt-6 p-6 ${className}`}>
|
||||
<div className="mb-6">
|
||||
<TremorTitle>Test Search Tool</TremorTitle>
|
||||
<h3 className="text-lg font-semibold">Test Search Tool</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col" style={{ minHeight: "600px" }}>
|
||||
{/* Search Bar at Top */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-stretch gap-3">
|
||||
<div
|
||||
className="flex items-center flex-1 bg-white rounded-lg px-4 transition-all duration-200"
|
||||
style={{
|
||||
<div
|
||||
className="flex items-center flex-1 bg-background rounded-lg px-4 transition-all duration-200"
|
||||
style={{
|
||||
border: isInputFocused ? "2px solid #3b82f6" : "2px solid #e5e7eb",
|
||||
boxShadow: isInputFocused ? "0 0 0 3px rgba(59, 130, 246, 0.1)" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
|
||||
height: "48px"
|
||||
height: "48px",
|
||||
}}
|
||||
>
|
||||
<SearchOutlined className="text-gray-400 mr-3" style={{ fontSize: "18px" }} />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="Enter your search query..."
|
||||
disabled={isLoading}
|
||||
bordered={false}
|
||||
style={{ fontSize: "15px", padding: 0, height: "100%", boxShadow: "none" }}
|
||||
/>
|
||||
<SearchOutlined className="text-muted-foreground mr-3 h-[18px] w-[18px]" />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="Enter your search query..."
|
||||
disabled={isLoading}
|
||||
className="border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-[15px] h-full p-0"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSearch}
|
||||
disabled={isLoading || !query.trim()}
|
||||
icon={<SearchOutlined />}
|
||||
loading={isLoading}
|
||||
style={{
|
||||
height: "48px",
|
||||
paddingLeft: "24px",
|
||||
paddingRight: "24px",
|
||||
borderRadius: "8px",
|
||||
fontWeight: 500,
|
||||
fontSize: "15px",
|
||||
backgroundColor: isLoading || !query.trim() ? undefined : "#1890ff",
|
||||
borderColor: isLoading || !query.trim() ? undefined : "#1890ff",
|
||||
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<SearchOutlined className="h-4 w-4" />
|
||||
)}
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -155,42 +146,46 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
<div className="flex-1">
|
||||
{!latestResults && !isLoading ? (
|
||||
<div className="h-full flex flex-col items-center justify-center p-8">
|
||||
<div className="flex items-center justify-center w-24 h-24 rounded-full bg-gray-100 mb-6">
|
||||
<SearchOutlined style={{ fontSize: "48px", color: "#9ca3af" }} />
|
||||
<div className="flex items-center justify-center w-24 h-24 rounded-full bg-muted mb-6">
|
||||
<SearchOutlined className="h-12 w-12 text-muted-foreground" />
|
||||
</div>
|
||||
<Text className="text-lg text-gray-600 font-medium">Test your search tool</Text>
|
||||
<Text className="text-sm text-gray-500 mt-2">Enter a query above to see search results</Text>
|
||||
<p className="text-lg text-foreground font-medium">Test your search tool</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">Enter a query above to see search results</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{isLoading && (
|
||||
<div className="flex flex-col justify-center items-center py-16">
|
||||
<Spin indicator={antIcon} />
|
||||
<Text className="mt-4 text-gray-600 font-medium">Searching...</Text>
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<p className="mt-4 text-muted-foreground font-medium">Searching...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{latestResults && !isLoading && (
|
||||
<>
|
||||
{/* Query Info Bar */}
|
||||
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg" style={{ boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}>
|
||||
<div
|
||||
className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-blue-950/30 dark:border-blue-900"
|
||||
style={{ boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<Text className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Search Query</Text>
|
||||
<div className="text-base font-semibold text-gray-900 mt-1.5">{latestResults.query}</div>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
Search Query
|
||||
</p>
|
||||
<div className="text-base font-semibold text-foreground mt-1.5">{latestResults.query}</div>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<Text className="text-xs text-gray-500">{formatTimestamp(latestResults.timestamp)}</Text>
|
||||
<p className="text-xs text-muted-foreground">{formatTimestamp(latestResults.timestamp)}</p>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<div className="text-sm font-semibold text-blue-600">
|
||||
{latestResults.response?.results?.length || 0} {latestResults.response?.results?.length === 1 ? 'result' : 'results'}
|
||||
{latestResults.response?.results?.length || 0}{" "}
|
||||
{latestResults.response?.results?.length === 1 ? "result" : "results"}
|
||||
</div>
|
||||
{latestResults.latency !== undefined && (
|
||||
<>
|
||||
<span className="text-gray-400">•</span>
|
||||
<div className="text-sm font-semibold text-green-600">
|
||||
{latestResults.latency}ms
|
||||
</div>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<div className="text-sm font-semibold text-green-600">{latestResults.latency}ms</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -205,17 +200,16 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
const isResultExpanded = expandedResults[`0-${resultIndex}`] || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={resultIndex}
|
||||
className="bg-white border border-gray-200 rounded-lg overflow-hidden transition-all duration-200"
|
||||
<div
|
||||
key={resultIndex}
|
||||
className="bg-background border border-border rounded-lg overflow-hidden transition-all duration-200"
|
||||
style={{ boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
|
||||
e.currentTarget.style.borderColor = "#e0e7ff";
|
||||
e.currentTarget.style.boxShadow =
|
||||
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.boxShadow = "0 1px 2px 0 rgba(0, 0, 0, 0.05)";
|
||||
e.currentTarget.style.borderColor = "#e5e7eb";
|
||||
}}
|
||||
>
|
||||
<div className="p-5">
|
||||
|
|
@ -225,47 +219,45 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
href={result.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-lg font-semibold text-blue-600 hover:text-blue-700 flex-1 leading-snug"
|
||||
style={{ textDecoration: "none" }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.textDecoration = "underline")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}
|
||||
className="text-lg font-semibold text-blue-600 hover:text-blue-700 hover:underline flex-1 leading-snug"
|
||||
>
|
||||
{result.title}
|
||||
</a>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="flex-shrink-0"
|
||||
icon={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex-shrink-0 text-muted-foreground"
|
||||
onClick={() => window.open(result.url, "_blank")}
|
||||
style={{ color: "#6b7280" }}
|
||||
/>
|
||||
aria-label="Open in new tab"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* URL */}
|
||||
<div className="text-sm text-green-700 mb-3 truncate font-medium">{result.url}</div>
|
||||
|
||||
|
||||
{/* Snippet Preview */}
|
||||
<div className="text-sm text-gray-700 leading-relaxed">
|
||||
{isResultExpanded ? result.snippet : `${result.snippet.substring(0, 200)}${result.snippet.length > 200 ? '...' : ''}`}
|
||||
<div className="text-sm text-foreground leading-relaxed">
|
||||
{isResultExpanded
|
||||
? result.snippet
|
||||
: `${result.snippet.substring(0, 200)}${result.snippet.length > 200 ? "..." : ""}`}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Expand/Collapse */}
|
||||
{result.snippet.length > 200 && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
className="mt-3 p-0 h-auto"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="mt-3 p-0 h-auto text-blue-600"
|
||||
onClick={() => toggleResultExpansion(0, resultIndex)}
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
color: "#3b82f6"
|
||||
}}
|
||||
>
|
||||
{isResultExpanded ? "Show less" : "Show more"}
|
||||
</Button>
|
||||
|
|
@ -276,31 +268,23 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 bg-gray-50 border border-gray-200 rounded-lg">
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 mx-auto mb-4">
|
||||
<SearchOutlined style={{ fontSize: "24px", color: "#9ca3af" }} />
|
||||
<div className="text-center py-12 bg-muted border border-border rounded-lg">
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-muted mx-auto mb-4">
|
||||
<SearchOutlined className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
<Text className="text-gray-600 font-medium">No results found</Text>
|
||||
<Text className="text-sm text-gray-500 mt-1">Try a different search query</Text>
|
||||
<p className="text-muted-foreground font-medium">No results found</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">Try a different search query</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Search History Sidebar */}
|
||||
{/* Search History */}
|
||||
{searchHistory.length > 1 && (
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<div className="mt-8 pt-6 border-t border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Text className="text-sm font-semibold text-gray-700">Previous Searches</Text>
|
||||
<Button
|
||||
onClick={clearHistory}
|
||||
size="small"
|
||||
type="link"
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-semibold text-foreground">Previous Searches</p>
|
||||
<Button onClick={clearHistory} variant="link" size="sm" className="text-sm">
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -308,22 +292,21 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
{searchHistory.slice(1, 6).map((entry, index) => (
|
||||
<div
|
||||
key={index + 1}
|
||||
className="p-3 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer transition-all duration-200 hover:bg-gray-100 hover:border-gray-300"
|
||||
className="p-3 bg-muted border border-border rounded-lg cursor-pointer transition-all duration-200 hover:bg-muted/80"
|
||||
onClick={() => {
|
||||
setQuery(entry.query);
|
||||
}}
|
||||
>
|
||||
<div className="text-sm font-medium text-gray-800 truncate">{entry.query}</div>
|
||||
<div className="text-xs text-gray-500 mt-1.5 flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-foreground truncate">{entry.query}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1.5 flex items-center gap-2">
|
||||
<span className="font-medium text-blue-600">
|
||||
{entry.response?.results?.length || 0} {entry.response?.results?.length === 1 ? 'result' : 'results'}
|
||||
{entry.response?.results?.length || 0}{" "}
|
||||
{entry.response?.results?.length === 1 ? "result" : "results"}
|
||||
</span>
|
||||
{entry.latency !== undefined && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="font-medium text-green-600">
|
||||
{entry.latency}ms
|
||||
</span>
|
||||
<span className="font-medium text-green-600">{entry.latency}ms</span>
|
||||
</>
|
||||
)}
|
||||
<span>•</span>
|
||||
|
|
@ -343,4 +326,3 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
};
|
||||
|
||||
export default SearchToolTester;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,19 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Form,
|
||||
Input as AntInput,
|
||||
Select,
|
||||
Table,
|
||||
} from "antd";
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import {
|
||||
|
|
@ -24,6 +29,7 @@ import {
|
|||
fetchSearchTools,
|
||||
updateSearchTool,
|
||||
} from "../networking";
|
||||
import { DataTable } from "../view_logs/table";
|
||||
import CreateSearchTool from "./CreateSearchTools";
|
||||
import { searchToolColumns } from "./SearchToolColumn";
|
||||
import { SearchToolView } from "./SearchToolView";
|
||||
|
|
@ -35,6 +41,15 @@ interface SearchToolsProps {
|
|||
userID: string | null;
|
||||
}
|
||||
|
||||
interface EditFormValues {
|
||||
search_tool_name: string;
|
||||
search_provider: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
timeout?: string;
|
||||
max_retries?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID }) => {
|
||||
const {
|
||||
|
|
@ -50,10 +65,7 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
enabled: !!accessToken,
|
||||
}) as { data: SearchTool[]; isLoading: boolean; refetch: () => void };
|
||||
|
||||
const {
|
||||
data: providersResponse,
|
||||
isLoading: isLoadingProviders,
|
||||
} = useQuery({
|
||||
const { data: providersResponse, isLoading: isLoadingProviders } = useQuery({
|
||||
queryKey: ["searchProviders"],
|
||||
queryFn: () => {
|
||||
if (!accessToken) throw new Error("Access Token required");
|
||||
|
|
@ -72,7 +84,23 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
const [editTool, setEditTool] = useState(false);
|
||||
const [isCreateModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [isEditModalVisible, setEditModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const editForm = useForm<EditFormValues>({
|
||||
defaultValues: {
|
||||
search_tool_name: "",
|
||||
search_provider: "",
|
||||
api_key: "",
|
||||
api_base: "",
|
||||
timeout: "",
|
||||
max_retries: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
|
||||
function handleDelete(toolId: string) {
|
||||
setToolToDelete(toolId);
|
||||
setIsDeleteModalOpen(true);
|
||||
}
|
||||
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
|
|
@ -84,14 +112,15 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
(toolId: string) => {
|
||||
const tool = searchTools?.find((t) => t.search_tool_id === toolId);
|
||||
if (tool) {
|
||||
form.setFieldsValue({
|
||||
editForm.reset({
|
||||
search_tool_name: tool.search_tool_name,
|
||||
search_provider: tool.litellm_params.search_provider,
|
||||
api_key: tool.litellm_params.api_key,
|
||||
api_base: tool.litellm_params.api_base,
|
||||
timeout: tool.litellm_params.timeout,
|
||||
max_retries: tool.litellm_params.max_retries,
|
||||
description: tool.search_tool_info?.description,
|
||||
api_key: tool.litellm_params.api_key ?? "",
|
||||
api_base: tool.litellm_params.api_base ?? "",
|
||||
timeout: tool.litellm_params.timeout !== undefined ? String(tool.litellm_params.timeout) : "",
|
||||
max_retries:
|
||||
tool.litellm_params.max_retries !== undefined ? String(tool.litellm_params.max_retries) : "",
|
||||
description: tool.search_tool_info?.description ?? "",
|
||||
});
|
||||
setSelectedToolId(toolId);
|
||||
setEditModalVisible(true);
|
||||
|
|
@ -100,14 +129,9 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
handleDelete,
|
||||
availableProviders,
|
||||
),
|
||||
[availableProviders, searchTools, form],
|
||||
[availableProviders, searchTools, editForm],
|
||||
);
|
||||
|
||||
function handleDelete(toolId: string) {
|
||||
setToolToDelete(toolId);
|
||||
setIsDeleteModalOpen(true);
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (toolIdToDelete == null || accessToken == null) {
|
||||
return;
|
||||
|
|
@ -137,16 +161,21 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
? availableProviders.find((p) => p.provider_name === toolToDelete.litellm_params.search_provider)
|
||||
: null;
|
||||
|
||||
const handleCreateSuccess = (newSearchTool: SearchTool) => {
|
||||
const handleCreateSuccess = (_newSearchTool: SearchTool) => {
|
||||
setCreateModalVisible(false);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
const handleEditCancel = () => {
|
||||
setEditModalVisible(false);
|
||||
editForm.reset();
|
||||
setSelectedToolId(null);
|
||||
};
|
||||
|
||||
const handleEditSubmit = editForm.handleSubmit(async (values) => {
|
||||
if (!accessToken || !selectedToolId) return;
|
||||
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const searchToolData = {
|
||||
search_tool_name: values.search_tool_name,
|
||||
litellm_params: {
|
||||
|
|
@ -156,69 +185,26 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
timeout: values.timeout ? parseFloat(values.timeout) : undefined,
|
||||
max_retries: values.max_retries ? parseInt(values.max_retries) : undefined,
|
||||
},
|
||||
search_tool_info: values.description ? {
|
||||
description: values.description,
|
||||
} : undefined,
|
||||
search_tool_info: values.description
|
||||
? {
|
||||
description: values.description,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
await updateSearchTool(accessToken, selectedToolId, searchToolData);
|
||||
NotificationsManager.success("Search tool updated successfully");
|
||||
setEditModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedToolId(null);
|
||||
handleEditCancel();
|
||||
refetch();
|
||||
} catch (error) {
|
||||
console.error("Failed to update search tool:", error);
|
||||
NotificationsManager.error("Failed to update search tool");
|
||||
}
|
||||
};
|
||||
|
||||
const renderEditForm = () => (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="search_tool_name"
|
||||
label="Search Tool Name"
|
||||
rules={[{ required: true, message: "Please enter a search tool name" }]}
|
||||
>
|
||||
<AntInput placeholder="e.g., my-perplexity-search" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="search_provider"
|
||||
label="Search Provider"
|
||||
rules={[{ required: true, message: "Please select a search provider" }]}
|
||||
>
|
||||
<Select placeholder="Select a search provider" loading={isLoadingProviders}>
|
||||
{availableProviders.map((provider) => (
|
||||
<Select.Option key={provider.provider_name} value={provider.provider_name}>
|
||||
{provider.ui_friendly_name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label="API Key"
|
||||
extra="API key for the search provider"
|
||||
>
|
||||
<AntInput.Password placeholder="Enter API key" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<AntInput.TextArea
|
||||
rows={3}
|
||||
placeholder="Description of this search tool"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
if (!accessToken || !userRole || !userID) {
|
||||
return (
|
||||
<div className="p-6 text-center text-muted-foreground">
|
||||
Missing required authentication parameters.
|
||||
</div>
|
||||
<div className="p-6 text-center text-muted-foreground">Missing required authentication parameters.</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -250,18 +236,13 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<Table
|
||||
bordered
|
||||
dataSource={searchTools || []}
|
||||
<DataTable
|
||||
columns={columns}
|
||||
rowKey={(record) =>
|
||||
record.search_tool_id || record.search_tool_name
|
||||
}
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: "No search tools configured",
|
||||
}}
|
||||
size="small"
|
||||
data={searchTools || []}
|
||||
renderSubComponent={() => <></>}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={false}
|
||||
noDataMessage="No search tools configured"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -276,14 +257,14 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
resourceInformation={
|
||||
toolToDelete
|
||||
? [
|
||||
{ label: "Name", value: toolToDelete.search_tool_name },
|
||||
{ label: "ID", value: toolToDelete.search_tool_id, code: true },
|
||||
{
|
||||
label: "Provider",
|
||||
value: providerInfo?.ui_friendly_name || toolToDelete.litellm_params.search_provider,
|
||||
},
|
||||
{ label: "Description", value: toolToDelete.search_tool_info?.description || "-" },
|
||||
]
|
||||
{ label: "Name", value: toolToDelete.search_tool_name },
|
||||
{ label: "ID", value: toolToDelete.search_tool_id, code: true },
|
||||
{
|
||||
label: "Provider",
|
||||
value: providerInfo?.ui_friendly_name || toolToDelete.litellm_params.search_provider,
|
||||
},
|
||||
{ label: "Description", value: toolToDelete.search_tool_info?.description || "-" },
|
||||
]
|
||||
: []
|
||||
}
|
||||
onCancel={cancelDelete}
|
||||
|
|
@ -300,46 +281,97 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
|
|||
/>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Dialog
|
||||
open={isEditModalVisible}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) {
|
||||
setEditModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedToolId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog open={isEditModalVisible} onOpenChange={(o) => (!o ? handleEditCancel() : undefined)}>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Search Tool</DialogTitle>
|
||||
</DialogHeader>
|
||||
{renderEditForm()}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedToolId(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEditSubmit}>OK</Button>
|
||||
</DialogFooter>
|
||||
<FormProvider {...editForm}>
|
||||
<form onSubmit={handleEditSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-search-tool-name">
|
||||
Search Tool Name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-search-tool-name"
|
||||
placeholder="e.g., my-perplexity-search"
|
||||
{...editForm.register("search_tool_name", {
|
||||
required: "Please enter a search tool name",
|
||||
})}
|
||||
/>
|
||||
{editForm.formState.errors.search_tool_name && (
|
||||
<p className="text-sm text-destructive">
|
||||
{editForm.formState.errors.search_tool_name.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-search-provider">
|
||||
Search Provider <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Controller
|
||||
control={editForm.control}
|
||||
name="search_provider"
|
||||
rules={{ required: "Please select a search provider" }}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange} disabled={isLoadingProviders}>
|
||||
<SelectTrigger id="edit-search-provider">
|
||||
<SelectValue placeholder="Select a search provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableProviders.map((provider) => (
|
||||
<SelectItem key={provider.provider_name} value={provider.provider_name}>
|
||||
{provider.ui_friendly_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
{editForm.formState.errors.search_provider && (
|
||||
<p className="text-sm text-destructive">
|
||||
{editForm.formState.errors.search_provider.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-api-key">API Key</Label>
|
||||
<Input
|
||||
id="edit-api-key"
|
||||
type="password"
|
||||
placeholder="Enter API key"
|
||||
{...editForm.register("api_key")}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">API key for the search provider</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description">Description</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
rows={3}
|
||||
placeholder="Description of this search tool"
|
||||
{...editForm.register("description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={handleEditCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">OK</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<h1 className="text-2xl font-semibold">Search Tools</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Configure and manage your search providers
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2">Configure and manage your search providers</p>
|
||||
{isAdminRole(userRole) && (
|
||||
<Button
|
||||
className="mt-4 mb-4"
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
>
|
||||
<Button className="mt-4 mb-4" onClick={() => setCreateModalVisible(true)}>
|
||||
+ Add New Search Tool
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue