diff --git a/app/(protected)/project/[project_id]/evaluations/[test_id]/page.tsx b/app/(protected)/project/[project_id]/evaluations/[test_id]/page.tsx index 5eabf586..da6bb2e5 100644 --- a/app/(protected)/project/[project_id]/evaluations/[test_id]/page.tsx +++ b/app/(protected)/project/[project_id]/evaluations/[test_id]/page.tsx @@ -2,6 +2,7 @@ import { ScaleType } from "@/components/evaluations/eval-scale-picker"; import { RangeScale } from "@/components/evaluations/range-scale"; +import UserLogo from "@/components/shared/user-logo"; import { VendorLogo } from "@/components/shared/vendor-metadata"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -10,6 +11,7 @@ import { cn, extractSystemPromptFromLlmInputs, formatDateTime, + safeStringify, } from "@/lib/utils"; import { Cross1Icon, EnterIcon } from "@radix-ui/react-icons"; import { ProgressCircle } from "@tremor/react"; @@ -23,7 +25,6 @@ import { } from "lucide-react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; -import Markdown from "react-markdown"; import { useQuery, useQueryClient } from "react-query"; import { toast } from "sonner"; @@ -315,7 +316,7 @@ export default function Page() { size="md" value={(page / totalPages) * 100} > - + {Math.round((page / totalPages) * 100)}% @@ -348,8 +349,8 @@ export default function Page() { className={cn( "ml-2 text-xs px-1 py-[2px] rounded-md", evaluationsData?.evaluations[0]?.id - ? "bg-green-400" - : "bg-orange-400" + ? "bg-green-400 dark:bg-green-800" + : "bg-orange-400 dark:bg-orange-800" )} > {evaluationsData?.evaluations[0]?.id @@ -474,49 +475,69 @@ function ConversationView({ span }: { span: any }) { if (!prompts && !responses) return

No data found

; return ( -
+
{prompts?.length > 0 && - JSON.parse(prompts).map((prompt: any, i: number) => ( -
-

- {prompt?.role - ? prompt?.role === "function" - ? `${prompt?.role} - ${prompt?.name}` - : prompt?.role - : "Input"} - : - {prompt?.content - ? " (content)" - : prompt?.function_call - ? " (function call)" - : ""} -

{" "} - - {prompt?.content - ? prompt?.content - : prompt?.function_call - ? JSON.stringify(prompt?.function_call) - : "No input found"} - -
- ))} + JSON.parse(prompts).map((prompt: any, i: number) => { + const role = prompt?.role ? prompt?.role?.toLowerCase() : "User"; + const content = prompt?.content + ? safeStringify(prompt?.content) + : prompt?.function_call + ? safeStringify(prompt?.function_call) + : "No input found"; + return ( +
+
+ {role === "user" ? ( + + ) : ( + + )} +

{role}

+ {role === "system" && ( +

+ Prompt +

+ )} +
+
+
+ ); + })} {responses?.length > 0 && - JSON.parse(responses).map((response: any, i: number) => ( -
-
- -

- {response?.message?.role || "Output"}: -

{" "} + JSON.parse(responses).map((response: any, i: number) => { + const role = + response?.role?.toLowerCase() || + response?.message?.role || + "Assistant"; + const content = + safeStringify(response?.content) || + safeStringify(response?.message?.content) || + safeStringify(response?.text) || + "No output found"; + return ( +
+
+ {role === "user" ? ( + + ) : ( + + )} +

{role}

+
+
- - {response?.message?.content || - response?.text || - response?.content || - "No output found"} - -
- ))} + ); + })}
); } diff --git a/components/evaluations/evaluation-row.tsx b/components/evaluations/evaluation-row.tsx index 31772c0a..f303f5cc 100644 --- a/components/evaluations/evaluation-row.tsx +++ b/components/evaluations/evaluation-row.tsx @@ -215,18 +215,12 @@ export default function EvaluationRow({

{model}

0 ? JSON.parse(prompts)[0]?.content : ""} + className="flex items-center text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" + values={prompts?.length > 0 ? JSON.parse(prompts) : []} /> 0 - ? JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content - : "" - } + className="flex items-center text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" + values={responses?.length > 0 ? JSON.parse(responses) : []} />

