mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/migrate-page-memory-9b2c09
# Conflicts: # ui/litellm-dashboard/eslint-suppressions.json
This commit is contained in:
commit
5a3ab862ee
6 changed files with 325 additions and 397 deletions
|
|
@ -1841,21 +1841,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/search-tools/_components/SearchConnectionTest.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/search-tools/_components/SearchToolTester.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/search-tools/_components/SearchToolView.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/search-tools/_components/SearchTools.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 2
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import SearchConnectionTest from "./SearchConnectionTest";
|
||||
import * as networking from "@/components/networking";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
testSearchToolConnection: vi.fn(),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
litellmParams: { search_provider: "tavily" },
|
||||
accessToken: "test-token",
|
||||
};
|
||||
|
||||
describe("SearchConnectionTest", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("passes the access token and params to the connection test", async () => {
|
||||
vi.mocked(networking.testSearchToolConnection).mockResolvedValue({
|
||||
status: "success",
|
||||
message: "ok",
|
||||
});
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networking.testSearchToolConnection).toHaveBeenCalledWith(
|
||||
defaultProps.accessToken,
|
||||
defaultProps.litellmParams,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a loading state naming the provider while the test is pending", () => {
|
||||
vi.mocked(networking.testSearchToolConnection).mockReturnValue(new Promise(() => {}));
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/Testing connection to tavily/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a success state with the test query and result count", async () => {
|
||||
vi.mocked(networking.testSearchToolConnection).mockResolvedValue({
|
||||
status: "success",
|
||||
message: "ok",
|
||||
test_query: "hello world",
|
||||
results_count: 3,
|
||||
});
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByText(/Connection to tavily successful/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("hello world")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Results retrieved: 3/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fires a success notification and completion callback on a successful test", async () => {
|
||||
const onTestComplete = vi.fn();
|
||||
vi.mocked(networking.testSearchToolConnection).mockResolvedValue({
|
||||
status: "success",
|
||||
message: "ok",
|
||||
});
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} onTestComplete={onTestComplete} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(NotificationsManager.success).toHaveBeenCalledWith("Connection test successful!");
|
||||
});
|
||||
expect(onTestComplete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("renders a failure state with a cleaned error message and error type", async () => {
|
||||
vi.mocked(networking.testSearchToolConnection).mockResolvedValue({
|
||||
status: "error",
|
||||
message: "litellm.AuthenticationError: Invalid API key\nstack trace: deep internals",
|
||||
error_type: "AuthenticationError",
|
||||
});
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByText(/Connection to tavily failed/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("Invalid API key")).toBeInTheDocument();
|
||||
expect(screen.getByText("AuthenticationError")).toBeInTheDocument();
|
||||
expect(screen.getByText("Verify your API key is correct and active")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reveals the raw error details when Show Details is toggled", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.testSearchToolConnection).mockResolvedValue({
|
||||
status: "error",
|
||||
message: "litellm.AuthenticationError: Invalid API key\nstack trace: deep internals",
|
||||
error_type: "AuthenticationError",
|
||||
});
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
const toggle = await screen.findByRole("button", { name: /show details/i });
|
||||
expect(screen.queryByText("Full Error Details")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(toggle);
|
||||
|
||||
expect(screen.getByText("Full Error Details")).toBeInTheDocument();
|
||||
expect(screen.getByText(/stack trace: deep internals/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("treats a rejected request as a connection failure", async () => {
|
||||
vi.mocked(networking.testSearchToolConnection).mockRejectedValue(new Error("network down"));
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByText(/Connection to tavily failed/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("network down")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("links out to the search documentation", async () => {
|
||||
vi.mocked(networking.testSearchToolConnection).mockResolvedValue({
|
||||
status: "success",
|
||||
message: "ok",
|
||||
});
|
||||
|
||||
render(<SearchConnectionTest {...defaultProps} />);
|
||||
|
||||
const docLink = await screen.findByRole("link", { name: /View Search Documentation/i });
|
||||
expect(docLink).toHaveAttribute("href", "https://docs.litellm.ai/docs/search");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { InfoCircleOutlined, WarningOutlined } from "@ant-design/icons";
|
||||
import { Button, Divider, Typography } from "antd";
|
||||
import { AlertTriangle, CheckCircle2, Info } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { testSearchToolConnection } from "@/components/networking";
|
||||
|
||||
const { Text } = Typography;
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
|
||||
interface SearchConnectionTestProps {
|
||||
litellmParams: Record<string, any>;
|
||||
|
|
@ -52,30 +52,23 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
|
|||
const getCleanErrorMessage = (errorMsg: string) => {
|
||||
if (!errorMsg) return "Unknown error";
|
||||
|
||||
// Remove stack traces
|
||||
const mainError = errorMsg.split("stack trace:")[0].trim();
|
||||
|
||||
// Remove litellm error prefixes
|
||||
const cleanedError = mainError.replace(/^litellm\.(.*?)Error:\s*/, "");
|
||||
|
||||
// Remove AuthenticationError prefix if it exists
|
||||
const finalError = cleanedError.replace(/^AuthenticationError:\s*/, "");
|
||||
|
||||
// If the error contains HTML (like a 401 page), extract just the key info
|
||||
if (finalError.includes("<html>") || finalError.includes("<!DOCTYPE")) {
|
||||
// Try to extract the title or main error from HTML
|
||||
const titleMatch = finalError.match(/<title>(.*?)<\/title>/);
|
||||
if (titleMatch) {
|
||||
return titleMatch[1];
|
||||
}
|
||||
// If it's a 401 error
|
||||
if (finalError.includes("401") || finalError.includes("Authorization Required")) {
|
||||
return "Authentication failed: Invalid API key or credentials";
|
||||
}
|
||||
return "Authentication error - please check your API key";
|
||||
}
|
||||
|
||||
// Limit very long error messages
|
||||
if (finalError.length > 200) {
|
||||
return finalError.substring(0, 200) + "...";
|
||||
}
|
||||
|
|
@ -87,34 +80,12 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ padding: "24px", borderRadius: "8px", backgroundColor: "#fff" }}>
|
||||
<div style={{ textAlign: "center", padding: "32px 20px" }}>
|
||||
<div className="loading-spinner" style={{ marginBottom: "16px" }}>
|
||||
<div
|
||||
style={{
|
||||
border: "3px solid #f3f3f3",
|
||||
borderTop: "3px solid #1890ff",
|
||||
borderRadius: "50%",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
animation: "spin 1s linear infinite",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Text style={{ fontSize: "16px" }}>
|
||||
<div className="rounded-lg bg-card p-6">
|
||||
<div className="flex flex-col items-center justify-center px-5 py-8">
|
||||
<UiLoadingSpinner className="mb-4 size-8 text-primary" />
|
||||
<p className="text-base text-foreground">
|
||||
Testing connection to {litellmParams.search_provider || "search provider"}...
|
||||
</Text>
|
||||
<style jsx>{`
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -125,147 +96,88 @@ const SearchConnectionTest: React.FC<SearchConnectionTestProps> = ({ litellmPara
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", borderRadius: "8px", backgroundColor: "#fff" }}>
|
||||
<div className="rounded-lg bg-card p-6">
|
||||
{testResult.status === "success" ? (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: "32px 20px" }}>
|
||||
<div style={{ color: "#52c41a", fontSize: "24px", display: "flex", alignItems: "center" }}>
|
||||
<svg
|
||||
viewBox="64 64 896 896"
|
||||
focusable="false"
|
||||
data-icon="check-circle"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ marginLeft: "12px" }}>
|
||||
<Text type="success" style={{ fontSize: "18px", fontWeight: 500, display: "block" }}>
|
||||
<div className="flex items-center justify-center px-5 py-8">
|
||||
<CheckCircle2 className="size-6 text-emerald-600" />
|
||||
<div className="ml-3">
|
||||
<p className="text-lg font-medium text-emerald-600">
|
||||
Connection to {litellmParams.search_provider} successful!
|
||||
</Text>
|
||||
</p>
|
||||
{testResult.test_query && (
|
||||
<Text style={{ fontSize: "14px", color: "#666", marginTop: "8px", display: "block" }}>
|
||||
Test query:{" "}
|
||||
<code style={{ backgroundColor: "#f0f0f0", padding: "2px 6px", borderRadius: "4px" }}>
|
||||
{testResult.test_query}
|
||||
</code>
|
||||
</Text>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Test query: <code className="rounded bg-muted px-1.5 py-0.5">{testResult.test_query}</code>
|
||||
</p>
|
||||
)}
|
||||
{testResult.results_count !== undefined && (
|
||||
<Text style={{ fontSize: "14px", color: "#666", display: "block" }}>
|
||||
Results retrieved: {testResult.results_count}
|
||||
</Text>
|
||||
<p className="text-sm text-muted-foreground">Results retrieved: {testResult.results_count}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<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
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-5 flex items-center">
|
||||
<AlertTriangle className="mr-3 size-6 text-destructive" />
|
||||
<p className="text-lg font-medium text-destructive">
|
||||
Connection to {litellmParams.search_provider || "search provider"} failed
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#fff2f0",
|
||||
border: "1px solid #ffccc7",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
marginBottom: "20px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.03)",
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px" }}>
|
||||
Error:{" "}
|
||||
</Text>
|
||||
<Text type="danger" style={{ fontSize: "14px", lineHeight: "1.5" }}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
<div className="mb-5 rounded-lg border border-destructive/30 bg-destructive/10 p-4">
|
||||
<p className="mb-2 font-semibold text-foreground">Error: </p>
|
||||
<p className="text-sm leading-relaxed text-destructive">{errorMessage}</p>
|
||||
|
||||
{testResult.error_type && (
|
||||
<div style={{ marginTop: "8px" }}>
|
||||
<Text style={{ fontSize: "13px", color: "#666" }}>
|
||||
Error type:{" "}
|
||||
<code
|
||||
style={{ backgroundColor: "#ffebee", padding: "2px 6px", borderRadius: "4px", color: "#d32f2f" }}
|
||||
>
|
||||
{testResult.error_type}
|
||||
</code>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testResult.message && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
style={{ paddingLeft: 0, height: "auto" }}
|
||||
>
|
||||
{showDetails ? "Hide Details" : "Show Details"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px", fontSize: "15px" }}>
|
||||
Full Error Details
|
||||
</Text>
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
maxHeight: "200px",
|
||||
overflow: "auto",
|
||||
border: "1px solid #e8e8e8",
|
||||
lineHeight: "1.5",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{testResult.message}
|
||||
</pre>
|
||||
{testResult.error_type && (
|
||||
<div className="mt-2">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Error type:{" "}
|
||||
<code className="rounded bg-destructive/10 px-1.5 py-0.5 text-destructive">
|
||||
{testResult.error_type}
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#fffbf0",
|
||||
border: "1px solid #ffe58f",
|
||||
borderLeft: "4px solid #faad14",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px", color: "#d48806" }}>
|
||||
Troubleshooting tips:
|
||||
</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's documentation for any additional requirements
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{testResult.message && (
|
||||
<div className="mt-3">
|
||||
<Button variant="link" size="sm" className="h-auto p-0" onClick={() => setShowDetails(!showDetails)}>
|
||||
{showDetails ? "Hide Details" : "Show Details"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
{showDetails && (
|
||||
<div className="mb-5">
|
||||
<p className="mb-2 text-[15px] font-semibold text-foreground">Full Error Details</p>
|
||||
<pre className="max-h-52 overflow-auto rounded-lg border border-border bg-muted p-4 text-[13px] leading-relaxed break-words whitespace-pre-wrap">
|
||||
{testResult.message}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-amber-200 border-l-4 border-l-amber-500 bg-amber-50 p-4">
|
||||
<p className="mb-2 font-semibold text-amber-700">Troubleshooting tips:</p>
|
||||
<ul className="my-2 list-disc pl-5 text-amber-800">
|
||||
<li className="mb-1.5">Verify your API key is correct and active</li>
|
||||
<li className="mb-1.5">Check if the search provider service is operational</li>
|
||||
<li className="mb-1.5">Ensure you have sufficient credits/quota with the provider</li>
|
||||
<li className="mb-1.5">Review the provider's documentation for any additional requirements</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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 />}>
|
||||
<Separator className="mt-6 mb-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/search"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
<Info className="size-4" />
|
||||
View Search Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button, Input, Typography, Spin } from "antd";
|
||||
import { ExternalLink, Search } from "lucide-react";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { SearchOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import { searchToolQueryCall } from "@/components/networking";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { Card, Title as TremorTitle } from "@tremor/react";
|
||||
|
||||
const { Text } = Typography;
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
|
||||
interface SearchResult {
|
||||
title: string;
|
||||
|
|
@ -36,7 +36,6 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
}[]
|
||||
>([]);
|
||||
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!query.trim()) {
|
||||
|
|
@ -60,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");
|
||||
|
|
@ -87,113 +85,79 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
}));
|
||||
};
|
||||
|
||||
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
|
||||
|
||||
const latestResults = searchHistory.length > 0 ? searchHistory[0] : null;
|
||||
|
||||
return (
|
||||
<Card className="mt-6">
|
||||
<div className="mb-6">
|
||||
<TremorTitle>Test Search Tool</TremorTitle>
|
||||
<Card className={`mt-6 ${className}`}>
|
||||
<div className="px-6">
|
||||
<h2 className="text-lg font-semibold text-foreground">Test Search Tool</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col" style={{ minHeight: "600px" }}>
|
||||
{/* Search Bar at Top */}
|
||||
<div className="flex min-h-[600px] flex-col px-6">
|
||||
<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={{
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<SearchOutlined className="text-gray-400 mr-3" style={{ fontSize: "18px" }} />
|
||||
<div className="relative flex-1">
|
||||
<Search className="pointer-events-none absolute top-1/2 left-3 size-[18px] -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="Enter your search query..."
|
||||
disabled={isLoading}
|
||||
bordered={false}
|
||||
style={{ fontSize: "15px", padding: 0, height: "100%", boxShadow: "none" }}
|
||||
className="h-12 pl-11 text-[15px]"
|
||||
/>
|
||||
</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)",
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleSearch} disabled={isLoading || !query.trim()} className="h-12 px-6 text-[15px]">
|
||||
{isLoading ? <UiLoadingSpinner className="size-4" /> : <Search className="size-4" />}
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Area */}
|
||||
<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 h-full flex-col items-center justify-center p-8">
|
||||
<div className="mb-6 flex size-24 items-center justify-center rounded-full bg-muted">
|
||||
<Search className="size-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 font-medium text-foreground">Test your search tool</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">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>
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<UiLoadingSpinner className="size-8 text-primary" />
|
||||
<p className="mt-4 font-medium text-muted-foreground">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 rounded-lg border border-border bg-muted/50 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<Text className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||
<p className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
Search Query
|
||||
</Text>
|
||||
<div className="text-base font-semibold text-gray-900 mt-1.5">{latestResults.query}</div>
|
||||
</p>
|
||||
<div className="mt-1.5 text-base font-semibold text-foreground">{latestResults.query}</div>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<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">
|
||||
<div className="ml-4 text-right">
|
||||
<p className="text-xs text-muted-foreground">{formatTimestamp(latestResults.timestamp)}</p>
|
||||
<div className="mt-1 flex items-center gap-3">
|
||||
<div className="text-sm font-semibold text-primary">
|
||||
{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-emerald-600">{latestResults.latency}ms</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -201,7 +165,6 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Results */}
|
||||
{latestResults.response &&
|
||||
latestResults.response.results &&
|
||||
latestResults.response.results.length > 0 ? (
|
||||
|
|
@ -212,73 +175,43 @@ export const SearchToolTester: React.FC<SearchToolTesterProps> = ({ searchToolNa
|
|||
return (
|
||||
<div
|
||||
key={resultIndex}
|
||||
className="bg-white border border-gray-200 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";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.boxShadow = "0 1px 2px 0 rgba(0, 0, 0, 0.05)";
|
||||
e.currentTarget.style.borderColor = "#e5e7eb";
|
||||
}}
|
||||
className="rounded-lg border border-border bg-card transition-shadow hover:shadow-md"
|
||||
>
|
||||
<div className="p-5">
|
||||
{/* Title and External Link */}
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<div className="mb-2 flex items-start justify-between gap-3">
|
||||
<a
|
||||
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="flex-1 text-lg leading-snug font-semibold text-primary hover:underline"
|
||||
>
|
||||
{result.title}
|
||||
</a>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="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="icon-sm"
|
||||
aria-label="Open result in new tab"
|
||||
className="shrink-0 text-muted-foreground"
|
||||
onClick={() => window.open(result.url, "_blank")}
|
||||
style={{ color: "#6b7280" }}
|
||||
/>
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* URL */}
|
||||
<div className="text-sm text-green-700 mb-3 truncate font-medium">{result.url}</div>
|
||||
<div className="mb-3 truncate text-sm font-medium text-emerald-700">{result.url}</div>
|
||||
|
||||
{/* Snippet Preview */}
|
||||
<div className="text-sm text-gray-700 leading-relaxed">
|
||||
<div className="text-sm leading-relaxed text-foreground">
|
||||
{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 h-auto p-0"
|
||||
onClick={() => toggleResultExpansion(0, resultIndex)}
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
color: "#3b82f6",
|
||||
}}
|
||||
>
|
||||
{isResultExpanded ? "Show less" : "Show more"}
|
||||
</Button>
|
||||
|
|
@ -289,31 +222,22 @@ 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="rounded-lg border border-border bg-muted/50 py-12 text-center">
|
||||
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
|
||||
<Search className="size-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="font-medium text-foreground">No results found</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Try a different search query</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Search History Sidebar */}
|
||||
{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>
|
||||
<Button
|
||||
onClick={clearHistory}
|
||||
size="small"
|
||||
type="link"
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<div className="mt-8 border-t border-border pt-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-foreground">Previous Searches</p>
|
||||
<Button variant="link" size="sm" className="h-auto p-0" onClick={clearHistory}>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -321,21 +245,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="cursor-pointer rounded-lg border border-border bg-muted/50 p-3 transition-colors hover:bg-muted"
|
||||
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">
|
||||
<span className="font-medium text-blue-600">
|
||||
<div className="truncate text-sm font-medium text-foreground">{entry.query}</div>
|
||||
<div className="mt-1.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-primary">
|
||||
{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-emerald-600">{entry.latency}ms</span>
|
||||
</>
|
||||
)}
|
||||
<span>•</span>
|
||||
|
|
|
|||
|
|
@ -155,13 +155,8 @@ describe("SearchToolView", () => {
|
|||
const toolNameContainer = screen.getByText("Test Search Tool").closest("div");
|
||||
expect(toolNameContainer).toBeInTheDocument();
|
||||
|
||||
const copyButtons = within(toolNameContainer!).getAllByRole("button");
|
||||
const nameCopyButton = copyButtons.find((button) => {
|
||||
return button.querySelector("svg") !== null;
|
||||
});
|
||||
|
||||
expect(nameCopyButton).toBeInTheDocument();
|
||||
await user.click(nameCopyButton!);
|
||||
const nameCopyButton = within(toolNameContainer!).getByRole("button");
|
||||
await user.click(nameCopyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("Test Search Tool");
|
||||
|
|
@ -176,13 +171,8 @@ describe("SearchToolView", () => {
|
|||
const toolIdContainer = screen.getByText("test-tool-id-123").closest("div");
|
||||
expect(toolIdContainer).toBeInTheDocument();
|
||||
|
||||
const copyButtons = within(toolIdContainer!).getAllByRole("button");
|
||||
const idCopyButton = copyButtons.find((button) => {
|
||||
return button.querySelector("svg") !== null;
|
||||
});
|
||||
|
||||
expect(idCopyButton).toBeInTheDocument();
|
||||
await user.click(idCopyButton!);
|
||||
const idCopyButton = within(toolIdContainer!).getByRole("button");
|
||||
await user.click(idCopyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("test-tool-id-123");
|
||||
|
|
@ -197,22 +187,14 @@ describe("SearchToolView", () => {
|
|||
render(<SearchToolView {...defaultProps} />);
|
||||
|
||||
const toolNameContainer = screen.getByText("Test Search Tool").closest("div");
|
||||
const copyButtons = within(toolNameContainer!).getAllByRole("button");
|
||||
const nameCopyButton = copyButtons.find((button) => {
|
||||
return button.querySelector("svg") !== null;
|
||||
});
|
||||
const nameCopyButton = within(toolNameContainer!).getByRole("button");
|
||||
|
||||
expect(nameCopyButton).toBeInTheDocument();
|
||||
expect(nameCopyButton.querySelector(".lucide-copy")).toBeInTheDocument();
|
||||
|
||||
const initialSvg = nameCopyButton!.querySelector("svg");
|
||||
expect(initialSvg).toBeInTheDocument();
|
||||
|
||||
await user.click(nameCopyButton!);
|
||||
await user.click(nameCopyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const updatedSvg = nameCopyButton!.querySelector("svg");
|
||||
expect(updatedSvg).toBeInTheDocument();
|
||||
expect(nameCopyButton).toHaveClass("text-green-600");
|
||||
expect(nameCopyButton.querySelector(".lucide-check")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -224,13 +206,9 @@ describe("SearchToolView", () => {
|
|||
render(<SearchToolView {...defaultProps} />);
|
||||
|
||||
const toolNameContainer = screen.getByText("Test Search Tool").closest("div");
|
||||
const copyButtons = within(toolNameContainer!).getAllByRole("button");
|
||||
const nameCopyButton = copyButtons.find((button) => {
|
||||
return button.querySelector("svg") !== null;
|
||||
});
|
||||
const nameCopyButton = within(toolNameContainer!).getByRole("button");
|
||||
|
||||
expect(nameCopyButton).toBeInTheDocument();
|
||||
await user.click(nameCopyButton!);
|
||||
await user.click(nameCopyButton);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
|
|
@ -239,7 +217,7 @@ describe("SearchToolView", () => {
|
|||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(nameCopyButton).not.toHaveClass("text-green-600");
|
||||
expect(nameCopyButton.querySelector(".lucide-check")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render SearchToolTester when accessToken is provided", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { Button, Card, Grid, Text, Title } from "@tremor/react";
|
||||
import { Button as AntdButton } from "antd";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { ArrowLeft, Check, Copy } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { SearchToolTester } from "./SearchToolTester";
|
||||
import { AvailableSearchProvider, SearchTool } from "./types";
|
||||
|
||||
|
|
@ -43,73 +42,73 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
|
|||
<div className="p-4 max-w-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button icon={ArrowLeftIcon} variant="light" className="mb-4" onClick={onBack}>
|
||||
<Button variant="ghost" size="sm" className="mb-4 -ml-2 text-muted-foreground" onClick={onBack}>
|
||||
<ArrowLeft className="mr-2 size-4" />
|
||||
Back to All Search Tools
|
||||
</Button>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Title>{searchTool.search_tool_name}</Title>
|
||||
<AntdButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["search-tool-name"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
<div className="flex items-center gap-1">
|
||||
<h1 className="text-2xl font-semibold text-foreground">{searchTool.search_tool_name}</h1>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label="Copy search tool name"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => copyToClipboard(searchTool.search_tool_name, "search-tool-name")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["search-tool-name"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
>
|
||||
{copiedStates["search-tool-name"] ? <Check /> : <Copy />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{searchTool.search_tool_id}</Text>
|
||||
<AntdButton
|
||||
type="text"
|
||||
size="small"
|
||||
icon={copiedStates["search-tool-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="font-mono text-sm text-muted-foreground">{searchTool.search_tool_id}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label="Copy search tool ID"
|
||||
className="text-muted-foreground"
|
||||
onClick={() => copyToClipboard(searchTool.search_tool_id, "search-tool-id")}
|
||||
className={`left-2 z-10 transition-all duration-200 ${
|
||||
copiedStates["search-tool-id"]
|
||||
? "text-green-600 bg-green-50 border-green-200"
|
||||
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
/>
|
||||
>
|
||||
{copiedStates["search-tool-id"] ? <Check /> : <Copy />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<Text>Provider</Text>
|
||||
<div className="mt-2">
|
||||
<Title>{getProviderDisplayName(searchTool.litellm_params.search_provider)}</Title>
|
||||
</div>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Provider</p>
|
||||
<p className="mt-2 text-lg font-semibold text-foreground">
|
||||
{getProviderDisplayName(searchTool.litellm_params.search_provider)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>API Key</Text>
|
||||
<div className="mt-2">
|
||||
<Text>{searchTool.litellm_params.api_key ? "****" : "Not set"}</Text>
|
||||
</div>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">API Key</p>
|
||||
<p className="mt-2 text-foreground">{searchTool.litellm_params.api_key ? "****" : "Not set"}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Created At</Text>
|
||||
<div className="mt-2">
|
||||
<Text>{searchTool.created_at ? new Date(searchTool.created_at).toLocaleString() : "Unknown"}</Text>
|
||||
</div>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Created At</p>
|
||||
<p className="mt-2 text-foreground">
|
||||
{searchTool.created_at ? new Date(searchTool.created_at).toLocaleString() : "Unknown"}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{searchTool.search_tool_info?.description && (
|
||||
<Card className="mt-6">
|
||||
<Text>Description</Text>
|
||||
<div className="mt-2">
|
||||
<Text>{searchTool.search_tool_info.description}</Text>
|
||||
</div>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Description</p>
|
||||
<p className="mt-2 text-foreground">{searchTool.search_tool_info.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Search Tool Tester */}
|
||||
<div className="mt-6">
|
||||
{accessToken && <SearchToolTester searchToolName={searchTool.search_tool_name} accessToken={accessToken} />}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue