feat(ui): vector_store_management/VectorStoreTester to shadcn

- antd Button/Card/Divider/Input(TextArea)/Spin/Typography + antd
  icons (Send/Database/Loading/Down/Right) \u2192 shadcn Button/Card/
  Textarea + lucide icons + native <hr>/<h4>.
- Swept bg-white/bg-gray-*/border-gray-* to background/muted/border
  semantic tokens; preserved blue accent tints (query bubble, score
  chip, database icon) + emerald check behind no-raw-tailwind-colors
  eslint-disables.
- Replaced antd Spin with lucide Loader2 (animate-spin).

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 21:22:43 +00:00
parent d4e08f4ae2
commit d90c70fed0
No known key found for this signature in database

View file

@ -1,13 +1,18 @@
import React, { useState } from "react";
import { Button, Input, Card, Typography, Spin, Divider } from "antd";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import MessageManager from "@/components/molecules/message_manager";
import { SendOutlined, DatabaseOutlined, LoadingOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
import {
ChevronDown,
ChevronRight,
Database,
Loader2,
Send,
} from "lucide-react";
import { vectorStoreSearchCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
const { TextArea } = Input;
const { Text, Title } = Typography;
interface VectorStoreContent {
text: string;
type: string;
@ -18,6 +23,7 @@ interface VectorStoreResult {
content: VectorStoreContent[];
file_id?: string;
filename?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributes?: Record<string, any>;
}
@ -33,7 +39,10 @@ interface VectorStoreTesterProps {
className?: string;
}
export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStoreId, accessToken, className = "" }) => {
export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({
vectorStoreId,
accessToken,
}) => {
const [query, setQuery] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [searchHistory, setSearchHistory] = useState<
@ -43,7 +52,9 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
timestamp: number;
}[]
>([]);
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
const [expandedResults, setExpandedResults] = useState<
Record<string, boolean>
>({});
const handleSearch = async () => {
if (!query.trim()) {
@ -54,7 +65,11 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
setIsLoading(true);
try {
const response = await vectorStoreSearchCall(accessToken, vectorStoreId, query);
const response = await vectorStoreSearchCall(
accessToken,
vectorStoreId,
query,
);
const historyEntry = {
query,
@ -101,15 +116,21 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
<Card className="w-full rounded-xl shadow-md">
<div className="flex flex-col h-[600px]">
{/* Header */}
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<div className="p-4 border-b border-border flex justify-between items-center">
<div className="flex items-center">
<DatabaseOutlined className="mr-2 text-blue-500" />
<Title level={4} className="mb-0">
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<Database className="mr-2 text-blue-500 h-4 w-4" />
<h4 className="text-base font-semibold m-0">
Test Vector Store
</Title>
</h4>
</div>
{searchHistory.length > 0 && (
<Button onClick={clearHistory} size="small">
<Button
type="button"
variant="outline"
size="sm"
onClick={clearHistory}
>
Clear History
</Button>
)}
@ -118,9 +139,11 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
{/* Results Area */}
<div className="flex-1 overflow-auto p-4 pb-0">
{searchHistory.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-gray-400">
<DatabaseOutlined style={{ fontSize: "48px", marginBottom: "16px" }} />
<Text>Test your vector store by entering a search query below</Text>
<div className="h-full flex flex-col items-center justify-center text-muted-foreground">
<Database className="h-12 w-12 mb-4" />
<span>
Test your vector store by entering a search query below
</span>
</div>
) : (
<div className="space-y-4">
@ -128,10 +151,13 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
<div key={index} className="space-y-2">
{/* User Query */}
<div className="text-right">
<div className="inline-block max-w-[80%] rounded-lg shadow-sm p-3 bg-blue-50 border border-blue-200">
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<div className="inline-block max-w-[80%] rounded-lg shadow-sm p-3 bg-blue-50 border border-blue-200 dark:bg-blue-950/30 dark:border-blue-900">
<div className="flex items-center gap-2 mb-1">
<strong className="text-sm">Query</strong>
<span className="text-xs text-gray-500">{formatTimestamp(entry.timestamp)}</span>
<span className="text-xs text-muted-foreground">
{formatTimestamp(entry.timestamp)}
</span>
</div>
<div className="text-left">{entry.query}</div>
</div>
@ -139,85 +165,128 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
{/* Vector Store Response */}
<div className="text-left">
<div className="inline-block max-w-[80%] rounded-lg shadow-sm p-3 bg-white border border-gray-200">
<div className="inline-block max-w-[80%] rounded-lg shadow-sm p-3 bg-background border border-border">
<div className="flex items-center gap-2 mb-2">
<DatabaseOutlined className="text-green-500" />
<strong className="text-sm">Vector Store Results</strong>
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<Database className="h-4 w-4 text-emerald-500" />
<strong className="text-sm">
Vector Store Results
</strong>
{entry.response && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-600">
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">
{entry.response.data?.length || 0} results
</span>
)}
</div>
{entry.response && entry.response.data && entry.response.data.length > 0 ? (
{entry.response &&
entry.response.data &&
entry.response.data.length > 0 ? (
<div className="space-y-3">
{entry.response.data.map((result, resultIndex) => {
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
const isExpanded =
expandedResults[`${index}-${resultIndex}`] ||
false;
return (
<div key={resultIndex} className="border rounded-lg overflow-hidden bg-gray-50">
<div
key={resultIndex}
className="border border-border rounded-lg overflow-hidden bg-muted"
>
{/* Clickable Header */}
<div
className="flex justify-between items-center p-3 cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() => toggleResultExpansion(index, resultIndex)}
className="flex justify-between items-center p-3 cursor-pointer hover:bg-muted/70 transition-colors"
onClick={() =>
toggleResultExpansion(index, resultIndex)
}
>
<div className="flex items-center">
{isExpanded ? (
<DownOutlined className="text-gray-500 mr-2" />
<ChevronDown className="h-4 w-4 text-muted-foreground mr-2" />
) : (
<RightOutlined className="text-gray-500 mr-2" />
)}
<span className="font-medium text-sm">Result {resultIndex + 1}</span>
{/* Show preview of content when collapsed */}
{!isExpanded && result.content && result.content[0] && (
<span className="ml-2 text-xs text-gray-500 truncate max-w-md">
- {result.content[0].text.substring(0, 100)}...
</span>
<ChevronRight className="h-4 w-4 text-muted-foreground mr-2" />
)}
<span className="font-medium text-sm">
Result {resultIndex + 1}
</span>
{!isExpanded &&
result.content &&
result.content[0] && (
<span className="ml-2 text-xs text-muted-foreground truncate max-w-md">
-{" "}
{result.content[0].text.substring(
0,
100,
)}
...
</span>
)}
</div>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded dark:bg-blue-950/40 dark:text-blue-200">
Score: {result.score.toFixed(4)}
</span>
</div>
{/* Expandable Content */}
{isExpanded && (
<div className="border-t bg-white p-3">
{/* Content */}
<div className="border-t border-border bg-background p-3">
{result.content &&
result.content.map((content, contentIndex) => (
<div key={contentIndex} className="mb-3">
<div className="text-xs text-gray-500 mb-1">Content ({content.type})</div>
<div className="text-sm bg-gray-50 p-3 rounded border text-gray-800 max-h-40 overflow-y-auto">
{content.text}
result.content.map(
(content, contentIndex) => (
<div
key={contentIndex}
className="mb-3"
>
<div className="text-xs text-muted-foreground mb-1">
Content ({content.type})
</div>
<div className="text-sm bg-muted p-3 rounded border border-border max-h-40 overflow-y-auto">
{content.text}
</div>
</div>
</div>
))}
),
)}
{/* Metadata */}
{(result.file_id || result.filename || result.attributes) && (
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="text-xs text-gray-500 mb-2 font-medium">Metadata</div>
{(result.file_id ||
result.filename ||
result.attributes) && (
<div className="mt-3 pt-3 border-t border-border">
<div className="text-xs text-muted-foreground mb-2 font-medium">
Metadata
</div>
<div className="space-y-2 text-xs">
{result.file_id && (
<div className="bg-gray-50 p-2 rounded">
<span className="font-medium">File ID:</span> {result.file_id}
<div className="bg-muted p-2 rounded">
<span className="font-medium">
File ID:
</span>{" "}
{result.file_id}
</div>
)}
{result.filename && (
<div className="bg-gray-50 p-2 rounded">
<span className="font-medium">Filename:</span> {result.filename}
</div>
)}
{result.attributes && Object.keys(result.attributes).length > 0 && (
<div className="bg-gray-50 p-2 rounded">
<span className="font-medium block mb-1">Attributes:</span>
<pre className="text-xs bg-white p-2 rounded border overflow-x-auto">
{JSON.stringify(result.attributes, null, 2)}
</pre>
<div className="bg-muted p-2 rounded">
<span className="font-medium">
Filename:
</span>{" "}
{result.filename}
</div>
)}
{result.attributes &&
Object.keys(result.attributes)
.length > 0 && (
<div className="bg-muted p-2 rounded">
<span className="font-medium block mb-1">
Attributes:
</span>
<pre className="text-xs bg-background p-2 rounded border border-border overflow-x-auto">
{JSON.stringify(
result.attributes,
null,
2,
)}
</pre>
</div>
)}
</div>
</div>
)}
@ -228,12 +297,16 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
})}
</div>
) : (
<div className="text-gray-500 text-sm">No results found</div>
<div className="text-muted-foreground text-sm">
No results found
</div>
)}
</div>
</div>
{index < searchHistory.length - 1 && <Divider />}
{index < searchHistory.length - 1 && (
<hr className="border-border" />
)}
</div>
))}
</div>
@ -241,32 +314,35 @@ export const VectorStoreTester: React.FC<VectorStoreTesterProps> = ({ vectorStor
{isLoading && (
<div className="flex justify-center items-center my-4">
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
<Loader2 className="h-6 w-6 animate-spin text-primary" />
</div>
)}
</div>
{/* Input Area */}
<div className="p-4 border-t border-gray-200 bg-white">
<div className="flex items-end space-x-2">
<div className="p-4 border-t border-border bg-background">
<div className="flex items-end gap-2">
<div className="flex-1">
<TextArea
<Textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter your search query... (Shift+Enter for new line)"
disabled={isLoading}
autoSize={{ minRows: 1, maxRows: 4 }}
style={{ resize: "none" }}
rows={1}
className="resize-none"
/>
</div>
<Button
type="primary"
type="button"
onClick={handleSearch}
disabled={isLoading || !query.trim()}
icon={<SendOutlined />}
loading={isLoading}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
Search
</Button>
</div>