import { memo, type JSX, useState } from "react"; import { type Row } from "@tanstack/react-table"; import { urlRegex } from "@langfuse/shared"; import { type JsonTableRow } from "@/src/components/table/utils/jsonExpansionUtils"; import { copyTextToClipboard } from "@/src/utils/clipboard"; import { Button } from "@/src/components/ui/button"; import { Copy, Check } from "lucide-react"; const MAX_STRING_LENGTH_FOR_LINK_DETECTION = 1500; export const MAX_CELL_DISPLAY_CHARS = 2000; const SMALL_ARRAY_THRESHOLD = 5; const ARRAY_PREVIEW_ITEMS = 3; const OBJECT_PREVIEW_KEYS = 2; const MONO_TEXT_CLASSES = "font-mono text-xs break-words"; const PREVIEW_TEXT_CLASSES = "italic text-gray-500 dark:text-gray-400"; function renderStringWithLinks(text: string): React.ReactNode { if (text.length >= MAX_STRING_LENGTH_FOR_LINK_DETECTION) { return text; } const localUrlRegex = new RegExp(urlRegex.source, "gi"); const parts = text.split(localUrlRegex); const matches = text.match(localUrlRegex) || []; const result: React.ReactNode[] = []; let matchIndex = 0; for (let i = 0; i < parts.length; i++) { if (parts[i]) { result.push(parts[i]); } if (matchIndex < matches.length) { const url = matches[matchIndex]; result.push( e.stopPropagation()} // no row expansion when clicking links > {url} , ); matchIndex++; } } return result; } function getValueType(value: unknown): JsonTableRow["type"] { if (value === null) return "null"; if (value === undefined) return "undefined"; if (Array.isArray(value)) return "array"; return typeof value as JsonTableRow["type"]; } function renderArrayValue(arr: unknown[]): JSX.Element { if (arr.length === 0) { return empty list; } if (arr.length <= SMALL_ARRAY_THRESHOLD) { // Show inline values for small arrays const displayItems = arr .map((item) => { const itemType = getValueType(item); if (itemType === "string") return `"${String(item)}"`; if (itemType === "object" && item !== null) { const obj = item as Record; const keys = Object.keys(obj); if (keys.length === 0) return "{}"; if (keys.length <= OBJECT_PREVIEW_KEYS) { const keyPreview = keys.map((k) => `"${k}": ...`).join(", "); return `{${keyPreview}}`; } else { return `{"${keys[0]}": ...}`; } } if (itemType === "array") return "..."; return String(item); }) .join(", "); return [{displayItems}]; } else { // Show truncated values for large arrays const preview = arr .slice(0, ARRAY_PREVIEW_ITEMS) .map((item) => { const itemType = getValueType(item); if (itemType === "string") return `"${String(item)}"`; if (itemType === "object" || itemType === "array") return "..."; return String(item); }) .join(", "); return ( [{preview}, ...{arr.length - ARRAY_PREVIEW_ITEMS} more] ); } } function renderObjectValue(obj: Record): JSX.Element { const keys = Object.keys(obj); if (keys.length === 0) { return empty object; } return {keys.length} items; } function getValueStringLength(value: unknown): number { if (typeof value === "string") { return value.length; } try { return JSON.stringify(value).length; } catch { return String(value).length; } } function getTruncatedValue(value: string, maxChars: number): string { if (value.length <= maxChars) { return value; } const truncated = value.substring(0, maxChars); const lastSpaceIndex = truncated.lastIndexOf(" "); // Try to truncate at word boundary if possible if (lastSpaceIndex > maxChars * 0.8) { return truncated.substring(0, lastSpaceIndex) + "..."; } return truncated + "..."; } function getCopyValue(value: unknown): string { if (typeof value === "string") { return value; // Return string without quotes } if (value === null) return "null"; if (value === undefined) return "undefined"; try { return JSON.stringify(value, null, 2); } catch { return String(value); } } export const ValueCell = memo( ({ row, expandedCells, toggleCellExpansion, }: { row: Row; expandedCells: Set; toggleCellExpansion: (cellId: string) => void; }) => { const { value, type } = row.original; const cellId = `${row.id}-value`; const isCellExpanded = expandedCells.has(cellId); const [showCopySuccess, setShowCopySuccess] = useState(false); const handleCopy = async (e: React.MouseEvent) => { e.stopPropagation(); const copyValue = getCopyValue(value); try { await copyTextToClipboard(copyValue); setShowCopySuccess(true); setTimeout(() => setShowCopySuccess(false), 1500); } catch { // Copy failed silently } }; const getDisplayValue = () => { switch (type) { case "string": { const stringValue = String(value); const needsTruncation = stringValue.length > MAX_CELL_DISPLAY_CHARS; const displayValue = needsTruncation && !isCellExpanded ? getTruncatedValue(stringValue, MAX_CELL_DISPLAY_CHARS) : stringValue; return { content: ( "{renderStringWithLinks(displayValue)}" ), needsTruncation, }; } case "number": return { content: ( {String(value)} ), needsTruncation: false, }; case "boolean": return { content: ( {String(value)} ), needsTruncation: false, }; case "null": return { content: ( null ), needsTruncation: false, }; case "undefined": return { content: ( undefined ), needsTruncation: false, }; case "array": { const arrayValue = value as unknown[]; // Arrays always show previews, never truncate return { content: renderArrayValue(arrayValue), needsTruncation: false, }; } case "object": { const objectValue = value as Record; // Objects always show previews, never truncate return { content: renderObjectValue(objectValue), needsTruncation: false, }; } default: { const stringValue = String(value); const needsTruncation = stringValue.length > MAX_CELL_DISPLAY_CHARS; const displayValue = needsTruncation && !isCellExpanded ? getTruncatedValue(stringValue, MAX_CELL_DISPLAY_CHARS) : stringValue; return { content: ( {displayValue} ), needsTruncation, }; } } }; const { content, needsTruncation } = getDisplayValue(); return (
{content} {needsTruncation && !row.original.hasChildren && (
{ e.stopPropagation(); toggleCellExpansion(cellId); }} > {isCellExpanded ? "\n...collapse" : `\n...expand (${getValueStringLength(value) - MAX_CELL_DISPLAY_CHARS} more characters)`}
)} {/* Copy button - appears on hover */}
); }, ); ValueCell.displayName = "ValueCell"; // Export utilities that might be needed elsewhere export { getValueStringLength };