feat(ui): i18n for search tools

This commit is contained in:
吹雪 2026-06-11 04:45:53 +09:00
parent 7ff15f970e
commit 6d04f50e80
7 changed files with 334 additions and 100 deletions

View file

@ -5,6 +5,7 @@ import { Button, TextInput } from "@tremor/react";
import { Form, Input, Modal, Select, Tooltip, Typography } from "antd";
import Image from "next/image";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
import { createSearchTool, fetchAvailableSearchProviders } from "../networking";
import SearchConnectionTest from "./SearchConnectionTest";
@ -60,6 +61,7 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
isModalVisible,
setModalVisible,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [formValues, setFormValues] = useState<Record<string, any>>({});
@ -104,14 +106,14 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
if (accessToken != null) {
const response = await createSearchTool(accessToken, payload);
NotificationsManager.success("Search tool created successfully");
NotificationsManager.success(t("searchTools.createSearchTools.createSuccess"));
form.resetFields();
setFormValues({});
setModalVisible(false);
onCreateSuccess(response);
}
} catch (error) {
NotificationsManager.error("Error creating search tool: " + error);
NotificationsManager.error(t("searchTools.createSearchTools.createError", { error }));
} finally {
setIsLoading(false);
}
@ -134,7 +136,7 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
// Show the modal with the fresh test
setIsTestModalVisible(true);
} catch (error) {
NotificationsManager.error("Please fill in Search Provider and API Key before testing");
NotificationsManager.error(t("searchTools.createSearchTools.testFillRequired"));
}
};
@ -154,7 +156,7 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
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>
<h2 className="text-xl font-semibold text-gray-900">{t("searchTools.createSearchTools.modalTitle")}</h2>
</div>
}
open={isModalVisible}
@ -179,23 +181,23 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
<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').">
{t("searchTools.createSearchTools.searchToolNameLabel")}
<Tooltip title={t("searchTools.createSearchTools.searchToolNameTooltip")}>
<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" },
{ required: true, message: t("searchTools.createSearchTools.searchToolNameRequired") },
{
pattern: /^[a-zA-Z0-9_-]+$/,
message: "Name can only contain letters, numbers, hyphens, and underscores",
message: t("searchTools.createSearchTools.searchToolNamePattern"),
},
]}
>
<TextInput
placeholder="e.g., perplexity-search, my-tavily-tool"
placeholder={t("searchTools.createSearchTools.searchToolNamePlaceholder")}
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
@ -203,17 +205,17 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
<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.">
{t("searchTools.createSearchTools.searchProviderLabel")}
<Tooltip title={t("searchTools.createSearchTools.searchProviderTooltip")}>
<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" }]}
rules={[{ required: true, message: t("searchTools.createSearchTools.searchProviderRequired") }]}
>
<Select
placeholder="Select a search provider"
placeholder={t("searchTools.createSearchTools.searchProviderPlaceholder")}
className="rounded-lg"
size="large"
loading={isLoadingProviders}
@ -244,46 +246,50 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
<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.">
{t("searchTools.createSearchTools.apiKeyLabel")}
<Tooltip title={t("searchTools.createSearchTools.apiKeyTooltip")}>
<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" }]}
rules={[{ required: false, message: t("searchTools.createSearchTools.apiKeyRequired") }]}
>
<TextInput
type="password"
placeholder="Enter your API key"
placeholder={t("searchTools.createSearchTools.apiKeyPlaceholder")}
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>}
label={
<span className="text-sm font-medium text-gray-700">
{t("searchTools.createSearchTools.descriptionLabel")}
</span>
}
name="description"
>
<TextArea
rows={3}
placeholder="Brief description of this search tool's purpose"
placeholder={t("searchTools.createSearchTools.descriptionPlaceholder")}
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">
<Tooltip title={t("searchTools.createSearchTools.needHelpTooltip")}>
<Typography.Link href="https://github.com/BerriAI/litellm/issues" target="_blank">
Need Help?
{t("searchTools.createSearchTools.needHelp")}
</Typography.Link>
</Tooltip>
<div className="space-x-2">
<Button onClick={handleTestConnection} loading={isTestingConnection}>
Test Connection
{t("searchTools.createSearchTools.testConnectionBtn")}
</Button>
<Button loading={isLoading} type="submit">
Add Search Tool
{t("searchTools.createSearchTools.addSearchToolBtn")}
</Button>
</div>
</div>
@ -292,7 +298,7 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
{/* Test Connection Results Modal */}
<Modal
title="Connection Test Results"
title={t("searchTools.createSearchTools.connectionTestResultsTitle")}
open={isTestModalVisible}
onCancel={() => {
setIsTestModalVisible(false);
@ -306,7 +312,7 @@ const CreateSearchTool: React.FC<CreateSearchToolProps> = ({
setIsTestingConnection(false);
}}
>
Close
{t("common.close")}
</Button>,
]}
width={700}

View file

@ -1,6 +1,7 @@
import { InfoCircleOutlined, WarningOutlined } from "@ant-design/icons";
import { Button, Divider, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import NotificationsManager from "../molecules/notifications_manager";
import { testSearchToolConnection } from "../networking";
@ -13,6 +14,7 @@ interface SearchConnectionTestProps {
}
const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmParams, accessToken, onTestComplete }) => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(true);
const [testResult, setTestResult] = useState<{
status: "success" | "error";
@ -30,12 +32,12 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
const result = await testSearchToolConnection(accessToken, litellmParams);
setTestResult(result);
if (result.status === "success") {
NotificationsManager.success("Connection test successful!");
NotificationsManager.success(t("searchTools.searchConnectionTest.testSuccessNotification"));
}
} catch (error) {
setTestResult({
status: "error",
message: error instanceof Error ? error.message : "Unknown error occurred",
message: error instanceof Error ? error.message : t("searchTools.searchConnectionTest.unknownErrorOccurred"),
error_type: "NetworkError",
});
} finally {
@ -50,7 +52,7 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
}, [accessToken, litellmParams, onTestComplete]);
const getCleanErrorMessage = (errorMsg: string) => {
if (!errorMsg) return "Unknown error";
if (!errorMsg) return t("searchTools.searchConnectionTest.unknownError");
// Remove stack traces
const mainError = errorMsg.split("stack trace:")[0].trim();
@ -70,9 +72,9 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
}
// If it's a 401 error
if (finalError.includes("401") || finalError.includes("Authorization Required")) {
return "Authentication failed: Invalid API key or credentials";
return t("searchTools.searchConnectionTest.authFailedInvalidKey");
}
return "Authentication error - please check your API key";
return t("searchTools.searchConnectionTest.authErrorCheckKey");
}
// Limit very long error messages
@ -83,7 +85,9 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
return finalError;
};
const errorMessage = testResult?.message ? getCleanErrorMessage(testResult.message) : "Unknown error";
const errorMessage = testResult?.message
? getCleanErrorMessage(testResult.message)
: t("searchTools.searchConnectionTest.unknownError");
if (isLoading) {
return (
@ -103,7 +107,9 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
/>
</div>
<Text style={{ fontSize: "16px" }}>
Testing connection to {litellmParams.search_provider || "search provider"}...
{t("searchTools.searchConnectionTest.testingConnection", {
provider: litellmParams.search_provider || t("searchTools.searchConnectionTest.searchProvider"),
})}
</Text>
<style jsx>{`
@keyframes spin {
@ -143,11 +149,11 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
</div>
<div style={{ marginLeft: "12px" }}>
<Text type="success" style={{ fontSize: "18px", fontWeight: 500, display: "block" }}>
Connection to {litellmParams.search_provider} successful!
{t("searchTools.searchConnectionTest.connectionSuccess", { provider: litellmParams.search_provider })}
</Text>
{testResult.test_query && (
<Text style={{ fontSize: "14px", color: "#666", marginTop: "8px", display: "block" }}>
Test query:{" "}
{t("searchTools.searchConnectionTest.testQueryLabel")}{" "}
<code style={{ backgroundColor: "#f0f0f0", padding: "2px 6px", borderRadius: "4px" }}>
{testResult.test_query}
</code>
@ -155,7 +161,7 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
)}
{testResult.results_count !== undefined && (
<Text style={{ fontSize: "14px", color: "#666", display: "block" }}>
Results retrieved: {testResult.results_count}
{t("searchTools.searchConnectionTest.resultsRetrieved", { count: testResult.results_count })}
</Text>
)}
</div>
@ -166,7 +172,9 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
<div style={{ display: "flex", alignItems: "center", marginBottom: "20px" }}>
<WarningOutlined style={{ color: "#ff4d4f", fontSize: "24px", marginRight: "12px" }} />
<Text type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
Connection to {litellmParams.search_provider || "search provider"} failed
{t("searchTools.searchConnectionTest.connectionFailed", {
provider: litellmParams.search_provider || t("searchTools.searchConnectionTest.searchProvider"),
})}
</Text>
</div>
@ -181,7 +189,7 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
}}
>
<Text strong style={{ display: "block", marginBottom: "8px" }}>
Error:{" "}
{t("searchTools.searchConnectionTest.errorLabel")}{" "}
</Text>
<Text type="danger" style={{ fontSize: "14px", lineHeight: "1.5" }}>
{errorMessage}
@ -190,7 +198,7 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
{testResult.error_type && (
<div style={{ marginTop: "8px" }}>
<Text style={{ fontSize: "13px", color: "#666" }}>
Error type:{" "}
{t("searchTools.searchConnectionTest.errorTypeLabel")}{" "}
<code
style={{ backgroundColor: "#ffebee", padding: "2px 6px", borderRadius: "4px", color: "#d32f2f" }}
>
@ -207,7 +215,9 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
onClick={() => setShowDetails(!showDetails)}
style={{ paddingLeft: 0, height: "auto" }}
>
{showDetails ? "Hide Details" : "Show Details"}
{showDetails
? t("searchTools.searchConnectionTest.hideDetails")
: t("searchTools.searchConnectionTest.showDetails")}
</Button>
</div>
)}
@ -216,7 +226,7 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
{showDetails && (
<div style={{ marginBottom: "20px" }}>
<Text strong style={{ display: "block", marginBottom: "8px", fontSize: "15px" }}>
Full Error Details
{t("searchTools.searchConnectionTest.fullErrorDetails")}
</Text>
<pre
style={{
@ -247,15 +257,13 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
}}
>
<Text strong style={{ display: "block", marginBottom: "8px", color: "#d48806" }}>
Troubleshooting tips:
{t("searchTools.searchConnectionTest.troubleshootingTips")}
</Text>
<ul style={{ margin: "8px 0", paddingLeft: "20px", color: "#ad6800" }}>
<li style={{ marginBottom: "6px" }}>Verify your API key is correct and active</li>
<li style={{ marginBottom: "6px" }}>Check if the search provider service is operational</li>
<li style={{ marginBottom: "6px" }}>Ensure you have sufficient credits/quota with the provider</li>
<li style={{ marginBottom: "6px" }}>
Review the provider&apos;s documentation for any additional requirements
</li>
<li style={{ marginBottom: "6px" }}>{t("searchTools.searchConnectionTest.tipVerifyApiKey")}</li>
<li style={{ marginBottom: "6px" }}>{t("searchTools.searchConnectionTest.tipCheckServiceStatus")}</li>
<li style={{ marginBottom: "6px" }}>{t("searchTools.searchConnectionTest.tipCheckCredits")}</li>
<li style={{ marginBottom: "6px" }}>{t("searchTools.searchConnectionTest.tipReviewDocs")}</li>
</ul>
</div>
</div>
@ -264,7 +272,7 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
<Divider style={{ margin: "24px 0 16px" }} />
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Button type="link" href="https://docs.litellm.ai/docs/search" target="_blank" icon={<InfoCircleOutlined />}>
View Search Documentation
{t("searchTools.searchConnectionTest.viewSearchDocs")}
</Button>
</div>
</div>

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Button, Input, Typography, Spin } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { SearchOutlined, LoadingOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { searchToolQueryCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
import { Card, Title as TremorTitle } from "@tremor/react";
@ -25,6 +26,7 @@ interface SearchToolTesterProps {
}
export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolName, accessToken, className = "" }) => {
const { t } = useTranslation();
const [query, setQuery] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [searchHistory, setSearchHistory] = useState<
@ -40,7 +42,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
const handleSearch = async () => {
if (!query.trim()) {
MessageManager.warning("Please enter a search query");
MessageManager.warning(t("searchTools.searchToolTester.enterQueryWarning"));
return;
}
@ -63,7 +65,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
// 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");
NotificationsManager.fromBackend(t("searchTools.searchToolTester.queryFailed"));
} finally {
setIsLoading(false);
}
@ -76,7 +78,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
const clearHistory = () => {
setSearchHistory([]);
setExpandedResults({});
NotificationsManager.success("Search history cleared");
NotificationsManager.success(t("searchTools.searchToolTester.historyCleared"));
};
const toggleResultExpansion = (historyIndex: number, resultIndex: number) => {
@ -94,7 +96,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
return (
<Card className="mt-6">
<div className="mb-6">
<TremorTitle>Test Search Tool</TremorTitle>
<TremorTitle>{t("searchTools.searchToolTester.title")}</TremorTitle>
</div>
<div className="flex flex-col" style={{ minHeight: "600px" }}>
@ -121,7 +123,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
handleSearch();
}
}}
placeholder="Enter your search query..."
placeholder={t("searchTools.searchToolTester.searchPlaceholder")}
disabled={isLoading}
bordered={false}
style={{ fontSize: "15px", padding: 0, height: "100%", boxShadow: "none" }}
@ -145,7 +147,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
boxShadow: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
}}
>
Search
{t("common.search")}
</Button>
</div>
</div>
@ -157,15 +159,17 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
<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>
<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>
<Text className="text-lg text-gray-600 font-medium">
{t("searchTools.searchToolTester.emptyStateTitle")}
</Text>
<Text className="text-sm text-gray-500 mt-2">{t("searchTools.searchToolTester.emptyStateSubtitle")}</Text>
</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>
<Text className="mt-4 text-gray-600 font-medium">{t("searchTools.searchToolTester.searching")}</Text>
</div>
)}
@ -179,7 +183,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
<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
{t("searchTools.searchToolTester.searchQueryLabel")}
</Text>
<div className="text-base font-semibold text-gray-900 mt-1.5">{latestResults.query}</div>
</div>
@ -187,8 +191,9 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
<Text className="text-xs text-gray-500">{formatTimestamp(latestResults.timestamp)}</Text>
<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"}
{t("searchTools.searchToolTester.resultCount", {
count: latestResults.response?.results?.length || 0,
})}
</div>
{latestResults.latency !== undefined && (
<>
@ -280,7 +285,9 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
color: "#3b82f6",
}}
>
{isResultExpanded ? "Show less" : "Show more"}
{isResultExpanded
? t("searchTools.searchToolTester.showLess")
: t("searchTools.searchToolTester.showMore")}
</Button>
)}
</div>
@ -293,8 +300,10 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
<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>
<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>
<Text className="text-gray-600 font-medium">{t("common.noResults")}</Text>
<Text className="text-sm text-gray-500 mt-1">
{t("searchTools.searchToolTester.tryDifferentQuery")}
</Text>
</div>
)}
</>
@ -304,7 +313,9 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
{searchHistory.length > 1 && (
<div className="mt-8 pt-6 border-t border-gray-200">
<div className="flex items-center justify-between mb-4">
<Text className="text-sm font-semibold text-gray-700">Previous Searches</Text>
<Text className="text-sm font-semibold text-gray-700">
{t("searchTools.searchToolTester.previousSearches")}
</Text>
<Button
onClick={clearHistory}
size="small"
@ -314,7 +325,7 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
fontWeight: 500,
}}
>
Clear All
{t("searchTools.searchToolTester.clearAll")}
</Button>
</div>
<div className="space-y-2">
@ -329,8 +340,9 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
<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">
<span className="font-medium text-blue-600">
{entry.response?.results?.length || 0}{" "}
{entry.response?.results?.length === 1 ? "result" : "results"}
{t("searchTools.searchToolTester.resultCount", {
count: entry.response?.results?.length || 0,
})}
</span>
{entry.latency !== undefined && (
<>

View file

@ -4,6 +4,7 @@ import { Button, Card, Grid, Text, Title } from "@tremor/react";
import { Button as AntdButton } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { SearchToolTester } from "./SearchToolTester";
import { AvailableSearchProvider, SearchTool } from "./types";
@ -22,6 +23,7 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
accessToken,
availableProviders,
}) => {
const { t } = useTranslation();
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const copyToClipboard = async (text: string | null | undefined, key: string) => {
@ -44,7 +46,7 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
<div className="flex justify-between items-center mb-6">
<div>
<Button icon={ArrowLeftIcon} variant="light" className="mb-4" onClick={onBack}>
Back to All Search Tools
{t("searchTools.searchToolView.backToAll")}
</Button>
<div className="flex items-center cursor-pointer">
<Title>{searchTool.search_tool_name}</Title>
@ -79,30 +81,32 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Provider</Text>
<Text>{t("searchTools.searchToolView.providerLabel")}</Text>
<div className="mt-2">
<Title>{getProviderDisplayName(searchTool.litellm_params.search_provider)}</Title>
</div>
</Card>
<Card>
<Text>API Key</Text>
<Text>{t("searchTools.searchToolView.apiKeyLabel")}</Text>
<div className="mt-2">
<Text>{searchTool.litellm_params.api_key ? "****" : "Not set"}</Text>
<Text>{searchTool.litellm_params.api_key ? "****" : t("searchTools.searchToolView.notSet")}</Text>
</div>
</Card>
<Card>
<Text>Created At</Text>
<Text>{t("common.createdAt")}</Text>
<div className="mt-2">
<Text>{searchTool.created_at ? new Date(searchTool.created_at).toLocaleString() : "Unknown"}</Text>
<Text>
{searchTool.created_at ? new Date(searchTool.created_at).toLocaleString() : t("common.unknown")}
</Text>
</div>
</Card>
</Grid>
{searchTool.search_tool_info?.description && (
<Card className="mt-6">
<Text>Description</Text>
<Text>{t("common.description")}</Text>
<div className="mt-2">
<Text>{searchTool.search_tool_info.description}</Text>
</div>

View file

@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query";
import { Button, Text, Title } from "@tremor/react";
import { Form, Input, Modal, Select, Spin, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import NotificationsManager from "../molecules/notifications_manager";
import { deleteSearchTool, fetchAvailableSearchProviders, fetchSearchTools, updateSearchTool } from "../networking";
@ -19,6 +20,7 @@ interface SearchToolsProps {
}
const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID }) => {
const { t } = useTranslation();
const {
data: searchTools,
isLoading: isLoadingTools,
@ -94,13 +96,13 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
setIsDeleting(true);
try {
await deleteSearchTool(accessToken, toolIdToDelete);
NotificationsManager.success("Deleted search tool successfully");
NotificationsManager.success(t("searchTools.searchTools.deleteSuccess"));
setIsDeleteModalOpen(false);
setToolToDelete(null);
refetch();
} catch (error) {
console.error("Error deleting the search tool:", error);
NotificationsManager.error("Failed to delete search tool");
NotificationsManager.error(t("searchTools.searchTools.deleteFailed"));
} finally {
setIsDeleting(false);
}
@ -143,14 +145,14 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
};
await updateSearchTool(accessToken, selectedToolId, searchToolData);
NotificationsManager.success("Search tool updated successfully");
NotificationsManager.success(t("searchTools.searchTools.updateSuccess"));
setEditModalVisible(false);
form.resetFields();
setSelectedToolId(null);
refetch();
} catch (error) {
console.error("Failed to update search tool:", error);
NotificationsManager.error("Failed to update search tool");
NotificationsManager.error(t("searchTools.searchTools.updateFailed"));
}
};
@ -158,18 +160,18 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
<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" }]}
label={t("searchTools.searchTools.editSearchToolNameLabel")}
rules={[{ required: true, message: t("searchTools.createSearchTools.searchToolNameRequired") }]}
>
<Input placeholder="e.g., my-perplexity-search" />
<Input placeholder={t("searchTools.searchTools.editSearchToolNamePlaceholder")} />
</Form.Item>
<Form.Item
name="search_provider"
label="Search Provider"
rules={[{ required: true, message: "Please select a search provider" }]}
label={t("searchTools.searchTools.editSearchProviderLabel")}
rules={[{ required: true, message: t("searchTools.createSearchTools.searchProviderRequired") }]}
>
<Select placeholder="Select a search provider" loading={isLoadingProviders}>
<Select placeholder={t("searchTools.createSearchTools.searchProviderPlaceholder")} loading={isLoadingProviders}>
{availableProviders.map((provider) => (
<Select.Option key={provider.provider_name} value={provider.provider_name}>
{provider.ui_friendly_name}
@ -178,19 +180,23 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
</Select>
</Form.Item>
<Form.Item name="api_key" label="API Key" extra="API key for the search provider">
<Input.Password placeholder="Enter API key" />
<Form.Item
name="api_key"
label={t("searchTools.searchTools.editApiKeyLabel")}
extra={t("searchTools.searchTools.editApiKeyExtra")}
>
<Input.Password placeholder={t("searchTools.searchTools.editApiKeyPlaceholder")} />
</Form.Item>
<Form.Item name="description" label="Description">
<Input.TextArea rows={3} placeholder="Description of this search tool" />
<Form.Item name="description" label={t("common.description")}>
<Input.TextArea rows={3} placeholder={t("searchTools.searchTools.editDescriptionPlaceholder")} />
</Form.Item>
</Form>
);
if (!accessToken || !userRole || !userID) {
console.log("Missing required authentication parameters", { accessToken, userRole, userID });
return <div className="p-6 text-center text-gray-500">Missing required authentication parameters.</div>;
return <div className="p-6 text-center text-gray-500">{t("searchTools.searchTools.missingAuthParams")}</div>;
}
const ToolsTab = () =>
@ -224,7 +230,7 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
rowKey={(record) => record.search_tool_id || record.search_tool_name}
pagination={false}
locale={{
emptyText: "No search tools configured",
emptyText: t("searchTools.searchTools.emptyText"),
}}
size="small"
/>
@ -236,19 +242,19 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
<div className="w-full h-full p-6">
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Search Tool"
message="Are you sure you want to delete this search tool? This action cannot be undone."
resourceInformationTitle="Search Tool Information"
title={t("searchTools.searchTools.deleteModalTitle")}
message={t("searchTools.searchTools.deleteModalMessage")}
resourceInformationTitle={t("searchTools.searchTools.deleteResourceInfoTitle")}
resourceInformation={
toolToDelete
? [
{ label: "Name", value: toolToDelete.search_tool_name },
{ label: t("common.name"), value: toolToDelete.search_tool_name },
{ label: "ID", value: toolToDelete.search_tool_id, code: true },
{
label: "Provider",
label: t("searchTools.searchTools.deleteResourceProviderLabel"),
value: providerInfo?.ui_friendly_name || toolToDelete.litellm_params.search_provider,
},
{ label: "Description", value: toolToDelete.search_tool_info?.description || "-" },
{ label: t("common.description"), value: toolToDelete.search_tool_info?.description || "-" },
]
: []
}
@ -267,7 +273,7 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
{/* Edit Modal */}
<Modal
title="Edit Search Tool"
title={t("searchTools.searchTools.editModalTitle")}
open={isEditModalVisible}
onOk={handleEditSubmit}
onCancel={() => {
@ -280,11 +286,11 @@ const SearchTools: React.FC<SearchToolsProps> = ({ accessToken, userRole, userID
{renderEditForm()}
</Modal>
<Title>Search Tools</Title>
<Text className="text-tremor-content mt-2">Configure and manage your search providers</Text>
<Title>{t("searchTools.searchTools.pageTitle")}</Title>
<Text className="text-tremor-content mt-2">{t("searchTools.searchTools.pageSubtitle")}</Text>
{isAdminRole(userRole) && (
<Button className="mt-4 mb-4" onClick={() => setCreateModalVisible(true)}>
+ Add New Search Tool
{t("searchTools.searchTools.addNewBtn")}
</Button>
)}

View file

@ -7154,5 +7154,104 @@
"usageExportHeader": {
"exportData": "Export Data"
}
},
"searchTools": {
"createSearchTools": {
"modalTitle": "Add New Search Tool",
"searchToolNameLabel": "Search Tool Name",
"searchToolNameTooltip": "A unique name to identify this search tool configuration (e.g., 'perplexity-search', 'tavily-news-search').",
"searchToolNameRequired": "Please enter a search tool name",
"searchToolNamePattern": "Name can only contain letters, numbers, hyphens, and underscores",
"searchToolNamePlaceholder": "e.g., perplexity-search, my-tavily-tool",
"searchProviderLabel": "Search Provider",
"searchProviderTooltip": "Select the search provider you want to use. Each provider has different capabilities and pricing.",
"searchProviderRequired": "Please select a search provider",
"searchProviderPlaceholder": "Select a search provider",
"apiKeyLabel": "API Key",
"apiKeyTooltip": "The API key for authenticating with the search provider. This will be securely stored.",
"apiKeyRequired": "Please enter an API key",
"apiKeyPlaceholder": "Enter your API key",
"descriptionLabel": "Description (Optional)",
"descriptionPlaceholder": "Brief description of this search tool's purpose",
"needHelpTooltip": "Get help on our github",
"needHelp": "Need Help?",
"testConnectionBtn": "Test Connection",
"addSearchToolBtn": "Add Search Tool",
"connectionTestResultsTitle": "Connection Test Results",
"createSuccess": "Search tool created successfully",
"createError": "Error creating search tool: {{error}}",
"testFillRequired": "Please fill in Search Provider and API Key before testing"
},
"searchConnectionTest": {
"testSuccessNotification": "Connection test successful!",
"unknownError": "Unknown error",
"unknownErrorOccurred": "Unknown error occurred",
"authFailedInvalidKey": "Authentication failed: Invalid API key or credentials",
"authErrorCheckKey": "Authentication error - please check your API key",
"searchProvider": "search provider",
"testingConnection": "Testing connection to {{provider}}...",
"connectionSuccess": "Connection to {{provider}} successful!",
"testQueryLabel": "Test query:",
"resultsRetrieved": "Results retrieved: {{count}}",
"connectionFailed": "Connection to {{provider}} failed",
"errorLabel": "Error:",
"errorTypeLabel": "Error type:",
"hideDetails": "Hide Details",
"showDetails": "Show Details",
"fullErrorDetails": "Full Error Details",
"troubleshootingTips": "Troubleshooting tips:",
"tipVerifyApiKey": "Verify your API key is correct and active",
"tipCheckServiceStatus": "Check if the search provider service is operational",
"tipCheckCredits": "Ensure you have sufficient credits/quota with the provider",
"tipReviewDocs": "Review the provider's documentation for any additional requirements",
"viewSearchDocs": "View Search Documentation"
},
"searchToolTester": {
"title": "Test Search Tool",
"enterQueryWarning": "Please enter a search query",
"queryFailed": "Failed to query search tool",
"historyCleared": "Search history cleared",
"searchPlaceholder": "Enter your search query...",
"emptyStateTitle": "Test your search tool",
"emptyStateSubtitle": "Enter a query above to see search results",
"searching": "Searching...",
"searchQueryLabel": "Search Query",
"resultCount_one": "{{count}} result",
"resultCount_other": "{{count}} results",
"tryDifferentQuery": "Try a different search query",
"previousSearches": "Previous Searches",
"clearAll": "Clear All",
"showLess": "Show less",
"showMore": "Show more"
},
"searchToolView": {
"backToAll": "Back to All Search Tools",
"providerLabel": "Provider",
"apiKeyLabel": "API Key",
"notSet": "Not set"
},
"searchTools": {
"pageTitle": "Search Tools",
"pageSubtitle": "Configure and manage your search providers",
"addNewBtn": "+ Add New Search Tool",
"emptyText": "No search tools configured",
"missingAuthParams": "Missing required authentication parameters.",
"deleteModalTitle": "Delete Search Tool",
"deleteModalMessage": "Are you sure you want to delete this search tool? This action cannot be undone.",
"deleteResourceInfoTitle": "Search Tool Information",
"deleteResourceProviderLabel": "Provider",
"deleteSuccess": "Deleted search tool successfully",
"deleteFailed": "Failed to delete search tool",
"editModalTitle": "Edit Search Tool",
"editSearchToolNameLabel": "Search Tool Name",
"editSearchToolNamePlaceholder": "e.g., my-perplexity-search",
"editSearchProviderLabel": "Search Provider",
"editApiKeyLabel": "API Key",
"editApiKeyExtra": "API key for the search provider",
"editApiKeyPlaceholder": "Enter API key",
"editDescriptionPlaceholder": "Description of this search tool",
"updateSuccess": "Search tool updated successfully",
"updateFailed": "Failed to update search tool"
}
}
}

View file

@ -7154,5 +7154,104 @@
"usageExportHeader": {
"exportData": "导出数据"
}
},
"searchTools": {
"createSearchTools": {
"modalTitle": "添加新搜索工具",
"searchToolNameLabel": "搜索工具名称",
"searchToolNameTooltip": "用于标识此搜索工具配置的唯一名称(例如 'perplexity-search'、'tavily-news-search')。",
"searchToolNameRequired": "请输入搜索工具名称",
"searchToolNamePattern": "名称只能包含字母、数字、连字符和下划线",
"searchToolNamePlaceholder": "例如:perplexity-search、my-tavily-tool",
"searchProviderLabel": "搜索提供商",
"searchProviderTooltip": "选择要使用的搜索提供商。每个提供商具有不同的功能和定价。",
"searchProviderRequired": "请选择搜索提供商",
"searchProviderPlaceholder": "选择搜索提供商",
"apiKeyLabel": "API 密钥",
"apiKeyTooltip": "用于向搜索提供商进行身份验证的 API 密钥,将被安全存储。",
"apiKeyRequired": "请输入 API 密钥",
"apiKeyPlaceholder": "输入您的 API 密钥",
"descriptionLabel": "描述(可选)",
"descriptionPlaceholder": "简要描述此搜索工具的用途",
"needHelpTooltip": "在我们的 GitHub 上获取帮助",
"needHelp": "需要帮助?",
"testConnectionBtn": "测试连接",
"addSearchToolBtn": "添加搜索工具",
"connectionTestResultsTitle": "连接测试结果",
"createSuccess": "搜索工具创建成功",
"createError": "创建搜索工具时出错:{{error}}",
"testFillRequired": "请在测试前填写搜索提供商和 API 密钥"
},
"searchConnectionTest": {
"testSuccessNotification": "连接测试成功!",
"unknownError": "未知错误",
"unknownErrorOccurred": "发生未知错误",
"authFailedInvalidKey": "身份验证失败:API 密钥或凭据无效",
"authErrorCheckKey": "身份验证错误 - 请检查您的 API 密钥",
"searchProvider": "搜索提供商",
"testingConnection": "正在测试与 {{provider}} 的连接...",
"connectionSuccess": "与 {{provider}} 连接成功!",
"testQueryLabel": "测试查询:",
"resultsRetrieved": "已检索结果:{{count}}",
"connectionFailed": "与 {{provider}} 连接失败",
"errorLabel": "错误:",
"errorTypeLabel": "错误类型:",
"hideDetails": "隐藏详情",
"showDetails": "显示详情",
"fullErrorDetails": "完整错误详情",
"troubleshootingTips": "故障排除提示:",
"tipVerifyApiKey": "确认您的 API 密钥正确且有效",
"tipCheckServiceStatus": "检查搜索提供商服务是否正常运行",
"tipCheckCredits": "确保您在该提供商处有足够的额度/配额",
"tipReviewDocs": "查阅提供商文档了解其他要求",
"viewSearchDocs": "查看搜索文档"
},
"searchToolTester": {
"title": "测试搜索工具",
"enterQueryWarning": "请输入搜索查询",
"queryFailed": "查询搜索工具失败",
"historyCleared": "搜索历史已清除",
"searchPlaceholder": "输入您的搜索查询...",
"emptyStateTitle": "测试您的搜索工具",
"emptyStateSubtitle": "在上方输入查询以查看搜索结果",
"searching": "搜索中...",
"searchQueryLabel": "搜索查询",
"resultCount_one": "{{count}} 条结果",
"resultCount_other": "{{count}} 条结果",
"tryDifferentQuery": "尝试不同的搜索查询",
"previousSearches": "历史搜索",
"clearAll": "清除全部",
"showLess": "收起",
"showMore": "展开"
},
"searchToolView": {
"backToAll": "返回所有搜索工具",
"providerLabel": "提供商",
"apiKeyLabel": "API 密钥",
"notSet": "未设置"
},
"searchTools": {
"pageTitle": "搜索工具",
"pageSubtitle": "配置和管理您的搜索提供商",
"addNewBtn": "+ 添加新搜索工具",
"emptyText": "未配置搜索工具",
"missingAuthParams": "缺少必要的身份验证参数。",
"deleteModalTitle": "删除搜索工具",
"deleteModalMessage": "确定要删除此搜索工具吗?此操作无法撤销。",
"deleteResourceInfoTitle": "搜索工具信息",
"deleteResourceProviderLabel": "提供商",
"deleteSuccess": "搜索工具已成功删除",
"deleteFailed": "删除搜索工具失败",
"editModalTitle": "编辑搜索工具",
"editSearchToolNameLabel": "搜索工具名称",
"editSearchToolNamePlaceholder": "例如:my-perplexity-search",
"editSearchProviderLabel": "搜索提供商",
"editApiKeyLabel": "API 密钥",
"editApiKeyExtra": "搜索提供商的 API 密钥",
"editApiKeyPlaceholder": "输入 API 密钥",
"editDescriptionPlaceholder": "此搜索工具的描述",
"updateSuccess": "搜索工具更新成功",
"updateFailed": "更新搜索工具失败"
}
}
}