{cost.total.toFixed(6) !== "0.000000" diff --git a/components/project/traces/trace-row.tsx b/components/project/traces/trace-row.tsx index a340926d..64922495 100644 --- a/components/project/traces/trace-row.tsx +++ b/components/project/traces/trace-row.tsx @@ -126,18 +126,12 @@ export const TraceRow = ({

{model}

0 ? JSON.parse(prompts)[0]?.content : ""} - className="max-w-fit text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" + values={prompts?.length > 0 ? JSON.parse(prompts) : []} + className="flex items-center max-w-fit text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" /> 0 - ? JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content - : "" - } - className="max-w-fit text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" + values={responses?.length > 0 ? JSON.parse(responses) : []} + className="flex items-center max-w-fit text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" />

{userId}

diff --git a/components/shared/hover-cell.tsx b/components/shared/hover-cell.tsx index 38e87d6c..4f9132fa 100644 --- a/components/shared/hover-cell.tsx +++ b/components/shared/hover-cell.tsx @@ -3,22 +3,61 @@ import { HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card"; -import Markdown from "react-markdown"; +import { safeStringify } from "@/lib/utils"; export function HoverCell({ - value, + values, className, }: { - value: string; + values: any[]; className?: string; }) { + const contents = values.map((value, i) => { + const role = value?.role + ? value?.role?.toLowerCase() + : value?.message?.role + ? value?.message?.role + : "User"; + const content = value?.content + ? safeStringify(value?.content) + : value?.function_call + ? safeStringify(value?.function_call) + : value?.message?.content + ? safeStringify(value?.message?.content) + : value?.text + ? safeStringify(value?.text) + : ""; + return { role, content }; + }); + + if (contents.length === 0) { + return null; + } + return ( - +

- {value} +
+ {contents.map((item, i) => ( +
+

+ {item.role} +

+
+
+ ))} +
); diff --git a/components/shared/llm-view.tsx b/components/shared/llm-view.tsx index 5b255b8a..be76166c 100644 --- a/components/shared/llm-view.tsx +++ b/components/shared/llm-view.tsx @@ -1,8 +1,7 @@ "use client"; import detectPII from "@/lib/pii"; -import { cn } from "@/lib/utils"; -import Markdown from "react-markdown"; +import { cn, safeStringify } from "@/lib/utils"; export const LLMView = ({ prompts, @@ -18,66 +17,68 @@ export const LLMView = ({ return (
{prompts?.length > 0 && - JSON.parse(prompts).map((prompt: any, i: number) => ( -

- - {prompt?.role - ? prompt?.role === "function" - ? `${prompt?.role} - ${prompt?.name}` - : prompt?.role - : "Q"} - : - {prompt?.content - ? " (content)" - : prompt?.function_call - ? " (function call)" - : ""} - {" "} - 0 && - "underline decoration-red-600 decoration-[3px]" - )} + JSON.parse(prompts).map((prompt: any, i: number) => { + const role = prompt?.role ? prompt?.role?.toLowerCase() : "User"; + const content = prompt?.content + ? prompt?.content + : prompt?.function_call + ? prompt?.function_call + : ""; + return ( +

- {prompt?.content - ? prompt?.content - : prompt?.function_call - ? JSON.stringify(prompt?.function_call) - : ""} - -

- ))} -
- - {JSON.parse(responses)[0]?.message?.role || "Assistant"}: - {" "} - {responses?.length > 0 ? ( - 0 && - "underline decoration-red-600 decoration-[3px]" - )} - > - {JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content || - JSON.parse(responses).message?.content} - - ) : ( - "" - )} - {Evaluate && } -
+ + {role} + {" "} +
0 && + "underline decoration-red-600 decoration-[3px]" + )} + dangerouslySetInnerHTML={{ __html: safeStringify(content) }} + /> +
+ ); + })} + {responses?.length > 0 && + JSON.parse(responses).map((response: any, i: number) => { + const role = + response?.role?.toLowerCase() || + response?.message?.role || + "Assistant"; + const content = + response?.content || + response?.message?.content || + response?.text || + ""; + + return ( +
+ + {role} + +
0 && + "underline decoration-red-600 decoration-[3px]" + )} + dangerouslySetInnerHTML={{ __html: safeStringify(content) }} + /> + {Evaluate && } +
+ ); + })}
); }; diff --git a/components/shared/user-logo.tsx b/components/shared/user-logo.tsx new file mode 100644 index 00000000..422b1d70 --- /dev/null +++ b/components/shared/user-logo.tsx @@ -0,0 +1,7 @@ +export default function UserLogo() { + return ( +
+ U +
+ ); +} diff --git a/components/shared/vendor-metadata.tsx b/components/shared/vendor-metadata.tsx index 54a145ee..4508ec0b 100644 --- a/components/shared/vendor-metadata.tsx +++ b/components/shared/vendor-metadata.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/lib/utils"; import { StackIcon } from "@radix-ui/react-icons"; import Image from "next/image"; @@ -37,6 +38,10 @@ export function vendorBadgeColor(vendor: string) { return "bg-indigo-500"; } + if (vendor.includes("cohere")) { + return "bg-red-500"; + } + return "bg-gray-500"; } @@ -73,6 +78,10 @@ export function vendorColor(vendor: string) { return "bg-indigo-200"; } + if (vendor.includes("cohere")) { + return "bg-red-200"; + } + return "bg-gray-800"; } @@ -87,7 +96,13 @@ export function serviceTypeColor(serviceType: string) { return "bg-gray-500"; } -export function VendorLogo({ span }: { span: Span }) { +export function VendorLogo({ + span, + variant = "default", +}: { + span: Span; + variant?: string; +}) { const attributes = span.attributes ? JSON.parse(span.attributes) : {}; let serviceName = ""; if (attributes["langtrace.service.name"]) { @@ -102,7 +117,10 @@ export function VendorLogo({ span }: { span: Span }) { src="/perplexity.png" width={20} height={20} - className={`${color} p-[3px] rounded-sm`} + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} /> ); } @@ -115,7 +133,10 @@ export function VendorLogo({ span }: { span: Span }) { src="/openai.svg" width={20} height={20} - className={`${color} p-[3px] rounded-sm`} + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} /> ); } @@ -128,7 +149,10 @@ export function VendorLogo({ span }: { span: Span }) { src="/anthropic.png" width={30} height={30} - className="p-[3px] rounded-md" + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} /> ); } @@ -141,7 +165,10 @@ export function VendorLogo({ span }: { span: Span }) { src="/pinecone.png" width={20} height={20} - className={`${color} p-[3px] rounded-sm`} + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} /> ); } @@ -154,7 +181,10 @@ export function VendorLogo({ span }: { span: Span }) { src="/chroma.png" width={25} height={25} - className={`${color} p-[3px] rounded-sm`} + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} /> ); } @@ -167,7 +197,10 @@ export function VendorLogo({ span }: { span: Span }) { src="/langchain.svg" width={30} height={30} - className={`${color} p-[3px] rounded-sm`} + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} /> ); } @@ -180,15 +213,38 @@ export function VendorLogo({ span }: { span: Span }) { src="/llamaindex.svg" width={60} height={80} - className={`${color} p-[3px] rounded-sm`} + className={cn( + `${color} p-[3px]`, + variant === "circular" ? "rounded-full" : "rounded-md" + )} + /> + ); + } + + if (span.name.includes("cohere") || serviceName.includes("cohere")) { + const color = vendorColor("cohere"); + return ( + Cohere Logo ); } - const color = vendorColor("langtrace"); return (
- +
); } diff --git a/lib/pii.ts b/lib/pii.ts index 1d631c9a..4577e1eb 100644 --- a/lib/pii.ts +++ b/lib/pii.ts @@ -1,8 +1,5 @@ export default function detectPII(text: string): string[] { try { - JSON.parse(text); - return []; - } catch (e) { // Define regular expressions for various types of PII const patterns = { phoneNumber: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, // Simplified phone number pattern (US-centric) @@ -24,7 +21,8 @@ export default function detectPII(text: string): string[] { foundPII.push(...matches.map((match) => `${type}: ${match}`)); } } - return foundPII; + } catch (error) { + return []; } } diff --git a/lib/utils.ts b/lib/utils.ts index 3935dfe9..9c04bda9 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -3,6 +3,7 @@ import { clsx, type ClassValue } from "clsx"; import { createHash, randomBytes } from "crypto"; import { TiktokenEncoding, getEncoding } from "js-tiktoken"; import { NextResponse } from "next/server"; +import { prettyPrintJson } from "pretty-print-json"; import { twMerge } from "tailwind-merge"; import { Span } from "./clients/scale3_clickhouse/models/span"; import { @@ -405,3 +406,12 @@ export const getChartColor = (value: number) => { return "green"; } }; + +export function safeStringify(value: any): string { + // Check if the value is already a string + if (typeof value === "string") { + return value; + } + // If it's not a string, stringify it + return prettyPrintJson.toHtml(value); +} diff --git a/package-lock.json b/package-lock.json index d5bd2206..4568a4a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "f": "^1.4.0", "framer-motion": "^11.0.5", "fs": "^0.0.1-security", + "interweave": "^13.1.0", "js-tiktoken": "^1.0.10", "langchain": "^0.1.20", "lucide-react": "^0.323.0", @@ -63,6 +64,7 @@ "next-themes": "^0.2.1", "openai": "^4.26.0", "pdf-parse": "^1.1.1", + "pretty-print-json": "^3.0.0", "prism": "^4.1.2", "prismjs": "^1.29.0", "react": "^18", @@ -5727,6 +5729,11 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7123,6 +7130,21 @@ "node": ">=12" } }, + "node_modules/interweave": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/interweave/-/interweave-13.1.0.tgz", + "integrity": "sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA==", + "dependencies": { + "escape-html": "^1.0.3" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10448,6 +10470,11 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, + "node_modules/pretty-print-json": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pretty-print-json/-/pretty-print-json-3.0.0.tgz", + "integrity": "sha512-1993HBv7RNqoPNgaajOK3RLm544t3jongECDFw6Y5faYiBeg2tg62jDT1EDmXRRDn296ykH49A8tphkQBHUSTA==" + }, "node_modules/prism": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/prism/-/prism-4.1.2.tgz", diff --git a/package.json b/package.json index 923018f0..8d8ba00f 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "next-themes": "^0.2.1", "openai": "^4.26.0", "pdf-parse": "^1.1.1", + "pretty-print-json": "^3.0.0", "prism": "^4.1.2", "prismjs": "^1.29.0", "react": "^18", diff --git a/public/cohere.png b/public/cohere.png new file mode 100644 index 00000000..db1cc73f Binary files /dev/null and b/public/cohere.png differ