mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
Refactor: Address code review feedback - use Antd components
Changes: - Use Antd Space component instead of manual flex layouts - Use Antd Text.copyable prop instead of custom clipboard utilities - Extract helper functions to utils.ts for testability - Remove clipboardUtils.ts (replaced with Antd built-in) - Update DrawerHeader, LogDetailsDrawer, and constants Benefits: - Cleaner code using standard Antd patterns - Better testability with separated utils - Consistent UX with Antd's copy tooltips - Reduced custom code maintenance Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2156db9f06
commit
c126e911bd
5 changed files with 152 additions and 170 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Button, Tag, Tooltip, Typography } from "antd";
|
||||
import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
|
||||
import { Button, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import { LogEntry } from "../columns";
|
||||
import { getProviderLogoAndName } from "../../provider_info_helpers";
|
||||
|
|
@ -20,7 +20,6 @@ const { Text } = Typography;
|
|||
interface DrawerHeaderProps {
|
||||
log: LogEntry;
|
||||
onClose: () => void;
|
||||
onCopyRequestId: () => void;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
statusLabel: string;
|
||||
|
|
@ -35,7 +34,6 @@ interface DrawerHeaderProps {
|
|||
export function DrawerHeader({
|
||||
log,
|
||||
onClose,
|
||||
onCopyRequestId,
|
||||
onPrevious,
|
||||
onNext,
|
||||
statusLabel,
|
||||
|
|
@ -61,7 +59,7 @@ export function DrawerHeader({
|
|||
|
||||
{/* Row 1: Request ID + Actions */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: SPACING_MEDIUM }}>
|
||||
<RequestIdSection requestId={log.request_id} onCopy={onCopyRequestId} />
|
||||
<RequestIdSection requestId={log.request_id} />
|
||||
<NavigationSection onPrevious={onPrevious} onNext={onNext} onClose={onClose} />
|
||||
</div>
|
||||
|
||||
|
|
@ -84,7 +82,7 @@ function ModelProviderSection({
|
|||
providerName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: SPACING_MEDIUM, marginBottom: SPACING_MEDIUM }}>
|
||||
<Space size={SPACING_MEDIUM} style={{ marginBottom: SPACING_MEDIUM }}>
|
||||
{providerLogo && (
|
||||
<img
|
||||
src={providerLogo}
|
||||
|
|
@ -96,50 +94,49 @@ function ModelProviderSection({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Space size={SPACING_MEDIUM} direction="horizontal">
|
||||
<Text strong style={{ fontSize: 14 }}>
|
||||
{model}
|
||||
</Text>
|
||||
{providerName && (
|
||||
<Text type="secondary" style={{ fontSize: 12, marginLeft: SPACING_MEDIUM }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{providerName}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request ID display with copy button
|
||||
* Request ID display with copy functionality
|
||||
*/
|
||||
function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: () => void }) {
|
||||
function RequestIdSection({ requestId }: { requestId: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: SPACING_MEDIUM, flex: 1, minWidth: 0 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Tooltip title={requestId}>
|
||||
<Text
|
||||
strong
|
||||
copyable={{ text: requestId, tooltips: ["Copy Request ID", "Copied!"] }}
|
||||
style={{
|
||||
fontSize: FONT_SIZE_HEADER,
|
||||
fontFamily: FONT_FAMILY_MONO,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{requestId}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Tooltip title="Copy Request ID">
|
||||
<Button type="text" size="small" icon={<CopyOutlined />} onClick={onCopy} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation controls (previous, next, close)
|
||||
* Shows keyboard shortcuts styled as buttons for visibility
|
||||
* Shows keyboard shortcuts with bounding boxes for visibility
|
||||
*/
|
||||
function NavigationSection({
|
||||
onPrevious,
|
||||
|
|
@ -168,7 +165,7 @@ function NavigationSection({
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: SPACING_SMALL }}>
|
||||
<Space size={SPACING_SMALL} split={<div style={{ width: 1, height: 20, background: COLOR_BORDER }} />}>
|
||||
<Button type="text" size="small" onClick={onPrevious}>
|
||||
<UpOutlined />
|
||||
<span style={keyboardShortcutStyle}>K</span>
|
||||
|
|
@ -177,13 +174,10 @@ function NavigationSection({
|
|||
<DownOutlined />
|
||||
<span style={keyboardShortcutStyle}>J</span>
|
||||
</Button>
|
||||
|
||||
<div style={{ width: 1, height: 20, background: COLOR_BORDER, margin: `0 ${SPACING_MEDIUM}px` }} />
|
||||
|
||||
<Tooltip title="ESC to close">
|
||||
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -202,13 +196,17 @@ function StatusBar({
|
|||
environment: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: SPACING_LARGE }}>
|
||||
<Space size={SPACING_LARGE}>
|
||||
<Tag color={statusColor}>{statusLabel}</Tag>
|
||||
<Tag>Env: {environment}</Tag>
|
||||
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
||||
{moment(log.startTime).format("MMM D, YYYY h:mm:ss A")}
|
||||
<span style={{ marginLeft: SPACING_MEDIUM }}>({moment(log.startTime).fromNow()})</span>
|
||||
</Text>
|
||||
</div>
|
||||
<Space size={SPACING_MEDIUM}>
|
||||
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
||||
{moment(log.startTime).format("MMM D, YYYY h:mm:ss A")}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
||||
({moment(log.startTime).fromNow()})
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse } from "antd";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import { Drawer, Typography, Space, Descriptions, Card, Tag, Tabs, Alert, Collapse } from "antd";
|
||||
import moment from "moment";
|
||||
import { LogEntry } from "../columns";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
|
|
@ -12,8 +11,16 @@ import { TruncatedValue } from "./TruncatedValue";
|
|||
import { TokenFlow } from "./TokenFlow";
|
||||
import { JsonViewer } from "./JsonViewer";
|
||||
import { DrawerHeader } from "./DrawerHeader";
|
||||
import { copyToClipboard } from "./clipboardUtils";
|
||||
import { useKeyboardNavigation } from "./useKeyboardNavigation";
|
||||
import {
|
||||
formatData,
|
||||
checkHasMessages,
|
||||
checkHasResponse,
|
||||
normalizeGuardrailEntries,
|
||||
calculateTotalMaskedEntities,
|
||||
getGuardrailLabel,
|
||||
checkHasVectorStoreData,
|
||||
} from "./utils";
|
||||
import {
|
||||
DRAWER_WIDTH,
|
||||
DRAWER_CONTENT_PADDING,
|
||||
|
|
@ -24,7 +31,7 @@ import {
|
|||
FONT_SIZE_SMALL,
|
||||
FONT_FAMILY_MONO,
|
||||
SPACING_XLARGE,
|
||||
MESSAGE_REQUEST_ID_COPIED,
|
||||
SPACING_MEDIUM,
|
||||
} from "./constants";
|
||||
import { ToolsSection } from "../ToolsSection";
|
||||
|
||||
|
|
@ -94,11 +101,6 @@ export function LogDetailsDrawer({
|
|||
const statusColor = metadata.status === "failure" ? ("error" as const) : ("success" as const);
|
||||
const environment = metadata?.user_api_key_team_alias || "default";
|
||||
|
||||
const handleCopyRequestId = () => {
|
||||
navigator.clipboard.writeText(logEntry.request_id);
|
||||
message.success(MESSAGE_REQUEST_ID_COPIED);
|
||||
};
|
||||
|
||||
const getRawRequest = () => {
|
||||
return formatData(logEntry.proxy_server_request || logEntry.messages);
|
||||
};
|
||||
|
|
@ -135,7 +137,6 @@ export function LogDetailsDrawer({
|
|||
<DrawerHeader
|
||||
log={logEntry}
|
||||
onClose={onClose}
|
||||
onCopyRequestId={handleCopyRequestId}
|
||||
onPrevious={selectPreviousLog}
|
||||
onNext={selectNextLog}
|
||||
statusLabel={statusLabel}
|
||||
|
|
@ -204,7 +205,6 @@ export function LogDetailsDrawer({
|
|||
{/* Request/Response JSON - Collapsible */}
|
||||
<RequestResponseSection
|
||||
hasResponse={hasResponse}
|
||||
onCopy={(data, label) => copyToClipboard(JSON.stringify(data, null, 2), label)}
|
||||
getRawRequest={getRawRequest}
|
||||
getFormattedResponse={getFormattedResponse}
|
||||
/>
|
||||
|
|
@ -217,7 +217,7 @@ export function LogDetailsDrawer({
|
|||
|
||||
{/* Metadata Card - Only show if there's metadata */}
|
||||
{logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && (
|
||||
<MetadataSection metadata={logEntry.metadata} onCopy={(data) => copyToClipboard(data, "Metadata")} />
|
||||
<MetadataSection metadata={logEntry.metadata} />
|
||||
)}
|
||||
|
||||
{/* Bottom spacing for scroll area */}
|
||||
|
|
@ -254,27 +254,27 @@ function TagsSection({ tags }: { tags: Record<string, any> }) {
|
|||
<Text strong style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
|
||||
Tags
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Space size={SPACING_MEDIUM} wrap>
|
||||
{Object.entries(tags).map(([key, value]) => (
|
||||
<Tag key={key}>
|
||||
{key}: {String(value)}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: number }) {
|
||||
return (
|
||||
<>
|
||||
<Space size={SPACING_MEDIUM}>
|
||||
<span>{label}</span>
|
||||
{maskedCount > 0 && (
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>
|
||||
<Tag color="blue">
|
||||
{maskedCount} masked
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -337,23 +337,20 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
|||
|
||||
interface RequestResponseSectionProps {
|
||||
hasResponse: boolean;
|
||||
onCopy: (data: any, label: string) => void;
|
||||
getRawRequest: () => any;
|
||||
getFormattedResponse: () => any;
|
||||
}
|
||||
|
||||
function RequestResponseSection({
|
||||
hasResponse,
|
||||
onCopy,
|
||||
getRawRequest,
|
||||
getFormattedResponse,
|
||||
}: RequestResponseSectionProps) {
|
||||
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
|
||||
|
||||
const handleCopy = () => {
|
||||
const getCopyText = () => {
|
||||
const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse();
|
||||
const label = activeTab === TAB_REQUEST ? "Request" : "Response";
|
||||
onCopy(data, label);
|
||||
return JSON.stringify(data, null, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -371,15 +368,13 @@ function RequestResponseSection({
|
|||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
||||
tabBarExtraContent={
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
<Text
|
||||
copyable={{
|
||||
text: getCopyText(),
|
||||
tooltips: ["Copy JSON", "Copied!"]
|
||||
}}
|
||||
disabled={activeTab === TAB_RESPONSE && !hasResponse}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
/>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
|
|
@ -417,7 +412,7 @@ function RequestResponseSection({
|
|||
);
|
||||
}
|
||||
|
||||
function MetadataSection({ metadata, onCopy }: { metadata: Record<string, any>; onCopy: (data: string) => void }) {
|
||||
function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
|
||||
<Collapse
|
||||
|
|
@ -430,14 +425,12 @@ function MetadataSection({ metadata, onCopy }: { metadata: Record<string, any>;
|
|||
children: (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => onCopy(JSON.stringify(metadata, null, 2))}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<Text
|
||||
copyable={{
|
||||
text: JSON.stringify(metadata, null, 2),
|
||||
tooltips: ["Copy Metadata", "Copied!"]
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<pre
|
||||
style={{
|
||||
|
|
@ -461,60 +454,3 @@ function MetadataSection({ metadata, onCopy }: { metadata: Record<string, any>;
|
|||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function formatData(input: any) {
|
||||
if (typeof input === "string") {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function checkHasMessages(messages: any): boolean {
|
||||
if (!messages) return false;
|
||||
if (Array.isArray(messages)) return messages.length > 0;
|
||||
if (typeof messages === "object") return Object.keys(messages).length > 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkHasResponse(response: any): boolean {
|
||||
if (!response) return false;
|
||||
return Object.keys(formatData(response)).length > 0;
|
||||
}
|
||||
|
||||
function normalizeGuardrailEntries(guardrailInfo: any): any[] {
|
||||
if (Array.isArray(guardrailInfo)) return guardrailInfo;
|
||||
if (guardrailInfo) return [guardrailInfo];
|
||||
return [];
|
||||
}
|
||||
|
||||
function calculateTotalMaskedEntities(entries: any[]): number {
|
||||
return entries.reduce((sum, entry) => {
|
||||
const maskedCounts = entry?.masked_entity_count;
|
||||
if (!maskedCounts) return sum;
|
||||
return (
|
||||
sum +
|
||||
Object.values(maskedCounts).reduce<number>((acc, count) => (typeof count === "number" ? acc + count : acc), 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getGuardrailLabel(entries: any[]): string {
|
||||
if (entries.length === 0) return "-";
|
||||
if (entries.length === 1) return entries[0]?.guardrail_name ?? "-";
|
||||
return `${entries.length} guardrails`;
|
||||
}
|
||||
|
||||
function checkHasVectorStoreData(metadata: Record<string, any>): boolean {
|
||||
return (
|
||||
metadata.vector_store_request_metadata &&
|
||||
Array.isArray(metadata.vector_store_request_metadata) &&
|
||||
metadata.vector_store_request_metadata.length > 0
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import { message } from "antd";
|
||||
import { MESSAGE_COPY_SUCCESS } from "./constants";
|
||||
|
||||
/**
|
||||
* Copies text to clipboard with fallback for non-secure contexts.
|
||||
* Shows success/error message to user.
|
||||
*
|
||||
* @param text - Text to copy to clipboard
|
||||
* @param label - Label for the copied content (e.g., "Request", "Metadata")
|
||||
* @returns Promise<boolean> - true if copy succeeded, false otherwise
|
||||
*/
|
||||
export async function copyToClipboard(text: string, label: string): Promise<boolean> {
|
||||
try {
|
||||
// Try modern clipboard API first
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
message.success(`${label} ${MESSAGE_COPY_SUCCESS}`);
|
||||
return true;
|
||||
} else {
|
||||
// Fallback for non-secure contexts (like 0.0.0.0)
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.opacity = "0";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (!successful) {
|
||||
throw new Error("execCommand failed");
|
||||
}
|
||||
message.success(`${label} ${MESSAGE_COPY_SUCCESS}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Copy failed:", error);
|
||||
message.error(`Failed to copy ${label}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,4 @@ export const SPACING_LARGE = 12;
|
|||
export const SPACING_XLARGE = 16;
|
||||
export const SPACING_XXLARGE = 24;
|
||||
|
||||
// Messages
|
||||
export const MESSAGE_COPY_SUCCESS = "copied to clipboard";
|
||||
export const MESSAGE_REQUEST_ID_COPIED = "Request ID copied to clipboard";
|
||||
// Messages (kept for backwards compatibility if needed elsewhere)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Utility functions for LogDetailsDrawer component.
|
||||
* These functions handle data formatting, validation, and guardrail calculations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formats data for display. If input is a string, attempts to parse as JSON.
|
||||
* @param input - Data to format (string or object)
|
||||
* @returns Parsed JSON object or original input
|
||||
*/
|
||||
export function formatData(input: any) {
|
||||
if (typeof input === "string") {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if messages array/object contains data.
|
||||
* @param messages - Messages to check
|
||||
* @returns True if messages exist and have content
|
||||
*/
|
||||
export function checkHasMessages(messages: any): boolean {
|
||||
if (!messages) return false;
|
||||
if (Array.isArray(messages)) return messages.length > 0;
|
||||
if (typeof messages === "object") return Object.keys(messages).length > 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if response object contains data.
|
||||
* @param response - Response to check
|
||||
* @returns True if response exists and has content
|
||||
*/
|
||||
export function checkHasResponse(response: any): boolean {
|
||||
if (!response) return false;
|
||||
return Object.keys(formatData(response)).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes guardrail information into an array.
|
||||
* @param guardrailInfo - Guardrail data (may be array, object, or null)
|
||||
* @returns Array of guardrail entries
|
||||
*/
|
||||
export function normalizeGuardrailEntries(guardrailInfo: any): any[] {
|
||||
if (Array.isArray(guardrailInfo)) return guardrailInfo;
|
||||
if (guardrailInfo) return [guardrailInfo];
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total number of masked entities across all guardrail entries.
|
||||
* @param entries - Array of guardrail entries
|
||||
* @returns Total count of masked entities
|
||||
*/
|
||||
export function calculateTotalMaskedEntities(entries: any[]): number {
|
||||
return entries.reduce((sum, entry) => {
|
||||
const maskedCounts = entry?.masked_entity_count;
|
||||
if (!maskedCounts) return sum;
|
||||
return (
|
||||
sum +
|
||||
Object.values(maskedCounts).reduce<number>((acc, count) => (typeof count === "number" ? acc + count : acc), 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a display label for guardrail(s).
|
||||
* @param entries - Array of guardrail entries
|
||||
* @returns Display string for guardrail label
|
||||
*/
|
||||
export function getGuardrailLabel(entries: any[]): string {
|
||||
if (entries.length === 0) return "-";
|
||||
if (entries.length === 1) return entries[0]?.guardrail_name ?? "-";
|
||||
return `${entries.length} guardrails`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if vector store data exists in metadata.
|
||||
* @param metadata - Metadata object to check
|
||||
* @returns True if vector store data exists and is non-empty
|
||||
*/
|
||||
export function checkHasVectorStoreData(metadata: Record<string, any>): boolean {
|
||||
return (
|
||||
metadata.vector_store_request_metadata &&
|
||||
Array.isArray(metadata.vector_store_request_metadata) &&
|
||||
metadata.vector_store_request_metadata.length > 0
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue